diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index c4ad714717..b1c3247315 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1568,14 +1568,7 @@ pulls.squash_merge_pull_request = Create squash commit pulls.merge_manually = Manually merged 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.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.merge_conflict = Merge Failed: There was a conflict whilst merging. Hint: Try a different strategy 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_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_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.pr_has_pending_merge_on_success = %[1]s scheduled this pull request to auto merge when all checks succeed %[2]s. -pulls.merge_pull_on_success_cancel = Cancel auto merge -pulls.pull_request_not_scheduled = This pull request is not scheduled to auto merge. -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.pull_request_canceled_scheduled_auto_merge = `canceled auto merging this pull request when all checks succeed %[1]s` + +pulls.auto_merge_button_when_succeed = (When checks succeed) +pulls.auto_merge_when_succeed = Auto merge when all checks succeed +pulls.auto_merge_newly_scheduled = The pull request was scheduled to merge when all checks succeed. +pulls.auto_merge_has_pending_schedule = %[1]s scheduled this pull request to auto merge when all checks succeed %[2]s. + +pulls.auto_merge_cancel_schedule = Cancel auto merge +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.open_tab = %d Open diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 8df4ccc607..d698f1c49a 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -20,6 +20,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" 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" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" @@ -36,6 +37,7 @@ import ( "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/routers/utils" 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/gitdiff" pull_service "code.gitea.io/gitea/services/pull" @@ -966,6 +968,22 @@ func MergePullRequest(ctx *context.Context) { 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 models.IsErrInvalidMergeStyle(err) { ctx.Flash.Error(ctx.Tr("repo.pulls.invalid_merge_option")) @@ -1070,6 +1088,26 @@ func MergePullRequest(ctx *context.Context) { 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 { if models.StopwatchExists(user.ID, issue.ID) { if err := models.CreateOrStopIssueStopwatch(user, issue); err != nil { diff --git a/routers/web/web.go b/routers/web/web.go index bf4c4662af..88a446d067 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1127,6 +1127,7 @@ func RegisterRoutes(m *web.Route) { m.Get(".patch", repo.DownloadPullPatch) m.Get("/commits", context.RepoRef(), repo.ViewPullCommits) 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("/set_allow_maintainer_edit", bindIgnErr(forms.UpdateAllowEditsForm{}), repo.SetAllowEdits) m.Post("/cleanup", context.RepoMustNotBeArchived(), context.RepoRef(), repo.CleanUpPullRequest) diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index 235f4c8fc2..0258a9f969 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -843,8 +843,8 @@ <span class="badge">{{svg "octicon-git-merge" 16}}</span> <span class="text grey"> <a class="author" href="{{.Poster.HomeLink}}">{{.Poster.GetDisplayName}}</a> - {{if eq .Type 34}}{{$.i18n.Tr "repo.pulls.pull_request_scheduled_auto_merge" $createdStr | Safe}} - {{else}}{{$.i18n.Tr "repo.pulls.pull_request_canceled_scheduled_auto_merge" $createdStr | Safe}}{{end}} + {{if eq .Type 34}}{{$.i18n.Tr "repo.pulls.auto_merge_newly_scheduled_comment" $createdStr | Safe}} + {{else}}{{$.i18n.Tr "repo.pulls.auto_merge_canceled_schedule_comment" $createdStr | Safe}}{{end}} </span> </div> {{end}} diff --git a/templates/repo/issue/view_content/pull.tmpl b/templates/repo/issue/view_content/pull.tmpl index c764138fa0..d2282f07f6 100644 --- a/templates/repo/issue/view_content/pull.tmpl +++ b/templates/repo/issue/view_content/pull.tmpl @@ -251,8 +251,14 @@ {{$.i18n.Tr (printf "repo.signing.wont_sign.%s" .WontSignReason) }} </div> {{end}} + {{$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}} <div class="item"> <i class="icon icon-octicon">{{svg "octicon-dot-fill"}}</i> @@ -277,7 +283,6 @@ {{end}} {{end}} - {{$canAutoMerge = true}} {{if (gt .Issue.PullRequest.CommitsBehind 0)}} <div class="ui divider"></div> <div class="item item-section"> @@ -317,112 +322,111 @@ </div> {{end}} - {{if and (or $.IsRepoAdmin (not $notAllOverridableChecksOk)) (or (not .AllowMerge) (not .RequireSigned) .WillSign)}} - {{if .AllowMerge}} - {{$prUnit := .Repository.MustGetUnit $.UnitTypePullRequests}} - {{$approvers := .Issue.PullRequest.GetApprovers}} - {{if or $prUnit.PullRequestsConfig.AllowMerge $prUnit.PullRequestsConfig.AllowRebase $prUnit.PullRequestsConfig.AllowRebaseMerge $prUnit.PullRequestsConfig.AllowSquash}} + {{if .AllowMerge}} {{/* user is allowed to merge */}} + {{$prUnit := .Repository.MustGetUnit $.UnitTypePullRequests}} + {{$approvers := .Issue.PullRequest.GetApprovers}} + {{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> - <!-- /* 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}}, + 'hasPendingPullRequestMerge': {{.HasPendingPullRequestMerge}}, + 'hasPendingPullRequestMergeTip': {{$hasPendingPullRequestMergeTip}}, + }; - 'allOverridableChecksOk': {{not $notAllOverridableChecksOk}}, - 'pullHeadCommitID': {{.PullHeadCommitID}}, - 'isPullBranchDeletable': {{.IsPullBranchDeletable}}, - 'defaultDeleteBranchAfterMerge': {{$prUnit.PullRequestsConfig.DefaultDeleteBranchAfterMerge}}, - 'mergeMessageFieldPlaceHolder': {{$.i18n.Tr "repo.editor.commit_message_desc"}}, - }; - mergeForm['mergeStyles'] = [ - { - 'name': 'merge', - 'allowed': {{$prUnit.PullRequestsConfig.AllowMerge}}, - 'textDoMerge': {{$.i18n.Tr "repo.pulls.merge_pull_request"}}, - 'mergeTitleFieldText': defaultMergeTitle, - 'mergeMessageFieldText': defaultMergeMessage, - }, - { - 'name': 'rebase', - '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"}}, - 'mergeTitleFieldText': defaultMergeTitle, - 'mergeMessageFieldText': defaultMergeMessage, - }, - { - 'name': 'squash', - 'allowed': {{$prUnit.PullRequestsConfig.AllowSquash}}, - 'textDoMerge': {{$.i18n.Tr "repo.pulls.squash_merge_pull_request"}}, - 'mergeTitleFieldText': defaultSquashMergeTitle, - 'mergeMessageFieldText': defaultMergeMessage, - }, - { - 'name': 'manually-merged', - 'allowed': {{and $prUnit.PullRequestsConfig.AllowManualMerge $.IsRepoAdmin}}, - 'textDoMerge': {{$.i18n.Tr "repo.pulls.merge_manually"}}, - 'hideMergeMessageTexts': true, - } - ]; - window.config.pageData.pullRequestMergeForm = mergeForm; - })(); - </script> + const generalHideAutoMerge = mergeForm.canMergeNow && mergeForm.allOverridableChecksOk; // if this PR can be merged now, then hide the auto merge + mergeForm['mergeStyles'] = [ + { + 'name': 'merge', + 'allowed': {{$prUnit.PullRequestsConfig.AllowMerge}}, + 'textDoMerge': {{$.i18n.Tr "repo.pulls.merge_pull_request"}}, + 'mergeTitleFieldText': defaultMergeTitle, + 'mergeMessageFieldText': defaultMergeMessage, + 'hideAutoMerge': generalHideAutoMerge, + }, + { + 'name': 'rebase', + 'allowed': {{$prUnit.PullRequestsConfig.AllowRebase}}, + 'textDoMerge': {{$.i18n.Tr "repo.pulls.rebase_merge_pull_request"}}, + 'hideMergeMessageTexts': true, + 'hideAutoMerge': generalHideAutoMerge, + }, + { + 'name': 'rebase-merge', + 'allowed': {{$prUnit.PullRequestsConfig.AllowRebaseMerge}}, + 'textDoMerge': {{$.i18n.Tr "repo.pulls.rebase_merge_commit_pull_request"}}, + 'mergeTitleFieldText': defaultMergeTitle, + 'mergeMessageFieldText': defaultMergeMessage, + 'hideAutoMerge': generalHideAutoMerge, + }, + { + 'name': 'squash', + 'allowed': {{$prUnit.PullRequestsConfig.AllowSquash}}, + 'textDoMerge': {{$.i18n.Tr "repo.pulls.squash_merge_pull_request"}}, + 'mergeTitleFieldText': defaultSquashMergeTitle, + 'mergeMessageFieldText': defaultMergeMessage, + 'hideAutoMerge': generalHideAutoMerge, + }, + { + 'name': 'manually-merged', + 'allowed': {{and $prUnit.PullRequestsConfig.AllowManualMerge $.IsRepoAdmin}}, + 'textDoMerge': {{$.i18n.Tr "repo.pulls.merge_manually"}}, + 'hideMergeMessageTexts': true, + 'hideAutoMerge': true, + } + ]; + window.config.pageData.pullRequestMergeForm = mergeForm; + })(); + </script> - <div id="pull-request-merge-form"></div> + <div id="pull-request-merge-form"></div> - {{if .ShowMergeInstructions}} - <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> - {{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> + {{if .ShowMergeInstructions}} + {{template "repo/issue/view_content/pull_merge_instruction" (dict "i18n" .i18n "Issue" .Issue)}} {{end}} {{else}} + {{/* no merge style was set in repo setting: not or ($prUnit.PullRequestsConfig.AllowMerge ...) */}} <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_access"}} + {{$.i18n.Tr "repo.pulls.no_merge_helper"}} </div> - {{end}} - {{end}} + {{end}} {{/* end if the repo was set to use any merge style */}} + {{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}} {{/* Merge conflict without specific file. Suggest manual merge, only if all reviews and status checks OK. */}} {{if .IsBlockedByApprovals}} diff --git a/templates/repo/issue/view_content/pull_merge_instruction.tmpl b/templates/repo/issue/view_content/pull_merge_instruction.tmpl new file mode 100644 index 0000000000..0ed70860f3 --- /dev/null +++ b/templates/repo/issue/view_content/pull_merge_instruction.tmpl @@ -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> diff --git a/web_src/js/components/PullRequestMergeForm.vue b/web_src/js/components/PullRequestMergeForm.vue index 40398a65cb..75fbceb800 100644 --- a/web_src/js/components/PullRequestMergeForm.vue +++ b/web_src/js/components/PullRequestMergeForm.vue @@ -1,9 +1,23 @@ <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> + <!-- eslint-disable --> + <div v-if="mergeForm.hasPendingPullRequestMerge" v-html="mergeForm.hasPendingPullRequestMergeTip" class="ui info message"></div> + <div class="ui form" v-if="showActionForm"> <form :action="mergeForm.baseLink+'/merge'" method="post"> <input type="hidden" name="_csrf" :value="csrfToken"> <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"> <div class="field"> @@ -14,39 +28,72 @@ </div> </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 }} + <template v-if="autoMergeWhenSucceed"> + {{ mergeForm.textAutoMergeButtonWhenSucceed }} + </template> </button> <button class="ui button merge-cancel" @click="toggleActionForm(false)"> {{ mergeForm.textCancel }} </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"> <label for="delete-branch-after-merge">{{ mergeForm.textDeleteBranch }}</label> </div> </form> </div> - <template v-if="!showActionForm"> - <div class="ui buttons merge-button" :class="[mergeForm.allOverridableChecksOk?'green':'red']" @click="toggleActionForm(true)"> + <div v-if="!showActionForm" class="df"> + <!-- the merge button --> + <div class="ui buttons merge-button" :class="mergeButtonStyleClass" @click="toggleActionForm(true)" > <button class="ui button"> <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> <div class="ui dropdown icon button no-text" @click.stop="showMergeStyleMenu = !showMergeStyleMenu" v-if="mergeStyleAllowedCount>1"> <svg-icon name="octicon-triangle-down" :size="14"/> <div class="menu" :class="{'show':showMergeStyleMenu}"> <template v-for="msd in mergeForm.mergeStyles"> - <div class="item" v-if="msd.allowed" :key="msd.name" @click.stop="mergeStyle=msd.name"> - {{ msd.textDoMerge }} + <!-- if can merge now, show one action "merge now", and an action "auto merge when succeed" --> + <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> </template> </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> </template> @@ -68,6 +115,7 @@ export default { mergeTitleFieldValue: '', mergeMessageFieldValue: '', deleteBranchAfterMerge: false, + autoMergeWhenSucceed: false, mergeStyle: '', mergeStyleDetail: { // dummy only, these values will come from one of the mergeForm.mergeStyles @@ -82,6 +130,13 @@ export default { showActionForm: false, }), + computed: { + mergeButtonStyleClass() { + if (this.mergeForm.allOverridableChecksOk) return 'green'; + return this.autoMergeWhenSucceed ? 'blue' : 'red'; + } + }, + watch: { mergeStyle(val) { this.mergeStyleDetail = this.mergeForm.mergeStyles.find((e) => e.name === val); @@ -90,7 +145,7 @@ export default { created() { 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() { @@ -111,7 +166,11 @@ export default { this.deleteBranchAfterMerge = this.mergeForm.defaultDeleteBranchAfterMerge; this.mergeTitleFieldValue = this.mergeStyleDetail.mergeTitleFieldText; this.mergeMessageFieldValue = this.mergeStyleDetail.mergeMessageFieldText; - } + }, + switchMergeStyle(name, autoMerge = false) { + this.mergeStyle = name; + this.autoMergeWhenSucceed = autoMerge; + }, }, }; </script> @@ -124,4 +183,59 @@ export default { .ui.checkbox label { 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> diff --git a/web_src/js/svg.js b/web_src/js/svg.js index 926f0a5d05..9c39852c30 100644 --- a/web_src/js/svg.js +++ b/web_src/js/svg.js @@ -1,6 +1,7 @@ import octiconChevronDown from '../../public/img/svg/octicon-chevron-down.svg'; import octiconChevronRight from '../../public/img/svg/octicon-chevron-right.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 octiconGitPullRequest from '../../public/img/svg/octicon-git-pull-request.svg'; import octiconIssueClosed from '../../public/img/svg/octicon-issue-closed.svg'; @@ -23,6 +24,7 @@ export const svgs = { 'octicon-chevron-down': octiconChevronDown, 'octicon-chevron-right': octiconChevronRight, 'octicon-copy': octiconCopy, + 'octicon-clock': octiconClock, 'octicon-git-merge': octiconGitMerge, 'octicon-git-pull-request': octiconGitPullRequest, 'octicon-issue-closed': octiconIssueClosed, diff --git a/web_src/less/_base.less b/web_src/less/_base.less index c029cb9485..4d7f69e3b3 100644 --- a/web_src/less/_base.less +++ b/web_src/less/_base.less @@ -2003,14 +2003,6 @@ table th[data-sortt-desc] { 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 { border-radius: 0; } diff --git a/web_src/less/_repository.less b/web_src/less/_repository.less index d73cb90330..37a5017fbd 100644 --- a/web_src/less/_repository.less +++ b/web_src/less/_repository.less @@ -1055,10 +1055,6 @@ .merge-section { background-color: var(--color-box-body); - .item { - padding: .25rem 0; - } - .item-section { display: flex; align-items: center;