Auto merge pull requests when all checks succeeded via WebUI (#19648)

Add WebUI part of Auto merge feature

close #19621

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: delvh <dev.lh@web.de>
This commit is contained in:
6543 2022-06-11 16:44:20 +02:00 committed by GitHub
parent ce3dd04c63
commit a9cc9c0f7a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 300 additions and 137 deletions

View file

@ -1568,14 +1568,7 @@ pulls.squash_merge_pull_request = Create squash commit
pulls.merge_manually = Manually merged pulls.merge_manually = Manually merged
pulls.merge_commit_id = The merge commit ID pulls.merge_commit_id = The merge commit ID
pulls.require_signed_wont_sign = The branch requires signed commits but this merge will not be signed pulls.require_signed_wont_sign = The branch requires signed commits but this merge will not be signed
pulls.merge_pull_request_now = Merge Pull Request Now
pulls.rebase_merge_pull_request_now = Rebase and Merge Now
pulls.rebase_merge_commit_pull_request_now = Rebase and Merge Now (--no-ff)
pulls.squash_merge_pull_request_now = Squash and Merge Now
pulls.merge_pull_request_on_status_success = Merge Pull Request When All Checks Succeed
pulls.rebase_merge_pull_request_on_status_success = Rebase and Merge When All Checks Succeed
pulls.rebase_merge_commit_pull_request_on_status_success = Rebase and Merge (--no-ff) When All Checks Succeed
pulls.squash_merge_pull_request_on_status_success = Squash and Merge When All Checks Succeed
pulls.invalid_merge_option = You cannot use this merge option for this pull request. pulls.invalid_merge_option = You cannot use this merge option for this pull request.
pulls.merge_conflict = Merge Failed: There was a conflict whilst merging. Hint: Try a different strategy pulls.merge_conflict = Merge Failed: There was a conflict whilst merging. Hint: Try a different strategy
pulls.merge_conflict_summary = Error Message pulls.merge_conflict_summary = Error Message
@ -1606,14 +1599,18 @@ pulls.reopened_at = `reopened this pull request <a id="%[1]s" href="#%[1]s">%[2]
pulls.merge_instruction_hint = `You can also view <a class="show-instruction">command line instructions</a>.` pulls.merge_instruction_hint = `You can also view <a class="show-instruction">command line instructions</a>.`
pulls.merge_instruction_step1_desc = From your project repository, check out a new branch and test the changes. pulls.merge_instruction_step1_desc = From your project repository, check out a new branch and test the changes.
pulls.merge_instruction_step2_desc = Merge the changes and update on Gitea. pulls.merge_instruction_step2_desc = Merge the changes and update on Gitea.
pulls.merge_on_status_success = The pull request was scheduled to merge when all checks succeed.
pulls.merge_on_status_success_already_scheduled = This pull request is already scheduled to merge when all checks succeed. pulls.auto_merge_button_when_succeed = (When checks succeed)
pulls.pr_has_pending_merge_on_success = %[1]s scheduled this pull request to auto merge when all checks succeed %[2]s. pulls.auto_merge_when_succeed = Auto merge when all checks succeed
pulls.merge_pull_on_success_cancel = Cancel auto merge pulls.auto_merge_newly_scheduled = The pull request was scheduled to merge when all checks succeed.
pulls.pull_request_not_scheduled = This pull request is not scheduled to auto merge. pulls.auto_merge_has_pending_schedule = %[1]s scheduled this pull request to auto merge when all checks succeed %[2]s.
pulls.pull_request_schedule_canceled = The auto merge was canceled for this pull request.
pulls.pull_request_scheduled_auto_merge = `scheduled this pull request to auto merge when all checks succeed %[1]s` pulls.auto_merge_cancel_schedule = Cancel auto merge
pulls.pull_request_canceled_scheduled_auto_merge = `canceled auto merging this pull request when all checks succeed %[1]s` pulls.auto_merge_not_scheduled = This pull request is not scheduled to auto merge.
pulls.auto_merge_canceled_schedule = The auto merge was canceled for this pull request.
pulls.auto_merge_newly_scheduled_comment = `scheduled this pull request to auto merge when all checks succeed %[1]s`
pulls.auto_merge_canceled_schedule_comment = `canceled auto merging this pull request when all checks succeed %[1]s`
milestones.new = New Milestone milestones.new = New Milestone
milestones.open_tab = %d Open milestones.open_tab = %d Open

View file

@ -20,6 +20,7 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/organization"
access_model "code.gitea.io/gitea/models/perm/access" access_model "code.gitea.io/gitea/models/perm/access"
pull_model "code.gitea.io/gitea/models/pull"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
@ -36,6 +37,7 @@ import (
"code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/routers/utils" "code.gitea.io/gitea/routers/utils"
asymkey_service "code.gitea.io/gitea/services/asymkey" asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/automerge"
"code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/gitdiff" "code.gitea.io/gitea/services/gitdiff"
pull_service "code.gitea.io/gitea/services/pull" pull_service "code.gitea.io/gitea/services/pull"
@ -966,6 +968,22 @@ func MergePullRequest(ctx *context.Context) {
message += "\n\n" + form.MergeMessageField message += "\n\n" + form.MergeMessageField
} }
if form.MergeWhenChecksSucceed {
// delete all scheduled auto merges
_ = pull_model.DeleteScheduledAutoMerge(ctx, pr.ID)
// schedule auto merge
scheduled, err := automerge.ScheduleAutoMerge(ctx, ctx.Doer, pr, repo_model.MergeStyle(form.Do), message)
if err != nil {
ctx.ServerError("ScheduleAutoMerge", err)
return
} else if scheduled {
// nothing more to do ...
ctx.Flash.Success(ctx.Tr("repo.pulls.auto_merge_newly_scheduled"))
ctx.Redirect(fmt.Sprintf("%s/pulls/%d", ctx.Repo.RepoLink, pr.Index))
return
}
}
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); err != nil {
if models.IsErrInvalidMergeStyle(err) { if models.IsErrInvalidMergeStyle(err) {
ctx.Flash.Error(ctx.Tr("repo.pulls.invalid_merge_option")) ctx.Flash.Error(ctx.Tr("repo.pulls.invalid_merge_option"))
@ -1070,6 +1088,26 @@ func MergePullRequest(ctx *context.Context) {
ctx.Redirect(issue.Link()) ctx.Redirect(issue.Link())
} }
// CancelAutoMergePullRequest cancels a scheduled pr
func CancelAutoMergePullRequest(ctx *context.Context) {
issue := checkPullInfo(ctx)
if ctx.Written() {
return
}
if err := automerge.RemoveScheduledAutoMerge(ctx, ctx.Doer, issue.PullRequest); err != nil {
if db.IsErrNotExist(err) {
ctx.Flash.Error(ctx.Tr("repo.pulls.auto_merge_not_scheduled"))
ctx.Redirect(fmt.Sprintf("%s/pulls/%d", ctx.Repo.RepoLink, issue.Index))
return
}
ctx.ServerError("RemoveScheduledAutoMerge", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.pulls.auto_merge_canceled_schedule"))
ctx.Redirect(fmt.Sprintf("%s/pulls/%d", ctx.Repo.RepoLink, issue.Index))
}
func stopTimerIfAvailable(user *user_model.User, issue *models.Issue) error { func stopTimerIfAvailable(user *user_model.User, issue *models.Issue) error {
if models.StopwatchExists(user.ID, issue.ID) { if models.StopwatchExists(user.ID, issue.ID) {
if err := models.CreateOrStopIssueStopwatch(user, issue); err != nil { if err := models.CreateOrStopIssueStopwatch(user, issue); err != nil {

View file

@ -1127,6 +1127,7 @@ func RegisterRoutes(m *web.Route) {
m.Get(".patch", repo.DownloadPullPatch) m.Get(".patch", repo.DownloadPullPatch)
m.Get("/commits", context.RepoRef(), repo.ViewPullCommits) m.Get("/commits", context.RepoRef(), repo.ViewPullCommits)
m.Post("/merge", context.RepoMustNotBeArchived(), bindIgnErr(forms.MergePullRequestForm{}), repo.MergePullRequest) m.Post("/merge", context.RepoMustNotBeArchived(), bindIgnErr(forms.MergePullRequestForm{}), repo.MergePullRequest)
m.Post("/cancel_auto_merge", context.RepoMustNotBeArchived(), repo.CancelAutoMergePullRequest)
m.Post("/update", repo.UpdatePullRequest) m.Post("/update", repo.UpdatePullRequest)
m.Post("/set_allow_maintainer_edit", bindIgnErr(forms.UpdateAllowEditsForm{}), repo.SetAllowEdits) m.Post("/set_allow_maintainer_edit", bindIgnErr(forms.UpdateAllowEditsForm{}), repo.SetAllowEdits)
m.Post("/cleanup", context.RepoMustNotBeArchived(), context.RepoRef(), repo.CleanUpPullRequest) m.Post("/cleanup", context.RepoMustNotBeArchived(), context.RepoRef(), repo.CleanUpPullRequest)

View file

@ -843,8 +843,8 @@
<span class="badge">{{svg "octicon-git-merge" 16}}</span> <span class="badge">{{svg "octicon-git-merge" 16}}</span>
<span class="text grey"> <span class="text grey">
<a class="author" href="{{.Poster.HomeLink}}">{{.Poster.GetDisplayName}}</a> <a class="author" href="{{.Poster.HomeLink}}">{{.Poster.GetDisplayName}}</a>
{{if eq .Type 34}}{{$.i18n.Tr "repo.pulls.pull_request_scheduled_auto_merge" $createdStr | Safe}} {{if eq .Type 34}}{{$.i18n.Tr "repo.pulls.auto_merge_newly_scheduled_comment" $createdStr | Safe}}
{{else}}{{$.i18n.Tr "repo.pulls.pull_request_canceled_scheduled_auto_merge" $createdStr | Safe}}{{end}} {{else}}{{$.i18n.Tr "repo.pulls.auto_merge_canceled_schedule_comment" $createdStr | Safe}}{{end}}
</span> </span>
</div> </div>
{{end}} {{end}}

View file

@ -251,8 +251,14 @@
{{$.i18n.Tr (printf "repo.signing.wont_sign.%s" .WontSignReason) }} {{$.i18n.Tr (printf "repo.signing.wont_sign.%s" .WontSignReason) }}
</div> </div>
{{end}} {{end}}
{{$notAllOverridableChecksOk := or .IsBlockedByApprovals .IsBlockedByRejection .IsBlockedByOfficialReviewRequests .IsBlockedByOutdatedBranch .IsBlockedByChangedProtectedFiles (and .EnableStatusCheck (not .RequiredStatusCheckState.IsSuccess))}} {{$notAllOverridableChecksOk := or .IsBlockedByApprovals .IsBlockedByRejection .IsBlockedByOfficialReviewRequests .IsBlockedByOutdatedBranch .IsBlockedByChangedProtectedFiles (and .EnableStatusCheck (not .RequiredStatusCheckState.IsSuccess))}}
{{if and (or $.IsRepoAdmin (not $notAllOverridableChecksOk)) (or (not .AllowMerge) (not .RequireSigned) .WillSign)}}
{{/* admin can merge without checks, writer can merge when checkes succeed */}}
{{$canMergeNow := and (or $.IsRepoAdmin (not $notAllOverridableChecksOk)) (or (not .AllowMerge) (not .RequireSigned) .WillSign)}}
{{/* admin and writer both can make an auto merge schedule */}}
{{if $canMergeNow}}
{{if $notAllOverridableChecksOk}} {{if $notAllOverridableChecksOk}}
<div class="item"> <div class="item">
<i class="icon icon-octicon">{{svg "octicon-dot-fill"}}</i> <i class="icon icon-octicon">{{svg "octicon-dot-fill"}}</i>
@ -277,7 +283,6 @@
{{end}} {{end}}
{{end}} {{end}}
{{$canAutoMerge = true}}
{{if (gt .Issue.PullRequest.CommitsBehind 0)}} {{if (gt .Issue.PullRequest.CommitsBehind 0)}}
<div class="ui divider"></div> <div class="ui divider"></div>
<div class="item item-section"> <div class="item item-section">
@ -317,112 +322,111 @@
</div> </div>
{{end}} {{end}}
{{if and (or $.IsRepoAdmin (not $notAllOverridableChecksOk)) (or (not .AllowMerge) (not .RequireSigned) .WillSign)}} {{if .AllowMerge}} {{/* user is allowed to merge */}}
{{if .AllowMerge}} {{$prUnit := .Repository.MustGetUnit $.UnitTypePullRequests}}
{{$prUnit := .Repository.MustGetUnit $.UnitTypePullRequests}} {{$approvers := .Issue.PullRequest.GetApprovers}}
{{$approvers := .Issue.PullRequest.GetApprovers}} {{if or $prUnit.PullRequestsConfig.AllowMerge $prUnit.PullRequestsConfig.AllowRebase $prUnit.PullRequestsConfig.AllowRebaseMerge $prUnit.PullRequestsConfig.AllowSquash}}
{{if or $prUnit.PullRequestsConfig.AllowMerge $prUnit.PullRequestsConfig.AllowRebase $prUnit.PullRequestsConfig.AllowRebaseMerge $prUnit.PullRequestsConfig.AllowSquash}} {{$hasPendingPullRequestMergeTip := ""}}
{{if .HasPendingPullRequestMerge}}
{{$createdPRMergeStr := TimeSinceUnix .PendingPullRequestMerge.CreatedUnix $.i18n.Lang}}
{{$hasPendingPullRequestMergeTip = $.i18n.Tr "repo.pulls.auto_merge_has_pending_schedule" .PendingPullRequestMerge.Doer.Name $createdPRMergeStr}}
{{end}}
<div class="ui divider"></div>
<script>
<!-- /* eslint-disable */ -->
(() => {
const defaultMergeTitle = {{.DefaultMergeMessage}};
const defaultSquashMergeTitle = {{.DefaultSquashMergeMessage}};
const defaultMergeMessage = 'Reviewed-on: ' + {{$.Issue.HTMLURL}} + '\n' + {{$approvers}};
const mergeForm = {
'baseLink': {{.Link}},
'textCancel': {{$.i18n.Tr "cancel"}},
'textDeleteBranch': {{$.i18n.Tr "repo.branch.delete" .HeadTarget}},
'textAutoMergeButtonWhenSucceed': {{$.i18n.Tr "repo.pulls.auto_merge_button_when_succeed"}},
'textAutoMergeWhenSucceed': {{$.i18n.Tr "repo.pulls.auto_merge_when_succeed"}},
'textAutoMergeCancelSchedule': {{$.i18n.Tr "repo.pulls.auto_merge_cancel_schedule"}},
<div class="ui divider"></div> 'canMergeNow': {{$canMergeNow}},
'allOverridableChecksOk': {{not $notAllOverridableChecksOk}},
'pullHeadCommitID': {{.PullHeadCommitID}},
'isPullBranchDeletable': {{.IsPullBranchDeletable}},
'defaultDeleteBranchAfterMerge': {{$prUnit.PullRequestsConfig.DefaultDeleteBranchAfterMerge}},
'mergeMessageFieldPlaceHolder': {{$.i18n.Tr "repo.editor.commit_message_desc"}},
<script> 'hasPendingPullRequestMerge': {{.HasPendingPullRequestMerge}},
<!-- /* eslint-disable */ --> 'hasPendingPullRequestMergeTip': {{$hasPendingPullRequestMergeTip}},
(() => { };
const defaultMergeTitle = {{.DefaultMergeMessage}};
const defaultSquashMergeTitle = {{.DefaultSquashMergeMessage}};
const defaultMergeMessage = 'Reviewed-on: ' + {{$.Issue.HTMLURL}} + '\n' + {{$approvers}};
const mergeForm = {
'baseLink': {{.Link}},
'textCancel': {{$.i18n.Tr "cancel"}},
'textDeleteBranch': {{$.i18n.Tr "repo.branch.delete" .HeadTarget}},
'allOverridableChecksOk': {{not $notAllOverridableChecksOk}}, const generalHideAutoMerge = mergeForm.canMergeNow && mergeForm.allOverridableChecksOk; // if this PR can be merged now, then hide the auto merge
'pullHeadCommitID': {{.PullHeadCommitID}}, mergeForm['mergeStyles'] = [
'isPullBranchDeletable': {{.IsPullBranchDeletable}}, {
'defaultDeleteBranchAfterMerge': {{$prUnit.PullRequestsConfig.DefaultDeleteBranchAfterMerge}}, 'name': 'merge',
'mergeMessageFieldPlaceHolder': {{$.i18n.Tr "repo.editor.commit_message_desc"}}, 'allowed': {{$prUnit.PullRequestsConfig.AllowMerge}},
}; 'textDoMerge': {{$.i18n.Tr "repo.pulls.merge_pull_request"}},
mergeForm['mergeStyles'] = [ 'mergeTitleFieldText': defaultMergeTitle,
{ 'mergeMessageFieldText': defaultMergeMessage,
'name': 'merge', 'hideAutoMerge': generalHideAutoMerge,
'allowed': {{$prUnit.PullRequestsConfig.AllowMerge}}, },
'textDoMerge': {{$.i18n.Tr "repo.pulls.merge_pull_request"}}, {
'mergeTitleFieldText': defaultMergeTitle, 'name': 'rebase',
'mergeMessageFieldText': defaultMergeMessage, 'allowed': {{$prUnit.PullRequestsConfig.AllowRebase}},
}, 'textDoMerge': {{$.i18n.Tr "repo.pulls.rebase_merge_pull_request"}},
{ 'hideMergeMessageTexts': true,
'name': 'rebase', 'hideAutoMerge': generalHideAutoMerge,
'allowed': {{$prUnit.PullRequestsConfig.AllowRebase}}, },
'textDoMerge': {{$.i18n.Tr "repo.pulls.rebase_merge_pull_request"}}, {
'hideMergeMessageTexts': true, 'name': 'rebase-merge',
}, 'allowed': {{$prUnit.PullRequestsConfig.AllowRebaseMerge}},
{ 'textDoMerge': {{$.i18n.Tr "repo.pulls.rebase_merge_commit_pull_request"}},
'name': 'rebase-merge', 'mergeTitleFieldText': defaultMergeTitle,
'allowed': {{$prUnit.PullRequestsConfig.AllowRebaseMerge}}, 'mergeMessageFieldText': defaultMergeMessage,
'textDoMerge': {{$.i18n.Tr "repo.pulls.rebase_merge_commit_pull_request"}}, 'hideAutoMerge': generalHideAutoMerge,
'mergeTitleFieldText': defaultMergeTitle, },
'mergeMessageFieldText': defaultMergeMessage, {
}, 'name': 'squash',
{ 'allowed': {{$prUnit.PullRequestsConfig.AllowSquash}},
'name': 'squash', 'textDoMerge': {{$.i18n.Tr "repo.pulls.squash_merge_pull_request"}},
'allowed': {{$prUnit.PullRequestsConfig.AllowSquash}}, 'mergeTitleFieldText': defaultSquashMergeTitle,
'textDoMerge': {{$.i18n.Tr "repo.pulls.squash_merge_pull_request"}}, 'mergeMessageFieldText': defaultMergeMessage,
'mergeTitleFieldText': defaultSquashMergeTitle, 'hideAutoMerge': generalHideAutoMerge,
'mergeMessageFieldText': defaultMergeMessage, },
}, {
{ 'name': 'manually-merged',
'name': 'manually-merged', 'allowed': {{and $prUnit.PullRequestsConfig.AllowManualMerge $.IsRepoAdmin}},
'allowed': {{and $prUnit.PullRequestsConfig.AllowManualMerge $.IsRepoAdmin}}, 'textDoMerge': {{$.i18n.Tr "repo.pulls.merge_manually"}},
'textDoMerge': {{$.i18n.Tr "repo.pulls.merge_manually"}}, 'hideMergeMessageTexts': true,
'hideMergeMessageTexts': true, 'hideAutoMerge': true,
} }
]; ];
window.config.pageData.pullRequestMergeForm = mergeForm; window.config.pageData.pullRequestMergeForm = mergeForm;
})(); })();
</script> </script>
<div id="pull-request-merge-form"></div> <div id="pull-request-merge-form"></div>
{{if .ShowMergeInstructions}} {{if .ShowMergeInstructions}}
<div class="instruct-toggle mt-3"> {{$.i18n.Tr "repo.pulls.merge_instruction_hint" | Safe}} </div> {{template "repo/issue/view_content/pull_merge_instruction" (dict "i18n" .i18n "Issue" .Issue)}}
<div class="instruct-content" style="display:none">
<div class="ui divider"></div>
<div><h3 class="di">{{$.i18n.Tr "step1"}} </h3>{{$.i18n.Tr "repo.pulls.merge_instruction_step1_desc"}}</div>
<div class="ui secondary segment">
{{if eq .Issue.PullRequest.Flow 0}}
<div>git checkout -b {{if ne .Issue.PullRequest.HeadRepo.ID .Issue.PullRequest.BaseRepo.ID}}{{.Issue.PullRequest.HeadRepo.OwnerName}}-{{end}}{{.Issue.PullRequest.HeadBranch}} {{.Issue.PullRequest.BaseBranch}}</div>
<div>git pull {{if ne .Issue.PullRequest.HeadRepo.ID .Issue.PullRequest.BaseRepo.ID}}{{.Issue.PullRequest.HeadRepo.HTMLURL}}{{else}}origin{{end}} {{.Issue.PullRequest.HeadBranch}}</div>
{{else}}
<div>git fetch origin {{.Issue.PullRequest.GetGitRefName}}:{{.Issue.PullRequest.HeadBranch}}</div>
{{end}}
</div>
<div><h3 class="di">{{$.i18n.Tr "step2"}} </h3>{{$.i18n.Tr "repo.pulls.merge_instruction_step2_desc"}}</div>
<div class="ui secondary segment">
<div>git checkout {{.Issue.PullRequest.BaseBranch}}</div>
<div>git merge --no-ff {{if ne .Issue.PullRequest.HeadRepo.ID .Issue.PullRequest.BaseRepo.ID}}{{.Issue.PullRequest.HeadRepo.OwnerName}}-{{end}}{{.Issue.PullRequest.HeadBranch}}</div>
<div>git push origin {{.Issue.PullRequest.BaseBranch}}</div>
</div>
</div>
{{end}}
{{else}}
<div class="ui divider"></div>
<div class="item text red">
{{svg "octicon-x"}}
{{$.i18n.Tr "repo.pulls.no_merge_desc"}}
</div>
<div class="item">
{{svg "octicon-info"}}
{{$.i18n.Tr "repo.pulls.no_merge_helper"}}
</div>
{{end}} {{end}}
{{else}} {{else}}
{{/* no merge style was set in repo setting: not or ($prUnit.PullRequestsConfig.AllowMerge ...) */}}
<div class="ui divider"></div> <div class="ui divider"></div>
<div class="item text red">
{{svg "octicon-x"}}
{{$.i18n.Tr "repo.pulls.no_merge_desc"}}
</div>
<div class="item"> <div class="item">
{{svg "octicon-info"}} {{svg "octicon-info"}}
{{$.i18n.Tr "repo.pulls.no_merge_access"}} {{$.i18n.Tr "repo.pulls.no_merge_helper"}}
</div> </div>
{{end}} {{end}} {{/* end if the repo was set to use any merge style */}}
{{end}} {{else}}
{{/* user is not allowed to merge */}}
<div class="ui divider"></div>
<div class="item">
{{svg "octicon-info"}}
{{$.i18n.Tr "repo.pulls.no_merge_access"}}
</div>
{{end}} {{/* end if user is allowed to merge or not */}}
{{else}} {{else}}
{{/* Merge conflict without specific file. Suggest manual merge, only if all reviews and status checks OK. */}} {{/* Merge conflict without specific file. Suggest manual merge, only if all reviews and status checks OK. */}}
{{if .IsBlockedByApprovals}} {{if .IsBlockedByApprovals}}

View file

@ -0,0 +1,19 @@
<div class="instruct-toggle mt-3"> {{$.i18n.Tr "repo.pulls.merge_instruction_hint" | Safe}} </div>
<div class="instruct-content" style="display:none">
<div class="ui divider"></div>
<div><h3 class="di">{{$.i18n.Tr "step1"}} </h3>{{$.i18n.Tr "repo.pulls.merge_instruction_step1_desc"}}</div>
<div class="ui secondary segment">
{{if eq $.Issue.PullRequest.Flow 0}}
<div>git checkout -b {{if ne $.Issue.PullRequest.HeadRepo.ID $.Issue.PullRequest.BaseRepo.ID}}{{$.Issue.PullRequest.HeadRepo.OwnerName}}-{{end}}{{$.Issue.PullRequest.HeadBranch}} {{$.Issue.PullRequest.BaseBranch}}</div>
<div>git pull {{if ne $.Issue.PullRequest.HeadRepo.ID $.Issue.PullRequest.BaseRepo.ID}}{{$.Issue.PullRequest.HeadRepo.HTMLURL}}{{else}}origin{{end}} {{$.Issue.PullRequest.HeadBranch}}</div>
{{else}}
<div>git fetch origin {{$.Issue.PullRequest.GetGitRefName}}:{{$.Issue.PullRequest.HeadBranch}}</div>
{{end}}
</div>
<div><h3 class="di">{{$.i18n.Tr "step2"}} </h3>{{$.i18n.Tr "repo.pulls.merge_instruction_step2_desc"}}</div>
<div class="ui secondary segment">
<div>git checkout {{$.Issue.PullRequest.BaseBranch}}</div>
<div>git merge --no-ff {{if ne $.Issue.PullRequest.HeadRepo.ID $.Issue.PullRequest.BaseRepo.ID}}{{$.Issue.PullRequest.HeadRepo.OwnerName}}-{{end}}{{$.Issue.PullRequest.HeadBranch}}</div>
<div>git push origin {{$.Issue.PullRequest.BaseBranch}}</div>
</div>
</div>

View file

@ -1,9 +1,23 @@
<template> <template>
<!--
if this component is shown, either the user is admin (can do merge without checks), or they is a writer who has the permission to do merge
if the user is a writer and can't do merge now (canMergeNow==false), then only show the Auto Merge for them
How to test the UI manually:
* Method 1: manually set some variables in pull.tmpl, eg: {{$notAllOverridableChecksOk = true}} {{$canMergeNow = false}}
* Method 2: make a protected branch, then set state=pending/success :
curl -X POST ${root_url}/api/v1/repos/${owner}/${repo}/statuses/${sha} \
-H "accept: application/json" -H "authorization: Basic $base64_auth" -H "Content-Type: application/json" \
-d '{"context": "test/context", "description": "description", "state": "${state}", "target_url": "http://localhost"}'
-->
<div> <div>
<!-- eslint-disable -->
<div v-if="mergeForm.hasPendingPullRequestMerge" v-html="mergeForm.hasPendingPullRequestMergeTip" class="ui info message"></div>
<div class="ui form" v-if="showActionForm"> <div class="ui form" v-if="showActionForm">
<form :action="mergeForm.baseLink+'/merge'" method="post"> <form :action="mergeForm.baseLink+'/merge'" method="post">
<input type="hidden" name="_csrf" :value="csrfToken"> <input type="hidden" name="_csrf" :value="csrfToken">
<input type="hidden" name="head_commit_id" v-model="mergeForm.pullHeadCommitID"> <input type="hidden" name="head_commit_id" v-model="mergeForm.pullHeadCommitID">
<input type="hidden" name="merge_when_checks_succeed" v-model="autoMergeWhenSucceed">
<template v-if="!mergeStyleDetail.hideMergeMessageTexts"> <template v-if="!mergeStyleDetail.hideMergeMessageTexts">
<div class="field"> <div class="field">
@ -14,39 +28,72 @@
</div> </div>
</template> </template>
<button class="ui button" :class="[mergeForm.allOverridableChecksOk?'green':'red']" type="submit" name="do" :value="mergeStyle"> <button class="ui button" :class="mergeButtonStyleClass" type="submit" name="do" :value="mergeStyle">
{{ mergeStyleDetail.textDoMerge }} {{ mergeStyleDetail.textDoMerge }}
<template v-if="autoMergeWhenSucceed">
{{ mergeForm.textAutoMergeButtonWhenSucceed }}
</template>
</button> </button>
<button class="ui button merge-cancel" @click="toggleActionForm(false)"> <button class="ui button merge-cancel" @click="toggleActionForm(false)">
{{ mergeForm.textCancel }} {{ mergeForm.textCancel }}
</button> </button>
<div class="ui checkbox ml-2" v-if="mergeForm.isPullBranchDeletable"> <div class="ui checkbox ml-2" v-if="mergeForm.isPullBranchDeletable && !autoMergeWhenSucceed">
<input name="delete_branch_after_merge" type="checkbox" v-model="deleteBranchAfterMerge" id="delete-branch-after-merge"> <input name="delete_branch_after_merge" type="checkbox" v-model="deleteBranchAfterMerge" id="delete-branch-after-merge">
<label for="delete-branch-after-merge">{{ mergeForm.textDeleteBranch }}</label> <label for="delete-branch-after-merge">{{ mergeForm.textDeleteBranch }}</label>
</div> </div>
</form> </form>
</div> </div>
<template v-if="!showActionForm"> <div v-if="!showActionForm" class="df">
<div class="ui buttons merge-button" :class="[mergeForm.allOverridableChecksOk?'green':'red']" @click="toggleActionForm(true)"> <!-- the merge button -->
<div class="ui buttons merge-button" :class="mergeButtonStyleClass" @click="toggleActionForm(true)" >
<button class="ui button"> <button class="ui button">
<svg-icon name="octicon-git-merge"/> <svg-icon name="octicon-git-merge"/>
<span class="button-text">{{ mergeStyleDetail.textDoMerge }}</span> <span class="button-text">
{{ mergeStyleDetail.textDoMerge }}
<template v-if="autoMergeWhenSucceed">
{{ mergeForm.textAutoMergeButtonWhenSucceed }}
</template>
</span>
</button> </button>
<div class="ui dropdown icon button no-text" @click.stop="showMergeStyleMenu = !showMergeStyleMenu" v-if="mergeStyleAllowedCount>1"> <div class="ui dropdown icon button no-text" @click.stop="showMergeStyleMenu = !showMergeStyleMenu" v-if="mergeStyleAllowedCount>1">
<svg-icon name="octicon-triangle-down" :size="14"/> <svg-icon name="octicon-triangle-down" :size="14"/>
<div class="menu" :class="{'show':showMergeStyleMenu}"> <div class="menu" :class="{'show':showMergeStyleMenu}">
<template v-for="msd in mergeForm.mergeStyles"> <template v-for="msd in mergeForm.mergeStyles">
<div class="item" v-if="msd.allowed" :key="msd.name" @click.stop="mergeStyle=msd.name"> <!-- if can merge now, show one action "merge now", and an action "auto merge when succeed" -->
{{ msd.textDoMerge }} <div class="item" v-if="msd.allowed && mergeForm.canMergeNow" :key="msd.name" @click.stop="switchMergeStyle(msd.name)">
<div class="action-text">
{{ msd.textDoMerge }}
</div>
<div v-if="!msd.hideAutoMerge" class="auto-merge-small" @click.stop="switchMergeStyle(msd.name, true)">
<svg-icon name="octicon-clock" :size="14"/>
<div class="auto-merge-tip">
{{ mergeForm.textAutoMergeWhenSucceed }}
</div>
</div>
</div>
<!-- if can NOT merge now, only show one action "auto merge when succeed" -->
<div class="item" v-if="msd.allowed && !mergeForm.canMergeNow && !msd.hideAutoMerge" :key="msd.name" @click.stop="switchMergeStyle(msd.name, true)">
<div class="action-text">
{{ msd.textDoMerge }} {{ mergeForm.textAutoMergeButtonWhenSucceed }}
</div>
</div> </div>
</template> </template>
</div> </div>
</div> </div>
</div> </div>
</template>
<!-- the cancel auto merge button -->
<form v-if="mergeForm.hasPendingPullRequestMerge" :action="mergeForm.baseLink+'/cancel_auto_merge'" method="post" class="ml-4">
<input type="hidden" name="_csrf" :value="csrfToken">
<button class="ui button">
{{ mergeForm.textAutoMergeCancelSchedule }}
</button>
</form>
</div>
</div> </div>
</template> </template>
@ -68,6 +115,7 @@ export default {
mergeTitleFieldValue: '', mergeTitleFieldValue: '',
mergeMessageFieldValue: '', mergeMessageFieldValue: '',
deleteBranchAfterMerge: false, deleteBranchAfterMerge: false,
autoMergeWhenSucceed: false,
mergeStyle: '', mergeStyle: '',
mergeStyleDetail: { // dummy only, these values will come from one of the mergeForm.mergeStyles mergeStyleDetail: { // dummy only, these values will come from one of the mergeForm.mergeStyles
@ -82,6 +130,13 @@ export default {
showActionForm: false, showActionForm: false,
}), }),
computed: {
mergeButtonStyleClass() {
if (this.mergeForm.allOverridableChecksOk) return 'green';
return this.autoMergeWhenSucceed ? 'blue' : 'red';
}
},
watch: { watch: {
mergeStyle(val) { mergeStyle(val) {
this.mergeStyleDetail = this.mergeForm.mergeStyles.find((e) => e.name === val); this.mergeStyleDetail = this.mergeForm.mergeStyles.find((e) => e.name === val);
@ -90,7 +145,7 @@ export default {
created() { created() {
this.mergeStyleAllowedCount = this.mergeForm.mergeStyles.reduce((v, msd) => v + (msd.allowed ? 1 : 0), 0); this.mergeStyleAllowedCount = this.mergeForm.mergeStyles.reduce((v, msd) => v + (msd.allowed ? 1 : 0), 0);
this.mergeStyle = this.mergeForm.mergeStyles.find((e) => e.allowed)?.name; this.switchMergeStyle(this.mergeForm.mergeStyles.find((e) => e.allowed)?.name, !this.mergeForm.canMergeNow);
}, },
mounted() { mounted() {
@ -111,7 +166,11 @@ export default {
this.deleteBranchAfterMerge = this.mergeForm.defaultDeleteBranchAfterMerge; this.deleteBranchAfterMerge = this.mergeForm.defaultDeleteBranchAfterMerge;
this.mergeTitleFieldValue = this.mergeStyleDetail.mergeTitleFieldText; this.mergeTitleFieldValue = this.mergeStyleDetail.mergeTitleFieldText;
this.mergeMessageFieldValue = this.mergeStyleDetail.mergeMessageFieldText; this.mergeMessageFieldValue = this.mergeStyleDetail.mergeMessageFieldText;
} },
switchMergeStyle(name, autoMerge = false) {
this.mergeStyle = name;
this.autoMergeWhenSucceed = autoMerge;
},
}, },
}; };
</script> </script>
@ -124,4 +183,59 @@ export default {
.ui.checkbox label { .ui.checkbox label {
cursor: pointer; cursor: pointer;
} }
/* make the dropdown list left-aligned */
.ui.merge-button {
position: relative;
}
.ui.merge-button .ui.dropdown {
position: static;
}
.ui.merge-button > .ui.dropdown:last-child > .menu:not(.left) {
left: 0;
right: auto;
}
.ui.merge-button .ui.dropdown .menu > .item {
display: flex;
align-items: stretch;
padding: 0 !important; /* polluted by semantic.css: .ui.dropdown .menu > .item { !important } */
}
/* merge style list item */
.action-text {
padding: 0.8rem;
flex: 1
}
.auto-merge-small {
width: 40px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.auto-merge-small .auto-merge-tip {
display: none;
left: 38px;
top: -1px;
bottom: -1px;
position: absolute;
align-items: center;
color: var(--color-info-text);
background-color: var(--color-info-bg);
border: 1px solid var(--color-info-border);
border-left: none;
padding-right: 1rem;
}
.auto-merge-small:hover {
color: var(--color-info-text);
background-color: var(--color-info-bg);
border: 1px solid var(--color-info-border);
}
.auto-merge-small:hover .auto-merge-tip {
display: flex;
}
</style> </style>

View file

@ -1,6 +1,7 @@
import octiconChevronDown from '../../public/img/svg/octicon-chevron-down.svg'; import octiconChevronDown from '../../public/img/svg/octicon-chevron-down.svg';
import octiconChevronRight from '../../public/img/svg/octicon-chevron-right.svg'; import octiconChevronRight from '../../public/img/svg/octicon-chevron-right.svg';
import octiconCopy from '../../public/img/svg/octicon-copy.svg'; import octiconCopy from '../../public/img/svg/octicon-copy.svg';
import octiconClock from '../../public/img/svg/octicon-clock.svg';
import octiconGitMerge from '../../public/img/svg/octicon-git-merge.svg'; import octiconGitMerge from '../../public/img/svg/octicon-git-merge.svg';
import octiconGitPullRequest from '../../public/img/svg/octicon-git-pull-request.svg'; import octiconGitPullRequest from '../../public/img/svg/octicon-git-pull-request.svg';
import octiconIssueClosed from '../../public/img/svg/octicon-issue-closed.svg'; import octiconIssueClosed from '../../public/img/svg/octicon-issue-closed.svg';
@ -23,6 +24,7 @@ export const svgs = {
'octicon-chevron-down': octiconChevronDown, 'octicon-chevron-down': octiconChevronDown,
'octicon-chevron-right': octiconChevronRight, 'octicon-chevron-right': octiconChevronRight,
'octicon-copy': octiconCopy, 'octicon-copy': octiconCopy,
'octicon-clock': octiconClock,
'octicon-git-merge': octiconGitMerge, 'octicon-git-merge': octiconGitMerge,
'octicon-git-pull-request': octiconGitPullRequest, 'octicon-git-pull-request': octiconGitPullRequest,
'octicon-issue-closed': octiconIssueClosed, 'octicon-issue-closed': octiconIssueClosed,

View file

@ -2003,14 +2003,6 @@ table th[data-sortt-desc] {
margin-right: 0 !important; margin-right: 0 !important;
} }
/* limit width of all direct dropdown menu children */
/* https://github.com/go-gitea/gitea/pull/10835 */
.dropdown:not(.selection) > .menu:not(.review-box) > *:not(.header) {
max-width: 300px;
overflow-x: hidden;
text-overflow: ellipsis;
}
.ui.dropdown .menu .item { .ui.dropdown .menu .item {
border-radius: 0; border-radius: 0;
} }

View file

@ -1055,10 +1055,6 @@
.merge-section { .merge-section {
background-color: var(--color-box-body); background-color: var(--color-box-body);
.item {
padding: .25rem 0;
}
.item-section { .item-section {
display: flex; display: flex;
align-items: center; align-items: center;