From 6965bf846f0896563a966032c71d77f29ffdd1d5 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Sun, 19 Dec 2021 20:08:04 +0000 Subject: [PATCH 1/4] Abort merge if head has been updated before pressing merge It is possible that a PR head may be pushed to between the merge page being shown and the merge button being pressed. Pass the current expected head in as a parameter and cancel the merge if it has changed. Fix #18028 Signed-off-by: Andrew Thornton --- options/locale/locale_en-US.ini | 1 + routers/api/v1/repo/pull.go | 5 ++++- routers/web/repo/pull.go | 7 ++++++- services/forms/repo_form.go | 1 + services/pull/merge.go | 20 +++++++++++++++++--- services/pull/update.go | 2 +- templates/repo/issue/view_content/pull.tmpl | 5 +++++ templates/swagger/v1_json.tmpl | 3 +++ 8 files changed, 38 insertions(+), 6 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 1d316bf90cfec..3b0092a5ef350 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1506,6 +1506,7 @@ pulls.rebase_conflict_summary = Error Message ; %[2]s
%[3]s
pulls.unrelated_histories = Merge Failed: The merge head and base do not share a common history. Hint: Try a different strategy pulls.merge_out_of_date = Merge Failed: Whilst generating the merge, the base was updated. Hint: Try again. +pulls.head_out_of_date = Merge Failed: Whilst generating the merge, the head was updated. Hint: Try again. pulls.push_rejected = Merge Failed: The push was rejected. Review the githooks for this repository. pulls.push_rejected_summary = Full Rejection Message pulls.push_rejected_no_message = Merge Failed: The push was rejected but there was no remote message.
Review the githooks for this repository diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 4c774e8af704e..2b7280d39be0c 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -838,7 +838,7 @@ func MergePullRequest(ctx *context.APIContext) { message += "\n\n" + form.MergeMessageField } - if err := pull_service.Merge(pr, ctx.User, ctx.Repo.GitRepo, repo_model.MergeStyle(form.Do), message); err != nil { + if err := pull_service.Merge(pr, ctx.User, ctx.Repo.GitRepo, repo_model.MergeStyle(form.Do), form.HeadCommitID, message); 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))) return @@ -854,6 +854,9 @@ func MergePullRequest(ctx *context.APIContext) { } else if git.IsErrPushOutOfDate(err) { ctx.Error(http.StatusConflict, "Merge", "merge push out of date") return + } else if models.IsErrSHADoesNotMatch(err) { + ctx.Error(http.StatusConflict, "Merge", "head out of date") + return } else if git.IsErrPushRejected(err) { errPushRej := err.(*git.ErrPushRejected) if len(errPushRej.Message) == 0 { diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 9445c1a48ba44..bf80a7e5dfe62 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -933,7 +933,7 @@ func MergePullRequest(ctx *context.Context) { return } - if err = pull_service.Merge(pr, ctx.User, ctx.Repo.GitRepo, repo_model.MergeStyle(form.Do), message); err != nil { + if err = pull_service.Merge(pr, ctx.User, 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")) ctx.Redirect(issue.Link()) @@ -976,6 +976,11 @@ func MergePullRequest(ctx *context.Context) { ctx.Flash.Error(ctx.Tr("repo.pulls.merge_out_of_date")) ctx.Redirect(issue.Link()) return + } else if models.IsErrSHADoesNotMatch(err) { + log.Debug("MergeHeadOutOfDate error: %v", err) + ctx.Flash.Error(ctx.Tr("repo.pulls.head_out_of_date")) + ctx.Redirect(issue.Link()) + return } else if git.IsErrPushRejected(err) { log.Debug("MergePushRejected error: %v", err) pushrejErr := err.(*git.ErrPushRejected) diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index c571951bb93d6..66ad358ae5181 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -571,6 +571,7 @@ type MergePullRequestForm struct { MergeTitleField string MergeMessageField string MergeCommitID string // only used for manually-merged + HeadCommitID string `json:head_commit_id,omitempty` ForceMerge *bool `json:"force_merge,omitempty"` DeleteBranchAfterMerge bool `json:"delete_branch_after_merge,omitempty"` } diff --git a/services/pull/merge.go b/services/pull/merge.go index ab1f43a50a45a..e541495bef4d2 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -34,7 +34,7 @@ import ( // Merge merges pull request to base repository. // Caller should check PR is ready to be merged (review and status checks) // FIXME: add repoWorkingPull make sure two merges does not happen at same time. -func Merge(pr *models.PullRequest, doer *user_model.User, baseGitRepo *git.Repository, mergeStyle repo_model.MergeStyle, message string) (err error) { +func Merge(pr *models.PullRequest, doer *user_model.User, baseGitRepo *git.Repository, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string) (err error) { if err = pr.LoadHeadRepo(); err != nil { log.Error("LoadHeadRepo: %v", err) return fmt.Errorf("LoadHeadRepo: %v", err) @@ -59,7 +59,7 @@ func Merge(pr *models.PullRequest, doer *user_model.User, baseGitRepo *git.Repos go AddTestPullRequestTask(doer, pr.BaseRepo.ID, pr.BaseBranch, false, "", "") }() - pr.MergedCommitID, err = rawMerge(pr, doer, mergeStyle, message) + pr.MergedCommitID, err = rawMerge(pr, doer, mergeStyle, expectedHeadCommitID, message) if err != nil { return err } @@ -114,7 +114,7 @@ func Merge(pr *models.PullRequest, doer *user_model.User, baseGitRepo *git.Repos } // rawMerge perform the merge operation without changing any pull information in database -func rawMerge(pr *models.PullRequest, doer *user_model.User, mergeStyle repo_model.MergeStyle, message string) (string, error) { +func rawMerge(pr *models.PullRequest, doer *user_model.User, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string) (string, error) { err := git.LoadGitVersion() if err != nil { log.Error("git.LoadGitVersion: %v", err) @@ -137,6 +137,20 @@ func rawMerge(pr *models.PullRequest, doer *user_model.User, mergeStyle repo_mod trackingBranch := "tracking" stagingBranch := "staging" + if expectedHeadCommitID != "" { + trackingCommitID, err := git.NewCommand("show-ref", "--hash", git.BranchPrefix+trackingBranch).RunInDir(tmpBasePath) + if err != nil { + log.Error("show-ref[%s] --hash refs/heads/trackingn: %v", tmpBasePath, git.BranchPrefix+trackingBranch, err) + return "", fmt.Errorf("getDiffTree: %v", err) + } + if strings.TrimSpace(trackingCommitID) != expectedHeadCommitID { + return "", models.ErrSHADoesNotMatch{ + GivenSHA: expectedHeadCommitID, + CurrentSHA: trackingCommitID, + } + } + } + var outbuf, errbuf strings.Builder // Enable sparse-checkout diff --git a/services/pull/update.go b/services/pull/update.go index 25c6d36308274..8ca7e4cee79a1 100644 --- a/services/pull/update.go +++ b/services/pull/update.go @@ -55,7 +55,7 @@ func Update(pull *models.PullRequest, doer *user_model.User, message string, reb return fmt.Errorf("HeadBranch of PR %d is up to date", pull.Index) } - _, err = rawMerge(pr, doer, style, message) + _, err = rawMerge(pr, doer, style, "", message) defer func() { if rebase { diff --git a/templates/repo/issue/view_content/pull.tmpl b/templates/repo/issue/view_content/pull.tmpl index 5a682ea06b302..bb80bba7c81a9 100644 --- a/templates/repo/issue/view_content/pull.tmpl +++ b/templates/repo/issue/view_content/pull.tmpl @@ -327,6 +327,7 @@