From e2104a1dd538071aa29e1ce2b11cf2c8f9d5a5f3 Mon Sep 17 00:00:00 2001 From: Louis <116039387+tototomate123@users.noreply.github.com> Date: Sat, 7 Feb 2026 02:34:29 +0100 Subject: [PATCH] Allow configuring default PR base branch (fixes #36412) (#36425) This adds a per-repository default PR base branch and wires it through PR entry points. It updates compare links and recently pushed branch prompts to respect the configured base branch, and prevents auto-merge cleanup from deleting the configured base branch on same-repo PRs. ## Behavior changes - New PR compare links on repo home/issue list and branch list honor the configured default PR base branch. - The "recently pushed new branches" prompt now compares against the configured base branch. - Auto-merge branch cleanup skips deleting the configured base branch (same-repo PRs only). --------- Signed-off-by: Louis <116039387+tototomate123@users.noreply.github.com> Co-authored-by: wxiaoguang Co-authored-by: silverwind --- models/git/branch.go | 29 +++++++--- models/repo/pull_request_default.go | 16 ++++++ models/repo/pull_request_default_test.go | 32 +++++++++++ models/repo/repo.go | 7 +-- models/repo/repo_unit.go | 1 + modules/structs/repo.go | 41 ++++++------- options/locale/locale_en-US.json | 9 ++- routers/api/v1/repo/branch.go | 2 +- routers/api/v1/repo/pull.go | 2 +- routers/web/repo/branch.go | 1 + routers/web/repo/compare.go | 6 +- routers/web/repo/issue.go | 5 -- routers/web/repo/setting/setting.go | 7 +++ services/context/api.go | 2 +- services/context/context.go | 2 +- services/context/repo.go | 73 +++++++++++++++++------- services/convert/repository.go | 3 + services/forms/repo_form.go | 1 + services/repository/branch.go | 5 +- templates/repo/branch/list.tmpl | 6 +- templates/repo/issue/list.tmpl | 5 +- templates/repo/settings/branches.tmpl | 27 ++++----- templates/repo/settings/options.tmpl | 15 +++++ templates/repo/view_content.tmpl | 11 +--- templates/swagger/v1_json.tmpl | 4 ++ 25 files changed, 213 insertions(+), 99 deletions(-) create mode 100644 models/repo/pull_request_default.go create mode 100644 models/repo/pull_request_default_test.go diff --git a/models/git/branch.go b/models/git/branch.go index 7fef9f5ca3..e5b73fb3e7 100644 --- a/models/git/branch.go +++ b/models/git/branch.go @@ -490,12 +490,25 @@ func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, o opts.CommitAfterUnix = time.Now().Add(-time.Hour * 2).Unix() } - baseBranch, err := GetBranch(ctx, opts.BaseRepo.ID, opts.BaseRepo.DefaultBranch) + var ignoredCommitIDs []string + baseDefaultBranch, err := GetBranch(ctx, opts.BaseRepo.ID, opts.BaseRepo.DefaultBranch) if err != nil { - return nil, err + log.Warn("GetBranch:DefaultBranch: %v", err) + } else { + ignoredCommitIDs = append(ignoredCommitIDs, baseDefaultBranch.CommitID) } - // find all related branches, these branches may already created PRs, we will check later + baseDefaultTargetBranchName := opts.BaseRepo.MustGetUnit(ctx, unit.TypePullRequests).PullRequestsConfig().DefaultTargetBranch + if baseDefaultTargetBranchName != "" && baseDefaultTargetBranchName != opts.BaseRepo.DefaultBranch { + baseDefaultTargetBranch, err := GetBranch(ctx, opts.BaseRepo.ID, baseDefaultTargetBranchName) + if err != nil { + log.Warn("GetBranch:DefaultTargetBranch: %v", err) + } else { + ignoredCommitIDs = append(ignoredCommitIDs, baseDefaultTargetBranch.CommitID) + } + } + + // find all related branches, these branches may already have PRs, we will check later var branches []*Branch if err := db.GetEngine(ctx). Where(builder.And( @@ -506,7 +519,7 @@ func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, o builder.Gte{"commit_time": opts.CommitAfterUnix}, builder.In("repo_id", repoIDs), // newly created branch have no changes, so skip them - builder.Neq{"commit_id": baseBranch.CommitID}, + builder.NotIn("commit_id", ignoredCommitIDs), )). OrderBy(db.SearchOrderByRecentUpdated.String()). Find(&branches); err != nil { @@ -514,10 +527,8 @@ func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, o } newBranches := make([]*RecentlyPushedNewBranch, 0, len(branches)) - if opts.MaxCount == 0 { - // by default we display 2 recently pushed new branch - opts.MaxCount = 2 - } + opts.MaxCount = util.IfZero(opts.MaxCount, 2) // by default, we display 2 recently pushed new branch + baseTargetBranchName := opts.BaseRepo.GetPullRequestTargetBranch(ctx) for _, branch := range branches { // whether the branch is protected protected, err := IsBranchProtected(ctx, branch.RepoID, branch.Name) @@ -555,7 +566,7 @@ func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, o BranchDisplayName: branchDisplayName, BranchName: branch.Name, BranchLink: fmt.Sprintf("%s/src/branch/%s", branch.Repo.Link(), util.PathEscapeSegments(branch.Name)), - BranchCompareURL: branch.Repo.ComposeBranchCompareURL(opts.BaseRepo, branch.Name), + BranchCompareURL: branch.Repo.ComposeBranchCompareURL(opts.BaseRepo, baseTargetBranchName, branch.Name), CommitTime: branch.CommitTime, }) } diff --git a/models/repo/pull_request_default.go b/models/repo/pull_request_default.go new file mode 100644 index 0000000000..89f8eb6a2c --- /dev/null +++ b/models/repo/pull_request_default.go @@ -0,0 +1,16 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "context" + + "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/util" +) + +func (repo *Repository) GetPullRequestTargetBranch(ctx context.Context) string { + unitPRConfig := repo.MustGetUnit(ctx, unit.TypePullRequests).PullRequestsConfig() + return util.IfZero(unitPRConfig.DefaultTargetBranch, repo.DefaultBranch) +} diff --git a/models/repo/pull_request_default_test.go b/models/repo/pull_request_default_test.go new file mode 100644 index 0000000000..1c4f585ed9 --- /dev/null +++ b/models/repo/pull_request_default_test.go @@ -0,0 +1,32 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "testing" + + "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/models/unittest" + + "github.com/stretchr/testify/assert" +) + +func TestDefaultTargetBranchSelection(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + ctx := t.Context() + repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 1}) + + assert.Equal(t, repo.DefaultBranch, repo.GetPullRequestTargetBranch(ctx)) + + repo.Units = nil + prUnit, err := repo.GetUnit(ctx, unit.TypePullRequests) + assert.NoError(t, err) + prConfig := prUnit.PullRequestsConfig() + prConfig.DefaultTargetBranch = "branch2" + prUnit.Config = prConfig + assert.NoError(t, UpdateRepoUnit(ctx, prUnit)) + repo.Units = nil + assert.Equal(t, "branch2", repo.GetPullRequestTargetBranch(ctx)) +} diff --git a/models/repo/repo.go b/models/repo/repo.go index 605a9e0f3f..0846dbcd05 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -613,16 +613,13 @@ func (repo *Repository) ComposeCompareURL(oldCommitID, newCommitID string) strin return fmt.Sprintf("%s/%s/compare/%s...%s", url.PathEscape(repo.OwnerName), url.PathEscape(repo.Name), util.PathEscapeSegments(oldCommitID), util.PathEscapeSegments(newCommitID)) } -func (repo *Repository) ComposeBranchCompareURL(baseRepo *Repository, branchName string) string { - if baseRepo == nil { - baseRepo = repo - } +func (repo *Repository) ComposeBranchCompareURL(baseRepo *Repository, baseBranch, branchName string) string { var cmpBranchEscaped string if repo.ID != baseRepo.ID { cmpBranchEscaped = fmt.Sprintf("%s/%s:", url.PathEscape(repo.OwnerName), url.PathEscape(repo.Name)) } cmpBranchEscaped = fmt.Sprintf("%s%s", cmpBranchEscaped, util.PathEscapeSegments(branchName)) - return fmt.Sprintf("%s/compare/%s...%s", baseRepo.Link(), util.PathEscapeSegments(baseRepo.DefaultBranch), cmpBranchEscaped) + return fmt.Sprintf("%s/compare/%s...%s", baseRepo.Link(), util.PathEscapeSegments(baseBranch), cmpBranchEscaped) } // IsOwnedBy returns true when user owns this repository diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go index ad0bb9d3f8..d03d5e1e6a 100644 --- a/models/repo/repo_unit.go +++ b/models/repo/repo_unit.go @@ -131,6 +131,7 @@ type PullRequestsConfig struct { DefaultDeleteBranchAfterMerge bool DefaultMergeStyle MergeStyle DefaultAllowMaintainerEdit bool + DefaultTargetBranch string } // FromDB fills up a PullRequestsConfig from serialized format. diff --git a/modules/structs/repo.go b/modules/structs/repo.go index 47973a5f6a..765546a5aa 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -58,26 +58,27 @@ type Repository struct { Fork bool `json:"fork"` Template bool `json:"template"` // the original repository if this repository is a fork, otherwise null - Parent *Repository `json:"parent,omitempty"` - Mirror bool `json:"mirror"` - Size int `json:"size"` - Language string `json:"language"` - LanguagesURL string `json:"languages_url"` - HTMLURL string `json:"html_url"` - URL string `json:"url"` - Link string `json:"link"` - SSHURL string `json:"ssh_url"` - CloneURL string `json:"clone_url"` - OriginalURL string `json:"original_url"` - Website string `json:"website"` - Stars int `json:"stars_count"` - Forks int `json:"forks_count"` - Watchers int `json:"watchers_count"` - OpenIssues int `json:"open_issues_count"` - OpenPulls int `json:"open_pr_counter"` - Releases int `json:"release_counter"` - DefaultBranch string `json:"default_branch"` - Archived bool `json:"archived"` + Parent *Repository `json:"parent,omitempty"` + Mirror bool `json:"mirror"` + Size int `json:"size"` + Language string `json:"language"` + LanguagesURL string `json:"languages_url"` + HTMLURL string `json:"html_url"` + URL string `json:"url"` + Link string `json:"link"` + SSHURL string `json:"ssh_url"` + CloneURL string `json:"clone_url"` + OriginalURL string `json:"original_url"` + Website string `json:"website"` + Stars int `json:"stars_count"` + Forks int `json:"forks_count"` + Watchers int `json:"watchers_count"` + OpenIssues int `json:"open_issues_count"` + OpenPulls int `json:"open_pr_counter"` + Releases int `json:"release_counter"` + DefaultBranch string `json:"default_branch"` + DefaultTargetBranch string `json:"default_target_branch,omitempty"` + Archived bool `json:"archived"` // swagger:strfmt date-time Created time.Time `json:"created_at"` // swagger:strfmt date-time diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 9ad81d5a8d..a4e13c5ca8 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -2124,6 +2124,8 @@ "repo.settings.pulls.ignore_whitespace": "Ignore Whitespace for Conflicts", "repo.settings.pulls.enable_autodetect_manual_merge": "Enable autodetect manual merge (Note: In some special cases, misjudgments can occur)", "repo.settings.pulls.allow_rebase_update": "Enable updating pull request branch by rebase", + "repo.settings.pulls.default_target_branch": "Default target branch for new pull requests", + "repo.settings.pulls.default_target_branch_default": "Default branch (%s)", "repo.settings.pulls.default_delete_branch_after_merge": "Delete pull request branch after merge by default", "repo.settings.pulls.default_allow_edits_from_maintainers": "Allow edits from maintainers by default", "repo.settings.releases_desc": "Enable Repository Releases", @@ -2436,9 +2438,10 @@ "repo.settings.block_outdated_branch_desc": "Merging will not be possible when head branch is behind base branch.", "repo.settings.block_admin_merge_override": "Administrators must follow branch protection rules", "repo.settings.block_admin_merge_override_desc": "Administrators must follow branch protection rules and cannot circumvent it.", - "repo.settings.default_branch_desc": "Select a default repository branch for pull requests and code commits:", + "repo.settings.default_branch_desc": "Select a default branch for code commits.", + "repo.settings.default_target_branch_desc": "Pull requests can use different default target branch if it is set in the Pull Requests section of Repository Advance Settings.", "repo.settings.merge_style_desc": "Merge Styles", - "repo.settings.default_merge_style_desc": "Default Merge Style", + "repo.settings.default_merge_style_desc": "Default merge style", "repo.settings.choose_branch": "Choose a branch…", "repo.settings.no_protected_branch": "There are no protected branches.", "repo.settings.edit_protected_branch": "Edit", @@ -2650,7 +2653,7 @@ "repo.branch.restore_success": "Branch \"%s\" has been restored.", "repo.branch.restore_failed": "Failed to restore branch \"%s\".", "repo.branch.protected_deletion_failed": "Branch \"%s\" is protected. It cannot be deleted.", - "repo.branch.default_deletion_failed": "Branch \"%s\" is the default branch. It cannot be deleted.", + "repo.branch.default_deletion_failed": "Branch \"%s\" is the default or pull request target branch. It cannot be deleted.", "repo.branch.default_branch_not_exist": "Default branch \"%s\" does not exist.", "repo.branch.restore": "Restore Branch \"%s\"", "repo.branch.download": "Download Branch \"%s\"", diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index 9bdc0c76b8..82fd68bdec 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -155,7 +155,7 @@ func DeleteBranch(ctx *context.APIContext) { 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")) + ctx.APIError(http.StatusForbidden, errors.New("can not delete default or pull request target branch")) case errors.Is(err, git_model.ErrBranchIsProtected): ctx.APIError(http.StatusForbidden, errors.New("branch protected")) default: diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 2f59bef7c7..e6f4dd62ce 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -1138,7 +1138,7 @@ func parseCompareInfo(ctx *context.APIContext, compareParam string) (result *git return nil, nil } - baseRef := ctx.Repo.GitRepo.UnstableGuessRefByShortName(util.IfZero(compareReq.BaseOriRef, baseRepo.DefaultBranch)) + baseRef := ctx.Repo.GitRepo.UnstableGuessRefByShortName(util.IfZero(compareReq.BaseOriRef, baseRepo.GetPullRequestTargetBranch(ctx))) headRef := headGitRepo.UnstableGuessRefByShortName(util.IfZero(compareReq.HeadOriRef, headRepo.DefaultBranch)) log.Trace("Repo path: %q, base ref: %q->%q, head ref: %q->%q", ctx.Repo.Repository.RelativePath(), compareReq.BaseOriRef, baseRef, compareReq.HeadOriRef, headRef) diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go index 2b0ba9072d..f563035600 100644 --- a/routers/web/repo/branch.go +++ b/routers/web/repo/branch.go @@ -41,6 +41,7 @@ func Branches(ctx *context.Context) { ctx.Data["AllowsPulls"] = ctx.Repo.Repository.AllowsPulls(ctx) ctx.Data["IsWriter"] = ctx.Repo.CanWrite(unit.TypeCode) ctx.Data["IsMirror"] = ctx.Repo.Repository.IsMirror + // TODO: Can be replaced by ctx.Repo.PullRequestCtx.CanCreateNewPull() ctx.Data["CanPull"] = ctx.Repo.CanWrite(unit.TypeCode) || (ctx.IsSigned && repo_model.HasForkedRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)) ctx.Data["PageIsViewCode"] = true diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index 150a8583c8..65c203d510 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -247,7 +247,7 @@ func ParseCompareInfo(ctx *context.Context) *git_service.CompareInfo { } // 4 get base and head refs - baseRefName := util.IfZero(compareReq.BaseOriRef, baseRepo.DefaultBranch) + baseRefName := util.IfZero(compareReq.BaseOriRef, baseRepo.GetPullRequestTargetBranch(ctx)) headRefName := util.IfZero(compareReq.HeadOriRef, headRepo.DefaultBranch) baseRef := ctx.Repo.GitRepo.UnstableGuessRefByShortName(baseRefName) @@ -276,10 +276,10 @@ func ParseCompareInfo(ctx *context.Context) *git_service.CompareInfo { ctx.Data["BaseBranch"] = baseRef.ShortName() // for legacy templates ctx.Data["HeadUser"] = headOwner ctx.Data["HeadBranch"] = headRef.ShortName() // for legacy templates - ctx.Repo.PullRequest.SameRepo = isSameRepo - ctx.Data["IsPull"] = true + context.InitRepoPullRequestCtx(ctx, baseRepo, headRepo) + // The current base and head repositories and branches may not // actually be the intended branches that the user wants to // create a pull-request from - but also determining the head diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 54b7e5df2a..eaec3b5789 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -109,11 +109,6 @@ func MustAllowPulls(ctx *context.Context) { ctx.NotFound(nil) return } - - // User can send pull request if owns a forked repository. - if ctx.IsSigned && repo_model.HasForkedRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID) { - ctx.Repo.PullRequest.Allowed = true - } } func retrieveProjectsInternal(ctx *context.Context, repo *repo_model.Repository) (open, closed []*project_model.Project) { diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index 0c73c1490f..f9e80a72e0 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -28,6 +28,7 @@ import ( "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/validation" "code.gitea.io/gitea/modules/web" + repo_router "code.gitea.io/gitea/routers/web/repo" actions_service "code.gitea.io/gitea/services/actions" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" @@ -88,6 +89,11 @@ func SettingsCtxData(ctx *context.Context) { return } ctx.Data["PushMirrors"] = pushMirrors + + repo_router.PrepareBranchList(ctx) + if ctx.Written() { + return + } } // Settings show a repository's settings page @@ -622,6 +628,7 @@ func handleSettingsPostAdvanced(ctx *context.Context) { DefaultDeleteBranchAfterMerge: form.DefaultDeleteBranchAfterMerge, DefaultMergeStyle: repo_model.MergeStyle(form.PullsDefaultMergeStyle), DefaultAllowMaintainerEdit: form.DefaultAllowMaintainerEdit, + DefaultTargetBranch: strings.TrimSpace(form.DefaultTargetBranch), })) } else if !unit_model.TypePullRequests.UnitGlobalDisabled() { deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePullRequests) diff --git a/services/context/api.go b/services/context/api.go index 591efadf37..a104d13588 100644 --- a/services/context/api.go +++ b/services/context/api.go @@ -221,7 +221,7 @@ func APIContexter() func(http.Handler) http.Handler { ctx := &APIContext{ Base: base, Cache: cache.GetCache(), - Repo: &Repository{PullRequest: &PullRequest{}}, + Repo: &Repository{}, Org: &APIOrganization{}, } diff --git a/services/context/context.go b/services/context/context.go index b19941cb8d..394a78aa9d 100644 --- a/services/context/context.go +++ b/services/context/context.go @@ -129,7 +129,7 @@ func NewWebContext(base *Base, render Render, session session.Store) *Context { Cache: cache.GetCache(), Link: setting.AppSubURL + strings.TrimSuffix(base.Req.URL.EscapedPath(), "/"), - Repo: &Repository{PullRequest: &PullRequest{}}, + Repo: &Repository{}, Org: &Organization{}, } ctx.TemplateContext = NewTemplateContextForWeb(ctx) diff --git a/services/context/repo.go b/services/context/repo.go index 3813335374..674da577b9 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -37,11 +37,46 @@ import ( "github.com/editorconfig/editorconfig-core-go/v2" ) -// PullRequest contains information to make a pull request -type PullRequest struct { - BaseRepo *repo_model.Repository - Allowed bool // it only used by the web tmpl: "PullRequestCtx.Allowed" - SameRepo bool // it only used by the web tmpl: "PullRequestCtx.SameRepo" +// PullRequestContext contains context information for making a new pull request +type PullRequestContext struct { + ctx *Context + + baseRepo, headRepo *repo_model.Repository + + canCreateNewPull *bool + defaultTargetBranch *string +} + +func (prc *PullRequestContext) SameRepo() bool { + return prc.baseRepo != nil && prc.headRepo != nil && prc.baseRepo.ID == prc.headRepo.ID +} + +func (prc *PullRequestContext) CanCreateNewPull() bool { + if prc.canCreateNewPull != nil { + return *prc.canCreateNewPull + } + ctx := prc.ctx + // People who have push access or have forked repository can propose a new pull request. + can := prc.baseRepo.CanContentChange() && + (ctx.Repo.CanWrite(unit_model.TypeCode) || (ctx.IsSigned && repo_model.HasForkedRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID))) + prc.canCreateNewPull = &can + return can +} + +func (prc *PullRequestContext) MakeDefaultCompareLink(headBranch string) string { + return prc.baseRepo.Link() + "/compare/" + + util.PathEscapeSegments(prc.DefaultTargetBranch()) + "..." + + util.Iif(prc.SameRepo(), "", util.PathEscapeSegments(prc.headRepo.OwnerName)+":") + + util.PathEscapeSegments(headBranch) +} + +func (prc *PullRequestContext) DefaultTargetBranch() string { + if prc.defaultTargetBranch != nil { + return *prc.defaultTargetBranch + } + branchName := prc.baseRepo.GetPullRequestTargetBranch(prc.ctx) + prc.defaultTargetBranch = &branchName + return branchName } // Repository contains information to operate a repository @@ -64,7 +99,7 @@ type Repository struct { CommitID string CommitsCount int64 - PullRequest *PullRequest + PullRequestCtx *PullRequestContext } // CanWriteToBranch checks if the branch is writable by the user @@ -418,6 +453,12 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) { ctx.Data["IsEmptyRepo"] = ctx.Repo.Repository.IsEmpty } +func InitRepoPullRequestCtx(ctx *Context, base, head *repo_model.Repository) { + ctx.Repo.PullRequestCtx = &PullRequestContext{ctx: ctx} + ctx.Repo.PullRequestCtx.baseRepo, ctx.Repo.PullRequestCtx.headRepo = base, head + ctx.Data["PullRequestCtx"] = ctx.Repo.PullRequestCtx +} + // RepoAssignment returns a middleware to handle repository assignment func RepoAssignment(ctx *Context) { if ctx.Data["Repository"] != nil { @@ -666,28 +707,16 @@ func RepoAssignment(ctx *Context) { ctx.Data["BranchesCount"] = branchesTotal - // People who have push access or have forked repository can propose a new pull request. - canPush := ctx.Repo.CanWrite(unit_model.TypeCode) || - (ctx.IsSigned && repo_model.HasForkedRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)) - canCompare := false - - // Pull request is allowed if this is a fork repository - // and base repository accepts pull requests. + // Pull request is allowed if this is a fork repository, and base repository accepts pull requests. if repo.BaseRepo != nil && repo.BaseRepo.AllowsPulls(ctx) { - canCompare = true + // TODO: this (and below) "BaseRepo" var is not clear and should be removed in the future ctx.Data["BaseRepo"] = repo.BaseRepo - ctx.Repo.PullRequest.BaseRepo = repo.BaseRepo - ctx.Repo.PullRequest.Allowed = canPush + InitRepoPullRequestCtx(ctx, repo.BaseRepo, repo) } else if repo.AllowsPulls(ctx) { // Or, this is repository accepts pull requests between branches. - canCompare = true ctx.Data["BaseRepo"] = repo - ctx.Repo.PullRequest.BaseRepo = repo - ctx.Repo.PullRequest.Allowed = canPush - ctx.Repo.PullRequest.SameRepo = true + InitRepoPullRequestCtx(ctx, repo, repo) } - ctx.Data["CanCompareOrPull"] = canCompare - ctx.Data["PullRequestCtx"] = ctx.Repo.PullRequest if ctx.Repo.Repository.Status == repo_model.RepositoryPendingTransfer { repoTransfer, err := repo_model.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository) diff --git a/services/convert/repository.go b/services/convert/repository.go index da4e59d7a9..150c952b15 100644 --- a/services/convert/repository.go +++ b/services/convert/repository.go @@ -103,6 +103,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR defaultDeleteBranchAfterMerge := false defaultMergeStyle := repo_model.MergeStyleMerge defaultAllowMaintainerEdit := false + defaultTargetBranch := "" if unit, err := repo.GetUnit(ctx, unit_model.TypePullRequests); err == nil { config := unit.PullRequestsConfig() hasPullRequests = true @@ -118,6 +119,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR defaultDeleteBranchAfterMerge = config.DefaultDeleteBranchAfterMerge defaultMergeStyle = config.GetDefaultMergeStyle() defaultAllowMaintainerEdit = config.DefaultAllowMaintainerEdit + defaultTargetBranch = config.DefaultTargetBranch } hasProjects := false projectsMode := repo_model.ProjectsModeAll @@ -235,6 +237,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR DefaultDeleteBranchAfterMerge: defaultDeleteBranchAfterMerge, DefaultMergeStyle: string(defaultMergeStyle), DefaultAllowMaintainerEdit: defaultAllowMaintainerEdit, + DefaultTargetBranch: defaultTargetBranch, AvatarURL: repo.AvatarLink(ctx), Internal: !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePrivate, MirrorInterval: mirrorInterval, diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index e4545570c8..765a723968 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -143,6 +143,7 @@ type RepoSettingForm struct { PullsAllowRebaseUpdate bool DefaultDeleteBranchAfterMerge bool DefaultAllowMaintainerEdit bool + DefaultTargetBranch string EnableTimetracker bool AllowOnlyContributorsToTrackTime bool EnableIssueDependencies bool diff --git a/services/repository/branch.go b/services/repository/branch.go index a580208af6..b3310b2e68 100644 --- a/services/repository/branch.go +++ b/services/repository/branch.go @@ -547,10 +547,11 @@ func UpdateBranch(ctx context.Context, repo *repo_model.Repository, gitRepo *git return gitrepo.Push(ctx, repo, repo, pushOpts) } -var ErrBranchIsDefault = util.ErrorWrap(util.ErrPermissionDenied, "branch is default") +var ErrBranchIsDefault = util.ErrorWrap(util.ErrPermissionDenied, "branch is default or pull request target") func CanDeleteBranch(ctx context.Context, repo *repo_model.Repository, branchName string, doer *user_model.User) error { - if branchName == repo.DefaultBranch { + unitPRConfig := repo.MustGetUnit(ctx, unit.TypePullRequests).PullRequestsConfig() + if branchName == repo.DefaultBranch || branchName == unitPRConfig.DefaultTargetBranch { return ErrBranchIsDefault } diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl index aa9e856fcf..9f6f24bbf4 100644 --- a/templates/repo/branch/list.tmpl +++ b/templates/repo/branch/list.tmpl @@ -133,14 +133,14 @@ {{svg "octicon-git-pull-request"}} {{ctx.Locale.Tr "repo.branch.included"}} - {{else if and (not .DBBranch.IsDeleted) $.AllowsPulls (gt .CommitsAhead 0)}} - + {{else if and (not .DBBranch.IsDeleted) $.AllowsPulls (gt .CommitsAhead 0)}} + {{end}} {{else if and .LatestPullRequest.HasMerged .MergeMovedOn}} {{if and (not .DBBranch.IsDeleted) $.AllowsPulls (gt .CommitsAhead 0)}} - + {{end}} diff --git a/templates/repo/issue/list.tmpl b/templates/repo/issue/list.tmpl index 1fe220e1b8..9aa75500e0 100644 --- a/templates/repo/issue/list.tmpl +++ b/templates/repo/issue/list.tmpl @@ -24,11 +24,12 @@ {{if .PageIsIssueList}} {{ctx.Locale.Tr "repo.issues.new"}} {{else}} - {{ctx.Locale.Tr "repo.pulls.new"}} + {{ctx.Locale.Tr "repo.pulls.new"}} {{end}} {{else}} + {{/* archived, view compare page only */}} {{if not .PageIsIssueList}} - {{ctx.Locale.Tr "action.compare_commits_general"}} + {{ctx.Locale.Tr "action.compare_commits_general"}} {{end}} {{end}} diff --git a/templates/repo/settings/branches.tmpl b/templates/repo/settings/branches.tmpl index 37ebab2a72..8488174ab6 100644 --- a/templates/repo/settings/branches.tmpl +++ b/templates/repo/settings/branches.tmpl @@ -9,22 +9,23 @@ {{ctx.Locale.Tr "repo.default_branch"}}
-

- {{ctx.Locale.Tr "repo.settings.default_branch_desc"}} -

-
+

{{ctx.Locale.Tr "repo.settings.default_branch_desc"}}

+ -
{{end}} + {{/* FIXME: need to split the "Advance Settings" by units, there are too many options here */}}

{{ctx.Locale.Tr "repo.settings.advanced_settings"}}

@@ -594,6 +595,20 @@
+
+ + +
diff --git a/templates/repo/view_content.tmpl b/templates/repo/view_content.tmpl index 8514566b3f..abda754271 100644 --- a/templates/repo/view_content.tmpl +++ b/templates/repo/view_content.tmpl @@ -20,15 +20,10 @@ "ShowViewAllRefsEntry" true }} - {{if and .CanCompareOrPull .RefFullName.IsBranch (not .Repository.IsArchived)}} - {{$cmpBranch := ""}} - {{if ne .Repository.ID .BaseRepo.ID}} - {{$cmpBranch = printf "%s/%s:" (.Repository.OwnerName|PathEscape) (.Repository.Name|PathEscape)}} - {{end}} - {{$cmpBranch = print $cmpBranch (.BranchName|PathEscapeSegments)}} - {{$compareLink := printf "%s/compare/%s...%s" .BaseRepo.Link (.BaseRepo.DefaultBranch|PathEscapeSegments) $cmpBranch}} + {{if and .PullRequestCtx.CanCreateNewPull .RefFullName.IsBranch}} + {{$compareLink := .PullRequestCtx.MakeDefaultCompareLink .BranchName}} + data-tooltip-content="{{ctx.Locale.Tr "repo.pulls.compare_changes"}}"> {{svg "octicon-git-pull-request"}} {{end}} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 45f4a84316..570747ca57 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -28147,6 +28147,10 @@ "type": "string", "x-go-name": "DefaultMergeStyle" }, + "default_target_branch": { + "type": "string", + "x-go-name": "DefaultTargetBranch" + }, "description": { "type": "string", "x-go-name": "Description"