diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 88297fe0d975e..671b8d74599dc 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1062,6 +1062,9 @@ ROUTER = console ;; ;; In addition to testing patches using the three-way merge method, re-test conflicting patches with git apply ;TEST_CONFLICTING_PATCHES_WITH_GIT_APPLY = false +;; +;; Retarget child pull requests to the parent pull request branch target on merge of parent pull request +;RETARGET_CHILDREN_ON_MERGE = false ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/docs/content/doc/administration/config-cheat-sheet.en-us.md b/docs/content/doc/administration/config-cheat-sheet.en-us.md index 76952df40e751..bff76d72cf66b 100644 --- a/docs/content/doc/administration/config-cheat-sheet.en-us.md +++ b/docs/content/doc/administration/config-cheat-sheet.en-us.md @@ -137,6 +137,7 @@ In addition there is _`StaticRootPath`_ which can be set as a built-in at build - `POPULATE_SQUASH_COMMENT_WITH_COMMIT_MESSAGES`: **false**: In default squash-merge messages include the commit message of all commits comprising the pull request. - `ADD_CO_COMMITTER_TRAILERS`: **true**: Add co-authored-by and co-committed-by trailers to merge commit messages if committer does not match author. - `TEST_CONFLICTING_PATCHES_WITH_GIT_APPLY`: **false**: PR patches are tested using a three-way merge method to discover if there are conflicts. If this setting is set to **true**, conflicting patches will be retested using `git apply` - This was the previous behaviour in 1.18 (and earlier) but is somewhat inefficient. Please report if you find that this setting is required. +- `RETARGET_CHILDREN_ON_MERGE`: **false**: Retarget child pull requests to the parent pull request branch target on merge of parent pull request. ### Repository - Issue (`repository.issue`) diff --git a/modules/setting/repository.go b/modules/setting/repository.go index bae3c658a4855..f9b66b7d91ec4 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -84,6 +84,7 @@ var ( PopulateSquashCommentWithCommitMessages bool AddCoCommitterTrailers bool TestConflictingPatchesWithGitApply bool + RetargetChildrenOnMerge bool } `ini:"repository.pull-request"` // Issue Setting @@ -207,6 +208,7 @@ var ( PopulateSquashCommentWithCommitMessages bool AddCoCommitterTrailers bool TestConflictingPatchesWithGitApply bool + RetargetChildrenOnMerge bool }{ WorkInProgressPrefixes: []string{"WIP:", "[WIP]"}, // Same as GitHub. See @@ -221,6 +223,7 @@ var ( DefaultMergeMessageOfficialApproversOnly: true, PopulateSquashCommentWithCommitMessages: false, AddCoCommitterTrailers: true, + RetargetChildrenOnMerge: false, }, // Issue settings diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 9b5ec0b3f8ed6..b3f3a53ef3c41 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -904,6 +904,10 @@ func MergePullRequest(ctx *context.APIContext) { } defer headRepo.Close() } + if err := pull_service.RetargetChildrenOnMerge(ctx, ctx.Doer, pr); err != nil { + ctx.Error(http.StatusInternalServerError, "RetargetChildrenOnMerge", err) + return + } if err := repo_service.DeleteBranch(ctx, ctx.Doer, pr.HeadRepo, headRepo, pr.HeadBranch); err != nil { switch { case git.IsErrBranchNotExist(err): diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 070fc109dcec9..45466fa339c3e 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -1401,6 +1401,12 @@ func CleanUpPullRequest(ctx *context.Context) { func deleteBranch(ctx *context.Context, pr *issues_model.PullRequest, gitRepo *git.Repository) { fullBranchName := pr.HeadRepo.FullName() + ":" + pr.HeadBranch + + if err := pull_service.RetargetChildrenOnMerge(ctx, ctx.Doer, pr); err != nil { + ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName)) + return + } + if err := repo_service.DeleteBranch(ctx, ctx.Doer, pr.HeadRepo, gitRepo, pr.HeadBranch); err != nil { switch { case git.IsErrBranchNotExist(err): diff --git a/services/pull/pull.go b/services/pull/pull.go index fe2f002010b1d..4172778a225a7 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -501,6 +501,42 @@ func (errs errlist) Error() string { return "" } +// RetargetChildrenOnMerge retarget children pull requests on merge if possible +func RetargetChildrenOnMerge(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) error { + if setting.Repository.PullRequest.RetargetChildrenOnMerge && pr.BaseRepoID == pr.HeadRepoID { + return RetargetBranchPulls(ctx, doer, pr.HeadRepoID, pr.HeadBranch, pr.BaseBranch) + } + return nil +} + +// RetargetBranchPulls change target branch for all pull requests who's base branch is the branch +func RetargetBranchPulls(ctx context.Context, doer *user_model.User, repoID int64, branch, targetBranch string) error { + prs, err := issues_model.GetUnmergedPullRequestsByBaseInfo(repoID, branch) + if err != nil { + return err + } + + if err := issues_model.PullRequestList(prs).LoadAttributes(); err != nil { + return err + } + + var errs errlist + for _, pr := range prs { + if err = pr.Issue.LoadAttributes(ctx); err != nil { + errs = append(errs, err) + } else if err = ChangeTargetBranch(ctx, pr, doer, targetBranch); err != nil && + !issues_model.IsErrIssueIsClosed(err) && !models.IsErrPullRequestHasMerged(err) && + !issues_model.IsErrPullRequestAlreadyExists(err) { + errs = append(errs, err) + } + } + + if len(errs) > 0 { + return errs + } + return nil +} + // CloseBranchPulls close all the pull requests who's head branch is the branch func CloseBranchPulls(doer *user_model.User, repoID int64, branch string) error { prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(repoID, branch, false)