Merge pull request 'feat: combine review requests comments' (#5695) from gusted/forgejo-combine-request-review into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5695
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
This commit is contained in:
Earl Warren 2024-10-27 04:49:23 +00:00
commit 0e0a153adb
9 changed files with 685 additions and 65 deletions

View file

@ -1825,6 +1825,7 @@ func ViewIssue(ctx *context.Context) {
// Combine multiple label assignments into a single comment
combineLabelComments(issue)
combineRequestReviewComments(issue)
getBranchData(ctx, issue)
if issue.IsPull {
@ -3665,6 +3666,127 @@ func attachmentsHTML(ctx *context.Context, attachments []*repo_model.Attachment,
return attachHTML
}
type RequestReviewTarget struct {
user *user_model.User
team *organization.Team
}
func (t *RequestReviewTarget) ID() int64 {
if t.user != nil {
return t.user.ID
}
return t.team.ID
}
func (t *RequestReviewTarget) Name() string {
if t.user != nil {
return t.user.GetDisplayName()
}
return t.team.Name
}
func (t *RequestReviewTarget) Type() string {
if t.user != nil {
return "user"
}
return "team"
}
// combineRequestReviewComments combine the nearby request review comments as one.
func combineRequestReviewComments(issue *issues_model.Issue) {
var prev, cur *issues_model.Comment
for i := 0; i < len(issue.Comments); i++ {
cur = issue.Comments[i]
if i > 0 {
prev = issue.Comments[i-1]
}
if i == 0 || cur.Type != issues_model.CommentTypeReviewRequest ||
(prev != nil && prev.PosterID != cur.PosterID) ||
(prev != nil && cur.CreatedUnix-prev.CreatedUnix >= 60) {
if cur.Type == issues_model.CommentTypeReviewRequest && (cur.Assignee != nil || cur.AssigneeTeam != nil) {
if cur.RemovedAssignee {
if cur.AssigneeTeam != nil {
cur.RemovedRequestReview = append(cur.RemovedRequestReview, &RequestReviewTarget{team: cur.AssigneeTeam})
} else {
cur.RemovedRequestReview = append(cur.RemovedRequestReview, &RequestReviewTarget{user: cur.Assignee})
}
} else {
if cur.AssigneeTeam != nil {
cur.AddedRequestReview = append(cur.AddedRequestReview, &RequestReviewTarget{team: cur.AssigneeTeam})
} else {
cur.AddedRequestReview = append(cur.AddedRequestReview, &RequestReviewTarget{user: cur.Assignee})
}
}
}
continue
}
// Previous comment is not a review request, so cannot group. Start a new group.
if prev.Type != issues_model.CommentTypeReviewRequest {
if cur.RemovedAssignee {
if cur.AssigneeTeam != nil {
cur.RemovedRequestReview = append(cur.RemovedRequestReview, &RequestReviewTarget{team: cur.AssigneeTeam})
} else {
cur.RemovedRequestReview = append(cur.RemovedRequestReview, &RequestReviewTarget{user: cur.Assignee})
}
} else {
if cur.AssigneeTeam != nil {
cur.AddedRequestReview = append(cur.AddedRequestReview, &RequestReviewTarget{team: cur.AssigneeTeam})
} else {
cur.AddedRequestReview = append(cur.AddedRequestReview, &RequestReviewTarget{user: cur.Assignee})
}
}
continue
}
// Start grouping.
if cur.RemovedAssignee {
addedIndex := slices.IndexFunc(prev.AddedRequestReview, func(t issues_model.RequestReviewTarget) bool {
if cur.AssigneeTeam != nil {
return cur.AssigneeTeam.ID == t.ID() && t.Type() == "team"
}
return cur.Assignee.ID == t.ID() && t.Type() == "user"
})
// If for this target a AddedRequestReview, then we remove that entry. If it's not found, then add it to the RemovedRequestReview.
if addedIndex == -1 {
if cur.AssigneeTeam != nil {
prev.RemovedRequestReview = append(prev.RemovedRequestReview, &RequestReviewTarget{team: cur.AssigneeTeam})
} else {
prev.RemovedRequestReview = append(prev.RemovedRequestReview, &RequestReviewTarget{user: cur.Assignee})
}
} else {
prev.AddedRequestReview = slices.Delete(prev.AddedRequestReview, addedIndex, addedIndex+1)
}
} else {
removedIndex := slices.IndexFunc(prev.RemovedRequestReview, func(t issues_model.RequestReviewTarget) bool {
if cur.AssigneeTeam != nil {
return cur.AssigneeTeam.ID == t.ID() && t.Type() == "team"
}
return cur.Assignee.ID == t.ID() && t.Type() == "user"
})
// If for this target a RemovedRequestReview, then we remove that entry. If it's not found, then add it to the AddedRequestReview.
if removedIndex == -1 {
if cur.AssigneeTeam != nil {
prev.AddedRequestReview = append(prev.AddedRequestReview, &RequestReviewTarget{team: cur.AssigneeTeam})
} else {
prev.AddedRequestReview = append(prev.AddedRequestReview, &RequestReviewTarget{user: cur.Assignee})
}
} else {
prev.RemovedRequestReview = slices.Delete(prev.RemovedRequestReview, removedIndex, removedIndex+1)
}
}
// Propoagate creation time.
prev.CreatedUnix = cur.CreatedUnix
// Remove the current comment since it has been combined to prev comment
issue.Comments = append(issue.Comments[:i], issue.Comments[i+1:]...)
i--
}
}
// combineLabelComments combine the nearby label comments as one.
func combineLabelComments(issue *issues_model.Issue) {
var prev, cur *issues_model.Comment

View file

@ -7,6 +7,8 @@ import (
"testing"
issues_model "code.gitea.io/gitea/models/issues"
org_model "code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user"
"github.com/stretchr/testify/assert"
)
@ -373,3 +375,432 @@ func TestCombineLabelComments(t *testing.T) {
})
}
}
func TestCombineReviewRequests(t *testing.T) {
testCases := []struct {
name string
beforeCombined []*issues_model.Comment
afterCombined []*issues_model.Comment
}{
{
name: "case 1",
beforeCombined: []*issues_model.Comment{
{
Type: issues_model.CommentTypeReviewRequest,
PosterID: 1,
Assignee: &user_model.User{
ID: 1,
Name: "Ghost",
},
CreatedUnix: 0,
},
{
Type: issues_model.CommentTypeReviewRequest,
PosterID: 1,
RemovedAssignee: true,
Assignee: &user_model.User{
ID: 1,
Name: "Ghost",
},
CreatedUnix: 0,
},
{
Type: issues_model.CommentTypeComment,
PosterID: 1,
Content: "test",
CreatedUnix: 0,
},
},
afterCombined: []*issues_model.Comment{
{
Type: issues_model.CommentTypeReviewRequest,
PosterID: 1,
CreatedUnix: 0,
AddedRequestReview: []issues_model.RequestReviewTarget{},
Assignee: &user_model.User{
ID: 1,
Name: "Ghost",
},
},
{
Type: issues_model.CommentTypeComment,
PosterID: 1,
Content: "test",
CreatedUnix: 0,
},
},
},
{
name: "case 2",
beforeCombined: []*issues_model.Comment{
{
Type: issues_model.CommentTypeReviewRequest,
PosterID: 1,
Assignee: &user_model.User{
ID: 1,
Name: "Ghost",
},
CreatedUnix: 0,
},
{
Type: issues_model.CommentTypeReviewRequest,
PosterID: 1,
Assignee: &user_model.User{
ID: 2,
Name: "Ghost 2",
},
CreatedUnix: 0,
},
},
afterCombined: []*issues_model.Comment{
{
Type: issues_model.CommentTypeReviewRequest,
PosterID: 1,
CreatedUnix: 0,
AddedRequestReview: []issues_model.RequestReviewTarget{
&RequestReviewTarget{
user: &user_model.User{
ID: 1,
Name: "Ghost",
},
},
&RequestReviewTarget{
user: &user_model.User{
ID: 2,
Name: "Ghost 2",
},
},
},
Assignee: &user_model.User{
ID: 1,
Name: "Ghost",
},
},
},
},
{
name: "case 3",
beforeCombined: []*issues_model.Comment{
{
Type: issues_model.CommentTypeReviewRequest,
PosterID: 1,
Assignee: &user_model.User{
ID: 1,
Name: "Ghost",
},
CreatedUnix: 0,
},
{
Type: issues_model.CommentTypeReviewRequest,
PosterID: 1,
RemovedAssignee: true,
AssigneeTeam: &org_model.Team{
ID: 1,
Name: "Team 1",
},
CreatedUnix: 0,
},
},
afterCombined: []*issues_model.Comment{
{
Type: issues_model.CommentTypeReviewRequest,
PosterID: 1,
CreatedUnix: 0,
AddedRequestReview: []issues_model.RequestReviewTarget{
&RequestReviewTarget{
user: &user_model.User{
ID: 1,
Name: "Ghost",
},
},
},
RemovedRequestReview: []issues_model.RequestReviewTarget{
&RequestReviewTarget{
team: &org_model.Team{
ID: 1,
Name: "Team 1",
},
},
},
Assignee: &user_model.User{
ID: 1,
Name: "Ghost",
},
},
},
},
{
name: "case 4",
beforeCombined: []*issues_model.Comment{
{
Type: issues_model.CommentTypeReviewRequest,
PosterID: 1,
Assignee: &user_model.User{
ID: 1,
Name: "Ghost",
},
CreatedUnix: 0,
},
{
Type: issues_model.CommentTypeReviewRequest,
PosterID: 1,
RemovedAssignee: true,
AssigneeTeam: &org_model.Team{
ID: 1,
Name: "Team 1",
},
CreatedUnix: 0,
},
{
Type: issues_model.CommentTypeReviewRequest,
PosterID: 1,
AssigneeTeam: &org_model.Team{
ID: 1,
Name: "Team 1",
},
CreatedUnix: 0,
},
},
afterCombined: []*issues_model.Comment{
{
Type: issues_model.CommentTypeReviewRequest,
PosterID: 1,
CreatedUnix: 0,
AddedRequestReview: []issues_model.RequestReviewTarget{
&RequestReviewTarget{
user: &user_model.User{
ID: 1,
Name: "Ghost",
},
},
},
RemovedRequestReview: []issues_model.RequestReviewTarget{},
Assignee: &user_model.User{
ID: 1,
Name: "Ghost",
},
},
},
},
{
name: "case 5",
beforeCombined: []*issues_model.Comment{
{
Type: issues_model.CommentTypeReviewRequest,
PosterID: 1,
Assignee: &user_model.User{
ID: 1,
Name: "Ghost",
},
CreatedUnix: 0,
},
{
Type: issues_model.CommentTypeReviewRequest,
PosterID: 1,
RemovedAssignee: true,
AssigneeTeam: &org_model.Team{
ID: 1,
Name: "Team 1",
},
CreatedUnix: 0,
},
{
Type: issues_model.CommentTypeReviewRequest,
PosterID: 1,
AssigneeTeam: &org_model.Team{
ID: 1,
Name: "Team 1",
},
CreatedUnix: 0,
},
{
Type: issues_model.CommentTypeReviewRequest,
PosterID: 1,
RemovedAssignee: true,
Assignee: &user_model.User{
ID: 1,
Name: "Ghost",
},
CreatedUnix: 0,
},
},
afterCombined: []*issues_model.Comment{
{
Type: issues_model.CommentTypeReviewRequest,
PosterID: 1,
CreatedUnix: 0,
AddedRequestReview: []issues_model.RequestReviewTarget{},
RemovedRequestReview: []issues_model.RequestReviewTarget{},
Assignee: &user_model.User{
ID: 1,
Name: "Ghost",
},
},
},
},
{
name: "case 6",
beforeCombined: []*issues_model.Comment{
{
Type: issues_model.CommentTypeReviewRequest,
PosterID: 1,
Assignee: &user_model.User{
ID: 1,
Name: "Ghost",
},
CreatedUnix: 0,
},
{
Type: issues_model.CommentTypeReviewRequest,
PosterID: 1,
RemovedAssignee: true,
AssigneeTeam: &org_model.Team{
ID: 1,
Name: "Team 1",
},
CreatedUnix: 0,
},
{
Type: issues_model.CommentTypeComment,
PosterID: 1,
Content: "test",
CreatedUnix: 0,
},
{
Type: issues_model.CommentTypeReviewRequest,
PosterID: 1,
AssigneeTeam: &org_model.Team{
ID: 1,
Name: "Team 1",
},
CreatedUnix: 0,
},
{
Type: issues_model.CommentTypeReviewRequest,
PosterID: 1,
RemovedAssignee: true,
Assignee: &user_model.User{
ID: 1,
Name: "Ghost",
},
CreatedUnix: 0,
},
},
afterCombined: []*issues_model.Comment{
{
Type: issues_model.CommentTypeReviewRequest,
PosterID: 1,
CreatedUnix: 0,
RemovedRequestReview: []issues_model.RequestReviewTarget{&RequestReviewTarget{
team: &org_model.Team{
ID: 1,
Name: "Team 1",
},
}},
AddedRequestReview: []issues_model.RequestReviewTarget{&RequestReviewTarget{
user: &user_model.User{
ID: 1,
Name: "Ghost",
},
}},
Assignee: &user_model.User{
ID: 1,
Name: "Ghost",
},
},
{
Type: issues_model.CommentTypeComment,
PosterID: 1,
Content: "test",
CreatedUnix: 0,
},
{
Type: issues_model.CommentTypeReviewRequest,
PosterID: 1,
CreatedUnix: 0,
AddedRequestReview: []issues_model.RequestReviewTarget{&RequestReviewTarget{
team: &org_model.Team{
ID: 1,
Name: "Team 1",
},
}},
RemovedRequestReview: []issues_model.RequestReviewTarget{&RequestReviewTarget{
user: &user_model.User{
ID: 1,
Name: "Ghost",
},
}},
AssigneeTeam: &org_model.Team{
ID: 1,
Name: "Team 1",
},
},
},
},
{
name: "case 7",
beforeCombined: []*issues_model.Comment{
{
Type: issues_model.CommentTypeReviewRequest,
PosterID: 1,
Assignee: &user_model.User{
ID: 1,
Name: "Ghost",
},
CreatedUnix: 0,
},
{
Type: issues_model.CommentTypeReviewRequest,
PosterID: 1,
AssigneeTeam: &org_model.Team{
ID: 1,
Name: "Team 1",
},
CreatedUnix: 61,
},
},
afterCombined: []*issues_model.Comment{
{
Type: issues_model.CommentTypeReviewRequest,
PosterID: 1,
CreatedUnix: 0,
AddedRequestReview: []issues_model.RequestReviewTarget{&RequestReviewTarget{
user: &user_model.User{
ID: 1,
Name: "Ghost",
},
}},
Assignee: &user_model.User{
ID: 1,
Name: "Ghost",
},
},
{
Type: issues_model.CommentTypeReviewRequest,
PosterID: 1,
CreatedUnix: 0,
RemovedRequestReview: []issues_model.RequestReviewTarget{&RequestReviewTarget{
team: &org_model.Team{
ID: 1,
Name: "Team 1",
},
}},
AssigneeTeam: &org_model.Team{
ID: 1,
Name: "Team 1",
},
},
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
issue := issues_model.Issue{
Comments: testCase.beforeCombined,
}
combineRequestReviewComments(&issue)
assert.EqualValues(t, testCase.afterCombined[0], issue.Comments[0])
})
}
}