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
|
||||
CommentTypeUnpin // 37 unpin Issue
|
||||
|
||||
CommentTypeAggregator // 38 Aggregator of comments
|
||||
)
|
||||
|
||||
var commentStrings = []string{
|
||||
|
@ -236,10 +238,30 @@ func (r RoleInRepo) LocaleHelper(lang translation.Locale) string {
|
|||
return lang.TrString("repo.issues.role." + string(r) + "_helper")
|
||||
}
|
||||
|
||||
type RequestReviewTarget interface {
|
||||
ID() int64
|
||||
Name() string
|
||||
Type() string
|
||||
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"
|
||||
}
|
||||
|
||||
// Comment represents a comment in commit and issue page.
|
||||
|
@ -254,6 +276,7 @@ type Comment struct {
|
|||
Issue *Issue `xorm:"-"`
|
||||
LabelID int64
|
||||
Label *Label `xorm:"-"`
|
||||
Aggregator *CommentAggregator `xorm:"-"`
|
||||
AddedLabels []*Label `xorm:"-"`
|
||||
RemovedLabels []*Label `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
|
||||
|
||||
// Combine multiple label assignments into a single comment
|
||||
combineLabelComments(issue)
|
||||
combineRequestReviewComments(issue)
|
||||
issues_model.CombineCommentsHistory(issue)
|
||||
|
||||
getBranchData(ctx, issue)
|
||||
if issue.IsPull {
|
||||
|
@ -3693,194 +3692,6 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
func handleTeamMentions(ctx *context.Context) {
|
||||
if ctx.Doer == nil || !ctx.Repo.Owner.IsOrganization() {
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -524,7 +524,7 @@
|
|||
</div>
|
||||
</div>
|
||||
{{else if eq .Type 27}}
|
||||
{{if or .AddedRequestReview .RemovedRequestReview}}
|
||||
{{if or .AddedRequestReview .RemovedRequestReview}}
|
||||
<div class="timeline-item event" id="{{.HashTag}}">
|
||||
<span class="badge">{{svg "octicon-tag"}}</span>
|
||||
{{template "shared/user/avatarlink" dict "user" .Poster}}
|
||||
|
@ -540,7 +540,7 @@
|
|||
{{end}}
|
||||
</span>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{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 and .Issue.IsClosed (gt .ID $.LatestCloseCommentID)}}
|
||||
|
@ -676,6 +676,73 @@
|
|||
{{else}}{{ctx.Locale.Tr "repo.issues.unpin_comment" $createdStr}}{{end}}
|
||||
</span>
|
||||
</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}}
|
||||
|
|
|
@ -840,6 +840,22 @@ td .commit-summary {
|
|||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
Loading…
Add table
Reference in a new issue