Add WebUI part of Auto merge feature close #19621 Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: delvh <dev.lh@web.de>tags/v1.18.0-dev
@@ -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 |
@@ -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 { |
@@ -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) |
@@ -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}} |
@@ -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}} |
@@ -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> |
@@ -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> |
@@ -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, |
@@ -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; | |||
} |
@@ -1055,10 +1055,6 @@ | |||
.merge-section { | |||
background-color: var(--color-box-body); | |||
.item { | |||
padding: .25rem 0; | |||
} | |||
.item-section { | |||
display: flex; | |||
align-items: center; |