mirror of https://github.com/go-gitea/gitea.git
Merge 79eb8ac354 into a440116a16
commit
2b53131380
@ -0,0 +1,29 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_26
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func AddCommitCommentTable(x *xorm.Engine) error {
|
||||
type CommitComment struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"INDEX"`
|
||||
CommitSHA string `xorm:"VARCHAR(64) INDEX"`
|
||||
TreePath string `xorm:"VARCHAR(4000)"`
|
||||
Line int64
|
||||
Content string `xorm:"LONGTEXT"`
|
||||
ContentVersion int `xorm:"NOT NULL DEFAULT 0"`
|
||||
PosterID int64 `xorm:"INDEX"`
|
||||
OriginalAuthor string
|
||||
OriginalAuthorID int64
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||
}
|
||||
|
||||
return x.Sync2(new(CommitComment))
|
||||
}
|
||||
@ -0,0 +1,194 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
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"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// CommitComment represents a comment on a specific line in a commit diff
|
||||
type CommitComment struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"INDEX"`
|
||||
Repo *Repository `xorm:"-"`
|
||||
CommitSHA string `xorm:"VARCHAR(64) INDEX"`
|
||||
TreePath string `xorm:"VARCHAR(4000)"` // File path (same field name as issue comments)
|
||||
Line int64 // - previous line / + proposed line
|
||||
Content string `xorm:"LONGTEXT"`
|
||||
ContentVersion int `xorm:"NOT NULL DEFAULT 0"`
|
||||
RenderedContent template.HTML `xorm:"-"`
|
||||
PosterID int64 `xorm:"INDEX"`
|
||||
Poster *user_model.User `xorm:"-"`
|
||||
OriginalAuthor string
|
||||
OriginalAuthorID int64
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||
Attachments []*Attachment `xorm:"-"`
|
||||
|
||||
// Fields for template compatibility with PR comments
|
||||
Review any `xorm:"-"` // Always nil for commit comments
|
||||
Invalidated bool `xorm:"-"` // Always false for commit comments
|
||||
ResolveDoer any `xorm:"-"` // Always nil for commit comments
|
||||
Reactions any `xorm:"-"` // Reactions for this comment
|
||||
}
|
||||
|
||||
// IsResolved returns false (commit comments don't support resolution)
|
||||
func (c *CommitComment) IsResolved() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// HasOriginalAuthor returns if a comment was migrated and has an original author
|
||||
func (c *CommitComment) HasOriginalAuthor() bool {
|
||||
return c.OriginalAuthor != "" && c.OriginalAuthorID != 0
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(CommitComment))
|
||||
}
|
||||
|
||||
// ErrCommitCommentNotExist represents a "CommitCommentNotExist" kind of error.
|
||||
type ErrCommitCommentNotExist struct {
|
||||
ID int64
|
||||
}
|
||||
|
||||
// IsErrCommitCommentNotExist checks if an error is a ErrCommitCommentNotExist.
|
||||
func IsErrCommitCommentNotExist(err error) bool {
|
||||
_, ok := err.(ErrCommitCommentNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrCommitCommentNotExist) Error() string {
|
||||
return fmt.Sprintf("commit comment does not exist [id: %d]", err.ID)
|
||||
}
|
||||
|
||||
// CreateCommitComment creates a new commit comment
|
||||
func CreateCommitComment(ctx context.Context, comment *CommitComment) error {
|
||||
return db.Insert(ctx, comment)
|
||||
}
|
||||
|
||||
// GetCommitCommentByID returns a commit comment by ID
|
||||
func GetCommitCommentByID(ctx context.Context, id int64) (*CommitComment, error) {
|
||||
comment := new(CommitComment)
|
||||
has, err := db.GetEngine(ctx).ID(id).Get(comment)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrCommitCommentNotExist{id}
|
||||
}
|
||||
return comment, nil
|
||||
}
|
||||
|
||||
// FindCommitCommentsOptions describes the conditions to find commit comments
|
||||
type FindCommitCommentsOptions struct {
|
||||
db.ListOptions
|
||||
RepoID int64
|
||||
CommitSHA string
|
||||
Path string
|
||||
Line int64
|
||||
}
|
||||
|
||||
// ToConds implements FindOptions interface
|
||||
func (opts FindCommitCommentsOptions) ToConds() builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
if opts.RepoID > 0 {
|
||||
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
|
||||
}
|
||||
if opts.CommitSHA != "" {
|
||||
cond = cond.And(builder.Eq{"commit_sha": opts.CommitSHA})
|
||||
}
|
||||
if opts.Path != "" {
|
||||
cond = cond.And(builder.Eq{"tree_path": opts.Path})
|
||||
}
|
||||
if opts.Line != 0 {
|
||||
cond = cond.And(builder.Eq{"line": opts.Line})
|
||||
}
|
||||
return cond
|
||||
}
|
||||
|
||||
// FindCommitComments returns commit comments based on options
|
||||
func FindCommitComments(ctx context.Context, opts FindCommitCommentsOptions) ([]*CommitComment, error) {
|
||||
comments := make([]*CommitComment, 0, 10)
|
||||
sess := db.GetEngine(ctx).Where(opts.ToConds())
|
||||
if opts.Page > 0 {
|
||||
sess = db.SetSessionPagination(sess, &opts)
|
||||
}
|
||||
return comments, sess.Find(&comments)
|
||||
}
|
||||
|
||||
// LoadPoster loads the poster user
|
||||
func (c *CommitComment) LoadPoster(ctx context.Context) error {
|
||||
if c.Poster != nil {
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
c.Poster, 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 err
|
||||
}
|
||||
|
||||
// LoadRepo loads the repository
|
||||
func (c *CommitComment) LoadRepo(ctx context.Context) error {
|
||||
if c.Repo != nil {
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
c.Repo, err = GetRepositoryByID(ctx, c.RepoID)
|
||||
return err
|
||||
}
|
||||
|
||||
// LoadAttachments loads attachments
|
||||
func (c *CommitComment) LoadAttachments(ctx context.Context) error {
|
||||
if len(c.Attachments) > 0 {
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
c.Attachments, err = GetAttachmentsByCommentID(ctx, c.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
// DiffSide returns "previous" if Line is negative and "proposed" if positive
|
||||
func (c *CommitComment) DiffSide() string {
|
||||
if c.Line < 0 {
|
||||
return "previous"
|
||||
}
|
||||
return "proposed"
|
||||
}
|
||||
|
||||
// UnsignedLine returns the absolute value of the line number
|
||||
func (c *CommitComment) UnsignedLine() uint64 {
|
||||
if c.Line < 0 {
|
||||
return uint64(c.Line * -1)
|
||||
}
|
||||
return uint64(c.Line)
|
||||
}
|
||||
|
||||
// HashTag returns unique hash tag for comment
|
||||
func (c *CommitComment) HashTag() string {
|
||||
return fmt.Sprintf("commitcomment-%d", c.ID)
|
||||
}
|
||||
|
||||
// UpdateCommitComment updates a commit comment
|
||||
func UpdateCommitComment(ctx context.Context, comment *CommitComment) error {
|
||||
_, err := db.GetEngine(ctx).ID(comment.ID).AllCols().Update(comment)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteCommitComment deletes a commit comment
|
||||
func DeleteCommitComment(ctx context.Context, comment *CommitComment) error {
|
||||
_, err := db.GetEngine(ctx).ID(comment.ID).Delete(comment)
|
||||
return err
|
||||
}
|
||||
@ -0,0 +1,438 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/context/upload"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
)
|
||||
|
||||
// RenderNewCommitCodeCommentForm renders the form for creating a new commit code comment
|
||||
func RenderNewCommitCodeCommentForm(ctx *context.Context) {
|
||||
commitSHA := ctx.PathParam("sha")
|
||||
|
||||
ctx.Data["PageIsCommitDiff"] = true
|
||||
ctx.Data["AfterCommitID"] = commitSHA
|
||||
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
|
||||
upload.AddUploadContext(ctx, "comment")
|
||||
|
||||
ctx.HTML(http.StatusOK, tplNewComment)
|
||||
}
|
||||
|
||||
// CreateCommitComment creates a new comment on a commit diff line
|
||||
func CreateCommitComment(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.CodeCommentForm)
|
||||
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Repo.CanWriteIssuesOrPulls(false) {
|
||||
ctx.HTTPError(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if form.Content == "" {
|
||||
log.Warn("Empty comment content")
|
||||
ctx.HTTPError(http.StatusBadRequest, "EmptyCommentContent")
|
||||
return
|
||||
}
|
||||
|
||||
signedLine := form.Line
|
||||
if form.Side == "previous" {
|
||||
signedLine *= -1
|
||||
}
|
||||
|
||||
var attachments []string
|
||||
if setting.Attachment.Enabled {
|
||||
attachments = form.Files
|
||||
}
|
||||
|
||||
_, err := repo_service.CreateCommitComment(ctx, &repo_service.CreateCommitCommentOptions{
|
||||
Repo: ctx.Repo.Repository,
|
||||
Doer: ctx.Doer,
|
||||
CommitSHA: form.CommitSHA,
|
||||
Path: form.TreePath,
|
||||
Line: signedLine,
|
||||
Content: form.Content,
|
||||
Attachments: attachments,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("CreateCommitComment", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch all comments for this line to show the full conversation
|
||||
allComments, err := repo_model.FindCommitComments(ctx, repo_model.FindCommitCommentsOptions{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
CommitSHA: form.CommitSHA,
|
||||
Path: form.TreePath,
|
||||
Line: signedLine,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("FindCommitComments", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Load and render all comments
|
||||
issueComments := make([]*issues_model.Comment, 0, len(allComments))
|
||||
for _, cc := range allComments {
|
||||
if err := cc.LoadPoster(ctx); err != nil {
|
||||
ctx.ServerError("LoadPoster", err)
|
||||
return
|
||||
}
|
||||
if err := cc.LoadAttachments(ctx); err != nil {
|
||||
ctx.ServerError("LoadAttachments", err)
|
||||
return
|
||||
}
|
||||
if err := repo_service.RenderCommitComment(ctx, cc); err != nil {
|
||||
ctx.ServerError("RenderCommitComment", err)
|
||||
return
|
||||
}
|
||||
// Load reactions for this comment
|
||||
reactions, _, err := issues_model.FindCommentReactions(ctx, 0, cc.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("FindCommentReactions", err)
|
||||
return
|
||||
}
|
||||
if _, err := reactions.LoadUsers(ctx, ctx.Repo.Repository); err != nil {
|
||||
ctx.ServerError("LoadUsers", err)
|
||||
return
|
||||
}
|
||||
cc.Reactions = reactions
|
||||
issueComments = append(issueComments, convertCommitCommentToIssueComment(cc))
|
||||
}
|
||||
|
||||
// Prepare data for template
|
||||
ctx.Data["comments"] = issueComments
|
||||
ctx.Data["SignedUserID"] = ctx.Doer.ID
|
||||
ctx.Data["SignedUser"] = ctx.Doer
|
||||
ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool {
|
||||
return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
|
||||
}
|
||||
ctx.Data["PageIsCommitDiff"] = true
|
||||
ctx.Data["AfterCommitID"] = form.CommitSHA
|
||||
|
||||
ctx.HTML(http.StatusOK, tplDiffConversation)
|
||||
}
|
||||
|
||||
// LoadCommitComments loads comments for a commit diff
|
||||
func LoadCommitComments(ctx *context.Context) {
|
||||
commitSHA := ctx.PathParam("sha")
|
||||
if commitSHA == "" {
|
||||
ctx.HTTPError(http.StatusBadRequest, "Missing commit SHA")
|
||||
return
|
||||
}
|
||||
|
||||
comments, err := repo_model.FindCommitComments(ctx, repo_model.FindCommitCommentsOptions{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
CommitSHA: commitSHA,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("FindCommitComments", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Load posters, attachments, and render comments
|
||||
for _, comment := range comments {
|
||||
if err := comment.LoadPoster(ctx); err != nil {
|
||||
ctx.ServerError("LoadPoster", err)
|
||||
return
|
||||
}
|
||||
if err := comment.LoadAttachments(ctx); err != nil {
|
||||
ctx.ServerError("LoadAttachments", err)
|
||||
return
|
||||
}
|
||||
if err := repo_service.RenderCommitComment(ctx, comment); err != nil {
|
||||
ctx.ServerError("RenderCommitComment", err)
|
||||
return
|
||||
}
|
||||
// Load reactions for this comment
|
||||
reactions, _, err := issues_model.FindCommentReactions(ctx, 0, comment.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("FindCommentReactions", err)
|
||||
return
|
||||
}
|
||||
if _, err := reactions.LoadUsers(ctx, ctx.Repo.Repository); err != nil {
|
||||
ctx.ServerError("LoadUsers", err)
|
||||
return
|
||||
}
|
||||
comment.Reactions = reactions
|
||||
}
|
||||
|
||||
// Group comments by file and line
|
||||
commentMap := make(map[string]map[string][]*repo_model.CommitComment)
|
||||
for _, comment := range comments {
|
||||
if commentMap[comment.TreePath] == nil {
|
||||
commentMap[comment.TreePath] = make(map[string][]*repo_model.CommitComment)
|
||||
}
|
||||
key := comment.DiffSide() + "_" + strconv.FormatUint(comment.UnsignedLine(), 10)
|
||||
commentMap[comment.TreePath][key] = append(commentMap[comment.TreePath][key], comment)
|
||||
}
|
||||
|
||||
ctx.Data["CommitComments"] = commentMap
|
||||
ctx.Data["SignedUserID"] = ctx.Doer.ID
|
||||
ctx.Data["SignedUser"] = ctx.Doer
|
||||
ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool {
|
||||
return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
|
||||
}
|
||||
ctx.Data["IsCommitComment"] = true
|
||||
ctx.Data["AfterCommitID"] = commitSHA
|
||||
|
||||
ctx.JSON(http.StatusOK, map[string]any{
|
||||
"ok": true,
|
||||
"comments": commentMap,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateCommitCommentContent updates the content of a commit comment
|
||||
func UpdateCommitCommentContent(ctx *context.Context) {
|
||||
comment, err := repo_model.GetCommitCommentByID(ctx, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
if repo_model.IsErrCommitCommentNotExist(err) {
|
||||
ctx.NotFound(err)
|
||||
} else {
|
||||
ctx.ServerError("GetCommitCommentByID", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if comment.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.NotFound(errors.New("repo ID mismatch"))
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(false)) {
|
||||
ctx.HTTPError(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
newContent := ctx.FormString("content")
|
||||
contentVersion := ctx.FormInt("content_version")
|
||||
if contentVersion != comment.ContentVersion {
|
||||
ctx.JSONError(ctx.Tr("repo.comments.edit.already_changed"))
|
||||
return
|
||||
}
|
||||
|
||||
if newContent != comment.Content {
|
||||
oldContent := comment.Content
|
||||
comment.Content = newContent
|
||||
|
||||
if err = repo_service.UpdateCommitComment(ctx, comment, contentVersion, ctx.Doer, oldContent); err != nil {
|
||||
ctx.ServerError("UpdateCommitComment", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := comment.LoadAttachments(ctx); err != nil {
|
||||
ctx.ServerError("LoadAttachments", err)
|
||||
return
|
||||
}
|
||||
|
||||
// when the update request doesn't intend to update attachments (eg: change checkbox state), ignore attachment updates
|
||||
if !ctx.FormBool("ignore_attachments") {
|
||||
if err := updateCommitCommentAttachments(ctx, comment, ctx.FormStrings("files[]")); err != nil {
|
||||
ctx.ServerError("UpdateAttachments", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, map[string]any{
|
||||
"content": string(comment.RenderedContent),
|
||||
"contentVersion": comment.ContentVersion,
|
||||
"attachments": renderCommitCommentAttachments(ctx, comment.Attachments, comment.Content),
|
||||
})
|
||||
}
|
||||
|
||||
// updateCommitCommentAttachments updates attachments for a commit comment
|
||||
func updateCommitCommentAttachments(ctx *context.Context, comment *repo_model.CommitComment, uuids []string) error {
|
||||
if len(uuids) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, uuids)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", uuids, err)
|
||||
}
|
||||
|
||||
for i := range attachments {
|
||||
attachments[i].CommentID = comment.ID
|
||||
if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil {
|
||||
return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
comment.Attachments = attachments
|
||||
return nil
|
||||
}
|
||||
|
||||
// convertCommitCommentToIssueComment converts a single CommitComment to Comment for template compatibility
|
||||
func convertCommitCommentToIssueComment(cc *repo_model.CommitComment) *issues_model.Comment {
|
||||
var reactions issues_model.ReactionList
|
||||
if cc.Reactions != nil {
|
||||
if r, ok := cc.Reactions.(issues_model.ReactionList); ok {
|
||||
reactions = r
|
||||
}
|
||||
}
|
||||
return &issues_model.Comment{
|
||||
ID: cc.ID,
|
||||
PosterID: cc.PosterID,
|
||||
Poster: cc.Poster,
|
||||
OriginalAuthor: cc.OriginalAuthor,
|
||||
OriginalAuthorID: cc.OriginalAuthorID,
|
||||
TreePath: cc.TreePath,
|
||||
Line: cc.Line,
|
||||
Content: cc.Content,
|
||||
ContentVersion: cc.ContentVersion,
|
||||
RenderedContent: cc.RenderedContent,
|
||||
CreatedUnix: cc.CreatedUnix,
|
||||
UpdatedUnix: cc.UpdatedUnix,
|
||||
Reactions: reactions,
|
||||
Attachments: cc.Attachments,
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteCommitComment deletes a commit comment
|
||||
func DeleteCommitComment(ctx *context.Context) {
|
||||
comment, err := repo_model.GetCommitCommentByID(ctx, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
if repo_model.IsErrCommitCommentNotExist(err) {
|
||||
ctx.NotFound(err)
|
||||
} else {
|
||||
ctx.ServerError("GetCommitCommentByID", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if comment.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.NotFound(errors.New("repo ID mismatch"))
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(false)) {
|
||||
ctx.HTTPError(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if err = repo_model.DeleteCommitComment(ctx, comment); err != nil {
|
||||
ctx.ServerError("DeleteCommitComment", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// ChangeCommitCommentReaction creates or removes a reaction for a commit comment
|
||||
func ChangeCommitCommentReaction(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.ReactionForm)
|
||||
comment, err := repo_model.GetCommitCommentByID(ctx, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
if repo_model.IsErrCommitCommentNotExist(err) {
|
||||
ctx.NotFound(err)
|
||||
} else {
|
||||
ctx.ServerError("GetCommitCommentByID", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if comment.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.NotFound(errors.New("repo ID mismatch"))
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.IsSigned {
|
||||
ctx.HTTPError(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
switch ctx.PathParam("action") {
|
||||
case "react":
|
||||
// Create reaction using IssueID=0 for commit comments
|
||||
reaction, err := issues_model.CreateReaction(ctx, &issues_model.ReactionOptions{
|
||||
Type: form.Content,
|
||||
DoerID: ctx.Doer.ID,
|
||||
IssueID: 0, // Use 0 for commit comments
|
||||
CommentID: comment.ID,
|
||||
})
|
||||
if err != nil {
|
||||
if issues_model.IsErrForbiddenIssueReaction(err) {
|
||||
ctx.ServerError("ChangeCommitCommentReaction", err)
|
||||
return
|
||||
}
|
||||
log.Info("CreateReaction: %s", err)
|
||||
break
|
||||
}
|
||||
log.Trace("Reaction for commit comment created: %d/%d/%d", ctx.Repo.Repository.ID, comment.ID, reaction.ID)
|
||||
case "unreact":
|
||||
if err := issues_model.DeleteCommentReaction(ctx, ctx.Doer.ID, 0, comment.ID, form.Content); err != nil {
|
||||
ctx.ServerError("DeleteCommentReaction", err)
|
||||
return
|
||||
}
|
||||
log.Trace("Reaction for commit comment removed: %d/%d", ctx.Repo.Repository.ID, comment.ID)
|
||||
default:
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Reload reactions
|
||||
reactions, _, err := issues_model.FindCommentReactions(ctx, 0, comment.ID)
|
||||
if err != nil {
|
||||
log.Info("FindCommentReactions: %s", err)
|
||||
}
|
||||
|
||||
// Load reaction users
|
||||
if _, err := reactions.LoadUsers(ctx, ctx.Repo.Repository); err != nil {
|
||||
log.Info("LoadUsers: %s", err)
|
||||
}
|
||||
|
||||
if len(reactions) == 0 {
|
||||
ctx.JSON(http.StatusOK, map[string]any{
|
||||
"empty": true,
|
||||
"html": "",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
html, err := ctx.RenderToHTML(tplReactions, map[string]any{
|
||||
"ActionURL": fmt.Sprintf("%s/commit-comments/%d/reactions", ctx.Repo.RepoLink, comment.ID),
|
||||
"Reactions": reactions.GroupByType(),
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("ChangeCommitCommentReaction.HTMLString", err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, map[string]any{
|
||||
"html": html,
|
||||
})
|
||||
}
|
||||
|
||||
// renderCommitCommentAttachments renders attachments HTML for commit comments
|
||||
func renderCommitCommentAttachments(ctx *context.Context, attachments []*repo_model.Attachment, content string) template.HTML {
|
||||
attachHTML, err := ctx.RenderToHTML(templates.TplName("repo/issue/view_content/attachments"), map[string]any{
|
||||
"ctxData": ctx.Data,
|
||||
"Attachments": attachments,
|
||||
"Content": content,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("renderCommitCommentAttachments.RenderToHTML", err)
|
||||
return ""
|
||||
}
|
||||
return attachHTML
|
||||
}
|
||||
@ -0,0 +1,95 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/renderhelper"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
)
|
||||
|
||||
// CreateCommitCommentOptions holds options for creating a commit comment
|
||||
type CreateCommitCommentOptions struct {
|
||||
Repo *repo_model.Repository
|
||||
Doer *user_model.User
|
||||
CommitSHA string
|
||||
Path string
|
||||
Line int64
|
||||
Content string
|
||||
Attachments []string
|
||||
}
|
||||
|
||||
// CreateCommitComment creates a new comment on a commit diff line
|
||||
func CreateCommitComment(ctx context.Context, opts *CreateCommitCommentOptions) (*repo_model.CommitComment, error) {
|
||||
comment := &repo_model.CommitComment{
|
||||
RepoID: opts.Repo.ID,
|
||||
CommitSHA: opts.CommitSHA,
|
||||
TreePath: opts.Path,
|
||||
Line: opts.Line,
|
||||
Content: opts.Content,
|
||||
PosterID: opts.Doer.ID,
|
||||
}
|
||||
|
||||
if err := repo_model.CreateCommitComment(ctx, comment); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Handle attachments
|
||||
if len(opts.Attachments) > 0 {
|
||||
attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, opts.Attachments)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetAttachmentsByUUIDs [uuids: %v]: %w", opts.Attachments, err)
|
||||
}
|
||||
for i := range attachments {
|
||||
attachments[i].CommentID = comment.ID
|
||||
if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil {
|
||||
return nil, fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err)
|
||||
}
|
||||
}
|
||||
comment.Attachments = attachments
|
||||
}
|
||||
|
||||
// Load poster for rendering
|
||||
if err := comment.LoadPoster(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return comment, nil
|
||||
}
|
||||
|
||||
// RenderCommitComment renders the comment content as markdown
|
||||
func RenderCommitComment(ctx context.Context, comment *repo_model.CommitComment) error {
|
||||
if err := comment.LoadRepo(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rctx := renderhelper.NewRenderContextRepoComment(ctx, comment.Repo)
|
||||
rendered, err := markdown.RenderString(rctx, comment.Content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
comment.RenderedContent = rendered
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateCommitComment updates a commit comment
|
||||
func UpdateCommitComment(ctx context.Context, comment *repo_model.CommitComment, contentVersion int, doer *user_model.User, oldContent string) error {
|
||||
if contentVersion != comment.ContentVersion {
|
||||
return errors.New("content version mismatch")
|
||||
}
|
||||
|
||||
comment.ContentVersion++
|
||||
|
||||
if err := repo_model.UpdateCommitComment(ctx, comment); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Re-render the comment
|
||||
return RenderCommitComment(ctx, comment)
|
||||
}
|
||||
Loading…
Reference in New Issue