0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-01-19 03:22:44 +01:00
gitea/models/git/commit_comment.go
2026-01-18 00:44:12 +05:30

273 lines
8.8 KiB
Go

// Copyright 2026 The Gitea Authors.
// SPDX-License-Identifier: MIT
package git
import (
"context"
"fmt"
"html/template"
"strings"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/log"
)
// CommitComment represents a comment on a single commit
type CommitComment struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX"`
CommitSHA string `xorm:"VARCHAR(64) INDEX"`
PosterID int64 `xorm:"INDEX"`
Poster *user_model.User `xorm:"-"`
Path string `xorm:"VARCHAR(4000)"`
Line int64 `xorm:"INDEX"`
Content string `xorm:"LONGTEXT"`
RenderedContent template.HTML `xorm:"-"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
}
func init() {
db.RegisterModel(new(CommitComment))
}
// CreateCommitComment inserts a new commit comment
func CreateCommitComment(ctx context.Context, c *CommitComment) error {
if c == nil {
return fmt.Errorf("nil commit comment")
}
if _, err := db.GetEngine(ctx).Insert(c); err != nil {
return fmt.Errorf("Insert CommitComment: %w", err)
}
return nil
}
// GetCommitCommentByID returns a commit comment by id
func GetCommitCommentByID(ctx context.Context, id int64) (*CommitComment, error) {
c := &CommitComment{ID: id}
has, err := db.GetEngine(ctx).ID(id).Get(c)
if err != nil {
return nil, fmt.Errorf("Get CommitComment by id: %w", err)
}
if !has {
return nil, fmt.Errorf("commit comment does not exist [id: %d]", id)
}
return c, nil
}
// ListCommitComments returns commit comments for a repo and commit sha
func ListCommitComments(ctx context.Context, repoID int64, commitSHA string) ([]*CommitComment, error) {
var cs []*CommitComment
err := db.GetEngine(ctx).Where("repo_id = ? AND commit_sha = ?", repoID, commitSHA).Asc("created_unix").Find(&cs)
if err != nil {
return nil, fmt.Errorf("List commit comments: %w", err)
}
return cs, nil
}
// ListCommitCommentsByLine returns commit comments for a given repo/commit/path/line
func ListCommitCommentsByLine(ctx context.Context, repoID int64, commitSHA, path string, line int64) ([]*CommitComment, error) {
cs, err := ListCommitComments(ctx, repoID, commitSHA)
if err != nil {
return nil, err
}
var res []*CommitComment
for _, c := range cs {
if c.Path == path && c.Line == line {
res = append(res, c)
}
}
return res, nil
}
// UpdateCommitComment updates an existing commit comment
func UpdateCommitComment(ctx context.Context, c *CommitComment) error {
if c == nil || c.ID == 0 {
return fmt.Errorf("invalid commit comment")
}
if _, err := db.GetEngine(ctx).ID(c.ID).Cols("content", "path", "line", "updated_unix").Update(c); err != nil {
return fmt.Errorf("Update CommitComment: %w", err)
}
return nil
}
// DeleteCommitComment deletes a commit comment by id
func DeleteCommitComment(ctx context.Context, id int64) error {
c := &CommitComment{ID: id}
_, err := db.GetEngine(ctx).ID(id).Delete(c)
if err != nil {
return fmt.Errorf("Delete CommitComment: %w", err)
}
return nil
}
// LoadPoster loads the poster user for the comment
func (c *CommitComment) LoadPoster(ctx context.Context) error {
if c == nil {
return fmt.Errorf("nil commit comment")
}
if c.Poster != nil {
return nil
}
u, err := user_model.GetPossibleUserByID(ctx, c.PosterID)
if err != nil {
if user_model.IsErrUserNotExist(err) {
c.PosterID = user_model.GhostUserID
c.Poster = user_model.NewGhostUser()
return nil
}
log.Error("getUserByID[%d]: %v", c.PosterID, err)
return err
}
c.Poster = u
return nil
}
// HashTag returns an id that can be used as a DOM anchor similar to issue comments
func (c *CommitComment) HashTag() string {
return fmt.Sprintf("commitcomment-%d", c.ID)
}
// UnsignedLine returns absolute line index for use in templates
func (c *CommitComment) UnsignedLine() int64 {
if c.Line < 0 {
return -c.Line
}
return c.Line
}
// DiffSide returns which side the comment belongs to
func (c *CommitComment) DiffSide() string {
if c.Line < 0 {
return "left"
}
return "right"
}
// TreePath exposes Path in a field-name compatible way with issue comment templates
func (c *CommitComment) TreePath() string {
return c.Path
}
// The following methods are provided to be compatible with the issue/pull comment templates
// which expect a richer comment shape (IsResolved, Invalidated, ResolveDoer, Review, ReviewID).
// Commit comments currently do not support review resolution, so these return zero-values.
// IsResolved indicates whether the conversation has been resolved
func (c *CommitComment) IsResolved() bool { return false }
// Invalidated indicates whether the comment has been invalidated/outdated
func (c *CommitComment) Invalidated() bool { return false }
// ResolveDoer returns the user who resolved the conversation (nil for commit comments)
func (c *CommitComment) ResolveDoer() *user_model.User { return nil }
// Review returns an associated review (nil for commit comments)
func (c *CommitComment) Review() any { return nil }
// ReviewID returns the ID of the review if any (0 for commit comments)
func (c *CommitComment) ReviewID() int64 { return 0 }
// OriginalAuthor returns original author name for migrated comments (empty for commit comments)
func (c *CommitComment) OriginalAuthor() string { return "" }
// Attachments returns attachments associated with the comment (none for commit comments)
// Return type is interface{} to avoid importing repo models and causing import cycles.
func (c *CommitComment) Attachments() interface{} { return nil }
// ContentVersion returns the content version for inline editing
func (c *CommitComment) ContentVersion() int { return 0 }
// ReactionListShim is a small, local-friendly substitute for templates that expect
// a list type with GroupByType, HasUser, GetFirstUsers and GetMoreUserCount methods.
// Implemented locally to avoid import cycles with models/issues.
type ReactionShim struct {
UserID int64
OriginalAuthor string
User *user_model.User
}
type ReactionListShim []*ReactionShim
// GroupByType groups ReactionShims by their type key
func (list ReactionListShim) GroupByType() map[string]ReactionListShim {
grouped := make(map[string]ReactionListShim)
// Expectation: each ReactionShim is annotated with its type in OriginalAuthor for grouping convenience
// However our storage does not keep Type on the shim, so this grouping is performed during construction
// The helpers that construct ReactionListShim should provide grouping at the top-level map instead.
return grouped
}
// HasUser checks if a user has reacted
func (list ReactionListShim) HasUser(userID int64) bool {
if userID == 0 {
return false
}
for _, reaction := range list {
if reaction.OriginalAuthor == "" && reaction.UserID == userID {
return true
}
}
return false
}
// GetFirstUsers returns a comma-separated list of first users
func (list ReactionListShim) GetFirstUsers() string { return "" }
// GetMoreUserCount returns remaining user count
func (list ReactionListShim) GetMoreUserCount() int { return 0 }
// Reactions returns a ReactionListShim (empty) so templates can safely call GroupByType
func (c *CommitComment) Reactions() ReactionListShim { return nil }
// LoadReactions loads and groups reactions for a commit comment by type
func LoadReactionsForCommitComment(ctx context.Context, commentID int64) (map[string]ReactionListShim, error) {
crs, err := FindCommitCommentReactions(ctx, commentID)
if err != nil {
return nil, err
}
grouped := make(map[string]ReactionListShim)
userIDs := make([]int64, 0, len(crs))
for _, cr := range crs {
r := &ReactionShim{UserID: cr.UserID, OriginalAuthor: cr.OriginalAuthor}
grouped[cr.Type] = append(grouped[cr.Type], r)
if cr.OriginalAuthor == "" && cr.UserID > 0 {
userIDs = append(userIDs, cr.UserID)
}
}
if len(userIDs) > 0 {
userMap := make(map[int64]*user_model.User)
if err := db.GetEngine(ctx).In("id", userIDs).Find(&userMap); err != nil {
return nil, err
}
for _, list := range grouped {
for _, r := range list {
if r.OriginalAuthor != "" {
// migrated/original author
r.User = user_model.NewGhostUser()
continue
}
if u, ok := userMap[r.UserID]; ok {
r.User = u
} else {
r.User = user_model.NewGhostUser()
}
}
}
}
return grouped, nil
}
// IsErrCommitCommentNotExist returns true when error indicates the commit comment wasn't found
func IsErrCommitCommentNotExist(err error) bool {
if err == nil {
return false
}
// GetCommitCommentByID returns fmt.Errorf("commit comment does not exist [id: %d]", id) when not found
return strings.HasPrefix(err.Error(), "commit comment does not exist")
}