Skip to content

Commit 0fc61c8

Browse files
oliverpoololiverpool
authored andcommitted
[BUG] split code conversations in diff tab (go-gitea#2306)
Follow-up of go-gitea#2282 and go-gitea#2296 (which tried to address go-gitea#2278) One of the issue with the previous PR is that when a conversation on the Files tab was marked as "resolved", it would fetch all the comments for that line (even the outdated ones, which should not be shown on this page - except when explicitly activated). To properly fix this, I have changed `FetchCodeCommentsByLine` to `FetchCodeConversation`. Its role is to fetch all comments related to a given (review, path, line) and reverted my changes in the template (which were based on a misunderstanding). Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/2306 Reviewed-by: Earl Warren <[email protected]> Reviewed-by: Gusted <[email protected]> Co-authored-by: oliverpool <[email protected]> Co-committed-by: oliverpool <[email protected]>
1 parent 1e364cc commit 0fc61c8

File tree

11 files changed

+350
-146
lines changed

11 files changed

+350
-146
lines changed

models/issues/comment_code.go

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,58 @@ import (
1414
"xorm.io/builder"
1515
)
1616

17-
// CodeComments represents comments on code by using this structure: FILENAME -> LINE (+ == proposed; - == previous) -> COMMENTS
18-
type CodeComments map[string]map[int64][]*Comment
17+
// CodeConversation contains the comment of a given review
18+
type CodeConversation []*Comment
19+
20+
// CodeConversationsAtLine contains the conversations for a given line
21+
type CodeConversationsAtLine map[int64][]CodeConversation
22+
23+
// CodeConversationsAtLineAndTreePath contains the conversations for a given TreePath and line
24+
type CodeConversationsAtLineAndTreePath map[string]CodeConversationsAtLine
25+
26+
func newCodeConversationsAtLineAndTreePath(comments []*Comment) CodeConversationsAtLineAndTreePath {
27+
tree := make(CodeConversationsAtLineAndTreePath)
28+
for _, comment := range comments {
29+
tree.insertComment(comment)
30+
}
31+
return tree
32+
}
33+
34+
func (tree CodeConversationsAtLineAndTreePath) insertComment(comment *Comment) {
35+
// attempt to append comment to existing conversations (i.e. list of comments belonging to the same review)
36+
for i, conversation := range tree[comment.TreePath][comment.Line] {
37+
if conversation[0].ReviewID == comment.ReviewID {
38+
tree[comment.TreePath][comment.Line][i] = append(conversation, comment)
39+
return
40+
}
41+
}
42+
43+
// no previous conversation was found at this line, create it
44+
if tree[comment.TreePath] == nil {
45+
tree[comment.TreePath] = make(map[int64][]CodeConversation)
46+
}
1947

20-
// FetchCodeComments will return a 2d-map: ["Path"]["Line"] = Comments at line
21-
func FetchCodeComments(ctx context.Context, issue *Issue, currentUser *user_model.User, showOutdatedComments bool) (CodeComments, error) {
22-
return fetchCodeCommentsByReview(ctx, issue, currentUser, nil, showOutdatedComments)
48+
tree[comment.TreePath][comment.Line] = append(tree[comment.TreePath][comment.Line], CodeConversation{comment})
2349
}
2450

25-
func fetchCodeCommentsByReview(ctx context.Context, issue *Issue, currentUser *user_model.User, review *Review, showOutdatedComments bool) (CodeComments, error) {
51+
// FetchCodeConversations will return a 2d-map: ["Path"]["Line"] = List of CodeConversation (one per review) for this line
52+
func FetchCodeConversations(ctx context.Context, issue *Issue, doer *user_model.User, showOutdatedComments bool) (CodeConversationsAtLineAndTreePath, error) {
53+
opts := FindCommentsOptions{
54+
Type: CommentTypeCode,
55+
IssueID: issue.ID,
56+
}
57+
comments, err := findCodeComments(ctx, opts, issue, doer, nil, showOutdatedComments)
58+
if err != nil {
59+
return nil, err
60+
}
61+
62+
return newCodeConversationsAtLineAndTreePath(comments), nil
63+
}
64+
65+
// CodeComments represents comments on code by using this structure: FILENAME -> LINE (+ == proposed; - == previous) -> COMMENTS
66+
type CodeComments map[string]map[int64][]*Comment
67+
68+
func fetchCodeCommentsByReview(ctx context.Context, issue *Issue, doer *user_model.User, review *Review, showOutdatedComments bool) (CodeComments, error) {
2669
pathToLineToComment := make(CodeComments)
2770
if review == nil {
2871
review = &Review{ID: 0}
@@ -33,7 +76,7 @@ func fetchCodeCommentsByReview(ctx context.Context, issue *Issue, currentUser *u
3376
ReviewID: review.ID,
3477
}
3578

36-
comments, err := findCodeComments(ctx, opts, issue, currentUser, review, showOutdatedComments)
79+
comments, err := findCodeComments(ctx, opts, issue, doer, review, showOutdatedComments)
3780
if err != nil {
3881
return nil, err
3982
}
@@ -47,7 +90,7 @@ func fetchCodeCommentsByReview(ctx context.Context, issue *Issue, currentUser *u
4790
return pathToLineToComment, nil
4891
}
4992

50-
func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issue, currentUser *user_model.User, review *Review, showOutdatedComments bool) ([]*Comment, error) {
93+
func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issue, doer *user_model.User, review *Review, showOutdatedComments bool) ([]*Comment, error) {
5194
var comments CommentList
5295
if review == nil {
5396
review = &Review{ID: 0}
@@ -91,7 +134,7 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu
91134
if re, ok := reviews[comment.ReviewID]; ok && re != nil {
92135
// If the review is pending only the author can see the comments (except if the review is set)
93136
if review.ID == 0 && re.Type == ReviewTypePending &&
94-
(currentUser == nil || currentUser.ID != re.ReviewerID) {
137+
(doer == nil || doer.ID != re.ReviewerID) {
95138
continue
96139
}
97140
comment.Review = re
@@ -121,13 +164,14 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu
121164
return comments[:n], nil
122165
}
123166

124-
// FetchCodeCommentsByLine fetches the code comments for a given treePath and line number
125-
func FetchCodeCommentsByLine(ctx context.Context, issue *Issue, currentUser *user_model.User, treePath string, line int64, showOutdatedComments bool) ([]*Comment, error) {
167+
// FetchCodeConversation fetches the code conversation of a given comment (same review, treePath and line number)
168+
func FetchCodeConversation(ctx context.Context, comment *Comment, doer *user_model.User) ([]*Comment, error) {
126169
opts := FindCommentsOptions{
127170
Type: CommentTypeCode,
128-
IssueID: issue.ID,
129-
TreePath: treePath,
130-
Line: line,
171+
IssueID: comment.IssueID,
172+
ReviewID: comment.ReviewID,
173+
TreePath: comment.TreePath,
174+
Line: comment.Line,
131175
}
132-
return findCodeComments(ctx, opts, issue, currentUser, nil, showOutdatedComments)
176+
return findCodeComments(ctx, opts, comment.Issue, doer, nil, true)
133177
}

models/issues/comment_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,20 +46,20 @@ func TestCreateComment(t *testing.T) {
4646
unittest.AssertInt64InRange(t, now, then, int64(updatedIssue.UpdatedUnix))
4747
}
4848

49-
func TestFetchCodeComments(t *testing.T) {
49+
func TestFetchCodeConversations(t *testing.T) {
5050
assert.NoError(t, unittest.PrepareTestDatabase())
5151

5252
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
5353
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
54-
res, err := issues_model.FetchCodeComments(db.DefaultContext, issue, user, false)
54+
res, err := issues_model.FetchCodeConversations(db.DefaultContext, issue, user, false)
5555
assert.NoError(t, err)
5656
assert.Contains(t, res, "README.md")
5757
assert.Contains(t, res["README.md"], int64(4))
5858
assert.Len(t, res["README.md"][4], 1)
59-
assert.Equal(t, int64(4), res["README.md"][4][0].ID)
59+
assert.Equal(t, int64(4), res["README.md"][4][0][0].ID)
6060

6161
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
62-
res, err = issues_model.FetchCodeComments(db.DefaultContext, issue, user2, false)
62+
res, err = issues_model.FetchCodeConversations(db.DefaultContext, issue, user2, false)
6363
assert.NoError(t, err)
6464
assert.Len(t, res, 1)
6565
}

routers/web/repo/pull_review.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ func UpdateResolveConversation(ctx *context.Context) {
153153
}
154154

155155
func renderConversation(ctx *context.Context, comment *issues_model.Comment, origin string) {
156-
comments, err := issues_model.FetchCodeCommentsByLine(ctx, comment.Issue, ctx.Doer, comment.TreePath, comment.Line, true)
156+
comments, err := issues_model.FetchCodeConversation(ctx, comment, ctx.Doer)
157157
if err != nil {
158158
ctx.ServerError("FetchCodeCommentsByLine", err)
159159
return

services/gitdiff/gitdiff.go

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
"html/template"
1414
"io"
1515
"net/url"
16-
"sort"
1716
"strings"
1817
"time"
1918

@@ -75,13 +74,13 @@ const (
7574

7675
// DiffLine represents a line difference in a DiffSection.
7776
type DiffLine struct {
78-
LeftIdx int
79-
RightIdx int
80-
Match int
81-
Type DiffLineType
82-
Content string
83-
Comments []*issues_model.Comment
84-
SectionInfo *DiffLineSectionInfo
77+
LeftIdx int
78+
RightIdx int
79+
Match int
80+
Type DiffLineType
81+
Content string
82+
Conversations []issues_model.CodeConversation
83+
SectionInfo *DiffLineSectionInfo
8584
}
8685

8786
// DiffLineSectionInfo represents diff line section meta data
@@ -118,15 +117,15 @@ func (d *DiffLine) GetHTMLDiffLineType() string {
118117

119118
// CanComment returns whether a line can get commented
120119
func (d *DiffLine) CanComment() bool {
121-
return len(d.Comments) == 0 && d.Type != DiffLineSection
120+
return len(d.Conversations) == 0 && d.Type != DiffLineSection
122121
}
123122

124123
// GetCommentSide returns the comment side of the first comment, if not set returns empty string
125124
func (d *DiffLine) GetCommentSide() string {
126-
if len(d.Comments) == 0 {
125+
if len(d.Conversations) == 0 || len(d.Conversations[0]) == 0 {
127126
return ""
128127
}
129-
return d.Comments[0].DiffSide()
128+
return d.Conversations[0][0].DiffSide()
130129
}
131130

132131
// GetLineTypeMarker returns the line type marker
@@ -467,23 +466,20 @@ type Diff struct {
467466

468467
// LoadComments loads comments into each line
469468
func (diff *Diff) LoadComments(ctx context.Context, issue *issues_model.Issue, currentUser *user_model.User, showOutdatedComments bool) error {
470-
allComments, err := issues_model.FetchCodeComments(ctx, issue, currentUser, showOutdatedComments)
469+
allConversations, err := issues_model.FetchCodeConversations(ctx, issue, currentUser, showOutdatedComments)
471470
if err != nil {
472471
return err
473472
}
474473
for _, file := range diff.Files {
475-
if lineCommits, ok := allComments[file.Name]; ok {
474+
if lineCommits, ok := allConversations[file.Name]; ok {
476475
for _, section := range file.Sections {
477476
for _, line := range section.Lines {
478-
if comments, ok := lineCommits[int64(line.LeftIdx*-1)]; ok {
479-
line.Comments = append(line.Comments, comments...)
477+
if conversations, ok := lineCommits[int64(line.LeftIdx*-1)]; ok {
478+
line.Conversations = append(line.Conversations, conversations...)
480479
}
481480
if comments, ok := lineCommits[int64(line.RightIdx)]; ok {
482-
line.Comments = append(line.Comments, comments...)
481+
line.Conversations = append(line.Conversations, comments...)
483482
}
484-
sort.SliceStable(line.Comments, func(i, j int) bool {
485-
return line.Comments[i].CreatedUnix < line.Comments[j].CreatedUnix
486-
})
487483
}
488484
}
489485
}

services/gitdiff/gitdiff_test.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -601,7 +601,7 @@ func TestDiff_LoadCommentsNoOutdated(t *testing.T) {
601601
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
602602
diff := setupDefaultDiff()
603603
assert.NoError(t, diff.LoadComments(db.DefaultContext, issue, user, false))
604-
assert.Len(t, diff.Files[0].Sections[0].Lines[0].Comments, 2)
604+
assert.Len(t, diff.Files[0].Sections[0].Lines[0].Conversations, 2)
605605
}
606606

607607
func TestDiff_LoadCommentsWithOutdated(t *testing.T) {
@@ -611,20 +611,22 @@ func TestDiff_LoadCommentsWithOutdated(t *testing.T) {
611611
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
612612
diff := setupDefaultDiff()
613613
assert.NoError(t, diff.LoadComments(db.DefaultContext, issue, user, true))
614-
assert.Len(t, diff.Files[0].Sections[0].Lines[0].Comments, 3)
614+
assert.Len(t, diff.Files[0].Sections[0].Lines[0].Conversations, 2)
615+
assert.Len(t, diff.Files[0].Sections[0].Lines[0].Conversations[0], 2)
616+
assert.Len(t, diff.Files[0].Sections[0].Lines[0].Conversations[1], 1)
615617
}
616618

617619
func TestDiffLine_CanComment(t *testing.T) {
618620
assert.False(t, (&DiffLine{Type: DiffLineSection}).CanComment())
619-
assert.False(t, (&DiffLine{Type: DiffLineAdd, Comments: []*issues_model.Comment{{Content: "bla"}}}).CanComment())
621+
assert.False(t, (&DiffLine{Type: DiffLineAdd, Conversations: []issues_model.CodeConversation{{{Content: "bla"}}}}).CanComment())
620622
assert.True(t, (&DiffLine{Type: DiffLineAdd}).CanComment())
621623
assert.True(t, (&DiffLine{Type: DiffLineDel}).CanComment())
622624
assert.True(t, (&DiffLine{Type: DiffLinePlain}).CanComment())
623625
}
624626

625627
func TestDiffLine_GetCommentSide(t *testing.T) {
626-
assert.Equal(t, "previous", (&DiffLine{Comments: []*issues_model.Comment{{Line: -3}}}).GetCommentSide())
627-
assert.Equal(t, "proposed", (&DiffLine{Comments: []*issues_model.Comment{{Line: 3}}}).GetCommentSide())
628+
assert.Equal(t, "previous", (&DiffLine{Conversations: []issues_model.CodeConversation{{{Line: -3}}}}).GetCommentSide())
629+
assert.Equal(t, "proposed", (&DiffLine{Conversations: []issues_model.CodeConversation{{{Line: 3}}}}).GetCommentSide())
628630
}
629631

630632
func TestGetDiffRangeWithWhitespaceBehavior(t *testing.T) {

services/repository/files/diff_test.go

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,11 @@ func TestGetDiffPreview(t *testing.T) {
5353
Name: "",
5454
Lines: []*gitdiff.DiffLine{
5555
{
56-
LeftIdx: 0,
57-
RightIdx: 0,
58-
Type: 4,
59-
Content: "@@ -1,3 +1,4 @@",
60-
Comments: nil,
56+
LeftIdx: 0,
57+
RightIdx: 0,
58+
Type: 4,
59+
Content: "@@ -1,3 +1,4 @@",
60+
Conversations: nil,
6161
SectionInfo: &gitdiff.DiffLineSectionInfo{
6262
Path: "README.md",
6363
LastLeftIdx: 0,
@@ -69,42 +69,42 @@ func TestGetDiffPreview(t *testing.T) {
6969
},
7070
},
7171
{
72-
LeftIdx: 1,
73-
RightIdx: 1,
74-
Type: 1,
75-
Content: " # repo1",
76-
Comments: nil,
72+
LeftIdx: 1,
73+
RightIdx: 1,
74+
Type: 1,
75+
Content: " # repo1",
76+
Conversations: nil,
7777
},
7878
{
79-
LeftIdx: 2,
80-
RightIdx: 2,
81-
Type: 1,
82-
Content: " ",
83-
Comments: nil,
79+
LeftIdx: 2,
80+
RightIdx: 2,
81+
Type: 1,
82+
Content: " ",
83+
Conversations: nil,
8484
},
8585
{
86-
LeftIdx: 3,
87-
RightIdx: 0,
88-
Match: 4,
89-
Type: 3,
90-
Content: "-Description for repo1",
91-
Comments: nil,
86+
LeftIdx: 3,
87+
RightIdx: 0,
88+
Match: 4,
89+
Type: 3,
90+
Content: "-Description for repo1",
91+
Conversations: nil,
9292
},
9393
{
94-
LeftIdx: 0,
95-
RightIdx: 3,
96-
Match: 3,
97-
Type: 2,
98-
Content: "+Description for repo1",
99-
Comments: nil,
94+
LeftIdx: 0,
95+
RightIdx: 3,
96+
Match: 3,
97+
Type: 2,
98+
Content: "+Description for repo1",
99+
Conversations: nil,
100100
},
101101
{
102-
LeftIdx: 0,
103-
RightIdx: 4,
104-
Match: -1,
105-
Type: 2,
106-
Content: "+this is a new line",
107-
Comments: nil,
102+
LeftIdx: 0,
103+
RightIdx: 4,
104+
Match: -1,
105+
Type: 2,
106+
Content: "+this is a new line",
107+
Conversations: nil,
108108
},
109109
},
110110
},
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{{range .conversations}}
2+
{{template "repo/diff/conversation" dict "." $ "comments" .}}
3+
{{end}}

0 commit comments

Comments
 (0)