mirror of https://github.com/go-gitea/gitea.git
Merge 79eb8ac354 into de69e7f16a
commit
030ade8924
|
|
@ -398,6 +398,7 @@ func prepareMigrationTasks() []*migration {
|
|||
// Gitea 1.25.0 ends at migration ID number 322 (database version 323)
|
||||
|
||||
newMigration(323, "Add support for actions concurrency", v1_26.AddActionsConcurrency),
|
||||
newMigration(324, "Add commit comment table", v1_26.AddCommitCommentTable),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -36,6 +36,7 @@ import (
|
|||
"code.gitea.io/gitea/services/gitdiff"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
"code.gitea.io/gitea/services/repository/gitgraph"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -272,6 +273,16 @@ func LoadBranchesAndTags(ctx *context.Context) {
|
|||
// Diff show different from current commit to previous commit
|
||||
func Diff(ctx *context.Context) {
|
||||
ctx.Data["PageIsDiff"] = true
|
||||
ctx.Data["PageIsCommitDiff"] = true // Enable comment buttons on commit diffs
|
||||
|
||||
// Set up user blocking function for comments (only if signed in)
|
||||
if ctx.IsSigned {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
userName := ctx.Repo.Owner.Name
|
||||
repoName := ctx.Repo.Repository.Name
|
||||
|
|
@ -363,6 +374,13 @@ func Diff(ctx *context.Context) {
|
|||
setCompareContext(ctx, parentCommit, commit, userName, repoName)
|
||||
ctx.Data["Title"] = commit.Summary() + " · " + base.ShortSha(commitID)
|
||||
ctx.Data["Commit"] = commit
|
||||
|
||||
// Load commit comments into the diff
|
||||
if err := loadCommitCommentsIntoDiff(ctx, diff, commitID); err != nil {
|
||||
ctx.ServerError("loadCommitCommentsIntoDiff", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["Diff"] = diff
|
||||
ctx.Data["DiffBlobExcerptData"] = diffBlobExcerptData
|
||||
|
||||
|
|
@ -427,6 +445,99 @@ func Diff(ctx *context.Context) {
|
|||
ctx.HTML(http.StatusOK, tplCommitPage)
|
||||
}
|
||||
|
||||
// loadCommitCommentsIntoDiff loads commit comments and attaches them to diff lines
|
||||
func loadCommitCommentsIntoDiff(ctx *context.Context, diff *gitdiff.Diff, commitSHA string) error {
|
||||
comments, err := repo_model.FindCommitComments(ctx, repo_model.FindCommitCommentsOptions{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
CommitSHA: commitSHA,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Load posters, attachments, reactions, and render comments
|
||||
for _, comment := range comments {
|
||||
if err := comment.LoadPoster(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := comment.LoadAttachments(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := repo_service.RenderCommitComment(ctx, comment); err != nil {
|
||||
return err
|
||||
}
|
||||
// Load reactions for this comment
|
||||
reactions, _, err := issues_model.FindCommentReactions(ctx, 0, comment.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := reactions.LoadUsers(ctx, ctx.Repo.Repository); err != nil {
|
||||
return err
|
||||
}
|
||||
comment.Reactions = reactions
|
||||
}
|
||||
|
||||
// Group comments by file and line number
|
||||
allComments := make(map[string]map[int64][]*repo_model.CommitComment)
|
||||
for _, comment := range comments {
|
||||
if allComments[comment.TreePath] == nil {
|
||||
allComments[comment.TreePath] = make(map[int64][]*repo_model.CommitComment)
|
||||
}
|
||||
allComments[comment.TreePath][comment.Line] = append(allComments[comment.TreePath][comment.Line], comment)
|
||||
}
|
||||
|
||||
// Attach comments to diff lines
|
||||
for _, file := range diff.Files {
|
||||
if lineComments, ok := allComments[file.Name]; ok {
|
||||
for _, section := range file.Sections {
|
||||
for _, line := range section.Lines {
|
||||
// Check for comments on the left side (previous/old)
|
||||
if comments, ok := lineComments[int64(line.LeftIdx*-1)]; ok {
|
||||
line.Comments = append(line.Comments, convertCommitCommentsToIssueComments(comments)...)
|
||||
}
|
||||
// Check for comments on the right side (proposed/new)
|
||||
if comments, ok := lineComments[int64(line.RightIdx)]; ok {
|
||||
line.Comments = append(line.Comments, convertCommitCommentsToIssueComments(comments)...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// convertCommitCommentsToIssueComments converts CommitComment to Comment interface for template compatibility
|
||||
func convertCommitCommentsToIssueComments(commitComments []*repo_model.CommitComment) []*issues_model.Comment {
|
||||
comments := make([]*issues_model.Comment, len(commitComments))
|
||||
for i, cc := range commitComments {
|
||||
var reactions issues_model.ReactionList
|
||||
if cc.Reactions != nil {
|
||||
if r, ok := cc.Reactions.(issues_model.ReactionList); ok {
|
||||
reactions = r
|
||||
}
|
||||
}
|
||||
// Create a minimal Comment struct that the template can use
|
||||
comments[i] = &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,
|
||||
}
|
||||
}
|
||||
return comments
|
||||
}
|
||||
|
||||
// RawDiff dumps diff results of repository in given commit ID to io.Writer
|
||||
func RawDiff(ctx *context.Context) {
|
||||
var gitRepo *git.Repository
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -30,6 +30,7 @@ const (
|
|||
tplConversationOutdated templates.TplName = "repo/diff/conversation_outdated"
|
||||
tplTimelineConversation templates.TplName = "repo/issue/view_content/conversation"
|
||||
tplNewComment templates.TplName = "repo/diff/new_comment"
|
||||
tplCommitConversation templates.TplName = "repo/diff/commit_conversation"
|
||||
)
|
||||
|
||||
// RenderNewCodeCommentForm will render the form for creating a new review comment
|
||||
|
|
|
|||
|
|
@ -1622,6 +1622,18 @@ func registerWebRoutes(m *web.Router) {
|
|||
m.Get("/graph", repo.Graph)
|
||||
m.Get("/commit/{sha:([a-f0-9]{7,64})$}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff)
|
||||
m.Get("/commit/{sha:([a-f0-9]{7,64})$}/load-branches-and-tags", repo.LoadBranchesAndTags)
|
||||
m.Group("/commit/{sha:([a-f0-9]{7,64})$}", func() {
|
||||
m.Combo("/code-comment/new").Get(repo.RenderNewCommitCodeCommentForm).
|
||||
Post(web.Bind(forms.CodeCommentForm{}), repo.CreateCommitComment)
|
||||
m.Get("/comments", repo.LoadCommitComments)
|
||||
}, reqSignIn, context.RepoMustNotBeArchived())
|
||||
|
||||
// Commit comment editing, deletion, and reactions
|
||||
m.Group("/commit-comments/{id}", func() {
|
||||
m.Post("", repo.UpdateCommitCommentContent)
|
||||
m.Post("/delete", repo.DeleteCommitComment)
|
||||
m.Post("/reactions/{action}", web.Bind(forms.ReactionForm{}), repo.ChangeCommitCommentReaction)
|
||||
}, reqSignIn, context.RepoMustNotBeArchived())
|
||||
|
||||
// FIXME: this route `/cherry-pick/{sha}` doesn't seem useful or right, the new code always uses `/_cherrypick/` which could handle branch name correctly
|
||||
m.Get("/cherry-pick/{sha:([a-f0-9]{7,64})$}", repo.SetEditorconfigIfExists, context.RepoRefByDefaultBranch(), repo.CherryPick)
|
||||
|
|
|
|||
|
|
@ -554,6 +554,7 @@ type CodeCommentForm struct {
|
|||
SingleReview bool `form:"single_review"`
|
||||
Reply int64 `form:"reply"`
|
||||
LatestCommitID string
|
||||
CommitSHA string // For commit comments (non-PR)
|
||||
Files []string
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -185,7 +185,7 @@
|
|||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<table class="chroma" data-new-comment-url="{{$.Issue.Link}}/files/reviews/new_comment" data-path="{{$file.Name}}">
|
||||
<table class="chroma" data-new-comment-url="{{if $.PageIsCommitDiff}}{{$.RepoLink}}/commit/{{$.AfterCommitID}}/code-comment/new{{else}}{{$.Issue.Link}}/files/reviews/new_comment{{end}}" data-path="{{$file.Name}}">
|
||||
{{if $.IsSplitStyle}}
|
||||
{{template "repo/diff/section_split" dict "file" . "root" $}}
|
||||
{{else}}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
{{if and $.root.SignedUserID (not $.Repository.IsArchived)}}
|
||||
<form class="ui form {{if $.hidden}}tw-hidden comment-form{{end}}" action="{{$.root.Issue.Link}}/files/reviews/comments" method="post">
|
||||
<form class="ui form {{if $.hidden}}tw-hidden comment-form{{end}}" action="{{if $.root.PageIsCommitDiff}}{{$.root.RepoLink}}/commit/{{$.root.AfterCommitID}}/code-comment/new{{else}}{{$.root.Issue.Link}}/files/reviews/comments{{end}}" method="post">
|
||||
{{$.root.CsrfTokenHtml}}
|
||||
<input type="hidden" name="origin" value="{{if $.root.PageIsPullFiles}}diff{{else}}timeline{{end}}">
|
||||
<input type="hidden" name="latest_commit_id" value="{{$.root.AfterCommitID}}">
|
||||
{{if $.root.PageIsCommitDiff}}<input type="hidden" name="commit_sha" value="{{$.root.AfterCommitID}}">{{end}}
|
||||
<input type="hidden" name="side" value="{{if $.Side}}{{$.Side}}{{end}}">
|
||||
<input type="hidden" name="line" value="{{if $.Line}}{{$.Line}}{{end}}">
|
||||
<input type="hidden" name="path" value="{{if $.File}}{{$.File}}{{end}}">
|
||||
|
|
@ -28,7 +29,9 @@
|
|||
|
||||
<div class="field footer">
|
||||
<div class="flex-text-block tw-justify-end">
|
||||
{{if $.reply}}
|
||||
{{if $.root.PageIsCommitDiff}}
|
||||
<button type="submit" class="ui submit primary tiny button btn-add-single">{{ctx.Locale.Tr "repo.diff.comment.add_single_comment"}}</button>
|
||||
{{else if $.reply}}
|
||||
<button class="ui submit primary tiny button btn-reply" type="submit">{{ctx.Locale.Tr "repo.diff.comment.reply"}}</button>
|
||||
<input type="hidden" name="reply" value="{{$.reply}}">
|
||||
<input type="hidden" name="single_review" value="true">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
{{range .comments}}
|
||||
|
||||
{{$createdStr:= DateUtils.TimeSince .CreatedUnix}}
|
||||
{{$commentPath := "comments"}}
|
||||
{{if $.root.PageIsCommitDiff}}{{$commentPath = "commit-comments"}}{{end}}
|
||||
<div class="comment" id="{{.HashTag}}">
|
||||
<div class="tw-mt-2 tw-mr-4">
|
||||
{{if .OriginalAuthor}}
|
||||
|
|
@ -51,7 +53,7 @@
|
|||
{{end}}
|
||||
{{end}}
|
||||
{{if not $.root.Repository.IsArchived}}
|
||||
{{template "repo/issue/view_content/add_reaction" dict "ActionURL" (printf "%s/comments/%d/reactions" $.root.RepoLink .ID)}}
|
||||
{{template "repo/issue/view_content/add_reaction" dict "ActionURL" (printf "%s/%s/%d/reactions" $.root.RepoLink $commentPath .ID)}}
|
||||
{{end}}
|
||||
{{template "repo/issue/view_content/context_menu" dict "item" . "delete" true "issue" false "diff" true "IsCommentPoster" (and $.root.IsSigned (eq $.root.SignedUserID .PosterID))}}
|
||||
</div>
|
||||
|
|
@ -65,14 +67,14 @@
|
|||
{{end}}
|
||||
</div>
|
||||
<div id="issuecomment-{{.ID}}-raw" class="raw-content tw-hidden">{{.Content}}</div>
|
||||
<div class="edit-content-zone tw-hidden" data-update-url="{{$.root.RepoLink}}/comments/{{.ID}}" data-content-version="{{.ContentVersion}}" data-context="{{$.root.RepoLink}}" data-attachment-url="{{$.root.RepoLink}}/comments/{{.ID}}/attachments"></div>
|
||||
<div class="edit-content-zone tw-hidden" data-update-url="{{$.root.RepoLink}}/{{if $.root.PageIsCommitDiff}}commit-comments{{else}}comments{{end}}/{{.ID}}" data-content-version="{{.ContentVersion}}" data-context="{{$.root.RepoLink}}" data-attachment-url="{{$.root.RepoLink}}/{{if $.root.PageIsCommitDiff}}commit-comments{{else}}comments{{end}}/{{.ID}}/attachments"></div>
|
||||
{{if .Attachments}}
|
||||
{{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{$reactions := .Reactions.GroupByType}}
|
||||
{{if $reactions}}
|
||||
{{template "repo/issue/view_content/reactions" dict "ActionURL" (printf "%s/comments/%d/reactions" $.root.RepoLink .ID) "Reactions" $reactions}}
|
||||
{{template "repo/issue/view_content/reactions" dict "ActionURL" (printf "%s/%s/%d/reactions" $.root.RepoLink $commentPath .ID) "Reactions" $reactions}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
<td class="lines-escape del-code lines-escape-old">{{if $line.LeftIdx}}{{if $leftDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $leftDiff}}"></button>{{end}}{{end}}</td>
|
||||
<td class="lines-type-marker lines-type-marker-old del-code"><span class="tw-font-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span></td>
|
||||
<td class="lines-code lines-code-old del-code">
|
||||
{{- if and $.root.SignedUserID $.root.PageIsPullFiles -}}
|
||||
{{- if and $.root.SignedUserID (or $.root.PageIsPullFiles $.root.PageIsCommitDiff) -}}
|
||||
<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-left{{if (not $line.CanComment)}} tw-invisible{{end}}" data-side="left" data-idx="{{$line.LeftIdx}}">
|
||||
{{- svg "octicon-plus" -}}
|
||||
</button>
|
||||
|
|
@ -43,7 +43,7 @@
|
|||
<td class="lines-escape add-code lines-escape-new">{{if $match.RightIdx}}{{if $rightDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $rightDiff}}"></button>{{end}}{{end}}</td>
|
||||
<td class="lines-type-marker lines-type-marker-new add-code">{{if $match.RightIdx}}<span class="tw-font-mono" data-type-marker="{{$match.GetLineTypeMarker}}"></span>{{end}}</td>
|
||||
<td class="lines-code lines-code-new add-code">
|
||||
{{- if and $.root.SignedUserID $.root.PageIsPullFiles -}}
|
||||
{{- if and $.root.SignedUserID (or $.root.PageIsPullFiles $.root.PageIsCommitDiff) -}}
|
||||
<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-right{{if (not $match.CanComment)}} tw-invisible{{end}}" data-side="right" data-idx="{{$match.RightIdx}}">
|
||||
{{- svg "octicon-plus" -}}
|
||||
</button>
|
||||
|
|
@ -60,7 +60,7 @@
|
|||
<td class="lines-escape lines-escape-old">{{if $line.LeftIdx}}{{if $inlineDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"></button>{{end}}{{end}}</td>
|
||||
<td class="lines-type-marker lines-type-marker-old">{{if $line.LeftIdx}}<span class="tw-font-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span>{{end}}</td>
|
||||
<td class="lines-code lines-code-old">
|
||||
{{- if and $.root.SignedUserID $.root.PageIsPullFiles (not (eq .GetType 2)) -}}
|
||||
{{- if and $.root.SignedUserID (or $.root.PageIsPullFiles $.root.PageIsCommitDiff) (not (eq .GetType 2)) -}}
|
||||
<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-left{{if (not $line.CanComment)}} tw-invisible{{end}}" data-side="left" data-idx="{{$line.LeftIdx}}">
|
||||
{{- svg "octicon-plus" -}}
|
||||
</button>
|
||||
|
|
@ -75,7 +75,7 @@
|
|||
<td class="lines-escape lines-escape-new">{{if $line.RightIdx}}{{if $inlineDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"></button>{{end}}{{end}}</td>
|
||||
<td class="lines-type-marker lines-type-marker-new">{{if $line.RightIdx}}<span class="tw-font-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span>{{end}}</td>
|
||||
<td class="lines-code lines-code-new">
|
||||
{{- if and $.root.SignedUserID $.root.PageIsPullFiles (not (eq .GetType 3)) -}}
|
||||
{{- if and $.root.SignedUserID (or $.root.PageIsPullFiles $.root.PageIsCommitDiff) (not (eq .GetType 3)) -}}
|
||||
<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-right{{if (not $line.CanComment)}} tw-invisible{{end}}" data-side="right" data-idx="{{$line.RightIdx}}">
|
||||
{{- svg "octicon-plus" -}}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@
|
|||
<td class="chroma lines-code blob-hunk">{{template "repo/diff/section_code" dict "diff" $inlineDiff}}</td>
|
||||
{{else}}
|
||||
<td class="chroma lines-code{{if (not $line.RightIdx)}} lines-code-old{{end}}">
|
||||
{{- if and $.root.SignedUserID $.root.PageIsPullFiles -}}
|
||||
{{- if and $.root.SignedUserID (or $.root.PageIsPullFiles $.root.PageIsCommitDiff) -}}
|
||||
<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-{{if $line.RightIdx}}right{{else}}left{{end}}{{if (not $line.CanComment)}} tw-invisible{{end}}" data-side="{{if $line.RightIdx}}right{{else}}left{{end}}" data-idx="{{if $line.RightIdx}}{{$line.RightIdx}}{{else}}{{$line.LeftIdx}}{{end}}">
|
||||
{{- svg "octicon-plus" -}}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@
|
|||
</a>
|
||||
<div class="menu">
|
||||
{{$referenceUrl := ""}}
|
||||
{{if .issue}}
|
||||
{{if ctx.RootData.PageIsCommitDiff}}
|
||||
{{$referenceUrl = printf "%s/commit/%s#%s" ctx.RootData.RepoLink ctx.RootData.AfterCommitID .item.HashTag}}
|
||||
{{else if .issue}}
|
||||
{{$referenceUrl = printf "%s#%s" ctx.RootData.Issue.Link .item.HashTag}}
|
||||
{{else}}
|
||||
{{$referenceUrl = printf "%s/files#%s" ctx.RootData.Issue.Link .item.HashTag}}
|
||||
|
|
@ -22,7 +24,13 @@
|
|||
<div class="divider"></div>
|
||||
<div class="item context js-aria-clickable edit-content">{{ctx.Locale.Tr "repo.issues.context.edit"}}</div>
|
||||
{{if .delete}}
|
||||
<div class="item context js-aria-clickable delete-comment" data-comment-id={{.item.HashTag}} data-url="{{ctx.RootData.RepoLink}}/comments/{{.item.ID}}/delete" data-locale="{{ctx.Locale.Tr "repo.issues.delete_comment_confirm"}}">{{ctx.Locale.Tr "repo.issues.context.delete"}}</div>
|
||||
{{$deleteURL := ""}}
|
||||
{{if ctx.RootData.PageIsCommitDiff}}
|
||||
{{$deleteURL = printf "%s/commit-comments/%d/delete" ctx.RootData.RepoLink .item.ID}}
|
||||
{{else}}
|
||||
{{$deleteURL = printf "%s/comments/%d/delete" ctx.RootData.RepoLink .item.ID}}
|
||||
{{end}}
|
||||
<div class="item context js-aria-clickable delete-comment" data-comment-id={{.item.HashTag}} data-url="{{$deleteURL}}" data-locale="{{ctx.Locale.Tr "repo.issues.delete_comment_confirm"}}">{{ctx.Locale.Tr "repo.issues.context.delete"}}</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
|
|
|||
|
|
@ -303,7 +303,7 @@ export function initRepoPullRequestReview() {
|
|||
});
|
||||
|
||||
// The following part is only for diff views
|
||||
if (!document.querySelector('.repository.pull.diff')) return;
|
||||
if (!document.querySelector('.repository.pull.diff, .repository.diff')) return;
|
||||
|
||||
const elReviewBtn = document.querySelector('.js-btn-review');
|
||||
const elReviewPanel = document.querySelector('.review-box-panel.tippy-target');
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
initRepoIssueBranchSelect, initRepoIssueCodeCommentCancel, initRepoIssueCommentDelete,
|
||||
initRepoIssueComments, initRepoIssueReferenceIssue,
|
||||
initRepoIssueTitleEdit, initRepoIssueWipNewTitle, initRepoIssueWipToggle,
|
||||
initRepoPullRequestReview,
|
||||
} from './repo-issue.ts';
|
||||
import {initUnicodeEscapeButton} from './repo-unicode-escape.ts';
|
||||
import {initRepoCloneButtons} from './repo-common.ts';
|
||||
|
|
@ -43,6 +44,7 @@ export function initRepository() {
|
|||
|
||||
initRepoBranchTagSelector();
|
||||
initRepoCommentFormAndSidebar();
|
||||
initRepoPullRequestReview(); // This handles code comment buttons on diffs
|
||||
|
||||
// Labels
|
||||
initCompLabelEdit('.page-content.repository.labels');
|
||||
|
|
@ -72,5 +74,14 @@ export function initRepository() {
|
|||
registerGlobalInitFunc('initRepoPullMergeBox', initRepoPullMergeBox);
|
||||
}
|
||||
|
||||
// Commit diff - enable code comments
|
||||
if (pageContent.matches('.page-content.repository.diff')) {
|
||||
initRepoIssueCommentEdit();
|
||||
initRepoIssueCommentDelete();
|
||||
initRepoIssueReferenceIssue();
|
||||
initRepoIssueCodeCommentCancel();
|
||||
initCompReactionSelector();
|
||||
}
|
||||
|
||||
initUnicodeEscapeButton();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue