Honor delete branch on merge repo setting when using merge API (#35488)
Some checks failed
release-nightly / nightly-binary (push) Has been cancelled
release-nightly / nightly-docker-rootful (push) Has been cancelled
release-nightly / nightly-docker-rootless (push) Has been cancelled

Fix #35463.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
Kemal Zebari
2025-10-21 22:06:56 -07:00
committed by GitHub
parent 5f0697243c
commit a9f2ea720b
20 changed files with 334 additions and 264 deletions

View File

@@ -46,14 +46,14 @@ func EnableOrDisableWorkflow(ctx *context.APIContext, workflowID string, isEnabl
func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, repo *repo_model.Repository, gitRepo *git.Repository, workflowID, ref string, processInputs func(model *model.WorkflowDispatch, inputs map[string]any) error) error {
if workflowID == "" {
return util.ErrorWrapLocale(
return util.ErrorWrapTranslatable(
util.NewNotExistErrorf("workflowID is empty"),
"actions.workflow.not_found", workflowID,
)
}
if ref == "" {
return util.ErrorWrapLocale(
return util.ErrorWrapTranslatable(
util.NewNotExistErrorf("ref is empty"),
"form.target_ref_not_exist", ref,
)
@@ -63,7 +63,7 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
cfgUnit := repo.MustGetUnit(ctx, unit.TypeActions)
cfg := cfgUnit.ActionsConfig()
if cfg.IsWorkflowDisabled(workflowID) {
return util.ErrorWrapLocale(
return util.ErrorWrapTranslatable(
util.NewPermissionDeniedErrorf("workflow is disabled"),
"actions.workflow.disabled",
)
@@ -82,7 +82,7 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
runTargetCommit, err = gitRepo.GetBranchCommit(ref)
}
if err != nil {
return util.ErrorWrapLocale(
return util.ErrorWrapTranslatable(
util.NewNotExistErrorf("ref %q doesn't exist", ref),
"form.target_ref_not_exist", ref,
)
@@ -122,7 +122,7 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
}
if entry == nil {
return util.ErrorWrapLocale(
return util.ErrorWrapTranslatable(
util.NewNotExistErrorf("workflow %q doesn't exist", workflowID),
"actions.workflow.not_found", workflowID,
)

View File

@@ -205,18 +205,6 @@ func handlePullRequestAutoMerge(pullID int64, sha string) {
return
}
var headGitRepo *git.Repository
if pr.BaseRepoID == pr.HeadRepoID {
headGitRepo = baseGitRepo
} else {
headGitRepo, err = gitrepo.OpenRepository(ctx, pr.HeadRepo)
if err != nil {
log.Error("OpenRepository %-v: %v", pr.HeadRepo, err)
return
}
defer headGitRepo.Close()
}
switch pr.Flow {
case issues_model.PullRequestFlowGithub:
headBranchExist := pr.HeadRepo != nil && gitrepo.IsBranchExist(ctx, pr.HeadRepo, pr.HeadBranch)
@@ -276,9 +264,12 @@ func handlePullRequestAutoMerge(pullID int64, sha string) {
return
}
if pr.Flow == issues_model.PullRequestFlowGithub && scheduledPRM.DeleteBranchAfterMerge {
if err := repo_service.DeleteBranch(ctx, doer, pr.HeadRepo, headGitRepo, pr.HeadBranch, pr); err != nil {
log.Error("DeletePullRequestHeadBranch: %v", err)
deleteBranchAfterMerge, err := pull_service.ShouldDeleteBranchAfterMerge(ctx, &scheduledPRM.DeleteBranchAfterMerge, pr.BaseRepo, pr)
if err != nil {
log.Error("ShouldDeleteBranchAfterMerge: %v", err)
} else if deleteBranchAfterMerge {
if err = repo_service.DeleteBranchAfterMerge(ctx, doer, pr.ID, nil); err != nil {
log.Error("DeleteBranchAfterMerge: %v", err)
}
}
}

View File

@@ -540,7 +540,7 @@ type MergePullRequestForm struct {
HeadCommitID string `json:"head_commit_id,omitempty"`
ForceMerge bool `json:"force_merge,omitempty"`
MergeWhenChecksSucceed bool `json:"merge_when_checks_succeed,omitempty"`
DeleteBranchAfterMerge bool `json:"delete_branch_after_merge,omitempty"`
DeleteBranchAfterMerge *bool `json:"delete_branch_after_merge,omitempty"`
}
// Validate validates the fields

View File

@@ -730,3 +730,24 @@ func SetMerged(ctx context.Context, pr *issues_model.PullRequest, mergedCommitID
return true, nil
})
}
func ShouldDeleteBranchAfterMerge(ctx context.Context, userOption *bool, repo *repo_model.Repository, pr *issues_model.PullRequest) (bool, error) {
if pr.Flow != issues_model.PullRequestFlowGithub {
// only support GitHub workflow (branch-based)
// for agit workflow, there is no branch, so nothing to delete
// TODO: maybe in the future, it should delete the "agit reference (refs/for/xxxx)"?
return false, nil
}
// if user has set an option, respect it
if userOption != nil {
return *userOption, nil
}
// otherwise, use repository default
prUnit, err := repo.GetUnit(ctx, unit.TypePullRequests)
if err != nil {
return false, err
}
return prUnit.PullRequestsConfig().DefaultDeleteBranchAfterMerge, nil
}

View File

@@ -484,10 +484,7 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, doer *user_m
return "", nil
}
// enmuerates all branch related errors
var (
ErrBranchIsDefault = errors.New("branch is default")
)
var ErrBranchIsDefault = util.ErrorWrap(util.ErrPermissionDenied, "branch is default")
func CanDeleteBranch(ctx context.Context, repo *repo_model.Repository, branchName string, doer *user_model.User) error {
if branchName == repo.DefaultBranch {
@@ -745,3 +742,89 @@ func GetBranchDivergingInfo(ctx reqctx.RequestContext, baseRepo *repo_model.Repo
info.BaseHasNewCommits = info.HeadCommitsBehind > 0
return info, nil
}
func DeleteBranchAfterMerge(ctx context.Context, doer *user_model.User, prID int64, outFullBranchName *string) error {
pr, err := issues_model.GetPullRequestByID(ctx, prID)
if err != nil {
return err
}
if err = pr.LoadIssue(ctx); err != nil {
return err
}
if err = pr.LoadBaseRepo(ctx); err != nil {
return err
}
if err := pr.LoadHeadRepo(ctx); err != nil {
return err
}
if pr.HeadRepo == nil {
// Forked repository has already been deleted
return util.ErrorWrapTranslatable(util.ErrNotExist, "repo.branch.deletion_failed", "(deleted-repo):"+pr.HeadBranch)
}
if err = pr.HeadRepo.LoadOwner(ctx); err != nil {
return err
}
fullBranchName := pr.HeadRepo.FullName() + ":" + pr.HeadBranch
if outFullBranchName != nil {
*outFullBranchName = fullBranchName
}
errFailedToDelete := func(err error) error {
return util.ErrorWrapTranslatable(err, "repo.branch.deletion_failed", fullBranchName)
}
// Don't clean up unmerged and unclosed PRs and agit PRs
if !pr.HasMerged && !pr.Issue.IsClosed && pr.Flow != issues_model.PullRequestFlowGithub {
return errFailedToDelete(util.ErrUnprocessableContent)
}
// Don't clean up 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 {
return err
}
if exist {
return errFailedToDelete(util.ErrUnprocessableContent)
}
if err := CanDeleteBranch(ctx, pr.HeadRepo, pr.HeadBranch, doer); err != nil {
if errors.Is(err, util.ErrPermissionDenied) {
return errFailedToDelete(err)
}
return err
}
gitBaseRepo, gitBaseCloser, err := gitrepo.RepositoryFromContextOrOpen(ctx, pr.BaseRepo)
if err != nil {
return err
}
defer gitBaseCloser.Close()
gitHeadRepo, gitHeadCloser, err := gitrepo.RepositoryFromContextOrOpen(ctx, pr.HeadRepo)
if err != nil {
return err
}
defer gitHeadCloser.Close()
// Check if branch has no new commits
headCommitID, err := gitBaseRepo.GetRefCommitID(pr.GetGitHeadRefName())
if err != nil {
log.Error("GetRefCommitID: %v", err)
return errFailedToDelete(err)
}
branchCommitID, err := gitHeadRepo.GetBranchCommitID(pr.HeadBranch)
if err != nil {
log.Error("GetBranchCommitID: %v", err)
return errFailedToDelete(err)
}
if headCommitID != branchCommitID {
return util.ErrorWrapTranslatable(util.ErrUnprocessableContent, "repo.branch.delete_branch_has_new_commits", fullBranchName)
}
err = DeleteBranch(ctx, doer, pr.HeadRepo, gitHeadRepo, pr.HeadBranch, pr)
if errors.Is(err, util.ErrPermissionDenied) || errors.Is(err, util.ErrNotExist) {
return errFailedToDelete(err)
}
return err
}