mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-07-22 01:17:57 +00:00
This is a reimplementation of a Codeberg patch to avoid blocking
operations generating multiple notifications.
c72a0a1c97
Co-authored-by: famfo <codeberg@famfo.xyz>
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/13383
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
603 lines
18 KiB
Go
603 lines
18 KiB
Go
// Copyright 2019 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package feed
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"path"
|
|
"strings"
|
|
|
|
activities_model "forgejo.org/models/activities"
|
|
"forgejo.org/models/db"
|
|
issues_model "forgejo.org/models/issues"
|
|
repo_model "forgejo.org/models/repo"
|
|
user_model "forgejo.org/models/user"
|
|
"forgejo.org/modules/git"
|
|
"forgejo.org/modules/graceful"
|
|
"forgejo.org/modules/json"
|
|
"forgejo.org/modules/log"
|
|
"forgejo.org/modules/optional"
|
|
"forgejo.org/modules/process"
|
|
"forgejo.org/modules/queue"
|
|
"forgejo.org/modules/repository"
|
|
"forgejo.org/modules/setting"
|
|
"forgejo.org/modules/timeutil"
|
|
"forgejo.org/modules/util"
|
|
federation_service "forgejo.org/services/federation"
|
|
notify_service "forgejo.org/services/notify"
|
|
)
|
|
|
|
const maxRetries = 3
|
|
|
|
type QueueableAction struct {
|
|
ID int64
|
|
UserID int64
|
|
OpType activities_model.ActionType
|
|
ActUserID int64
|
|
RepoID int64
|
|
CommentID int64
|
|
RefName string
|
|
IsPrivate bool
|
|
Content string
|
|
CreatedUnix timeutil.TimeStamp
|
|
}
|
|
|
|
type actionNotifier struct {
|
|
notify_service.NullNotifier
|
|
}
|
|
|
|
type notificationQueueItem struct {
|
|
Action QueueableAction
|
|
LocalCount uint
|
|
LocalOut optional.Option[[]QueueableAction]
|
|
FederationCount uint
|
|
}
|
|
|
|
var (
|
|
_ notify_service.Notifier = &actionNotifier{}
|
|
notificationQueue *queue.WorkerPoolQueue[notificationQueueItem]
|
|
)
|
|
|
|
func (a *QueueableAction) ToAction() activities_model.Action {
|
|
return activities_model.Action{
|
|
ID: a.ID,
|
|
UserID: a.UserID,
|
|
OpType: a.OpType,
|
|
ActUserID: a.ActUserID,
|
|
RepoID: a.RepoID,
|
|
CommentID: a.CommentID,
|
|
RefName: a.RefName,
|
|
IsPrivate: a.IsPrivate,
|
|
Content: a.Content,
|
|
CreatedUnix: a.CreatedUnix,
|
|
}
|
|
}
|
|
|
|
func FromAction(a activities_model.Action) QueueableAction {
|
|
return QueueableAction{
|
|
ID: a.ID,
|
|
UserID: a.UserID,
|
|
OpType: a.OpType,
|
|
ActUserID: a.UserID,
|
|
RepoID: a.RepoID,
|
|
CommentID: a.CommentID,
|
|
RefName: a.RefName,
|
|
IsPrivate: a.IsPrivate,
|
|
Content: a.Content,
|
|
CreatedUnix: a.CreatedUnix,
|
|
}
|
|
}
|
|
|
|
func initNotificationQueue() error {
|
|
notificationQueue = queue.CreateSimpleQueue(graceful.GetManager().ShutdownContext(), "notification_queue", notificationQueueHandler)
|
|
if notificationQueue == nil {
|
|
return fmt.Errorf("Failed to create notification_queue")
|
|
}
|
|
|
|
go graceful.GetManager().RunWithCancel(notificationQueue)
|
|
|
|
return nil
|
|
}
|
|
|
|
func notificationQueueHandler(notificationItems ...notificationQueueItem) (unhandled []notificationQueueItem) {
|
|
for _, notification := range notificationItems {
|
|
err := deliverNotification(¬ification)
|
|
if err != nil {
|
|
log.Error("Failed to deliver notification: %e", err)
|
|
|
|
if notification.LocalCount > maxRetries || notification.FederationCount > maxRetries {
|
|
log.Warn("Dropping notification after exceeding retry count (%d)", maxRetries)
|
|
continue
|
|
}
|
|
|
|
unhandled = append(unhandled, notification)
|
|
}
|
|
}
|
|
|
|
return unhandled
|
|
}
|
|
|
|
func deliverNotification(notification *notificationQueueItem) error {
|
|
hammerCtx := graceful.GetManager().HammerContext()
|
|
ctx, _, finished := process.GetManager().AddContext(hammerCtx, "Delivering notification")
|
|
defer finished()
|
|
|
|
if !notification.LocalOut.Has() {
|
|
action := notification.Action.ToAction()
|
|
out, err := activities_model.NotifyWatchers(ctx, &action)
|
|
if err != nil {
|
|
notification.LocalCount++
|
|
return err
|
|
}
|
|
|
|
// convert database Actions to QueueableActions
|
|
queueableActionOut := []QueueableAction{}
|
|
for _, a := range out {
|
|
queueableActionOut = append(queueableActionOut, FromAction(a))
|
|
}
|
|
|
|
notification.LocalOut = optional.Some(queueableActionOut)
|
|
}
|
|
|
|
_, out := notification.LocalOut.Get()
|
|
// convert QueueableActions to database Actions
|
|
actionOut := []activities_model.Action{}
|
|
for _, a := range out {
|
|
actionOut = append(actionOut, a.ToAction())
|
|
}
|
|
|
|
err := federation_service.NotifyActivityPubFollowers(ctx, actionOut)
|
|
if err != nil {
|
|
notification.FederationCount++
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func Init() error {
|
|
notify_service.RegisterNotifier(NewNotifier())
|
|
return initNotificationQueue()
|
|
}
|
|
|
|
// NewNotifier create a new actionNotifier notifier
|
|
func NewNotifier() notify_service.Notifier {
|
|
return &actionNotifier{}
|
|
}
|
|
|
|
func notifyAll(ctx context.Context, action QueueableAction) {
|
|
db.AfterTx(ctx, func() {
|
|
err := notificationQueue.Push(notificationQueueItem{
|
|
Action: action,
|
|
LocalCount: 0,
|
|
LocalOut: optional.None[[]QueueableAction](),
|
|
FederationCount: 0,
|
|
})
|
|
if err != nil {
|
|
log.Error("Failed to enqueue notification for delivery: %s", err.Error())
|
|
}
|
|
})
|
|
}
|
|
|
|
func notifyAllActions(ctx context.Context, acts []*activities_model.Action) error {
|
|
out, err := activities_model.NotifyWatchersActions(ctx, acts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return federation_service.NotifyActivityPubFollowers(ctx, out)
|
|
}
|
|
|
|
func (a *actionNotifier) NewIssue(ctx context.Context, issue *issues_model.Issue, mentions []*user_model.User) {
|
|
if err := issue.LoadPoster(ctx); err != nil {
|
|
log.Error("issue.LoadPoster: %v", err)
|
|
return
|
|
}
|
|
if err := issue.LoadRepo(ctx); err != nil {
|
|
log.Error("issue.LoadRepo: %v", err)
|
|
return
|
|
}
|
|
repo := issue.Repo
|
|
|
|
notifyAll(ctx, QueueableAction{
|
|
ActUserID: issue.Poster.ID,
|
|
OpType: activities_model.ActionCreateIssue,
|
|
Content: encodeContent(fmt.Sprintf("%d", issue.Index), issue.Title),
|
|
RepoID: repo.ID,
|
|
IsPrivate: repo.IsPrivate,
|
|
})
|
|
}
|
|
|
|
// IssueChangeStatus notifies close or reopen issue to notifiers
|
|
func (a *actionNotifier) IssueChangeStatus(ctx context.Context, doer *user_model.User, commitID string, issue *issues_model.Issue, actionComment *issues_model.Comment, closeOrReopen bool) {
|
|
// Compose comment action, could be plain comment, close or reopen issue/pull request.
|
|
// This object will be used to notify watchers in the end of function.
|
|
act := QueueableAction{
|
|
ActUserID: doer.ID,
|
|
Content: encodeContent(fmt.Sprintf("%d", issue.Index), ""),
|
|
RepoID: issue.Repo.ID,
|
|
CommentID: actionComment.ID,
|
|
IsPrivate: issue.Repo.IsPrivate,
|
|
}
|
|
// Check comment type.
|
|
if closeOrReopen {
|
|
act.OpType = activities_model.ActionCloseIssue
|
|
if issue.IsPull {
|
|
act.OpType = activities_model.ActionClosePullRequest
|
|
}
|
|
} else {
|
|
act.OpType = activities_model.ActionReopenIssue
|
|
if issue.IsPull {
|
|
act.OpType = activities_model.ActionReopenPullRequest
|
|
}
|
|
}
|
|
|
|
// Notify watchers for whatever action comes in, ignore if no action type.
|
|
notifyAll(ctx, act)
|
|
}
|
|
|
|
// CreateIssueComment notifies comment on an issue to notifiers
|
|
func (a *actionNotifier) CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository,
|
|
issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User,
|
|
) {
|
|
act := QueueableAction{
|
|
ActUserID: doer.ID,
|
|
RepoID: issue.Repo.ID,
|
|
CommentID: comment.ID,
|
|
IsPrivate: issue.Repo.IsPrivate,
|
|
Content: encodeContent(fmt.Sprintf("%d", issue.Index), abbreviatedComment(comment.Content)),
|
|
}
|
|
|
|
if issue.IsPull {
|
|
act.OpType = activities_model.ActionCommentPull
|
|
} else {
|
|
act.OpType = activities_model.ActionCommentIssue
|
|
}
|
|
|
|
// Notify watchers for whatever action comes in, ignore if no action type.
|
|
notifyAll(ctx, act)
|
|
}
|
|
|
|
func (a *actionNotifier) NewPullRequest(ctx context.Context, pull *issues_model.PullRequest, mentions []*user_model.User) {
|
|
if err := pull.LoadIssue(ctx); err != nil {
|
|
log.Error("pull.LoadIssue: %v", err)
|
|
return
|
|
}
|
|
if err := pull.Issue.LoadRepo(ctx); err != nil {
|
|
log.Error("pull.Issue.LoadRepo: %v", err)
|
|
return
|
|
}
|
|
if err := pull.Issue.LoadPoster(ctx); err != nil {
|
|
log.Error("pull.Issue.LoadPoster: %v", err)
|
|
return
|
|
}
|
|
|
|
notifyAll(ctx, QueueableAction{
|
|
ActUserID: pull.Issue.Poster.ID,
|
|
OpType: activities_model.ActionCreatePullRequest,
|
|
Content: encodeContent(fmt.Sprintf("%d", pull.Issue.Index), pull.Issue.Title),
|
|
RepoID: pull.Issue.Repo.ID,
|
|
IsPrivate: pull.Issue.Repo.IsPrivate,
|
|
})
|
|
}
|
|
|
|
func (a *actionNotifier) RenameRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldRepoName string) {
|
|
notifyAll(ctx, QueueableAction{
|
|
ActUserID: doer.ID,
|
|
OpType: activities_model.ActionRenameRepo,
|
|
RepoID: repo.ID,
|
|
IsPrivate: repo.IsPrivate,
|
|
Content: oldRepoName,
|
|
})
|
|
}
|
|
|
|
func (a *actionNotifier) TransferRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldOwnerName string) {
|
|
notifyAll(ctx, QueueableAction{
|
|
ActUserID: doer.ID,
|
|
OpType: activities_model.ActionTransferRepo,
|
|
RepoID: repo.ID,
|
|
IsPrivate: repo.IsPrivate,
|
|
Content: path.Join(oldOwnerName, repo.Name),
|
|
})
|
|
}
|
|
|
|
func (a *actionNotifier) CreateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) {
|
|
notifyAll(ctx, QueueableAction{
|
|
ActUserID: doer.ID,
|
|
OpType: activities_model.ActionCreateRepo,
|
|
RepoID: repo.ID,
|
|
IsPrivate: repo.IsPrivate,
|
|
})
|
|
}
|
|
|
|
func (a *actionNotifier) ForkRepository(ctx context.Context, doer *user_model.User, oldRepo, repo *repo_model.Repository) {
|
|
notifyAll(ctx, QueueableAction{
|
|
ActUserID: doer.ID,
|
|
OpType: activities_model.ActionCreateRepo,
|
|
RepoID: repo.ID,
|
|
IsPrivate: repo.IsPrivate,
|
|
})
|
|
}
|
|
|
|
func (a *actionNotifier) PullRequestReview(ctx context.Context, pr *issues_model.PullRequest, review *issues_model.Review, comment *issues_model.Comment, mentions []*user_model.User) {
|
|
if err := review.LoadReviewer(ctx); err != nil {
|
|
log.Error("LoadReviewer '%d/%d': %v", review.ID, review.ReviewerID, err)
|
|
return
|
|
}
|
|
if err := review.LoadCodeComments(ctx); err != nil {
|
|
log.Error("LoadCodeComments '%d/%d': %v", review.Reviewer.ID, review.ID, err)
|
|
return
|
|
}
|
|
|
|
actions := make([]*activities_model.Action, 0, 10)
|
|
for _, lines := range review.CodeComments {
|
|
for _, comments := range lines {
|
|
for _, comm := range comments {
|
|
actions = append(actions, &activities_model.Action{
|
|
ActUserID: review.Reviewer.ID,
|
|
ActUser: review.Reviewer,
|
|
Content: encodeContent(fmt.Sprintf("%d", review.Issue.Index), abbreviatedComment(comm.Content)),
|
|
OpType: activities_model.ActionCommentPull,
|
|
RepoID: review.Issue.RepoID,
|
|
Repo: review.Issue.Repo,
|
|
IsPrivate: review.Issue.Repo.IsPrivate,
|
|
Comment: comm,
|
|
CommentID: comm.ID,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
if review.Type != issues_model.ReviewTypeComment || strings.TrimSpace(comment.Content) != "" {
|
|
action := &activities_model.Action{
|
|
ActUserID: review.Reviewer.ID,
|
|
ActUser: review.Reviewer,
|
|
Content: encodeContent(fmt.Sprintf("%d", review.Issue.Index), abbreviatedComment(comment.Content)),
|
|
RepoID: review.Issue.RepoID,
|
|
Repo: review.Issue.Repo,
|
|
IsPrivate: review.Issue.Repo.IsPrivate,
|
|
Comment: comment,
|
|
CommentID: comment.ID,
|
|
}
|
|
|
|
switch review.Type {
|
|
case issues_model.ReviewTypeApprove:
|
|
action.OpType = activities_model.ActionApprovePullRequest
|
|
case issues_model.ReviewTypeReject:
|
|
action.OpType = activities_model.ActionRejectPullRequest
|
|
default:
|
|
action.OpType = activities_model.ActionCommentPull
|
|
}
|
|
|
|
actions = append(actions, action)
|
|
}
|
|
|
|
if err := notifyAllActions(ctx, actions); err != nil {
|
|
log.Error("notify watchers '%d/%d': %v", review.Reviewer.ID, review.Issue.RepoID, err)
|
|
}
|
|
}
|
|
|
|
func (*actionNotifier) MergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) {
|
|
notifyAll(ctx, QueueableAction{
|
|
ActUserID: doer.ID,
|
|
OpType: activities_model.ActionMergePullRequest,
|
|
Content: encodeContent(fmt.Sprintf("%d", pr.Issue.Index), pr.Issue.Title),
|
|
RepoID: pr.Issue.Repo.ID,
|
|
IsPrivate: pr.Issue.Repo.IsPrivate,
|
|
})
|
|
}
|
|
|
|
func (*actionNotifier) AutoMergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) {
|
|
notifyAll(ctx, QueueableAction{
|
|
ActUserID: doer.ID,
|
|
OpType: activities_model.ActionAutoMergePullRequest,
|
|
Content: encodeContent(fmt.Sprintf("%d", pr.Issue.Index), pr.Issue.Title),
|
|
RepoID: pr.Issue.Repo.ID,
|
|
IsPrivate: pr.Issue.Repo.IsPrivate,
|
|
})
|
|
}
|
|
|
|
func (*actionNotifier) PullReviewDismiss(ctx context.Context, doer *user_model.User, review *issues_model.Review, comment *issues_model.Comment) {
|
|
if err := review.LoadReviewer(ctx); err != nil {
|
|
log.Error("LoadReviewer '%d/%d': %v", review.ID, review.ReviewerID, err)
|
|
return
|
|
}
|
|
reviewerName := review.Reviewer.Name
|
|
if len(review.OriginalAuthor) > 0 {
|
|
reviewerName = review.OriginalAuthor
|
|
}
|
|
notifyAll(ctx, QueueableAction{
|
|
ActUserID: doer.ID,
|
|
OpType: activities_model.ActionPullReviewDismissed,
|
|
Content: encodeContent(fmt.Sprintf("%d", review.Issue.Index), reviewerName, abbreviatedComment(comment.Content)),
|
|
RepoID: review.Issue.Repo.ID,
|
|
IsPrivate: review.Issue.Repo.IsPrivate,
|
|
CommentID: comment.ID,
|
|
})
|
|
}
|
|
|
|
func (a *actionNotifier) PushCommits(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, opts *repository.PushUpdateOptions, commits *repository.PushCommits) {
|
|
commits = prepareCommitsForFeed(commits)
|
|
|
|
data, err := json.Marshal(commits)
|
|
if err != nil {
|
|
log.Error("Marshal: %v", err)
|
|
return
|
|
}
|
|
|
|
opType := activities_model.ActionCommitRepo
|
|
|
|
// Check it's tag push or branch.
|
|
if opts.RefFullName.IsTag() {
|
|
opType = activities_model.ActionPushTag
|
|
if opts.IsDelRef() {
|
|
opType = activities_model.ActionDeleteTag
|
|
}
|
|
} else if opts.IsDelRef() {
|
|
opType = activities_model.ActionDeleteBranch
|
|
}
|
|
|
|
notifyAll(ctx, QueueableAction{
|
|
ActUserID: pusher.ID,
|
|
OpType: opType,
|
|
Content: string(data),
|
|
RepoID: repo.ID,
|
|
RefName: opts.RefFullName.String(),
|
|
IsPrivate: repo.IsPrivate,
|
|
})
|
|
}
|
|
|
|
func (a *actionNotifier) CreateRef(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, refFullName git.RefName, refID string) {
|
|
opType := activities_model.ActionCommitRepo
|
|
if refFullName.IsTag() {
|
|
// has sent same action in `PushCommits`, so skip it.
|
|
return
|
|
}
|
|
notifyAll(ctx, QueueableAction{
|
|
ActUserID: doer.ID,
|
|
OpType: opType,
|
|
RepoID: repo.ID,
|
|
IsPrivate: repo.IsPrivate,
|
|
RefName: refFullName.String(),
|
|
})
|
|
}
|
|
|
|
func (a *actionNotifier) DeleteRef(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, refFullName git.RefName) {
|
|
opType := activities_model.ActionDeleteBranch
|
|
if refFullName.IsTag() {
|
|
// has sent same action in `PushCommits`, so skip it.
|
|
return
|
|
}
|
|
notifyAll(ctx, QueueableAction{
|
|
ActUserID: doer.ID,
|
|
OpType: opType,
|
|
RepoID: repo.ID,
|
|
IsPrivate: repo.IsPrivate,
|
|
RefName: refFullName.String(),
|
|
})
|
|
}
|
|
|
|
func (a *actionNotifier) SyncPushCommits(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, opts *repository.PushUpdateOptions, commits *repository.PushCommits) {
|
|
commits = prepareCommitsForFeed(commits)
|
|
|
|
data, err := json.Marshal(commits)
|
|
if err != nil {
|
|
log.Error("json.Marshal: %v", err)
|
|
return
|
|
}
|
|
|
|
notifyAll(ctx, QueueableAction{
|
|
ActUserID: repo.OwnerID,
|
|
OpType: activities_model.ActionMirrorSyncPush,
|
|
RepoID: repo.ID,
|
|
IsPrivate: repo.IsPrivate,
|
|
RefName: opts.RefFullName.String(),
|
|
Content: string(data),
|
|
})
|
|
}
|
|
|
|
func (a *actionNotifier) SyncCreateRef(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, refFullName git.RefName, refID string) {
|
|
notifyAll(ctx, QueueableAction{
|
|
ActUserID: repo.OwnerID,
|
|
OpType: activities_model.ActionMirrorSyncCreate,
|
|
RepoID: repo.ID,
|
|
IsPrivate: repo.IsPrivate,
|
|
RefName: refFullName.String(),
|
|
})
|
|
}
|
|
|
|
func (a *actionNotifier) SyncDeleteRef(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, refFullName git.RefName) {
|
|
notifyAll(ctx, QueueableAction{
|
|
ActUserID: repo.OwnerID,
|
|
OpType: activities_model.ActionMirrorSyncDelete,
|
|
RepoID: repo.ID,
|
|
IsPrivate: repo.IsPrivate,
|
|
RefName: refFullName.String(),
|
|
})
|
|
}
|
|
|
|
func (a *actionNotifier) NewRelease(ctx context.Context, rel *repo_model.Release) {
|
|
if err := rel.LoadAttributes(ctx); err != nil {
|
|
log.Error("LoadAttributes: %v", err)
|
|
return
|
|
}
|
|
notifyAll(ctx, QueueableAction{
|
|
ActUserID: rel.PublisherID,
|
|
OpType: activities_model.ActionPublishRelease,
|
|
RepoID: rel.RepoID,
|
|
IsPrivate: rel.Repo.IsPrivate,
|
|
Content: rel.Title,
|
|
RefName: rel.TagName, // FIXME: use a full ref name?
|
|
})
|
|
}
|
|
|
|
// ... later decoded in models/activities/action.go:GetIssueInfos
|
|
func encodeContent(params ...string) string {
|
|
contentEncoded, err := json.Marshal(params)
|
|
if err != nil {
|
|
log.Error("encodeContent: Unexpected json encoding error: %v", err)
|
|
}
|
|
return string(contentEncoded)
|
|
}
|
|
|
|
// Given a comment of arbitrary-length Markdown text, create an abbreviated Markdown text appropriate for the
|
|
// activity feed.
|
|
func abbreviatedComment(comment string) string {
|
|
firstLine := strings.Split(comment, "\n")[0]
|
|
|
|
if strings.HasPrefix(firstLine, "```") {
|
|
// First line is is a fenced code block... with no special abbreviate we would display a blank block, or in the
|
|
// worst-case a ```mermaid would display an error. Better to omit the comment.
|
|
return ""
|
|
}
|
|
|
|
truncatedContent, truncatedRight := util.SplitStringAtByteN(firstLine, 200)
|
|
if truncatedRight != "" {
|
|
// in case the content is in a Latin family language, we remove the last broken word.
|
|
lastSpaceIdx := strings.LastIndex(truncatedContent, " ")
|
|
if lastSpaceIdx != -1 && (len(truncatedContent)-lastSpaceIdx < 15) {
|
|
truncatedContent = truncatedContent[:lastSpaceIdx] + "…"
|
|
}
|
|
}
|
|
|
|
return truncatedContent
|
|
}
|
|
|
|
// Return a clone of the incoming repository.PushCommits that is appropriately tweaked for the activity feed. The struct
|
|
// is cloned rather than modified in-place because the same data will be sent to multiple notifiers. Transformations
|
|
// applied are: # of commits are limited to FeedMaxCommitNum, commit messages are trimmed to just the content displayed
|
|
// in the activity feed.
|
|
func prepareCommitsForFeed(commits *repository.PushCommits) *repository.PushCommits {
|
|
numCommits := min(len(commits.Commits), setting.UI.FeedMaxCommitNum)
|
|
retval := repository.PushCommits{
|
|
Commits: make([]*repository.PushCommit, 0, numCommits),
|
|
HeadCommit: nil,
|
|
CompareURL: commits.CompareURL,
|
|
Len: commits.Len,
|
|
}
|
|
if commits.HeadCommit != nil {
|
|
retval.HeadCommit = prepareCommitForFeed(commits.HeadCommit)
|
|
}
|
|
for i, commit := range commits.Commits {
|
|
if i == numCommits {
|
|
break
|
|
}
|
|
retval.Commits = append(retval.Commits, prepareCommitForFeed(commit))
|
|
}
|
|
return &retval
|
|
}
|
|
|
|
func prepareCommitForFeed(commit *repository.PushCommit) *repository.PushCommit {
|
|
return &repository.PushCommit{
|
|
Sha1: commit.Sha1,
|
|
Message: abbreviatedComment(commit.Message),
|
|
AuthorEmail: commit.AuthorEmail,
|
|
AuthorName: commit.AuthorName,
|
|
CommitterEmail: commit.CommitterEmail,
|
|
CommitterName: commit.CommitterName,
|
|
Signature: commit.Signature,
|
|
Verification: commit.Verification,
|
|
Timestamp: commit.Timestamp,
|
|
}
|
|
}
|