diff --git a/models/activities/action.go b/models/activities/action.go index 147511ede..cad3263c2 100644 --- a/models/activities/action.go +++ b/models/activities/action.go @@ -64,6 +64,7 @@ const ( ActionPublishRelease // 24 ActionPullReviewDismissed // 25 ActionPullRequestReadyForReview // 26 + ActionAutoMergePullRequest // 27 ) // Action represents user operation type and other information to @@ -550,7 +551,7 @@ func notifyWatchers(ctx context.Context, actions ...*Action) error { if !permIssue[i] { continue } - case ActionCreatePullRequest, ActionCommentPull, ActionMergePullRequest, ActionClosePullRequest, ActionReopenPullRequest: + case ActionCreatePullRequest, ActionCommentPull, ActionMergePullRequest, ActionClosePullRequest, ActionReopenPullRequest, ActionAutoMergePullRequest: if !permPR[i] { continue } diff --git a/modules/notification/action/action.go b/modules/notification/action/action.go index d3ff8b156..44d115f3d 100644 --- a/modules/notification/action/action.go +++ b/modules/notification/action/action.go @@ -283,6 +283,20 @@ func (*actionNotifier) NotifyMergePullRequest(pr *issues_model.PullRequest, doer } } +func (*actionNotifier) NotifyAutoMergePullRequest(pr *issues_model.PullRequest, doer *user_model.User) { + if err := activities_model.NotifyWatchers(&activities_model.Action{ + ActUserID: doer.ID, + ActUser: doer, + OpType: activities_model.ActionAutoMergePullRequest, + Content: fmt.Sprintf("%d|%s", pr.Issue.Index, pr.Issue.Title), + RepoID: pr.Issue.Repo.ID, + Repo: pr.Issue.Repo, + IsPrivate: pr.Issue.Repo.IsPrivate, + }); err != nil { + log.Error("NotifyWatchers [%d]: %v", pr.ID, err) + } +} + func (*actionNotifier) NotifyPullRevieweDismiss(doer *user_model.User, review *issues_model.Review, comment *issues_model.Comment) { reviewerName := review.Reviewer.Name if len(review.OriginalAuthor) > 0 { diff --git a/modules/notification/base/notifier.go b/modules/notification/base/notifier.go index d6cec92e1..9edab8213 100644 --- a/modules/notification/base/notifier.go +++ b/modules/notification/base/notifier.go @@ -34,7 +34,8 @@ type Notifier interface { NotifyIssueChangeLabels(doer *user_model.User, issue *issues_model.Issue, addedLabels, removedLabels []*issues_model.Label) NotifyNewPullRequest(pr *issues_model.PullRequest, mentions []*user_model.User) - NotifyMergePullRequest(*issues_model.PullRequest, *user_model.User) + NotifyMergePullRequest(pr *issues_model.PullRequest, doer *user_model.User) + NotifyAutoMergePullRequest(pr *issues_model.PullRequest, doer *user_model.User) NotifyPullRequestSynchronized(doer *user_model.User, pr *issues_model.PullRequest) NotifyPullRequestReview(pr *issues_model.PullRequest, review *issues_model.Review, comment *issues_model.Comment, mentions []*user_model.User) NotifyPullRequestCodeComment(pr *issues_model.PullRequest, comment *issues_model.Comment, mentions []*user_model.User) diff --git a/modules/notification/base/null.go b/modules/notification/base/null.go index b113ae79e..f051fbc26 100644 --- a/modules/notification/base/null.go +++ b/modules/notification/base/null.go @@ -54,6 +54,10 @@ func (*NullNotifier) NotifyPullRequestCodeComment(pr *issues_model.PullRequest, func (*NullNotifier) NotifyMergePullRequest(pr *issues_model.PullRequest, doer *user_model.User) { } +// NotifyAutoMergePullRequest places a place holder function +func (*NullNotifier) NotifyAutoMergePullRequest(pr *issues_model.PullRequest, doer *user_model.User) { +} + // NotifyPullRequestSynchronized places a place holder function func (*NullNotifier) NotifyPullRequestSynchronized(doer *user_model.User, pr *issues_model.PullRequest) { } diff --git a/modules/notification/mail/mail.go b/modules/notification/mail/mail.go index 100b4eb36..54f561839 100644 --- a/modules/notification/mail/mail.go +++ b/modules/notification/mail/mail.go @@ -153,6 +153,16 @@ func (m *mailNotifier) NotifyMergePullRequest(pr *issues_model.PullRequest, doer } } +func (m *mailNotifier) NotifyAutoMergePullRequest(pr *issues_model.PullRequest, doer *user_model.User) { + if err := pr.LoadIssue(); err != nil { + log.Error("pr.LoadIssue: %v", err) + return + } + if err := mailer.MailParticipants(pr.Issue, doer, activities_model.ActionAutoMergePullRequest, nil); err != nil { + log.Error("MailParticipants: %v", err) + } +} + func (m *mailNotifier) NotifyPullRequestPushCommits(doer *user_model.User, pr *issues_model.PullRequest, comment *issues_model.Comment) { ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("mailNotifier.NotifyPullRequestPushCommits Pull[%d] #%d in [%d]", pr.ID, pr.Index, pr.BaseRepoID)) defer finished() diff --git a/modules/notification/notification.go b/modules/notification/notification.go index 693c7f5c2..7bdc0a04c 100644 --- a/modules/notification/notification.go +++ b/modules/notification/notification.go @@ -98,6 +98,13 @@ func NotifyMergePullRequest(pr *issues_model.PullRequest, doer *user_model.User) } } +// NotifyAutoMergePullRequest notifies merge pull request to notifiers +func NotifyAutoMergePullRequest(pr *issues_model.PullRequest, doer *user_model.User) { + for _, notifier := range notifiers { + notifier.NotifyAutoMergePullRequest(pr, doer) + } +} + // NotifyNewPullRequest notifies new pull request to notifiers func NotifyNewPullRequest(pr *issues_model.PullRequest, mentions []*user_model.User) { for _, notifier := range notifiers { diff --git a/modules/notification/ui/ui.go b/modules/notification/ui/ui.go index 4d96a6b0e..0e2b3e67c 100644 --- a/modules/notification/ui/ui.go +++ b/modules/notification/ui/ui.go @@ -119,6 +119,10 @@ func (ns *notificationService) NotifyMergePullRequest(pr *issues_model.PullReque }) } +func (ns *notificationService) NotifyAutoMergePullRequest(pr *issues_model.PullRequest, doer *user_model.User) { + ns.NotifyMergePullRequest(pr, doer) +} + func (ns *notificationService) NotifyNewPullRequest(pr *issues_model.PullRequest, mentions []*user_model.User) { if err := pr.LoadIssue(); err != nil { log.Error("Unable to load issue: %d for pr: %d: Error: %v", pr.IssueID, pr.ID, err) diff --git a/modules/notification/webhook/webhook.go b/modules/notification/webhook/webhook.go index 630b56598..c591e1e34 100644 --- a/modules/notification/webhook/webhook.go +++ b/modules/notification/webhook/webhook.go @@ -632,6 +632,11 @@ func (m *webhookNotifier) NotifyPushCommits(pusher *user_model.User, repo *repo_ } } +func (m *webhookNotifier) NotifyAutoMergePullRequest(pr *issues_model.PullRequest, doer *user_model.User) { + // just redirect to the NotifyMergePullRequest + m.NotifyMergePullRequest(pr, doer) +} + func (*webhookNotifier) NotifyMergePullRequest(pr *issues_model.PullRequest, doer *user_model.User) { ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("webhook.NotifyMergePullRequest Pull[%d] #%d in [%d]", pr.ID, pr.Index, pr.BaseRepoID)) defer finished() diff --git a/modules/templates/helper.go b/modules/templates/helper.go index a72329144..a127b98dc 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -905,7 +905,7 @@ func ActionIcon(opType activities_model.ActionType) string { return "git-pull-request" case activities_model.ActionCommentIssue, activities_model.ActionCommentPull: return "comment-discussion" - case activities_model.ActionMergePullRequest: + case activities_model.ActionMergePullRequest, activities_model.ActionAutoMergePullRequest: return "git-merge" case activities_model.ActionCloseIssue, activities_model.ActionClosePullRequest: return "issue-closed" diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index ac2b2ecc9..8ffb7a5b2 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -3003,6 +3003,7 @@ reopen_pull_request = `reopened pull request %[3]s#%[2]s` comment_issue = `commented on issue %[3]s#%[2]s` comment_pull = `commented on pull request %[3]s#%[2]s` merge_pull_request = `merged pull request %[3]s#%[2]s` +auto_merge_pull_request = `automatically merged pull request %[3]s#%[2]s` transfer_repo = transferred repository %s to %s push_tag = pushed tag %[3]s to %[4]s delete_tag = deleted tag %[2]s from %[3]s diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index ebb9c0f26..f7e82dab3 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -839,7 +839,7 @@ func MergePullRequest(ctx *context.APIContext) { } } - if err := pull_service.Merge(ctx, pr, ctx.Doer, ctx.Repo.GitRepo, repo_model.MergeStyle(form.Do), form.HeadCommitID, message); err != nil { + if err := pull_service.Merge(ctx, pr, ctx.Doer, ctx.Repo.GitRepo, repo_model.MergeStyle(form.Do), form.HeadCommitID, message, false); err != nil { if models.IsErrInvalidMergeStyle(err) { ctx.Error(http.StatusMethodNotAllowed, "Invalid merge style", fmt.Errorf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do))) } else if models.IsErrMergeConflicts(err) { diff --git a/routers/web/feed/convert.go b/routers/web/feed/convert.go index 306ecf7d6..c6fc352b6 100644 --- a/routers/web/feed/convert.go +++ b/routers/web/feed/convert.go @@ -115,6 +115,12 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio link.Href = pullLink } title += ctx.TrHTMLEscapeArgs("action.merge_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath()) + case activities_model.ActionAutoMergePullRequest: + pullLink := toPullLink(act) + if link.Href == "#" { + link.Href = pullLink + } + title += ctx.TrHTMLEscapeArgs("action.auto_merge_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath()) case activities_model.ActionCloseIssue: issueLink := toIssueLink(act) if link.Href == "#" { @@ -221,7 +227,7 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio if len(comment) != 0 { desc += "\n\n" + renderMarkdown(ctx, act, comment) } - case activities_model.ActionMergePullRequest: + case activities_model.ActionMergePullRequest, activities_model.ActionAutoMergePullRequest: desc = act.GetIssueInfos()[1] case activities_model.ActionCloseIssue, activities_model.ActionReopenIssue, activities_model.ActionClosePullRequest, activities_model.ActionReopenPullRequest: desc = act.GetIssueTitle() diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index fc95bbf24..41eac7cc3 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -1002,7 +1002,7 @@ func MergePullRequest(ctx *context.Context) { } } - if err := pull_service.Merge(ctx, pr, ctx.Doer, ctx.Repo.GitRepo, repo_model.MergeStyle(form.Do), form.HeadCommitID, message); err != nil { + if err := pull_service.Merge(ctx, pr, ctx.Doer, ctx.Repo.GitRepo, repo_model.MergeStyle(form.Do), form.HeadCommitID, message, false); err != nil { if models.IsErrInvalidMergeStyle(err) { ctx.Flash.Error(ctx.Tr("repo.pulls.invalid_merge_option")) ctx.Redirect(issue.Link()) diff --git a/services/automerge/automerge.go b/services/automerge/automerge.go index ca008ebfe..3ee8af234 100644 --- a/services/automerge/automerge.go +++ b/services/automerge/automerge.go @@ -257,7 +257,7 @@ func handlePull(pullID int64, sha string) { defer baseGitRepo.Close() } - if err := pull_service.Merge(ctx, pr, doer, baseGitRepo, scheduledPRM.MergeStyle, "", scheduledPRM.Message); err != nil { + if err := pull_service.Merge(ctx, pr, doer, baseGitRepo, scheduledPRM.MergeStyle, "", scheduledPRM.Message, true); err != nil { log.Error("pull_service.Merge: %v", err) return } diff --git a/services/mailer/mail.go b/services/mailer/mail.go index a5bfa496f..85a7d107e 100644 --- a/services/mailer/mail.go +++ b/services/mailer/mail.go @@ -340,7 +340,7 @@ func createReference(issue *issues_model.Issue, comment *issues_model.Comment, a extra = fmt.Sprintf("/close/%d", time.Now().UnixNano()/1e6) case activities_model.ActionReopenIssue, activities_model.ActionReopenPullRequest: extra = fmt.Sprintf("/reopen/%d", time.Now().UnixNano()/1e6) - case activities_model.ActionMergePullRequest: + case activities_model.ActionMergePullRequest, activities_model.ActionAutoMergePullRequest: extra = fmt.Sprintf("/merge/%d", time.Now().UnixNano()/1e6) case activities_model.ActionPullRequestReadyForReview: extra = fmt.Sprintf("/ready/%d", time.Now().UnixNano()/1e6) @@ -451,7 +451,7 @@ func actionToTemplate(issue *issues_model.Issue, actionType activities_model.Act name = "close" case activities_model.ActionReopenIssue, activities_model.ActionReopenPullRequest: name = "reopen" - case activities_model.ActionMergePullRequest: + case activities_model.ActionMergePullRequest, activities_model.ActionAutoMergePullRequest: name = "merge" case activities_model.ActionPullReviewDismissed: name = "review_dismissed" diff --git a/services/mailer/mail_issue.go b/services/mailer/mail_issue.go index 33a20694e..61e276805 100644 --- a/services/mailer/mail_issue.go +++ b/services/mailer/mail_issue.go @@ -25,11 +25,12 @@ func fallbackMailSubject(issue *issues_model.Issue) string { type mailCommentContext struct { context.Context - Issue *issues_model.Issue - Doer *user_model.User - ActionType activities_model.ActionType - Content string - Comment *issues_model.Comment + Issue *issues_model.Issue + Doer *user_model.User + ActionType activities_model.ActionType + Content string + Comment *issues_model.Comment + ForceDoerNotification bool } const ( @@ -93,7 +94,7 @@ func mailIssueCommentToParticipants(ctx *mailCommentContext, mentions []*user_mo visited := make(container.Set[int64], len(unfiltered)+len(mentions)+1) // Avoid mailing the doer - if ctx.Doer.EmailNotificationsPreference != user_model.EmailNotificationsAndYourOwn { + if ctx.Doer.EmailNotificationsPreference != user_model.EmailNotificationsAndYourOwn && !ctx.ForceDoerNotification { visited.Add(ctx.Doer.ID) } @@ -181,17 +182,19 @@ func MailParticipants(issue *issues_model.Issue, doer *user_model.User, opType a content := issue.Content if opType == activities_model.ActionCloseIssue || opType == activities_model.ActionClosePullRequest || opType == activities_model.ActionReopenIssue || opType == activities_model.ActionReopenPullRequest || - opType == activities_model.ActionMergePullRequest { + opType == activities_model.ActionMergePullRequest || opType == activities_model.ActionAutoMergePullRequest { content = "" } + forceDoerNotification := opType == activities_model.ActionAutoMergePullRequest if err := mailIssueCommentToParticipants( &mailCommentContext{ - Context: context.TODO(), // TODO: use a correct context - Issue: issue, - Doer: doer, - ActionType: opType, - Content: content, - Comment: nil, + Context: context.TODO(), // TODO: use a correct context + Issue: issue, + Doer: doer, + ActionType: opType, + Content: content, + Comment: nil, + ForceDoerNotification: forceDoerNotification, }, mentions); err != nil { log.Error("mailIssueCommentToParticipants: %v", err) } diff --git a/services/pull/merge.go b/services/pull/merge.go index 0ca373018..56ee9c9a7 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -133,7 +133,7 @@ func GetDefaultMergeMessage(baseGitRepo *git.Repository, pr *issues_model.PullRe // Merge merges pull request to base repository. // Caller should check PR is ready to be merged (review and status checks) -func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, baseGitRepo *git.Repository, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string) error { +func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, baseGitRepo *git.Repository, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string, wasAutoMerged bool) error { if err := pr.LoadHeadRepo(); err != nil { log.Error("LoadHeadRepo: %v", err) return fmt.Errorf("LoadHeadRepo: %w", err) @@ -193,7 +193,11 @@ func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.U log.Error("GetOwner for issue repo [%d]: %v", pr.ID, err) } - notification.NotifyMergePullRequest(pr, doer) + if wasAutoMerged { + notification.NotifyAutoMergePullRequest(pr, doer) + } else { + notification.NotifyMergePullRequest(pr, doer) + } // Reset cached commit count cache.Remove(pr.Issue.Repo.GetCommitsCountCacheKey(pr.BaseBranch, true)) diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go index 9bd430084..bec85e8a8 100644 --- a/tests/integration/pull_merge_test.go +++ b/tests/integration/pull_merge_test.go @@ -245,11 +245,11 @@ func TestCantMergeConflict(t *testing.T) { gitRepo, err := git.OpenRepository(git.DefaultContext, repo_model.RepoPath(user1.Name, repo1.Name)) assert.NoError(t, err) - err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleMerge, "", "CONFLICT") + err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleMerge, "", "CONFLICT", false) assert.Error(t, err, "Merge should return an error due to conflict") assert.True(t, models.IsErrMergeConflicts(err), "Merge error is not a conflict error") - err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleRebase, "", "CONFLICT") + err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleRebase, "", "CONFLICT", false) assert.Error(t, err, "Merge should return an error due to conflict") assert.True(t, models.IsErrRebaseConflicts(err), "Merge error is not a conflict error") gitRepo.Close() @@ -344,7 +344,7 @@ func TestCantMergeUnrelated(t *testing.T) { BaseBranch: "base", }) - err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleMerge, "", "UNRELATED") + err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleMerge, "", "UNRELATED", false) assert.Error(t, err, "Merge should return an error due to unrelated") assert.True(t, models.IsErrMergeUnrelatedHistories(err), "Merge error is not a unrelated histories error") gitRepo.Close()