diff --git a/models/issues/comment_code.go b/models/issues/comment_code.go index 55e67a1243..6020edc95a 100644 --- a/models/issues/comment_code.go +++ b/models/issues/comment_code.go @@ -19,11 +19,11 @@ import ( type CodeComments map[string]map[int64][]*Comment // FetchCodeComments will return a 2d-map: ["Path"]["Line"] = Comments at line -func FetchCodeComments(ctx context.Context, issue *Issue, currentUser *user_model.User, showOutdatedComments bool) (CodeComments, error) { - return fetchCodeCommentsByReview(ctx, issue, currentUser, nil, showOutdatedComments) +func FetchCodeComments(ctx context.Context, issue *Issue, currentUser *user_model.User, showOutdatedComments bool, filePath *string) (CodeComments, error) { + return fetchCodeCommentsByReview(ctx, issue, currentUser, nil, showOutdatedComments, filePath) } -func fetchCodeCommentsByReview(ctx context.Context, issue *Issue, currentUser *user_model.User, review *Review, showOutdatedComments bool) (CodeComments, error) { +func fetchCodeCommentsByReview(ctx context.Context, issue *Issue, currentUser *user_model.User, review *Review, showOutdatedComments bool, filePath *string) (CodeComments, error) { pathToLineToComment := make(CodeComments) if review == nil { review = &Review{ID: 0} @@ -34,6 +34,15 @@ func fetchCodeCommentsByReview(ctx context.Context, issue *Issue, currentUser *u ReviewID: review.ID, } + if filePath != nil { + opts = FindCommentsOptions{ + Type: CommentTypeCode, + IssueID: issue.ID, + ReviewID: review.ID, + TreePath: *filePath, + } + } + comments, err := findCodeComments(ctx, opts, issue, currentUser, review, showOutdatedComments) if err != nil { return nil, err diff --git a/models/issues/comment_test.go b/models/issues/comment_test.go index c08e3b970d..eb37be56e5 100644 --- a/models/issues/comment_test.go +++ b/models/issues/comment_test.go @@ -68,7 +68,7 @@ func TestFetchCodeComments(t *testing.T) { issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) - res, err := issues_model.FetchCodeComments(db.DefaultContext, issue, user, false) + res, err := issues_model.FetchCodeComments(db.DefaultContext, issue, user, false, nil) assert.NoError(t, err) assert.Contains(t, res, "README.md") assert.Contains(t, res["README.md"], int64(4)) @@ -76,7 +76,7 @@ func TestFetchCodeComments(t *testing.T) { assert.Equal(t, int64(4), res["README.md"][4][0].ID) user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - res, err = issues_model.FetchCodeComments(db.DefaultContext, issue, user2, false) + res, err = issues_model.FetchCodeComments(db.DefaultContext, issue, user2, false, nil) assert.NoError(t, err) assert.Len(t, res, 1) } diff --git a/models/issues/review.go b/models/issues/review.go index 71fdb7456f..bff76c6245 100644 --- a/models/issues/review.go +++ b/models/issues/review.go @@ -159,7 +159,7 @@ func (r *Review) LoadCodeComments(ctx context.Context) (err error) { if err = r.LoadIssue(ctx); err != nil { return err } - r.CodeComments, err = fetchCodeCommentsByReview(ctx, r.Issue, nil, r, false) + r.CodeComments, err = fetchCodeCommentsByReview(ctx, r.Issue, nil, r, false, nil) return err } diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index de34a9375c..9d6d5b53be 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -14,6 +14,7 @@ import ( "net/http" "net/url" "path/filepath" + "sort" "strings" "code.gitea.io/gitea/models/db" @@ -41,6 +42,7 @@ import ( "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context/upload" "code.gitea.io/gitea/services/gitdiff" + user_service "code.gitea.io/gitea/services/user" ) const ( @@ -881,6 +883,10 @@ func ExcerptBlob(ctx *context.Context) { direction := ctx.FormString("direction") filePath := ctx.FormString("path") gitRepo := ctx.Repo.GitRepo + if ctx.FormBool("pull") { + ctx.Data["PageIsPullFiles"] = true + } + if ctx.Data["PageIsWiki"] == true { var err error gitRepo, err = gitrepo.OpenRepository(ctx, ctx.Repo.Repository.WikiStorageRepo()) @@ -941,7 +947,9 @@ func ExcerptBlob(ctx *context.Context) { RightIdx: idxRight, LeftHunkSize: leftHunkSize, RightHunkSize: rightHunkSize, + HasComments: false, }, + Comments: nil, } switch direction { case "up": @@ -950,10 +958,69 @@ func ExcerptBlob(ctx *context.Context) { section.Lines = append(section.Lines, lineSection) } } + issueIndex := ctx.FormInt64("issue_index") + if ctx.FormBool("pull") && issueIndex > 0 { + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, issueIndex) + if err != nil { + ctx.ServerError("GetIssueByIndex", err) + return + } + allComments, err := issues_model.FetchCodeComments(ctx, issue, ctx.Doer, false, &filePath) + if err != nil { + ctx.ServerError("FetchCodeComments", err) + return + } + lineCommits := allComments[filePath] + for index, line := range section.Lines { + if line.SectionInfo != nil && line.Type == 4 && !(line.SectionInfo.LastRightIdx == 0 && index+1 == len(section.Lines)) { + start := int64(line.SectionInfo.LastRightIdx + 1) + end := int64(line.SectionInfo.RightIdx - 1) + for start <= end { + if _, ok := lineCommits[start]; ok { + if !line.SectionInfo.HasComments { + line.SectionInfo.HasComments = true + break + } + } + start++ + } + } + if comments, ok := lineCommits[int64(line.LeftIdx*-1)]; ok { + line.Comments = append(line.Comments, comments...) + } + if comments, ok := lineCommits[int64(line.RightIdx)]; ok { + line.Comments = append(line.Comments, comments...) + } + + sort.SliceStable(line.Comments, func(i, j int) bool { + return line.Comments[i].CreatedUnix < line.Comments[j].CreatedUnix + }) + } + for _, line := range section.Lines { + for _, comment := range line.Comments { + if err := comment.LoadAttachments(ctx); err != nil { + ctx.ServerError("LoadAttachments", err) + return + } + } + } + ctx.Data["Issue"] = issue + ctx.Data["IssueIndex"] = issue.Index + } ctx.Data["section"] = section ctx.Data["FileNameHash"] = git.HashFilePathForWebUI(filePath) ctx.Data["AfterCommitID"] = commitID ctx.Data["Anchor"] = anchor + ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool { + return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee) + } + if ctx.Data["SignedUserID"] == nil { + ctx.Data["SignedUserID"] = ctx.Doer.ID + } + ctx.Data["SignedUser"] = ctx.Doer + ctx.Data["IsSigned"] = ctx.Doer != nil + ctx.Data["Repository"] = ctx.Repo.Repository + ctx.Data["Permission"] = &ctx.Repo.Permission ctx.HTML(http.StatusOK, tplBlobExcerpt) } @@ -982,6 +1049,7 @@ func getExcerptLines(commit *git.Commit, filePath string, idxLeft, idxRight, chu RightIdx: line + 1, Type: gitdiff.DiffLinePlain, Content: " " + lineText, + Comments: []*issues_model.Comment{}, } diffLines = append(diffLines, diffLine) } diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index f662152e2e..30b9f7139b 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -129,6 +129,7 @@ func getPullInfo(ctx *context.Context) (issue *issues_model.Issue, ok bool) { } ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, emoji.ReplaceAliases(issue.Title)) ctx.Data["Issue"] = issue + ctx.Data["IssueIndex"] = issue.Index if !issue.IsPull { ctx.NotFound(nil) diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index 9964329876..792b8c0b2a 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -95,6 +95,7 @@ type DiffLineSectionInfo struct { RightIdx int LeftHunkSize int RightHunkSize int + HasComments bool } // DiffHTMLOperation is the HTML version of diffmatchpatch.Diff @@ -189,6 +190,7 @@ func getDiffLineSectionInfo(treePath, line string, lastLeftIdx, lastRightIdx int RightIdx: rightLine, LeftHunkSize: leftHunk, RightHunkSize: righHunk, + HasComments: false, } } @@ -386,6 +388,7 @@ func (diffFile *DiffFile) GetTailSectionAndLimitedContent(leftCommit, rightCommi LastRightIdx: lastLine.RightIdx, LeftIdx: leftLineCount, RightIdx: rightLineCount, + HasComments: false, }, } tailSection := &DiffSection{FileName: diffFile.Name, Lines: []*DiffLine{tailDiffLine}} @@ -456,14 +459,28 @@ type Diff struct { // LoadComments loads comments into each line func (diff *Diff) LoadComments(ctx context.Context, issue *issues_model.Issue, currentUser *user_model.User, showOutdatedComments bool) error { - allComments, err := issues_model.FetchCodeComments(ctx, issue, currentUser, showOutdatedComments) + allComments, err := issues_model.FetchCodeComments(ctx, issue, currentUser, showOutdatedComments, nil) if err != nil { return err } + for _, file := range diff.Files { if lineCommits, ok := allComments[file.Name]; ok { for _, section := range file.Sections { - for _, line := range section.Lines { + for index, line := range section.Lines { + if line.SectionInfo != nil && line.Type == 4 && !(line.SectionInfo.LastRightIdx == 0 && index+1 == len(section.Lines)) { + start := int64(line.SectionInfo.LastRightIdx + 1) + end := int64(line.SectionInfo.RightIdx - 1) + for start <= end { + if _, ok := lineCommits[start]; ok { + if !line.SectionInfo.HasComments { + line.SectionInfo.HasComments = true + break + } + } + start++ + } + } if comments, ok := lineCommits[int64(line.LeftIdx*-1)]; ok { line.Comments = append(line.Comments, comments...) } diff --git a/services/repository/files/diff_test.go b/services/repository/files/diff_test.go index ae702e4189..fd1d7caebc 100644 --- a/services/repository/files/diff_test.go +++ b/services/repository/files/diff_test.go @@ -62,6 +62,7 @@ func TestGetDiffPreview(t *testing.T) { RightIdx: 1, LeftHunkSize: 3, RightHunkSize: 4, + HasComments: false, }, }, { diff --git a/templates/repo/diff/blob_excerpt.tmpl b/templates/repo/diff/blob_excerpt.tmpl index 4089d8fb33..941ac44ae2 100644 --- a/templates/repo/diff/blob_excerpt.tmpl +++ b/templates/repo/diff/blob_excerpt.tmpl @@ -1,10 +1,37 @@ {{$blobExcerptLink := print $.RepoLink (Iif $.PageIsWiki "/wiki" "") "/blob_excerpt/" (PathEscape $.AfterCommitID) (QueryBuild "?" "anchor" $.Anchor)}} {{if $.IsSplitStyle}} {{range $k, $line := $.section.Lines}} + {{$inlineDiff := $.section.GetComputedInlineDiffFor $line ctx.Locale}} {{if eq .GetType 4}} {{$expandDirection := $line.GetExpandDirection}} +
+
+ {{if $line.SectionInfo.HasComments}} + + {{end}} +
+
+ {{if or (eq $expandDirection 3) (eq $expandDirection 5)}} + + {{end}} + {{if or (eq $expandDirection 3) (eq $expandDirection 4)}} + + {{end}} + {{if eq $expandDirection 2}} + + {{end}} +
+
{{if or (eq $expandDirection 3) (eq $expandDirection 5)}} {{end}} {{if $line.LeftIdx}}{{end}} - - {{- if $line.LeftIdx -}} - {{- template "repo/diff/section_code" dict "diff" $inlineDiff -}} - {{- else -}} - - {{- end -}} - + {{/* + */}}{{if and $.SignedUserID $.PageIsPullFiles}}{{/* + */}}{{/* + */}}{{end}}{{/* + */}}{{if $line.LeftIdx}}{{template "repo/diff/section_code" dict "diff" $inlineDiff}}{{else}}{{/* + */}}{{/* + */}}{{end}}{{/* + */}} {{if and $line.RightIdx $inlineDiff.EscapeStatus.Escaped}}{{end}} {{if $line.RightIdx}}{{end}} - - {{- if $line.RightIdx -}} - {{- template "repo/diff/section_code" dict "diff" $inlineDiff -}} - {{- else -}} - - {{- end -}} - + {{/* + */}}{{if and $.SignedUserID $.PageIsPullFiles}}{{/* + */}}{{/* + */}}{{end}}{{/* + */}}{{if $line.RightIdx}}{{template "repo/diff/section_code" dict "diff" $inlineDiff}}{{else}}{{/* + */}}{{/* + */}}{{end}}{{/* + */}} {{end}} + {{if $line.Comments}} + + + {{template "repo/diff/conversation" dict "." $ "comments" $line.Comments}}} + + + {{end}} {{end}} {{else}} {{range $k, $line := $.section.Lines}} + {{$inlineDiff := $.section.GetComputedInlineDiffFor $line ctx.Locale}} {{if eq .GetType 4}} {{$expandDirection := $line.GetExpandDirection}} -
- {{if or (eq $expandDirection 3) (eq $expandDirection 5)}} - - {{end}} - {{if or (eq $expandDirection 3) (eq $expandDirection 4)}} - - {{end}} - {{if eq $expandDirection 2}} - - {{end}} +
+
+ {{if $line.SectionInfo.HasComments}} + + {{end}} +
+
+ {{if or (eq $expandDirection 3) (eq $expandDirection 5)}} + + {{end}} + {{if or (eq $expandDirection 3) (eq $expandDirection 4)}} + + {{end}} + {{if eq $expandDirection 2}} + + {{end}} +
{{else}} {{end}} - {{$inlineDiff := $.section.GetComputedInlineDiffFor $line ctx.Locale}} {{if $inlineDiff.EscapeStatus.Escaped}}{{end}} - {{$inlineDiff.Content}} + + {{if and $.SignedUserID $.PageIsPullFiles}} + + {{end}} + {{$inlineDiff.Content}} + + {{if $line.Comments}} + + + {{template "repo/diff/conversation" dict "." $ "comments" $line.Comments}} + + + + + {{end}} {{end}} {{end}} diff --git a/templates/repo/diff/section_split.tmpl b/templates/repo/diff/section_split.tmpl index 9953db5eb2..a05ac8b836 100644 --- a/templates/repo/diff/section_split.tmpl +++ b/templates/repo/diff/section_split.tmpl @@ -1,4 +1,6 @@ {{$file := .file}} +{{$blobExcerptRepoLink := or ctx.RootData.CommitRepoLink ctx.RootData.RepoLink}} +{{$issueIndex := or ctx.RootData.IssueIndex $.root.IssueIndex}} {{$blobExcerptLink := print (or ctx.RootData.CommitRepoLink ctx.RootData.RepoLink) (Iif $.root.PageIsWiki "/wiki" "") "/blob_excerpt/" (PathEscape $.root.AfterCommitID) "?"}} @@ -18,22 +20,31 @@ {{if eq .GetType 4}} {{$expandDirection := $line.GetExpandDirection}} -
- {{if or (eq $expandDirection 3) (eq $expandDirection 5)}} - - {{end}} - {{if or (eq $expandDirection 3) (eq $expandDirection 4)}} - - {{end}} - {{if eq $expandDirection 2}} - - {{end}} +
+
+ {{if $line.SectionInfo.HasComments}} + + {{end}} +
+
+ {{if or (eq $expandDirection 3) (eq $expandDirection 5)}} + + {{end}} + {{if or (eq $expandDirection 3) (eq $expandDirection 4)}} + + {{end}} + {{if eq $expandDirection 2}} + + {{end}} +
{{$inlineDiff := $section.GetComputedInlineDiffFor $line ctx.Locale}} {{if $inlineDiff.EscapeStatus.Escaped}}{{end}} diff --git a/templates/repo/diff/section_unified.tmpl b/templates/repo/diff/section_unified.tmpl index cb612bc27c..948c67f178 100644 --- a/templates/repo/diff/section_unified.tmpl +++ b/templates/repo/diff/section_unified.tmpl @@ -1,4 +1,6 @@ {{$file := .file}} +{{$blobExcerptRepoLink := or ctx.RootData.CommitRepoLink ctx.RootData.RepoLink}} +{{$issueIndex := or ctx.RootData.IssueIndex $.root.IssueIndex}} {{$repoLink := or ctx.RootData.CommitRepoLink ctx.RootData.RepoLink}} {{$afterCommitID := or $.root.AfterCommitID "no-after-commit-id"}}{{/* this tmpl is also used by the PR Conversation page, so the "AfterCommitID" may not exist */}} {{$blobExcerptLink := print $repoLink (Iif $.root.PageIsWiki "/wiki" "") "/blob_excerpt/" (PathEscape $afterCommitID) "?"}} @@ -16,22 +18,31 @@ {{if $.root.AfterCommitID}} {{$expandDirection := $line.GetExpandDirection}} -
- {{if or (eq $expandDirection 3) (eq $expandDirection 5)}} - - {{end}} - {{if or (eq $expandDirection 3) (eq $expandDirection 4)}} - - {{end}} - {{if eq $expandDirection 2}} - - {{end}} +
+
+ {{if $line.SectionInfo.HasComments}} + + {{end}} +
+
+ {{if or (eq $expandDirection 3) (eq $expandDirection 5)}} + + {{end}} + {{if or (eq $expandDirection 3) (eq $expandDirection 4)}} + + {{end}} + {{if eq $expandDirection 2}} + + {{end}} +
{{else}} diff --git a/web_src/css/base.css b/web_src/css/base.css index b415a70cb8..9f032ce992 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -896,6 +896,17 @@ overflow-menu .ui.label { font-family: var(--fonts-regular); } +.lines-comment { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1px; +} + +.section-comment-icon { + cursor: default; +} + .lines-commit .blame-info .blame-data .blame-message { flex-grow: 2; overflow: hidden; diff --git a/web_src/js/features/repo-diff.ts b/web_src/js/features/repo-diff.ts index bde7ec0324..7439061e78 100644 --- a/web_src/js/features/repo-diff.ts +++ b/web_src/js/features/repo-diff.ts @@ -10,6 +10,7 @@ import {POST, GET} from '../modules/fetch.ts'; import {createTippy} from '../modules/tippy.ts'; import {invertFileFolding} from './file-fold.ts'; import {parseDom} from '../utils.ts'; +import {initGlobalDropdown} from './common-page.ts'; import {registerGlobalSelectorFunc} from '../modules/observer.ts'; const {i18n} = window.config; @@ -92,6 +93,12 @@ function initRepoDiffConversationForm() { } }); + addDelegatedEventListener(document, 'click', '.pull-request-diff-comments', async (el, e) => { + e.preventDefault(); + initGlobalDropdown(); + // Post initiation cleaning up the buttons and scripts + el.remove(); + }); addDelegatedEventListener(document, 'click', '.resolve-conversation', async (el, e) => { e.preventDefault(); const comment_id = el.getAttribute('data-comment-id');