mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-01-20 16:50:28 -05:00
Compare commits
4 commits
2fc217b921
...
233c3f4819
Author | SHA1 | Date | |
---|---|---|---|
|
233c3f4819 | ||
|
c993e4a157 | ||
|
8ae2e4b2ef | ||
|
faad7e0017 |
7 changed files with 1027 additions and 922 deletions
|
@ -114,6 +114,8 @@ const (
|
||||||
|
|
||||||
CommentTypePin // 36 pin Issue
|
CommentTypePin // 36 pin Issue
|
||||||
CommentTypeUnpin // 37 unpin Issue
|
CommentTypeUnpin // 37 unpin Issue
|
||||||
|
|
||||||
|
CommentTypeAggregator // 38 Aggregator of comments
|
||||||
)
|
)
|
||||||
|
|
||||||
var commentStrings = []string{
|
var commentStrings = []string{
|
||||||
|
@ -236,10 +238,30 @@ func (r RoleInRepo) LocaleHelper(lang translation.Locale) string {
|
||||||
return lang.TrString("repo.issues.role." + string(r) + "_helper")
|
return lang.TrString("repo.issues.role." + string(r) + "_helper")
|
||||||
}
|
}
|
||||||
|
|
||||||
type RequestReviewTarget interface {
|
type RequestReviewTarget struct {
|
||||||
ID() int64
|
User *user_model.User
|
||||||
Name() string
|
Team *organization.Team
|
||||||
Type() string
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Comment represents a comment in commit and issue page.
|
// Comment represents a comment in commit and issue page.
|
||||||
|
@ -254,6 +276,7 @@ type Comment struct {
|
||||||
Issue *Issue `xorm:"-"`
|
Issue *Issue `xorm:"-"`
|
||||||
LabelID int64
|
LabelID int64
|
||||||
Label *Label `xorm:"-"`
|
Label *Label `xorm:"-"`
|
||||||
|
Aggregator *CommentAggregator `xorm:"-"`
|
||||||
AddedLabels []*Label `xorm:"-"`
|
AddedLabels []*Label `xorm:"-"`
|
||||||
RemovedLabels []*Label `xorm:"-"`
|
RemovedLabels []*Label `xorm:"-"`
|
||||||
AddedRequestReview []RequestReviewTarget `xorm:"-"`
|
AddedRequestReview []RequestReviewTarget `xorm:"-"`
|
||||||
|
|
328
models/issues/comment_aggregator.go
Normal file
328
models/issues/comment_aggregator.go
Normal file
|
@ -0,0 +1,328 @@
|
||||||
|
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package issues
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CommentAggregator struct {
|
||||||
|
StartUnix int64
|
||||||
|
Indexes []int
|
||||||
|
PosterID int64
|
||||||
|
|
||||||
|
PrevClosed bool
|
||||||
|
IsClosed bool
|
||||||
|
|
||||||
|
AddedLabels []*Label
|
||||||
|
RemovedLabels []*Label
|
||||||
|
|
||||||
|
AddedRequestReview []RequestReviewTarget
|
||||||
|
RemovedRequestReview []RequestReviewTarget
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Aggregate also
|
||||||
|
// - Dependency added / removed
|
||||||
|
// - Added / Removed due date
|
||||||
|
// - Milestone Added / Removed
|
||||||
|
func (agg *CommentAggregator) aggregateComment(c *Comment, index int) {
|
||||||
|
agg.Indexes = append(agg.Indexes, index)
|
||||||
|
|
||||||
|
if c.Type == CommentTypeClose {
|
||||||
|
agg.IsClosed = true
|
||||||
|
} else if c.Type == CommentTypeReopen {
|
||||||
|
agg.IsClosed = false
|
||||||
|
} else if c.Type == CommentTypeReviewRequest {
|
||||||
|
if c.AssigneeID > 0 {
|
||||||
|
req := RequestReviewTarget{User: c.Assignee}
|
||||||
|
if c.RemovedAssignee {
|
||||||
|
agg.delReviewRequest(req)
|
||||||
|
} else {
|
||||||
|
agg.addReviewRequest(req)
|
||||||
|
}
|
||||||
|
} else if c.AssigneeTeamID > 0 {
|
||||||
|
req := RequestReviewTarget{Team: c.AssigneeTeam}
|
||||||
|
if c.RemovedAssignee {
|
||||||
|
agg.delReviewRequest(req)
|
||||||
|
} else {
|
||||||
|
agg.addReviewRequest(req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range c.RemovedRequestReview {
|
||||||
|
agg.delReviewRequest(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range c.AddedRequestReview {
|
||||||
|
agg.addReviewRequest(r)
|
||||||
|
}
|
||||||
|
} else if c.Type == CommentTypeLabel {
|
||||||
|
if c.Content == "1" {
|
||||||
|
agg.addLabel(c.Label)
|
||||||
|
} else {
|
||||||
|
agg.delLabel(c.Label)
|
||||||
|
}
|
||||||
|
} else if c.Type == CommentTypeAggregator {
|
||||||
|
agg.Merge(c.Aggregator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge a past CommentAggregator with the next one in the issue comments list
|
||||||
|
func (agg *CommentAggregator) Merge(next *CommentAggregator) {
|
||||||
|
agg.IsClosed = next.IsClosed
|
||||||
|
|
||||||
|
for _, l := range next.AddedLabels {
|
||||||
|
agg.addLabel(l)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, l := range next.RemovedLabels {
|
||||||
|
agg.delLabel(l)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range next.AddedRequestReview {
|
||||||
|
agg.addReviewRequest(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range next.RemovedRequestReview {
|
||||||
|
agg.delReviewRequest(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a comment can be aggregated or not depending on its type
|
||||||
|
func (agg *CommentAggregator) IsAggregated(t *CommentType) bool {
|
||||||
|
a := false
|
||||||
|
a = a || (*t == CommentTypeAggregator)
|
||||||
|
a = a || (*t == CommentTypeClose)
|
||||||
|
a = a || (*t == CommentTypeReopen)
|
||||||
|
a = a || (*t == CommentTypeLabel)
|
||||||
|
a = a || (*t == CommentTypeReviewRequest)
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a label to the aggregated list
|
||||||
|
func (agg *CommentAggregator) addLabel(lbl *Label) {
|
||||||
|
for l, agglbl := range agg.RemovedLabels {
|
||||||
|
if agglbl.ID == lbl.ID {
|
||||||
|
agg.RemovedLabels = append(agg.RemovedLabels[:l], agg.RemovedLabels[l+1:]...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !slices.ContainsFunc(agg.AddedLabels, func(l *Label) bool { return l.ID == lbl.ID }) {
|
||||||
|
agg.AddedLabels = append(agg.AddedLabels, lbl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove a label from the aggregated list
|
||||||
|
func (agg *CommentAggregator) delLabel(lbl *Label) {
|
||||||
|
for l, agglbl := range agg.AddedLabels {
|
||||||
|
if agglbl.ID == lbl.ID {
|
||||||
|
agg.AddedLabels = append(agg.AddedLabels[:l], agg.AddedLabels[l+1:]...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !slices.ContainsFunc(agg.RemovedLabels, func(l *Label) bool { return l.ID == lbl.ID }) {
|
||||||
|
agg.RemovedLabels = append(agg.RemovedLabels, lbl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a review request to the aggregated list
|
||||||
|
func (agg *CommentAggregator) addReviewRequest(req RequestReviewTarget) {
|
||||||
|
reqid := req.ID()
|
||||||
|
reqty := req.Type()
|
||||||
|
for r, aggreq := range agg.RemovedRequestReview {
|
||||||
|
if (aggreq.ID() == reqid) && (aggreq.Type() == reqty) {
|
||||||
|
agg.RemovedRequestReview = append(agg.RemovedRequestReview[:r], agg.RemovedRequestReview[r+1:]...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !slices.ContainsFunc(agg.AddedRequestReview, func(r RequestReviewTarget) bool { return (r.ID() == reqid) && (r.Type() == reqty) }) {
|
||||||
|
agg.AddedRequestReview = append(agg.AddedRequestReview, req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a review request from the aggregated list
|
||||||
|
func (agg *CommentAggregator) delReviewRequest(req RequestReviewTarget) {
|
||||||
|
reqid := req.ID()
|
||||||
|
reqty := req.Type()
|
||||||
|
for r, aggreq := range agg.AddedRequestReview {
|
||||||
|
if (aggreq.ID() == reqid) && (aggreq.Type() == reqty) {
|
||||||
|
agg.AddedRequestReview = append(agg.AddedRequestReview[:r], agg.AddedRequestReview[r+1:]...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !slices.ContainsFunc(agg.RemovedRequestReview, func(r RequestReviewTarget) bool { return (r.ID() == reqid) && (r.Type() == reqty) }) {
|
||||||
|
agg.RemovedRequestReview = append(agg.RemovedRequestReview, req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if anything has changed with this aggregated list of comments
|
||||||
|
func (agg *CommentAggregator) Changed() bool {
|
||||||
|
changed := false
|
||||||
|
changed = changed || (agg.IsClosed != agg.PrevClosed)
|
||||||
|
changed = changed || (len(agg.AddedLabels) > 0)
|
||||||
|
changed = changed || (len(agg.RemovedLabels) > 0)
|
||||||
|
changed = changed || (len(agg.AddedRequestReview) > 0)
|
||||||
|
changed = changed || (len(agg.RemovedRequestReview) > 0)
|
||||||
|
return changed
|
||||||
|
}
|
||||||
|
|
||||||
|
func (agg *CommentAggregator) OnlyLabelsChanged() bool {
|
||||||
|
l := (len(agg.AddedLabels) > 0) || (len(agg.RemovedLabels) > 0)
|
||||||
|
l = l && (len(agg.AddedRequestReview) == 0) && (len(agg.RemovedRequestReview) == 0)
|
||||||
|
l = l && (agg.PrevClosed == agg.IsClosed)
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (agg *CommentAggregator) OnlyRequestReview() bool {
|
||||||
|
l := (len(agg.AddedRequestReview) > 0) || (len(agg.RemovedRequestReview) > 0)
|
||||||
|
l = l && (len(agg.AddedLabels) == 0) && (len(agg.RemovedLabels) == 0)
|
||||||
|
l = l && (agg.PrevClosed == agg.IsClosed)
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (agg *CommentAggregator) OnlyClosedReopened() bool {
|
||||||
|
l := (agg.IsClosed != agg.PrevClosed)
|
||||||
|
l = l && (len(agg.AddedLabels) == 0) && (len(agg.RemovedLabels) == 0)
|
||||||
|
l = l && (len(agg.AddedRequestReview) == 0) && (len(agg.RemovedRequestReview) == 0)
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the aggregator to start a new aggregating context
|
||||||
|
func (agg *CommentAggregator) Reset(cur *Comment) {
|
||||||
|
agg.StartUnix = int64(cur.CreatedUnix)
|
||||||
|
agg.PosterID = cur.PosterID
|
||||||
|
|
||||||
|
agg.PrevClosed = agg.IsClosed
|
||||||
|
|
||||||
|
agg.Indexes = []int{}
|
||||||
|
agg.AddedLabels = []*Label{}
|
||||||
|
agg.RemovedLabels = []*Label{}
|
||||||
|
agg.AddedRequestReview = []RequestReviewTarget{}
|
||||||
|
agg.RemovedRequestReview = []RequestReviewTarget{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function that replaces all the comments aggregated with a single one
|
||||||
|
// Its CommentType depend on whether multiple type of comments are been aggregated or not
|
||||||
|
// If nothing has changed, we remove all the comments that get nullified
|
||||||
|
//
|
||||||
|
// The function returns how many comments has been removed, in order for the "for" loop
|
||||||
|
// of the main algorithm to change its index
|
||||||
|
func (agg *CommentAggregator) createAggregatedComment(issue *Issue, final bool) int {
|
||||||
|
endind := agg.Indexes[len(agg.Indexes)-1]
|
||||||
|
startind := agg.Indexes[0]
|
||||||
|
|
||||||
|
// If the aggregation of comments make the whole thing null, erase all the comments
|
||||||
|
if !agg.Changed() {
|
||||||
|
if final {
|
||||||
|
issue.Comments = issue.Comments[:startind]
|
||||||
|
} else {
|
||||||
|
issue.Comments = append(issue.Comments[:startind], issue.Comments[endind+1:]...)
|
||||||
|
}
|
||||||
|
return endind - startind
|
||||||
|
}
|
||||||
|
|
||||||
|
newAgg := *agg // Trigger a memory allocation, get a COPY of the aggregator
|
||||||
|
|
||||||
|
// Keep the same author, time, etc... But reset the parts we may want to use
|
||||||
|
comment := issue.Comments[startind]
|
||||||
|
comment.Content = ""
|
||||||
|
comment.Label = nil
|
||||||
|
comment.Aggregator = nil
|
||||||
|
comment.Assignee = nil
|
||||||
|
comment.AssigneeID = -1
|
||||||
|
comment.AssigneeTeam = nil
|
||||||
|
comment.AssigneeTeamID = -1
|
||||||
|
comment.RemovedAssignee = false
|
||||||
|
comment.AddedLabels = nil
|
||||||
|
comment.RemovedLabels = nil
|
||||||
|
|
||||||
|
// In case there's only a single change, create a comment of this type
|
||||||
|
// instead of an aggregator
|
||||||
|
if agg.OnlyLabelsChanged() {
|
||||||
|
comment.Type = CommentTypeLabel
|
||||||
|
} else if agg.OnlyClosedReopened() {
|
||||||
|
if agg.IsClosed {
|
||||||
|
comment.Type = CommentTypeClose
|
||||||
|
} else {
|
||||||
|
comment.Type = CommentTypeReopen
|
||||||
|
}
|
||||||
|
} else if agg.OnlyRequestReview() {
|
||||||
|
comment.Type = CommentTypeReviewRequest
|
||||||
|
} else {
|
||||||
|
comment.Type = CommentTypeAggregator
|
||||||
|
comment.Aggregator = &newAgg
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(newAgg.AddedLabels) > 0 {
|
||||||
|
comment.AddedLabels = newAgg.AddedLabels
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(newAgg.RemovedLabels) > 0 {
|
||||||
|
comment.RemovedLabels = newAgg.RemovedLabels
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(newAgg.AddedRequestReview) > 0 {
|
||||||
|
comment.AddedRequestReview = newAgg.AddedRequestReview
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(newAgg.RemovedRequestReview) > 0 {
|
||||||
|
comment.RemovedRequestReview = newAgg.RemovedRequestReview
|
||||||
|
}
|
||||||
|
|
||||||
|
if final {
|
||||||
|
issue.Comments = append(issue.Comments[:startind], comment)
|
||||||
|
} else {
|
||||||
|
issue.Comments = append(append(issue.Comments[:startind], comment), issue.Comments[endind+1:]...)
|
||||||
|
}
|
||||||
|
return endind - startind
|
||||||
|
}
|
||||||
|
|
||||||
|
// combineCommentsHistory combines nearby elements in the history as one
|
||||||
|
func CombineCommentsHistory(issue *Issue) {
|
||||||
|
// Initialise a new empty aggregator, ready to combine comments
|
||||||
|
var agg CommentAggregator
|
||||||
|
agg.StartUnix = 0
|
||||||
|
agg.PosterID = -1
|
||||||
|
reqReset := false
|
||||||
|
|
||||||
|
for i := 0; i < len(issue.Comments); i++ {
|
||||||
|
cur := issue.Comments[i]
|
||||||
|
if reqReset {
|
||||||
|
agg.Reset(cur)
|
||||||
|
reqReset = false
|
||||||
|
}
|
||||||
|
// If the comment we encounter is not accepted inside an aggregator
|
||||||
|
if !agg.IsAggregated(&cur.Type) {
|
||||||
|
// If we aggregated some data, create the resulting comment for it
|
||||||
|
if len(agg.Indexes) > 0 {
|
||||||
|
i -= agg.createAggregatedComment(issue, false)
|
||||||
|
}
|
||||||
|
reqReset = true
|
||||||
|
|
||||||
|
// Do not need to continue the aggregation loop, skip to next comment
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the comment we encounter cannot be aggregated with the current aggregator,
|
||||||
|
// we create a new empty aggregator
|
||||||
|
if ((int64(cur.CreatedUnix) - agg.StartUnix) > 60) || (cur.PosterID != agg.PosterID) {
|
||||||
|
// First, create the aggregated comment if there's data in it
|
||||||
|
if len(agg.Indexes) > 0 {
|
||||||
|
i -= agg.createAggregatedComment(issue, false)
|
||||||
|
}
|
||||||
|
agg.Reset(cur)
|
||||||
|
}
|
||||||
|
|
||||||
|
agg.aggregateComment(cur, i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the aggregated comment if there's data in it
|
||||||
|
if !reqReset && len(agg.Indexes) > 0 {
|
||||||
|
agg.createAggregatedComment(issue, true)
|
||||||
|
}
|
||||||
|
}
|
1
release-notes/6523.md
Normal file
1
release-notes/6523.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Reduce noise in issue / PR comments. If performed in under 60 secs, cancel out contradictory actions, aggregate many actions under the same comment
|
|
@ -1831,8 +1831,7 @@ func ViewIssue(ctx *context.Context) {
|
||||||
ctx.Data["LatestCloseCommentID"] = latestCloseCommentID
|
ctx.Data["LatestCloseCommentID"] = latestCloseCommentID
|
||||||
|
|
||||||
// Combine multiple label assignments into a single comment
|
// Combine multiple label assignments into a single comment
|
||||||
combineLabelComments(issue)
|
issues_model.CombineCommentsHistory(issue)
|
||||||
combineRequestReviewComments(issue)
|
|
||||||
|
|
||||||
getBranchData(ctx, issue)
|
getBranchData(ctx, issue)
|
||||||
if issue.IsPull {
|
if issue.IsPull {
|
||||||
|
@ -3693,194 +3692,6 @@ func attachmentsHTML(ctx *context.Context, attachments []*repo_model.Attachment,
|
||||||
return attachHTML
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Propagate 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
|
|
||||||
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.CommentTypeLabel ||
|
|
||||||
(prev != nil && prev.PosterID != cur.PosterID) ||
|
|
||||||
(prev != nil && cur.CreatedUnix-prev.CreatedUnix >= 60) {
|
|
||||||
if cur.Type == issues_model.CommentTypeLabel && cur.Label != nil {
|
|
||||||
if cur.Content != "1" {
|
|
||||||
cur.RemovedLabels = append(cur.RemovedLabels, cur.Label)
|
|
||||||
} else {
|
|
||||||
cur.AddedLabels = append(cur.AddedLabels, cur.Label)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if cur.Label != nil { // now cur MUST be label comment
|
|
||||||
if prev.Type == issues_model.CommentTypeLabel { // we can combine them only prev is a label comment
|
|
||||||
if cur.Content != "1" {
|
|
||||||
// remove labels from the AddedLabels list if the label that was removed is already
|
|
||||||
// in this list, and if it's not in this list, add the label to RemovedLabels
|
|
||||||
addedAndRemoved := false
|
|
||||||
for i, label := range prev.AddedLabels {
|
|
||||||
if cur.Label.ID == label.ID {
|
|
||||||
prev.AddedLabels = append(prev.AddedLabels[:i], prev.AddedLabels[i+1:]...)
|
|
||||||
addedAndRemoved = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !addedAndRemoved {
|
|
||||||
prev.RemovedLabels = append(prev.RemovedLabels, cur.Label)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// remove labels from the RemovedLabels list if the label that was added is already
|
|
||||||
// in this list, and if it's not in this list, add the label to AddedLabels
|
|
||||||
removedAndAdded := false
|
|
||||||
for i, label := range prev.RemovedLabels {
|
|
||||||
if cur.Label.ID == label.ID {
|
|
||||||
prev.RemovedLabels = append(prev.RemovedLabels[:i], prev.RemovedLabels[i+1:]...)
|
|
||||||
removedAndAdded = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !removedAndAdded {
|
|
||||||
prev.AddedLabels = append(prev.AddedLabels, cur.Label)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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--
|
|
||||||
} else { // if prev is not a label comment, start a new group
|
|
||||||
if cur.Content != "1" {
|
|
||||||
cur.RemovedLabels = append(cur.RemovedLabels, cur.Label)
|
|
||||||
} else {
|
|
||||||
cur.AddedLabels = append(cur.AddedLabels, cur.Label)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// get all teams that current user can mention
|
// get all teams that current user can mention
|
||||||
func handleTeamMentions(ctx *context.Context) {
|
func handleTeamMentions(ctx *context.Context) {
|
||||||
if ctx.Doer == nil || !ctx.Repo.Owner.IsOrganization() {
|
if ctx.Doer == nil || !ctx.Repo.Owner.IsOrganization() {
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -524,7 +524,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{else if eq .Type 27}}
|
{{else if eq .Type 27}}
|
||||||
{{if or .AddedRequestReview .RemovedRequestReview}}
|
{{if or .AddedRequestReview .RemovedRequestReview}}
|
||||||
<div class="timeline-item event" id="{{.HashTag}}">
|
<div class="timeline-item event" id="{{.HashTag}}">
|
||||||
<span class="badge">{{svg "octicon-tag"}}</span>
|
<span class="badge">{{svg "octicon-tag"}}</span>
|
||||||
{{template "shared/user/avatarlink" dict "user" .Poster}}
|
{{template "shared/user/avatarlink" dict "user" .Poster}}
|
||||||
|
@ -540,7 +540,7 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{else if and (eq .Type 29) (or (gt .CommitsNum 0) .IsForcePush)}}
|
{{else if and (eq .Type 29) (or (gt .CommitsNum 0) .IsForcePush)}}
|
||||||
<!-- If PR is closed, the comments whose type is CommentTypePullRequestPush(29) after latestCloseCommentID won't be rendered. //-->
|
<!-- If PR is closed, the comments whose type is CommentTypePullRequestPush(29) after latestCloseCommentID won't be rendered. //-->
|
||||||
{{if and .Issue.IsClosed (gt .ID $.LatestCloseCommentID)}}
|
{{if and .Issue.IsClosed (gt .ID $.LatestCloseCommentID)}}
|
||||||
|
@ -676,6 +676,73 @@
|
||||||
{{else}}{{ctx.Locale.Tr "repo.issues.unpin_comment" $createdStr}}{{end}}
|
{{else}}{{ctx.Locale.Tr "repo.issues.unpin_comment" $createdStr}}{{end}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
{{else if eq .Type 38}}
|
||||||
|
<div class="timeline-item event" id="{{.HashTag}}">
|
||||||
|
<span class="badge">{{svg "octicon-list-unordered" 16}}</span>
|
||||||
|
{{template "shared/user/avatarlink" dict "user" .Poster}}
|
||||||
|
|
||||||
|
<span class="text grey muted-links">
|
||||||
|
{{template "shared/user/authorlink" .Poster}}
|
||||||
|
{{$createdStr}}
|
||||||
|
|
||||||
|
<ul class="comment-aggregator">
|
||||||
|
|
||||||
|
<!-- OPEN / CLOSE -->
|
||||||
|
{{if and .Aggregator.PrevClosed (not .Aggregator.IsClosed)}}
|
||||||
|
<li>
|
||||||
|
<span class="badge tw-bg-green tw-text-white">{{svg "octicon-dot-fill"}}</span>
|
||||||
|
{{if .Issue.IsPull}}
|
||||||
|
{{ctx.Locale.Tr "repo.pulls.reopened_at" "" ""}}
|
||||||
|
{{else}}
|
||||||
|
{{ctx.Locale.Tr "repo.issues.reopened_at" "" ""}}
|
||||||
|
{{end}}
|
||||||
|
</li>
|
||||||
|
{{else if and (not .Aggregator.PrevClosed) .Aggregator.IsClosed}}
|
||||||
|
<span class="badge tw-bg-red tw-text-white">{{svg "octicon-circle-slash"}}</span>
|
||||||
|
<li>
|
||||||
|
{{if .Issue.IsPull}}
|
||||||
|
{{ctx.Locale.Tr "repo.pulls.closed_at" "" ""}}
|
||||||
|
{{else}}
|
||||||
|
{{ctx.Locale.Tr "repo.issues.closed_at" "" ""}}
|
||||||
|
{{end}}
|
||||||
|
</li>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<!-- Add labels -->
|
||||||
|
{{if or .AddedLabels .RemovedLabels}}
|
||||||
|
<li>
|
||||||
|
<span class="badge">{{svg "octicon-tag" 20}}</span>
|
||||||
|
{{if and .AddedLabels (not .RemovedLabels)}}
|
||||||
|
{{ctx.Locale.TrN (len .AddedLabels) "repo.issues.add_label" "repo.issues.add_labels" (RenderLabels $.Context ctx.Locale .AddedLabels $.RepoLink .Issue.IsPull) ""}}
|
||||||
|
{{else if and (not .AddedLabels) .RemovedLabels}}
|
||||||
|
{{ctx.Locale.TrN (len .RemovedLabels) "repo.issues.remove_label" "repo.issues.remove_labels" (RenderLabels $.Context ctx.Locale .RemovedLabels $.RepoLink .Issue.IsPull) ""}}
|
||||||
|
{{else}}
|
||||||
|
{{ctx.Locale.Tr "repo.issues.add_remove_labels" (RenderLabels $.Context ctx.Locale .AddedLabels $.RepoLink .Issue.IsPull) (RenderLabels $.Context ctx.Locale .RemovedLabels $.RepoLink .Issue.IsPull) ""}}
|
||||||
|
{{end}}
|
||||||
|
</li>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if or .AddedRequestReview .RemovedRequestReview}}
|
||||||
|
<li>
|
||||||
|
<span class="badge">{{svg "octicon-tag" 20}}</span>
|
||||||
|
<span class="text grey muted-links">
|
||||||
|
|
||||||
|
{{if and (eq (len .RemovedRequestReview) 1) (eq (len .AddedRequestReview) 0) (eq ((index .RemovedRequestReview 0).ID) .PosterID) (eq ((index .RemovedRequestReview 0).Type) "user")}}
|
||||||
|
<span class="review-request-list">{{ctx.Locale.Tr "repo.issues.review.remove_review_request_self" ""}}</span>
|
||||||
|
{{else if and .AddedRequestReview (not .RemovedRequestReview)}}
|
||||||
|
{{ctx.Locale.TrN (len .AddedRequestReview) "repo.issues.review.add_review_request" "repo.issues.review.add_review_requests" (RenderReviewRequest .AddedRequestReview) ""}}
|
||||||
|
{{else if and (not .AddedRequestReview) .RemovedRequestReview}}
|
||||||
|
{{ctx.Locale.TrN (len .RemovedRequestReview) "repo.issues.review.remove_review_request" "repo.issues.review.remove_review_requests" (RenderReviewRequest .RemovedRequestReview) ""}}
|
||||||
|
{{else}}
|
||||||
|
{{ctx.Locale.Tr "repo.issues.review.add_remove_review_requests" (RenderReviewRequest .AddedRequestReview) (RenderReviewRequest .RemovedRequestReview) ""}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
@ -840,6 +840,22 @@ td .commit-summary {
|
||||||
margin-right: 0.25em;
|
margin-right: 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.comment-aggregator {
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repository.view.issue .comment-list .timeline-item .comment-aggregator .badge {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin-top: 5px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repository.view.issue .comment-list .timeline-item .comment-aggregator .badge .svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.singular-commit {
|
.singular-commit {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
Loading…
Add table
Reference in a new issue