mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-14 05:56:14 +01:00
Merge pull request '[BUG] split code conversations in diff tab' (#2362) from oliverpool/forgejo:bg2306 into v1.21/forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/2362 Reviewed-by: Gusted <gusted@noreply.codeberg.org>
This commit is contained in:
commit
8283305d53
11 changed files with 348 additions and 144 deletions
|
@ -14,15 +14,58 @@ import (
|
|||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// CodeConversation contains the comment of a given review
|
||||
type CodeConversation []*Comment
|
||||
|
||||
// CodeConversationsAtLine contains the conversations for a given line
|
||||
type CodeConversationsAtLine map[int64][]CodeConversation
|
||||
|
||||
// CodeConversationsAtLineAndTreePath contains the conversations for a given TreePath and line
|
||||
type CodeConversationsAtLineAndTreePath map[string]CodeConversationsAtLine
|
||||
|
||||
func newCodeConversationsAtLineAndTreePath(comments []*Comment) CodeConversationsAtLineAndTreePath {
|
||||
tree := make(CodeConversationsAtLineAndTreePath)
|
||||
for _, comment := range comments {
|
||||
tree.insertComment(comment)
|
||||
}
|
||||
return tree
|
||||
}
|
||||
|
||||
func (tree CodeConversationsAtLineAndTreePath) insertComment(comment *Comment) {
|
||||
// attempt to append comment to existing conversations (i.e. list of comments belonging to the same review)
|
||||
for i, conversation := range tree[comment.TreePath][comment.Line] {
|
||||
if conversation[0].ReviewID == comment.ReviewID {
|
||||
tree[comment.TreePath][comment.Line][i] = append(conversation, comment)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// no previous conversation was found at this line, create it
|
||||
if tree[comment.TreePath] == nil {
|
||||
tree[comment.TreePath] = make(map[int64][]CodeConversation)
|
||||
}
|
||||
|
||||
tree[comment.TreePath][comment.Line] = append(tree[comment.TreePath][comment.Line], CodeConversation{comment})
|
||||
}
|
||||
|
||||
// FetchCodeConversations will return a 2d-map: ["Path"]["Line"] = List of CodeConversation (one per review) for this line
|
||||
func FetchCodeConversations(ctx context.Context, issue *Issue, doer *user_model.User, showOutdatedComments bool) (CodeConversationsAtLineAndTreePath, error) {
|
||||
opts := FindCommentsOptions{
|
||||
Type: CommentTypeCode,
|
||||
IssueID: issue.ID,
|
||||
}
|
||||
comments, err := findCodeComments(ctx, opts, issue, doer, nil, showOutdatedComments)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newCodeConversationsAtLineAndTreePath(comments), nil
|
||||
}
|
||||
|
||||
// CodeComments represents comments on code by using this structure: FILENAME -> LINE (+ == proposed; - == previous) -> COMMENTS
|
||||
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 fetchCodeCommentsByReview(ctx context.Context, issue *Issue, currentUser *user_model.User, review *Review, showOutdatedComments bool) (CodeComments, error) {
|
||||
func fetchCodeCommentsByReview(ctx context.Context, issue *Issue, doer *user_model.User, review *Review, showOutdatedComments bool) (CodeComments, error) {
|
||||
pathToLineToComment := make(CodeComments)
|
||||
if review == nil {
|
||||
review = &Review{ID: 0}
|
||||
|
@ -33,7 +76,7 @@ func fetchCodeCommentsByReview(ctx context.Context, issue *Issue, currentUser *u
|
|||
ReviewID: review.ID,
|
||||
}
|
||||
|
||||
comments, err := findCodeComments(ctx, opts, issue, currentUser, review, showOutdatedComments)
|
||||
comments, err := findCodeComments(ctx, opts, issue, doer, review, showOutdatedComments)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -47,7 +90,7 @@ func fetchCodeCommentsByReview(ctx context.Context, issue *Issue, currentUser *u
|
|||
return pathToLineToComment, nil
|
||||
}
|
||||
|
||||
func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issue, currentUser *user_model.User, review *Review, showOutdatedComments bool) ([]*Comment, error) {
|
||||
func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issue, doer *user_model.User, review *Review, showOutdatedComments bool) ([]*Comment, error) {
|
||||
var comments CommentList
|
||||
if review == nil {
|
||||
review = &Review{ID: 0}
|
||||
|
@ -91,7 +134,7 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu
|
|||
if re, ok := reviews[comment.ReviewID]; ok && re != nil {
|
||||
// If the review is pending only the author can see the comments (except if the review is set)
|
||||
if review.ID == 0 && re.Type == ReviewTypePending &&
|
||||
(currentUser == nil || currentUser.ID != re.ReviewerID) {
|
||||
(doer == nil || doer.ID != re.ReviewerID) {
|
||||
continue
|
||||
}
|
||||
comment.Review = re
|
||||
|
@ -121,13 +164,14 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu
|
|||
return comments[:n], nil
|
||||
}
|
||||
|
||||
// FetchCodeCommentsByLine fetches the code comments for a given treePath and line number
|
||||
func FetchCodeCommentsByLine(ctx context.Context, issue *Issue, currentUser *user_model.User, treePath string, line int64, showOutdatedComments bool) ([]*Comment, error) {
|
||||
// FetchCodeConversation fetches the code conversation of a given comment (same review, treePath and line number)
|
||||
func FetchCodeConversation(ctx context.Context, comment *Comment, doer *user_model.User) ([]*Comment, error) {
|
||||
opts := FindCommentsOptions{
|
||||
Type: CommentTypeCode,
|
||||
IssueID: issue.ID,
|
||||
TreePath: treePath,
|
||||
Line: line,
|
||||
IssueID: comment.IssueID,
|
||||
ReviewID: comment.ReviewID,
|
||||
TreePath: comment.TreePath,
|
||||
Line: comment.Line,
|
||||
}
|
||||
return findCodeComments(ctx, opts, issue, currentUser, nil, showOutdatedComments)
|
||||
return findCodeComments(ctx, opts, comment.Issue, doer, nil, true)
|
||||
}
|
||||
|
|
|
@ -45,20 +45,20 @@ func TestCreateComment(t *testing.T) {
|
|||
unittest.AssertInt64InRange(t, now, then, int64(updatedIssue.UpdatedUnix))
|
||||
}
|
||||
|
||||
func TestFetchCodeComments(t *testing.T) {
|
||||
func TestFetchCodeConversations(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
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.FetchCodeConversations(db.DefaultContext, issue, user, false)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, res, "README.md")
|
||||
assert.Contains(t, res["README.md"], int64(4))
|
||||
assert.Len(t, res["README.md"][4], 1)
|
||||
assert.Equal(t, int64(4), res["README.md"][4][0].ID)
|
||||
assert.Equal(t, int64(4), res["README.md"][4][0][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.FetchCodeConversations(db.DefaultContext, issue, user2, false)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, res, 1)
|
||||
}
|
||||
|
|
|
@ -153,7 +153,7 @@ func UpdateResolveConversation(ctx *context.Context) {
|
|||
}
|
||||
|
||||
func renderConversation(ctx *context.Context, comment *issues_model.Comment, origin string) {
|
||||
comments, err := issues_model.FetchCodeCommentsByLine(ctx, comment.Issue, ctx.Doer, comment.TreePath, comment.Line, true)
|
||||
comments, err := issues_model.FetchCodeConversation(ctx, comment, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.ServerError("FetchCodeCommentsByLine", err)
|
||||
return
|
||||
|
|
|
@ -13,7 +13,6 @@ import (
|
|||
"html/template"
|
||||
"io"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -75,13 +74,13 @@ const (
|
|||
|
||||
// DiffLine represents a line difference in a DiffSection.
|
||||
type DiffLine struct {
|
||||
LeftIdx int
|
||||
RightIdx int
|
||||
Match int
|
||||
Type DiffLineType
|
||||
Content string
|
||||
Comments []*issues_model.Comment
|
||||
SectionInfo *DiffLineSectionInfo
|
||||
LeftIdx int
|
||||
RightIdx int
|
||||
Match int
|
||||
Type DiffLineType
|
||||
Content string
|
||||
Conversations []issues_model.CodeConversation
|
||||
SectionInfo *DiffLineSectionInfo
|
||||
}
|
||||
|
||||
// DiffLineSectionInfo represents diff line section meta data
|
||||
|
@ -118,15 +117,15 @@ func (d *DiffLine) GetHTMLDiffLineType() string {
|
|||
|
||||
// CanComment returns whether a line can get commented
|
||||
func (d *DiffLine) CanComment() bool {
|
||||
return len(d.Comments) == 0 && d.Type != DiffLineSection
|
||||
return len(d.Conversations) == 0 && d.Type != DiffLineSection
|
||||
}
|
||||
|
||||
// GetCommentSide returns the comment side of the first comment, if not set returns empty string
|
||||
func (d *DiffLine) GetCommentSide() string {
|
||||
if len(d.Comments) == 0 {
|
||||
if len(d.Conversations) == 0 || len(d.Conversations[0]) == 0 {
|
||||
return ""
|
||||
}
|
||||
return d.Comments[0].DiffSide()
|
||||
return d.Conversations[0][0].DiffSide()
|
||||
}
|
||||
|
||||
// GetLineTypeMarker returns the line type marker
|
||||
|
@ -467,23 +466,20 @@ 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)
|
||||
allConversations, err := issues_model.FetchCodeConversations(ctx, issue, currentUser, showOutdatedComments)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, file := range diff.Files {
|
||||
if lineCommits, ok := allComments[file.Name]; ok {
|
||||
if lineCommits, ok := allConversations[file.Name]; ok {
|
||||
for _, section := range file.Sections {
|
||||
for _, line := range section.Lines {
|
||||
if comments, ok := lineCommits[int64(line.LeftIdx*-1)]; ok {
|
||||
line.Comments = append(line.Comments, comments...)
|
||||
if conversations, ok := lineCommits[int64(line.LeftIdx*-1)]; ok {
|
||||
line.Conversations = append(line.Conversations, conversations...)
|
||||
}
|
||||
if comments, ok := lineCommits[int64(line.RightIdx)]; ok {
|
||||
line.Comments = append(line.Comments, comments...)
|
||||
line.Conversations = append(line.Conversations, comments...)
|
||||
}
|
||||
sort.SliceStable(line.Comments, func(i, j int) bool {
|
||||
return line.Comments[i].CreatedUnix < line.Comments[j].CreatedUnix
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -601,7 +601,7 @@ func TestDiff_LoadCommentsNoOutdated(t *testing.T) {
|
|||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
diff := setupDefaultDiff()
|
||||
assert.NoError(t, diff.LoadComments(db.DefaultContext, issue, user, false))
|
||||
assert.Len(t, diff.Files[0].Sections[0].Lines[0].Comments, 2)
|
||||
assert.Len(t, diff.Files[0].Sections[0].Lines[0].Conversations, 2)
|
||||
}
|
||||
|
||||
func TestDiff_LoadCommentsWithOutdated(t *testing.T) {
|
||||
|
@ -611,20 +611,22 @@ func TestDiff_LoadCommentsWithOutdated(t *testing.T) {
|
|||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
diff := setupDefaultDiff()
|
||||
assert.NoError(t, diff.LoadComments(db.DefaultContext, issue, user, true))
|
||||
assert.Len(t, diff.Files[0].Sections[0].Lines[0].Comments, 3)
|
||||
assert.Len(t, diff.Files[0].Sections[0].Lines[0].Conversations, 2)
|
||||
assert.Len(t, diff.Files[0].Sections[0].Lines[0].Conversations[0], 2)
|
||||
assert.Len(t, diff.Files[0].Sections[0].Lines[0].Conversations[1], 1)
|
||||
}
|
||||
|
||||
func TestDiffLine_CanComment(t *testing.T) {
|
||||
assert.False(t, (&DiffLine{Type: DiffLineSection}).CanComment())
|
||||
assert.False(t, (&DiffLine{Type: DiffLineAdd, Comments: []*issues_model.Comment{{Content: "bla"}}}).CanComment())
|
||||
assert.False(t, (&DiffLine{Type: DiffLineAdd, Conversations: []issues_model.CodeConversation{{{Content: "bla"}}}}).CanComment())
|
||||
assert.True(t, (&DiffLine{Type: DiffLineAdd}).CanComment())
|
||||
assert.True(t, (&DiffLine{Type: DiffLineDel}).CanComment())
|
||||
assert.True(t, (&DiffLine{Type: DiffLinePlain}).CanComment())
|
||||
}
|
||||
|
||||
func TestDiffLine_GetCommentSide(t *testing.T) {
|
||||
assert.Equal(t, "previous", (&DiffLine{Comments: []*issues_model.Comment{{Line: -3}}}).GetCommentSide())
|
||||
assert.Equal(t, "proposed", (&DiffLine{Comments: []*issues_model.Comment{{Line: 3}}}).GetCommentSide())
|
||||
assert.Equal(t, "previous", (&DiffLine{Conversations: []issues_model.CodeConversation{{{Line: -3}}}}).GetCommentSide())
|
||||
assert.Equal(t, "proposed", (&DiffLine{Conversations: []issues_model.CodeConversation{{{Line: 3}}}}).GetCommentSide())
|
||||
}
|
||||
|
||||
func TestGetDiffRangeWithWhitespaceBehavior(t *testing.T) {
|
||||
|
|
|
@ -53,11 +53,11 @@ func TestGetDiffPreview(t *testing.T) {
|
|||
Name: "",
|
||||
Lines: []*gitdiff.DiffLine{
|
||||
{
|
||||
LeftIdx: 0,
|
||||
RightIdx: 0,
|
||||
Type: 4,
|
||||
Content: "@@ -1,3 +1,4 @@",
|
||||
Comments: nil,
|
||||
LeftIdx: 0,
|
||||
RightIdx: 0,
|
||||
Type: 4,
|
||||
Content: "@@ -1,3 +1,4 @@",
|
||||
Conversations: nil,
|
||||
SectionInfo: &gitdiff.DiffLineSectionInfo{
|
||||
Path: "README.md",
|
||||
LastLeftIdx: 0,
|
||||
|
@ -69,42 +69,42 @@ func TestGetDiffPreview(t *testing.T) {
|
|||
},
|
||||
},
|
||||
{
|
||||
LeftIdx: 1,
|
||||
RightIdx: 1,
|
||||
Type: 1,
|
||||
Content: " # repo1",
|
||||
Comments: nil,
|
||||
LeftIdx: 1,
|
||||
RightIdx: 1,
|
||||
Type: 1,
|
||||
Content: " # repo1",
|
||||
Conversations: nil,
|
||||
},
|
||||
{
|
||||
LeftIdx: 2,
|
||||
RightIdx: 2,
|
||||
Type: 1,
|
||||
Content: " ",
|
||||
Comments: nil,
|
||||
LeftIdx: 2,
|
||||
RightIdx: 2,
|
||||
Type: 1,
|
||||
Content: " ",
|
||||
Conversations: nil,
|
||||
},
|
||||
{
|
||||
LeftIdx: 3,
|
||||
RightIdx: 0,
|
||||
Match: 4,
|
||||
Type: 3,
|
||||
Content: "-Description for repo1",
|
||||
Comments: nil,
|
||||
LeftIdx: 3,
|
||||
RightIdx: 0,
|
||||
Match: 4,
|
||||
Type: 3,
|
||||
Content: "-Description for repo1",
|
||||
Conversations: nil,
|
||||
},
|
||||
{
|
||||
LeftIdx: 0,
|
||||
RightIdx: 3,
|
||||
Match: 3,
|
||||
Type: 2,
|
||||
Content: "+Description for repo1",
|
||||
Comments: nil,
|
||||
LeftIdx: 0,
|
||||
RightIdx: 3,
|
||||
Match: 3,
|
||||
Type: 2,
|
||||
Content: "+Description for repo1",
|
||||
Conversations: nil,
|
||||
},
|
||||
{
|
||||
LeftIdx: 0,
|
||||
RightIdx: 4,
|
||||
Match: -1,
|
||||
Type: 2,
|
||||
Content: "+this is a new line",
|
||||
Comments: nil,
|
||||
LeftIdx: 0,
|
||||
RightIdx: 4,
|
||||
Match: -1,
|
||||
Type: 2,
|
||||
Content: "+this is a new line",
|
||||
Conversations: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
3
templates/repo/diff/conversations.tmpl
Normal file
3
templates/repo/diff/conversations.tmpl
Normal file
|
@ -0,0 +1,3 @@
|
|||
{{range .conversations}}
|
||||
{{template "repo/diff/conversation" dict "." $ "comments" .}}
|
||||
{{end}}
|
|
@ -108,44 +108,44 @@
|
|||
</tr>
|
||||
{{if and (eq .GetType 3) $hasmatch}}
|
||||
{{$match := index $section.Lines $line.Match}}
|
||||
{{if or $line.Comments $match.Comments}}
|
||||
{{if or $line.Conversations $match.Conversations}}
|
||||
<tr class="add-comment" data-line-type="{{.GetHTMLDiffLineType}}">
|
||||
<td class="add-comment-left" colspan="4">
|
||||
{{if $line.Comments}}
|
||||
{{if $line.Conversations}}
|
||||
{{if eq $line.GetCommentSide "previous"}}
|
||||
{{template "repo/diff/conversation" dict "." $.root "comments" $line.Comments}}
|
||||
{{template "repo/diff/conversations" dict "." $.root "conversations" $line.Conversations}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{if $match.Comments}}
|
||||
{{if $match.Conversations}}
|
||||
{{if eq $match.GetCommentSide "previous"}}
|
||||
{{template "repo/diff/conversation" dict "." $.root "comments" $match.Comments}}
|
||||
{{template "repo/diff/conversations" dict "." $.root "conversations" $match.Conversations}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
</td>
|
||||
<td class="add-comment-right" colspan="4">
|
||||
{{if $line.Comments}}
|
||||
{{if $line.Conversations}}
|
||||
{{if eq $line.GetCommentSide "proposed"}}
|
||||
{{template "repo/diff/conversation" dict "." $.root "comments" $line.Comments}}
|
||||
{{template "repo/diff/conversations" dict "." $.root "conversations" $line.Conversations}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{if $match.Comments}}
|
||||
{{if $match.Conversations}}
|
||||
{{if eq $match.GetCommentSide "proposed"}}
|
||||
{{template "repo/diff/conversation" dict "." $.root "comments" $match.Comments}}
|
||||
{{template "repo/diff/conversations" dict "." $.root "conversations" $match.Conversations}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{else if $line.Comments}}
|
||||
{{else if $line.Conversations}}
|
||||
<tr class="add-comment" data-line-type="{{.GetHTMLDiffLineType}}">
|
||||
<td class="add-comment-left" colspan="4">
|
||||
{{if eq $line.GetCommentSide "previous"}}
|
||||
{{template "repo/diff/conversation" dict "." $.root "comments" $line.Comments}}
|
||||
{{template "repo/diff/conversations" dict "." $.root "conversations" $line.Conversations}}
|
||||
{{end}}
|
||||
</td>
|
||||
<td class="add-comment-right" colspan="4">
|
||||
{{if eq $line.GetCommentSide "proposed"}}
|
||||
{{template "repo/diff/conversation" dict "." $.root "comments" $line.Comments}}
|
||||
{{template "repo/diff/conversations" dict "." $.root "conversations" $line.Conversations}}
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -60,10 +60,10 @@
|
|||
*/}}</td>
|
||||
{{end}}
|
||||
</tr>
|
||||
{{if $line.Comments}}
|
||||
{{if $line.Conversations}}
|
||||
<tr class="add-comment" data-line-type="{{.GetHTMLDiffLineType}}">
|
||||
<td class="add-comment-left add-comment-right" colspan="5">
|
||||
{{template "repo/diff/conversation" dict "." $.root "comments" $line.Comments}}
|
||||
{{template "repo/diff/conversations" dict "." $.root "conversations" $line.Conversations}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
{{$invalid := (index .comments 0).Invalidated}}
|
||||
{{$resolved := (index .comments 0).IsResolved}}
|
||||
{{$resolveDoer := (index .comments 0).ResolveDoer}}
|
||||
{{$hideByDefault := or $resolved (and $invalid (not .ShowOutdatedComments))}}
|
||||
{{$isNotPending := (not (eq (index .comments 0).Review.Type 0))}}
|
||||
<div class="ui segments conversation-holder">
|
||||
<div class="ui segment collapsible-comment-box gt-py-3 gt-df gt-ac gt-sb">
|
||||
|
@ -15,7 +14,7 @@
|
|||
</div>
|
||||
<div>
|
||||
{{if or $invalid $resolved}}
|
||||
<button id="show-outdated-{{(index .comments 0).ID}}" data-comment="{{(index .comments 0).ID}}" class="{{if not $hideByDefault}}gt-hidden {{end}}ui compact labeled button show-outdated gt-df gt-ac">
|
||||
<button id="show-outdated-{{(index .comments 0).ID}}" data-comment="{{(index .comments 0).ID}}" class="{{if not $resolved}}gt-hidden {{end}}ui compact labeled button show-outdated gt-df gt-ac">
|
||||
{{svg "octicon-unfold" 16 "gt-mr-3"}}
|
||||
{{if $resolved}}
|
||||
{{ctx.Locale.Tr "repo.issues.review.show_resolved"}}
|
||||
|
@ -23,7 +22,7 @@
|
|||
{{ctx.Locale.Tr "repo.issues.review.show_outdated"}}
|
||||
{{end}}
|
||||
</button>
|
||||
<button id="hide-outdated-{{(index .comments 0).ID}}" data-comment="{{(index .comments 0).ID}}" class="{{if $hideByDefault}}gt-hidden {{end}}ui compact labeled button hide-outdated gt-df gt-ac">
|
||||
<button id="hide-outdated-{{(index .comments 0).ID}}" data-comment="{{(index .comments 0).ID}}" class="{{if $resolved}}gt-hidden {{end}}ui compact labeled button hide-outdated gt-df gt-ac">
|
||||
{{svg "octicon-fold" 16 "gt-mr-3"}}
|
||||
{{if $resolved}}
|
||||
{{ctx.Locale.Tr "repo.issues.review.hide_resolved"}}
|
||||
|
@ -37,7 +36,7 @@
|
|||
{{$diff := (CommentMustAsDiff (index .comments 0))}}
|
||||
{{if $diff}}
|
||||
{{$file := (index $diff.Files 0)}}
|
||||
<div id="code-preview-{{(index .comments 0).ID}}" class="ui table segment{{if $hideByDefault}} gt-hidden{{end}}">
|
||||
<div id="code-preview-{{(index .comments 0).ID}}" class="ui table segment{{if $resolved}} gt-hidden{{end}}">
|
||||
<div class="diff-file-box diff-box file-content {{TabSizeClass $.Editorconfig $file.Name}}">
|
||||
<div class="file-body file-code code-view code-diff code-diff-unified unicode-escaped">
|
||||
<table>
|
||||
|
@ -49,7 +48,7 @@
|
|||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<div id="code-comments-{{(index .comments 0).ID}}" class="comment-code-cloud ui segment{{if $hideByDefault}} gt-hidden{{end}}">
|
||||
<div id="code-comments-{{(index .comments 0).ID}}" class="comment-code-cloud ui segment{{if $resolved}} gt-hidden{{end}}">
|
||||
<div class="ui comments gt-mb-0">
|
||||
{{range .comments}}
|
||||
{{$createdSubStr:= TimeSinceUnix .CreatedUnix ctx.Locale}}
|
||||
|
|
|
@ -10,8 +10,10 @@ import (
|
|||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
@ -26,6 +28,13 @@ func TestPullView_ReviewerMissed(t *testing.T) {
|
|||
session.MakeRequest(t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
func loadComment(t *testing.T, commentID string) *issues.Comment {
|
||||
t.Helper()
|
||||
id, err := strconv.ParseInt(commentID, 10, 64)
|
||||
assert.NoError(t, err)
|
||||
return unittest.AssertExistsAndLoadBean(t, &issues.Comment{ID: id})
|
||||
}
|
||||
|
||||
func TestPullView_ResolveInvalidatedReviewComment(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
session := loginUser(t, "user1")
|
||||
|
@ -33,60 +42,211 @@ func TestPullView_ResolveInvalidatedReviewComment(t *testing.T) {
|
|||
req := NewRequest(t, "GET", "/user2/repo1/pulls/3/files")
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
req = NewRequest(t, "GET", "/user2/repo1/pulls/3/files/reviews/new_comment")
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
t.Run("single outdated review (line 1)", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
req := NewRequest(t, "GET", "/user2/repo1/pulls/3/files/reviews/new_comment")
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
doc := NewHTMLParser(t, resp.Body)
|
||||
req = NewRequestWithValues(t, "POST", "/user2/repo1/pulls/3/files/reviews/comments", map[string]string{
|
||||
"_csrf": doc.GetInputValueByName("_csrf"),
|
||||
"origin": doc.GetInputValueByName("origin"),
|
||||
"latest_commit_id": doc.GetInputValueByName("latest_commit_id"),
|
||||
"side": "proposed",
|
||||
"line": "1",
|
||||
"path": "iso-8859-1.txt",
|
||||
"diff_start_cid": doc.GetInputValueByName("diff_start_cid"),
|
||||
"diff_end_cid": doc.GetInputValueByName("diff_end_cid"),
|
||||
"diff_base_cid": doc.GetInputValueByName("diff_base_cid"),
|
||||
"content": "nitpicking comment",
|
||||
"pending_review": "",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
doc := NewHTMLParser(t, resp.Body)
|
||||
req = NewRequestWithValues(t, "POST", "/user2/repo1/pulls/3/files/reviews/comments", map[string]string{
|
||||
"_csrf": doc.GetInputValueByName("_csrf"),
|
||||
"origin": doc.GetInputValueByName("origin"),
|
||||
"latest_commit_id": doc.GetInputValueByName("latest_commit_id"),
|
||||
"side": "proposed",
|
||||
"line": "1",
|
||||
"path": "iso-8859-1.txt",
|
||||
"diff_start_cid": doc.GetInputValueByName("diff_start_cid"),
|
||||
"diff_end_cid": doc.GetInputValueByName("diff_end_cid"),
|
||||
"diff_base_cid": doc.GetInputValueByName("diff_base_cid"),
|
||||
"content": "nitpicking comment",
|
||||
"pending_review": "",
|
||||
req = NewRequestWithValues(t, "POST", "/user2/repo1/pulls/3/files/reviews/submit", map[string]string{
|
||||
"_csrf": doc.GetInputValueByName("_csrf"),
|
||||
"commit_id": doc.GetInputValueByName("latest_commit_id"),
|
||||
"content": "looks good",
|
||||
"type": "comment",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// retrieve comment_id by reloading the comment page
|
||||
req = NewRequest(t, "GET", "/user2/repo1/pulls/3")
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
doc = NewHTMLParser(t, resp.Body)
|
||||
commentID, ok := doc.Find(`[data-action="Resolve"]`).Attr("data-comment-id")
|
||||
assert.True(t, ok)
|
||||
|
||||
// adjust the database to mark the comment as invalidated
|
||||
// (to invalidate it properly, one should push a commit which should trigger this logic,
|
||||
// in the meantime, use this quick-and-dirty trick)
|
||||
comment := loadComment(t, commentID)
|
||||
assert.NoError(t, issues.UpdateCommentInvalidate(context.Background(), &issues.Comment{
|
||||
ID: comment.ID,
|
||||
Invalidated: true,
|
||||
}))
|
||||
|
||||
req = NewRequestWithValues(t, "POST", "/user2/repo1/issues/resolve_conversation", map[string]string{
|
||||
"_csrf": doc.GetInputValueByName("_csrf"),
|
||||
"origin": "timeline",
|
||||
"action": "Resolve",
|
||||
"comment_id": commentID,
|
||||
})
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// even on template error, the page returns HTTP 200
|
||||
// count the comments to ensure success.
|
||||
doc = NewHTMLParser(t, resp.Body)
|
||||
assert.Len(t, doc.Find(`.comments > .comment`).Nodes, 1)
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
req = NewRequestWithValues(t, "POST", "/user2/repo1/pulls/3/files/reviews/submit", map[string]string{
|
||||
"_csrf": doc.GetInputValueByName("_csrf"),
|
||||
"commit_id": doc.GetInputValueByName("latest_commit_id"),
|
||||
"content": "looks good",
|
||||
"type": "comment",
|
||||
t.Run("outdated and newer review (line 2)", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
req := NewRequest(t, "GET", "/user2/repo1/pulls/3/files/reviews/new_comment")
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
doc := NewHTMLParser(t, resp.Body)
|
||||
|
||||
var firstReviewID int64
|
||||
{
|
||||
// first (outdated) review
|
||||
req = NewRequestWithValues(t, "POST", "/user2/repo1/pulls/3/files/reviews/comments", map[string]string{
|
||||
"_csrf": doc.GetInputValueByName("_csrf"),
|
||||
"origin": doc.GetInputValueByName("origin"),
|
||||
"latest_commit_id": doc.GetInputValueByName("latest_commit_id"),
|
||||
"side": "proposed",
|
||||
"line": "2",
|
||||
"path": "iso-8859-1.txt",
|
||||
"diff_start_cid": doc.GetInputValueByName("diff_start_cid"),
|
||||
"diff_end_cid": doc.GetInputValueByName("diff_end_cid"),
|
||||
"diff_base_cid": doc.GetInputValueByName("diff_base_cid"),
|
||||
"content": "nitpicking comment",
|
||||
"pending_review": "",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
req = NewRequestWithValues(t, "POST", "/user2/repo1/pulls/3/files/reviews/submit", map[string]string{
|
||||
"_csrf": doc.GetInputValueByName("_csrf"),
|
||||
"commit_id": doc.GetInputValueByName("latest_commit_id"),
|
||||
"content": "looks good",
|
||||
"type": "comment",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// retrieve comment_id by reloading the comment page
|
||||
req = NewRequest(t, "GET", "/user2/repo1/pulls/3")
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
doc = NewHTMLParser(t, resp.Body)
|
||||
commentID, ok := doc.Find(`[data-action="Resolve"]`).Attr("data-comment-id")
|
||||
assert.True(t, ok)
|
||||
|
||||
// adjust the database to mark the comment as invalidated
|
||||
// (to invalidate it properly, one should push a commit which should trigger this logic,
|
||||
// in the meantime, use this quick-and-dirty trick)
|
||||
comment := loadComment(t, commentID)
|
||||
assert.NoError(t, issues.UpdateCommentInvalidate(context.Background(), &issues.Comment{
|
||||
ID: comment.ID,
|
||||
Invalidated: true,
|
||||
}))
|
||||
firstReviewID = comment.ReviewID
|
||||
assert.NotZero(t, firstReviewID)
|
||||
}
|
||||
|
||||
// ID of the first comment for the second (up-to-date) review
|
||||
var commentID string
|
||||
|
||||
{
|
||||
// second (up-to-date) review on the same line
|
||||
// make a second review
|
||||
req = NewRequestWithValues(t, "POST", "/user2/repo1/pulls/3/files/reviews/comments", map[string]string{
|
||||
"_csrf": doc.GetInputValueByName("_csrf"),
|
||||
"origin": doc.GetInputValueByName("origin"),
|
||||
"latest_commit_id": doc.GetInputValueByName("latest_commit_id"),
|
||||
"side": "proposed",
|
||||
"line": "2",
|
||||
"path": "iso-8859-1.txt",
|
||||
"diff_start_cid": doc.GetInputValueByName("diff_start_cid"),
|
||||
"diff_end_cid": doc.GetInputValueByName("diff_end_cid"),
|
||||
"diff_base_cid": doc.GetInputValueByName("diff_base_cid"),
|
||||
"content": "nitpicking comment",
|
||||
"pending_review": "",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
req = NewRequestWithValues(t, "POST", "/user2/repo1/pulls/3/files/reviews/submit", map[string]string{
|
||||
"_csrf": doc.GetInputValueByName("_csrf"),
|
||||
"commit_id": doc.GetInputValueByName("latest_commit_id"),
|
||||
"content": "looks better",
|
||||
"type": "comment",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// retrieve comment_id by reloading the comment page
|
||||
req = NewRequest(t, "GET", "/user2/repo1/pulls/3")
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
doc = NewHTMLParser(t, resp.Body)
|
||||
|
||||
commentIDs := doc.Find(`[data-action="Resolve"]`).Map(func(i int, elt *goquery.Selection) string {
|
||||
v, _ := elt.Attr("data-comment-id")
|
||||
return v
|
||||
})
|
||||
assert.Len(t, commentIDs, 2) // 1 for the outdated review, 1 for the current review
|
||||
|
||||
// check that the first comment is for the previous review
|
||||
comment := loadComment(t, commentIDs[0])
|
||||
assert.Equal(t, comment.ReviewID, firstReviewID)
|
||||
|
||||
// check that the second comment is for a different review
|
||||
comment = loadComment(t, commentIDs[1])
|
||||
assert.NotZero(t, comment.ReviewID)
|
||||
assert.NotEqual(t, comment.ReviewID, firstReviewID)
|
||||
|
||||
commentID = commentIDs[1] // save commentID for later
|
||||
}
|
||||
|
||||
req = NewRequestWithValues(t, "POST", "/user2/repo1/issues/resolve_conversation", map[string]string{
|
||||
"_csrf": doc.GetInputValueByName("_csrf"),
|
||||
"origin": "timeline",
|
||||
"action": "Resolve",
|
||||
"comment_id": commentID,
|
||||
})
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// even on template error, the page returns HTTP 200
|
||||
// count the comments to ensure success.
|
||||
doc = NewHTMLParser(t, resp.Body)
|
||||
comments := doc.Find(`.comments > .comment`)
|
||||
assert.Len(t, comments.Nodes, 1) // the outdated comment belongs to another review and should not be shown
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// retrieve comment_id by reloading the comment page
|
||||
req = NewRequest(t, "GET", "/user2/repo1/pulls/3")
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
doc = NewHTMLParser(t, resp.Body)
|
||||
commentID, ok := doc.Find(`[data-action="Resolve"]`).Attr("data-comment-id")
|
||||
assert.True(t, ok)
|
||||
t.Run("Files Changed tab", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
for _, c := range []struct {
|
||||
style, outdated string
|
||||
expectedCount int
|
||||
}{
|
||||
{"unified", "true", 3}, // 1 comment on line 1 + 2 comments on line 3
|
||||
{"unified", "false", 1}, // 1 comment on line 3 is not outdated
|
||||
{"split", "true", 3}, // 1 comment on line 1 + 2 comments on line 3
|
||||
{"split", "false", 1}, // 1 comment on line 3 is not outdated
|
||||
} {
|
||||
t.Run(c.style+"+"+c.outdated, func(t *testing.T) {
|
||||
req := NewRequest(t, "GET", "/user2/repo1/pulls/3/files?style="+c.style+"&show-outdated="+c.outdated)
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// adjust the database to mark the comment as invalidated
|
||||
// (to invalidate it properly, one should push a commit which should trigger this logic,
|
||||
// in the meantime, use this quick-and-dirty trick)
|
||||
id, err := strconv.ParseInt(commentID, 10, 64)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, issues.UpdateCommentInvalidate(context.Background(), &issues.Comment{
|
||||
ID: id,
|
||||
Invalidated: true,
|
||||
}))
|
||||
|
||||
req = NewRequestWithValues(t, "POST", "/user2/repo1/issues/resolve_conversation", map[string]string{
|
||||
"_csrf": doc.GetInputValueByName("_csrf"),
|
||||
"origin": "timeline",
|
||||
"action": "Resolve",
|
||||
"comment_id": commentID,
|
||||
doc := NewHTMLParser(t, resp.Body)
|
||||
comments := doc.Find(`.comments > .comment`)
|
||||
assert.Len(t, comments.Nodes, c.expectedCount)
|
||||
})
|
||||
}
|
||||
})
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// even on template error, the page returns HTTP 200
|
||||
// search the button to mark the comment as unresolved to ensure success.
|
||||
doc = NewHTMLParser(t, resp.Body)
|
||||
assert.Len(t, doc.Find(`[data-action="UnResolve"][data-comment-id="`+commentID+`"]`).Nodes, 1)
|
||||
t.Run("Conversation tab", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
req := NewRequest(t, "GET", "/user2/repo1/pulls/3")
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
doc := NewHTMLParser(t, resp.Body)
|
||||
comments := doc.Find(`.comments > .comment`)
|
||||
assert.Len(t, comments.Nodes, 3) // 1 comment on line 1 + 2 comments on line 3
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue