mirror of
https://github.com/go-gitea/gitea.git
synced 2026-04-04 03:35:05 +02:00
refactor: use Comment table with junction table for commit comments
Per @lunny's feedback, rework to reuse the existing Comment table instead of a standalone commit_comment table. The junction table (commit_comment) now only stores repo_id, commit_sha, comment_id. Actual comment data (content, tree_path, line, patch, poster) lives in the Comment table with Type = CommentTypeCommitComment (39). This gives commit comments reactions, attachments, and all existing comment infrastructure for free.
This commit is contained in:
parent
9a4cc1ce4a
commit
3a08115768
@ -116,6 +116,8 @@ const (
|
||||
CommentTypeUnpin // 37 unpin Issue/PullRequest
|
||||
|
||||
CommentTypeChangeTimeEstimate // 38 Change time estimate
|
||||
|
||||
CommentTypeCommitComment // 39 Inline comment on a commit diff (not part of a PR review)
|
||||
)
|
||||
|
||||
var commentStrings = []string{
|
||||
@ -158,6 +160,7 @@ var commentStrings = []string{
|
||||
"pin",
|
||||
"unpin",
|
||||
"change_time_estimate",
|
||||
"commit_comment",
|
||||
}
|
||||
|
||||
func (t CommentType) String() string {
|
||||
@ -175,7 +178,7 @@ func AsCommentType(typeName string) CommentType {
|
||||
|
||||
func (t CommentType) HasContentSupport() bool {
|
||||
switch t {
|
||||
case CommentTypeComment, CommentTypeCode, CommentTypeReview, CommentTypeDismissReview:
|
||||
case CommentTypeComment, CommentTypeCode, CommentTypeReview, CommentTypeDismissReview, CommentTypeCommitComment:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@ -183,7 +186,7 @@ func (t CommentType) HasContentSupport() bool {
|
||||
|
||||
func (t CommentType) HasAttachmentSupport() bool {
|
||||
switch t {
|
||||
case CommentTypeComment, CommentTypeCode, CommentTypeReview:
|
||||
case CommentTypeComment, CommentTypeCode, CommentTypeReview, CommentTypeCommitComment:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
139
models/issues/commit_comment.go
Normal file
139
models/issues/commit_comment.go
Normal file
@ -0,0 +1,139 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package issues
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
)
|
||||
|
||||
// CommitComment is a junction table linking a commit (repo + SHA) to
|
||||
// a Comment entry. The comment content, tree_path, line, poster, etc.
|
||||
// are stored in the Comment table with Type = CommentTypeCommitComment.
|
||||
type CommitComment struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"INDEX NOT NULL"`
|
||||
CommitSHA string `xorm:"VARCHAR(64) INDEX NOT NULL"`
|
||||
CommentID int64 `xorm:"UNIQUE NOT NULL"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(CommitComment))
|
||||
}
|
||||
|
||||
// FileCommitComments holds commit comments for a single file,
|
||||
// split by side (left = old, right = new) with int keys matching DiffLine indices.
|
||||
type FileCommitComments struct {
|
||||
Left map[int][]*Comment
|
||||
Right map[int][]*Comment
|
||||
}
|
||||
|
||||
// CommitCommentsForDiff maps file paths to their commit comments.
|
||||
type CommitCommentsForDiff map[string]*FileCommitComments
|
||||
|
||||
// FindCommitCommentsByCommitSHA returns all comments for a given commit in a repo.
|
||||
func FindCommitCommentsByCommitSHA(ctx context.Context, repoID int64, commitSHA string) ([]*Comment, error) {
|
||||
var refs []CommitComment
|
||||
if err := db.GetEngine(ctx).
|
||||
Where("repo_id = ? AND commit_sha = ?", repoID, commitSHA).
|
||||
Find(&refs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(refs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
commentIDs := make([]int64, 0, len(refs))
|
||||
for _, ref := range refs {
|
||||
commentIDs = append(commentIDs, ref.CommentID)
|
||||
}
|
||||
|
||||
comments := make([]*Comment, 0, len(commentIDs))
|
||||
if err := db.GetEngine(ctx).
|
||||
In("id", commentIDs).
|
||||
OrderBy("created_unix ASC").
|
||||
Find(&comments); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, c := range comments {
|
||||
if err := c.LoadPoster(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return comments, nil
|
||||
}
|
||||
|
||||
// FindCommitCommentsForDiff returns comments grouped by path and side for rendering in a diff view.
|
||||
func FindCommitCommentsForDiff(ctx context.Context, repoID int64, commitSHA string) (CommitCommentsForDiff, error) {
|
||||
comments, err := FindCommitCommentsByCommitSHA(ctx, repoID, commitSHA)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(CommitCommentsForDiff)
|
||||
for _, c := range comments {
|
||||
fcc, ok := result[c.TreePath]
|
||||
if !ok {
|
||||
fcc = &FileCommitComments{
|
||||
Left: make(map[int][]*Comment),
|
||||
Right: make(map[int][]*Comment),
|
||||
}
|
||||
result[c.TreePath] = fcc
|
||||
}
|
||||
if c.Line < 0 {
|
||||
idx := int(-c.Line)
|
||||
fcc.Left[idx] = append(fcc.Left[idx], c)
|
||||
} else {
|
||||
idx := int(c.Line)
|
||||
fcc.Right[idx] = append(fcc.Right[idx], c)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// CreateCommitComment creates a Comment with type CommitComment and a
|
||||
// corresponding CommitComment junction record, within a transaction.
|
||||
func CreateCommitComment(ctx context.Context, repoID int64, commitSHA string, comment *Comment) error {
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
if _, err := db.GetEngine(ctx).Insert(comment); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ref := &CommitComment{
|
||||
RepoID: repoID,
|
||||
CommitSHA: commitSHA,
|
||||
CommentID: comment.ID,
|
||||
}
|
||||
_, err := db.GetEngine(ctx).Insert(ref)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteCommitComment deletes both the junction record and the Comment entry.
|
||||
func DeleteCommitComment(ctx context.Context, commentID int64) error {
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
if _, err := db.GetEngine(ctx).Where("comment_id = ?", commentID).Delete(&CommitComment{}); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := db.GetEngine(ctx).ID(commentID).Delete(&Comment{})
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// GetCommitCommentByID returns a commit comment by loading the Comment entry.
|
||||
func GetCommitCommentByID(ctx context.Context, commentID int64) (*Comment, error) {
|
||||
c := &Comment{}
|
||||
has, err := db.GetEngine(ctx).ID(commentID).Get(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, db.ErrNotExist{Resource: "CommitComment", ID: commentID}
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
@ -4,23 +4,19 @@
|
||||
package v1_26
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func AddCommitCommentTable(x *xorm.Engine) error {
|
||||
// CommitComment is a junction table that maps commit-specific context
|
||||
// (repo, commit SHA) to a Comment entry. The actual comment content,
|
||||
// tree_path, line, poster, etc. live in the Comment table with
|
||||
// type = CommentTypeCommitComment (39).
|
||||
type CommitComment struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"INDEX NOT NULL"`
|
||||
CommitSHA string `xorm:"VARCHAR(64) INDEX NOT NULL"`
|
||||
TreePath string `xorm:"VARCHAR(4000) NOT NULL"`
|
||||
Line int64 `xorm:"NOT NULL"`
|
||||
PosterID int64 `xorm:"INDEX NOT NULL"`
|
||||
Content string `xorm:"LONGTEXT NOT NULL"`
|
||||
Patch string `xorm:"LONGTEXT"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"INDEX NOT NULL"`
|
||||
CommitSHA string `xorm:"VARCHAR(64) INDEX NOT NULL"`
|
||||
CommentID int64 `xorm:"UNIQUE NOT NULL"`
|
||||
}
|
||||
|
||||
return x.Sync(new(CommitComment))
|
||||
|
||||
@ -1,155 +1,8 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// This file intentionally left minimal. The CommitComment junction table
|
||||
// and all query methods now live in models/issues/commit_comment.go
|
||||
// alongside the Comment model they reference.
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
)
|
||||
|
||||
// CommitComment represents an inline comment on a commit diff.
|
||||
type CommitComment struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"INDEX NOT NULL"`
|
||||
CommitSHA string `xorm:"VARCHAR(64) INDEX NOT NULL"`
|
||||
TreePath string `xorm:"VARCHAR(4000) NOT NULL"`
|
||||
Line int64 `xorm:"NOT NULL"` // negative = old side, positive = new side
|
||||
PosterID int64 `xorm:"INDEX NOT NULL"`
|
||||
Poster *user_model.User `xorm:"-"`
|
||||
Content string `xorm:"LONGTEXT NOT NULL"`
|
||||
RenderedContent template.HTML `xorm:"-"`
|
||||
Patch string `xorm:"LONGTEXT"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(CommitComment))
|
||||
}
|
||||
|
||||
// HashTag returns a unique tag for the comment, used for anchoring.
|
||||
func (c *CommitComment) HashTag() string {
|
||||
return fmt.Sprintf("commitcomment-%d", c.ID)
|
||||
}
|
||||
|
||||
// UnsignedLine returns the absolute value of the line number.
|
||||
func (c *CommitComment) UnsignedLine() int64 {
|
||||
if c.Line < 0 {
|
||||
return -c.Line
|
||||
}
|
||||
return c.Line
|
||||
}
|
||||
|
||||
// GetCommentSide returns "previous" for old side (negative Line), "proposed" for new side.
|
||||
func (c *CommitComment) GetCommentSide() string {
|
||||
if c.Line < 0 {
|
||||
return "previous"
|
||||
}
|
||||
return "proposed"
|
||||
}
|
||||
|
||||
// LoadPoster loads the poster user for a commit comment.
|
||||
func (c *CommitComment) LoadPoster(ctx context.Context) error {
|
||||
if c.Poster != nil || c.PosterID <= 0 {
|
||||
return nil
|
||||
}
|
||||
poster, err := user_model.GetUserByID(ctx, c.PosterID)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
c.PosterID = user_model.GhostUserID
|
||||
c.Poster = user_model.NewGhostUser()
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
c.Poster = poster
|
||||
return nil
|
||||
}
|
||||
|
||||
// FileCommitComments holds commit comments for a single file,
|
||||
// split by side (left = old, right = new) with int keys matching DiffLine indices.
|
||||
type FileCommitComments struct {
|
||||
Left map[int][]*CommitComment
|
||||
Right map[int][]*CommitComment
|
||||
}
|
||||
|
||||
// CommitCommentsForDiff maps file paths to their commit comments.
|
||||
type CommitCommentsForDiff map[string]*FileCommitComments
|
||||
|
||||
// FindCommitCommentsByCommitSHA returns all comments for a given commit in a repo.
|
||||
func FindCommitCommentsByCommitSHA(ctx context.Context, repoID int64, commitSHA string) ([]*CommitComment, error) {
|
||||
comments := make([]*CommitComment, 0, 10)
|
||||
return comments, db.GetEngine(ctx).
|
||||
Where("repo_id = ? AND commit_sha = ?", repoID, commitSHA).
|
||||
OrderBy("created_unix ASC").
|
||||
Find(&comments)
|
||||
}
|
||||
|
||||
// FindCommitCommentsForDiff returns comments grouped by path and side for rendering in a diff view.
|
||||
func FindCommitCommentsForDiff(ctx context.Context, repoID int64, commitSHA string) (CommitCommentsForDiff, error) {
|
||||
comments, err := FindCommitCommentsByCommitSHA(ctx, repoID, commitSHA)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(CommitCommentsForDiff)
|
||||
for _, c := range comments {
|
||||
if err := c.LoadPoster(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fcc, ok := result[c.TreePath]
|
||||
if !ok {
|
||||
fcc = &FileCommitComments{
|
||||
Left: make(map[int][]*CommitComment),
|
||||
Right: make(map[int][]*CommitComment),
|
||||
}
|
||||
result[c.TreePath] = fcc
|
||||
}
|
||||
if c.Line < 0 {
|
||||
idx := int(-c.Line)
|
||||
fcc.Left[idx] = append(fcc.Left[idx], c)
|
||||
} else {
|
||||
idx := int(c.Line)
|
||||
fcc.Right[idx] = append(fcc.Right[idx], c)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// CreateCommitComment inserts a new commit comment.
|
||||
func CreateCommitComment(ctx context.Context, c *CommitComment) error {
|
||||
_, err := db.GetEngine(ctx).Insert(c)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetCommitCommentByID returns a commit comment by its ID.
|
||||
func GetCommitCommentByID(ctx context.Context, id int64) (*CommitComment, error) {
|
||||
c := &CommitComment{}
|
||||
has, err := db.GetEngine(ctx).ID(id).Get(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, db.ErrNotExist{Resource: "CommitComment", ID: id}
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// DeleteCommitComment deletes a commit comment by ID.
|
||||
func DeleteCommitComment(ctx context.Context, id int64) error {
|
||||
_, err := db.GetEngine(ctx).ID(id).Delete(&CommitComment{})
|
||||
return err
|
||||
}
|
||||
|
||||
// CountCommitCommentsByCommitSHA returns the count of comments for a commit.
|
||||
func CountCommitCommentsByCommitSHA(ctx context.Context, repoID int64, commitSHA string) (int64, error) {
|
||||
return db.GetEngine(ctx).
|
||||
Where("repo_id = ? AND commit_sha = ?", repoID, commitSHA).
|
||||
Count(&CommitComment{})
|
||||
}
|
||||
|
||||
@ -426,7 +426,7 @@ func Diff(ctx *context.Context) {
|
||||
}
|
||||
|
||||
// Load inline commit comments for the diff view
|
||||
commitComments, err := repo_model.FindCommitCommentsForDiff(ctx, ctx.Repo.Repository.ID, commitID)
|
||||
commitComments, err := issues_model.FindCommitCommentsForDiff(ctx, ctx.Repo.Repository.ID, commitID)
|
||||
if err != nil {
|
||||
log.Error("FindCommitCommentsForDiff: %v", err)
|
||||
}
|
||||
|
||||
@ -6,8 +6,8 @@ package repo
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/renderhelper"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
@ -51,12 +51,10 @@ func CreateCommitComment(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Negate line number for "previous" (old) side
|
||||
if side == "previous" {
|
||||
line = -line
|
||||
}
|
||||
|
||||
// Resolve full commit SHA
|
||||
commit, err := ctx.Repo.GitRepo.GetCommit(commitSHA)
|
||||
if err != nil {
|
||||
if git.IsErrNotExist(err) {
|
||||
@ -98,18 +96,18 @@ func CreateCommitComment(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
comment := &repo_model.CommitComment{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
comment := &issues_model.Comment{
|
||||
Type: issues_model.CommentTypeCommitComment,
|
||||
PosterID: ctx.Doer.ID,
|
||||
Poster: ctx.Doer,
|
||||
CommitSHA: fullSHA,
|
||||
TreePath: treePath,
|
||||
Line: line,
|
||||
PosterID: ctx.Doer.ID,
|
||||
Poster: ctx.Doer,
|
||||
Content: content,
|
||||
Patch: patch,
|
||||
TreePath: treePath,
|
||||
Line: line,
|
||||
Content: content,
|
||||
Patch: patch,
|
||||
}
|
||||
|
||||
if err := repo_model.CreateCommitComment(ctx, comment); err != nil {
|
||||
if err := issues_model.CreateCommitComment(ctx, ctx.Repo.Repository.ID, fullSHA, comment); err != nil {
|
||||
ctx.ServerError("CreateCommitComment", err)
|
||||
return
|
||||
}
|
||||
@ -121,9 +119,8 @@ func CreateCommitComment(ctx *context.Context) {
|
||||
log.Error("RenderString for commit comment %d: %v", comment.ID, err)
|
||||
}
|
||||
|
||||
// Return the conversation HTML so JS can replace the form inline
|
||||
ctx.Data["CommitID"] = fullSHA
|
||||
ctx.Data["comments"] = []*repo_model.CommitComment{comment}
|
||||
ctx.Data["comments"] = []*issues_model.Comment{comment}
|
||||
ctx.HTML(http.StatusOK, tplCommitConversation)
|
||||
}
|
||||
|
||||
@ -135,19 +132,18 @@ func DeleteCommitComment(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
comment, err := repo_model.GetCommitCommentByID(ctx, commentID)
|
||||
comment, err := issues_model.GetCommitCommentByID(ctx, commentID)
|
||||
if err != nil {
|
||||
ctx.NotFound(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Only the poster or repo admin can delete
|
||||
if comment.PosterID != ctx.Doer.ID && !ctx.Repo.IsAdmin() {
|
||||
ctx.JSONError("permission denied")
|
||||
return
|
||||
}
|
||||
|
||||
if err := repo_model.DeleteCommitComment(ctx, commentID); err != nil {
|
||||
if err := issues_model.DeleteCommitComment(ctx, commentID); err != nil {
|
||||
ctx.ServerError("DeleteCommitComment", err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{{if .comments}}
|
||||
{{$comment := index .comments 0}}
|
||||
<div class="conversation-holder" data-path="{{$comment.TreePath}}" data-side="{{if eq $comment.GetCommentSide "previous"}}left{{else}}right{{end}}" data-idx="{{$comment.UnsignedLine}}">
|
||||
<div class="conversation-holder" data-path="{{$comment.TreePath}}" data-side="{{if eq $comment.DiffSide "previous"}}left{{else}}right{{end}}" data-idx="{{$comment.UnsignedLine}}">
|
||||
<div id="code-comments-{{$comment.ID}}" class="field comment-code-cloud">
|
||||
<div class="comment-list">
|
||||
<div class="ui comments">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user