Honor delete branch on merge repo setting when using merge API (#35488) (#35726)

Backport #35488 by @kemzeb

Fix #35463.

---------

Co-authored-by: Kemal Zebari <60799661+kemzeb@users.noreply.github.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
Giteabot
2025-10-23 00:41:40 +08:00
committed by GitHub
parent c84d17b1bb
commit 0925089b5e
20 changed files with 335 additions and 265 deletions

View File

@@ -13,7 +13,6 @@ import (
"time"
activities_model "code.gitea.io/gitea/models/activities"
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access"
pull_model "code.gitea.io/gitea/models/pull"
@@ -938,7 +937,7 @@ func MergePullRequest(ctx *context.APIContext) {
} else if errors.Is(err, pull_service.ErrNoPermissionToMerge) {
ctx.APIError(http.StatusMethodNotAllowed, "User not allowed to merge PR")
} else if errors.Is(err, pull_service.ErrHasMerged) {
ctx.APIError(http.StatusMethodNotAllowed, "")
ctx.APIError(http.StatusMethodNotAllowed, "The PR is already merged")
} else if errors.Is(err, pull_service.ErrIsWorkInProgress) {
ctx.APIError(http.StatusMethodNotAllowed, "Work in progress PRs cannot be merged")
} else if errors.Is(err, pull_service.ErrNotMergeableState) {
@@ -989,8 +988,14 @@ func MergePullRequest(ctx *context.APIContext) {
message += "\n\n" + form.MergeMessageField
}
deleteBranchAfterMerge, err := pull_service.ShouldDeleteBranchAfterMerge(ctx, form.DeleteBranchAfterMerge, ctx.Repo.Repository, pr)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if form.MergeWhenChecksSucceed {
scheduled, err := automerge.ScheduleAutoMerge(ctx, ctx.Doer, pr, repo_model.MergeStyle(form.Do), message, form.DeleteBranchAfterMerge)
scheduled, err := automerge.ScheduleAutoMerge(ctx, ctx.Doer, pr, repo_model.MergeStyle(form.Do), message, deleteBranchAfterMerge)
if err != nil {
if pull_model.IsErrAlreadyScheduledToAutoMerge(err) {
ctx.APIError(http.StatusConflict, err)
@@ -1035,47 +1040,10 @@ func MergePullRequest(ctx *context.APIContext) {
}
log.Trace("Pull request merged: %d", pr.ID)
// for agit flow, we should not delete the agit reference after merge
if form.DeleteBranchAfterMerge && pr.Flow == issues_model.PullRequestFlowGithub {
// check permission even it has been checked in repo_service.DeleteBranch so that we don't need to
// do RetargetChildrenOnMerge
if err := repo_service.CanDeleteBranch(ctx, pr.HeadRepo, pr.HeadBranch, ctx.Doer); err == nil {
// Don't cleanup when there are other PR's that use this branch as head branch.
exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pr.HeadRepoID, pr.HeadBranch)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if exist {
ctx.Status(http.StatusOK)
return
}
var headRepo *git.Repository
if ctx.Repo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == pr.HeadRepoID && ctx.Repo.GitRepo != nil {
headRepo = ctx.Repo.GitRepo
} else {
headRepo, err = gitrepo.OpenRepository(ctx, pr.HeadRepo)
if err != nil {
ctx.APIErrorInternal(err)
return
}
defer headRepo.Close()
}
if err := repo_service.DeleteBranch(ctx, ctx.Doer, pr.HeadRepo, headRepo, pr.HeadBranch, pr); err != nil {
switch {
case git.IsErrBranchNotExist(err):
ctx.APIErrorNotFound(err)
case errors.Is(err, repo_service.ErrBranchIsDefault):
ctx.APIError(http.StatusForbidden, errors.New("can not delete default branch"))
case errors.Is(err, git_model.ErrBranchIsProtected):
ctx.APIError(http.StatusForbidden, errors.New("branch protected"))
default:
ctx.APIErrorInternal(err)
}
return
}
if deleteBranchAfterMerge {
if err = repo_service.DeleteBranchAfterMerge(ctx, ctx.Doer, pr.ID, nil); err != nil {
// no way to tell users that what error happens, and the PR has been merged, so ignore the error
log.Debug("DeleteBranchAfterMerge: pr %d, err: %v", pr.ID, err)
}
}

View File

@@ -850,8 +850,8 @@ func Run(ctx *context_module.Context) {
return nil
})
if err != nil {
if errLocale := util.ErrorAsLocale(err); errLocale != nil {
ctx.Flash.Error(ctx.Tr(errLocale.TrKey, errLocale.TrArgs...))
if errTr := util.ErrorAsTranslatable(err); errTr != nil {
ctx.Flash.Error(errTr.Translate(ctx.Locale))
ctx.Redirect(redirectURL)
} else {
ctx.ServerError("DispatchActionWorkflow", err)

View File

@@ -41,7 +41,7 @@ func NewDiffPatchPost(ctx *context.Context) {
Committer: parsed.GitCommitter,
})
if err != nil {
err = util.ErrorWrapLocale(err, "repo.editor.fail_to_apply_patch")
err = util.ErrorWrapTranslatable(err, "repo.editor.fail_to_apply_patch")
}
if err != nil {
editorHandleFileOperationError(ctx, parsed.NewBranchName, err)

View File

@@ -74,7 +74,7 @@ func CherryPickPost(ctx *context.Context) {
opts.Content = buf.String()
_, err = files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, opts)
if err != nil {
err = util.ErrorWrapLocale(err, "repo.editor.fail_to_apply_patch")
err = util.ErrorWrapTranslatable(err, "repo.editor.fail_to_apply_patch")
}
}
if err != nil {

View File

@@ -38,8 +38,8 @@ func editorHandleFileOperationErrorRender(ctx *context_service.Context, message,
}
func editorHandleFileOperationError(ctx *context_service.Context, targetBranchName string, err error) {
if errAs := util.ErrorAsLocale(err); errAs != nil {
ctx.JSONError(ctx.Tr(errAs.TrKey, errAs.TrArgs...))
if errAs := util.ErrorAsTranslatable(err); errAs != nil {
ctx.JSONError(errAs.Translate(ctx.Locale))
} else if errAs, ok := errorAs[git.ErrNotExist](err); ok {
ctx.JSONError(ctx.Tr("repo.editor.file_modifying_no_longer_exists", errAs.RelPath))
} else if errAs, ok := errorAs[git_model.ErrLFSFileLocked](err); ok {

View File

@@ -1095,11 +1095,17 @@ func MergePullRequest(ctx *context.Context) {
message += "\n\n" + form.MergeMessageField
}
deleteBranchAfterMerge, err := pull_service.ShouldDeleteBranchAfterMerge(ctx, form.DeleteBranchAfterMerge, ctx.Repo.Repository, pr)
if err != nil {
ctx.ServerError("ShouldDeleteBranchAfterMerge", err)
return
}
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, form.DeleteBranchAfterMerge)
scheduled, err := automerge.ScheduleAutoMerge(ctx, ctx.Doer, pr, repo_model.MergeStyle(form.Do), message, deleteBranchAfterMerge)
if err != nil {
ctx.ServerError("ScheduleAutoMerge", err)
return
@@ -1185,37 +1191,29 @@ func MergePullRequest(ctx *context.Context) {
log.Trace("Pull request merged: %d", pr.ID)
if !form.DeleteBranchAfterMerge {
ctx.JSONRedirect(issue.Link())
return
}
// Don't cleanup when other pr use this branch as head branch
exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pr.HeadRepoID, pr.HeadBranch)
if err != nil {
ctx.ServerError("HasUnmergedPullRequestsByHeadInfo", err)
return
}
if exist {
ctx.JSONRedirect(issue.Link())
return
}
var headRepo *git.Repository
if ctx.Repo != nil && ctx.Repo.Repository != nil && pr.HeadRepoID == ctx.Repo.Repository.ID && ctx.Repo.GitRepo != nil {
headRepo = ctx.Repo.GitRepo
} else {
headRepo, err = gitrepo.OpenRepository(ctx, pr.HeadRepo)
if err != nil {
ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.HeadRepo.FullName()), err)
if deleteBranchAfterMerge {
deleteBranchAfterMergeAndFlashMessage(ctx, pr.ID)
if ctx.Written() {
return
}
defer headRepo.Close()
}
deleteBranch(ctx, pr, headRepo)
ctx.JSONRedirect(issue.Link())
}
func deleteBranchAfterMergeAndFlashMessage(ctx *context.Context, prID int64) {
var fullBranchName string
err := repo_service.DeleteBranchAfterMerge(ctx, ctx.Doer, prID, &fullBranchName)
if errTr := util.ErrorAsTranslatable(err); errTr != nil {
ctx.Flash.Error(errTr.Translate(ctx.Locale))
return
} else if err == nil {
ctx.Flash.Success(ctx.Tr("repo.branch.deletion_success", fullBranchName))
return
}
// catch unknown errors
ctx.ServerError("DeleteBranchAfterMerge", err)
}
// CancelAutoMergePullRequest cancels a scheduled pr
func CancelAutoMergePullRequest(ctx *context.Context) {
issue, ok := getPullInfo(ctx)
@@ -1402,131 +1400,17 @@ func CompareAndPullRequestPost(ctx *context.Context) {
}
// CleanUpPullRequest responses for delete merged branch when PR has been merged
// Used by "DeleteBranchLink" for "delete branch" button
func CleanUpPullRequest(ctx *context.Context) {
issue, ok := getPullInfo(ctx)
if !ok {
return
}
pr := issue.PullRequest
// Don't cleanup unmerged and unclosed PRs and agit PRs
if !pr.HasMerged && !issue.IsClosed && pr.Flow != issues_model.PullRequestFlowGithub {
ctx.NotFound(nil)
deleteBranchAfterMergeAndFlashMessage(ctx, issue.PullRequest.ID)
if ctx.Written() {
return
}
// Don't cleanup when there are other PR's that use this branch as head branch.
exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pr.HeadRepoID, pr.HeadBranch)
if err != nil {
ctx.ServerError("HasUnmergedPullRequestsByHeadInfo", err)
return
}
if exist {
ctx.NotFound(nil)
return
}
if err := pr.LoadHeadRepo(ctx); err != nil {
ctx.ServerError("LoadHeadRepo", err)
return
} else if pr.HeadRepo == nil {
// Forked repository has already been deleted
ctx.NotFound(nil)
return
} else if err = pr.LoadBaseRepo(ctx); err != nil {
ctx.ServerError("LoadBaseRepo", err)
return
} else if err = pr.HeadRepo.LoadOwner(ctx); err != nil {
ctx.ServerError("HeadRepo.LoadOwner", err)
return
}
if err := repo_service.CanDeleteBranch(ctx, pr.HeadRepo, pr.HeadBranch, ctx.Doer); err != nil {
if errors.Is(err, util.ErrPermissionDenied) {
ctx.NotFound(nil)
} else {
ctx.ServerError("CanDeleteBranch", err)
}
return
}
fullBranchName := pr.HeadRepo.Owner.Name + "/" + pr.HeadBranch
var gitBaseRepo *git.Repository
// Assume that the base repo is the current context (almost certainly)
if ctx.Repo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == pr.BaseRepoID && ctx.Repo.GitRepo != nil {
gitBaseRepo = ctx.Repo.GitRepo
} else {
// If not just open it
gitBaseRepo, err = gitrepo.OpenRepository(ctx, pr.BaseRepo)
if err != nil {
ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.BaseRepo.FullName()), err)
return
}
defer gitBaseRepo.Close()
}
// Now assume that the head repo is the same as the base repo (reasonable chance)
gitRepo := gitBaseRepo
// But if not: is it the same as the context?
if pr.BaseRepoID != pr.HeadRepoID && ctx.Repo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == pr.HeadRepoID && ctx.Repo.GitRepo != nil {
gitRepo = ctx.Repo.GitRepo
} else if pr.BaseRepoID != pr.HeadRepoID {
// Otherwise just load it up
gitRepo, err = gitrepo.OpenRepository(ctx, pr.HeadRepo)
if err != nil {
ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.HeadRepo.FullName()), err)
return
}
defer gitRepo.Close()
}
defer func() {
ctx.JSONRedirect(issue.Link())
}()
// Check if branch has no new commits
headCommitID, err := gitBaseRepo.GetRefCommitID(pr.GetGitHeadRefName())
if err != nil {
log.Error("GetRefCommitID: %v", err)
ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
return
}
branchCommitID, err := gitRepo.GetBranchCommitID(pr.HeadBranch)
if err != nil {
log.Error("GetBranchCommitID: %v", err)
ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
return
}
if headCommitID != branchCommitID {
ctx.Flash.Error(ctx.Tr("repo.branch.delete_branch_has_new_commits", fullBranchName))
return
}
deleteBranch(ctx, pr, gitRepo)
}
func deleteBranch(ctx *context.Context, pr *issues_model.PullRequest, gitRepo *git.Repository) {
fullBranchName := pr.HeadRepo.FullName() + ":" + pr.HeadBranch
if err := repo_service.DeleteBranch(ctx, ctx.Doer, pr.HeadRepo, gitRepo, pr.HeadBranch, pr); err != nil {
switch {
case git.IsErrBranchNotExist(err):
ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
case errors.Is(err, repo_service.ErrBranchIsDefault):
ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
case errors.Is(err, git_model.ErrBranchIsProtected):
ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
default:
log.Error("DeleteBranch: %v", err)
ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
}
return
}
ctx.Flash.Success(ctx.Tr("repo.branch.deletion_success", fullBranchName))
ctx.JSONRedirect(issue.Link())
}
// DownloadPullDiff render a pull's raw diff