fix!: add DEFAULT_TITLE_SOURCE setting for pull request title default behavior (#37465) (#37766)

Backport #37465

Make DEFAULT_TITLE_SOURCE default to "auto" like GitHub

---------

Co-authored-by: 0xGREG <28388707+0xGREG@users.noreply.github.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Nicolas <bircni@icloud.com>
This commit is contained in:
wxiaoguang
2026-05-19 02:01:09 +08:00
committed by GitHub
parent 9c0ad8291b
commit edfba678ec
4 changed files with 103 additions and 19 deletions
+5
View File
@@ -1161,6 +1161,11 @@ LEVEL = Info
;; Retarget child pull requests to the parent pull request branch target on merge of parent pull request. It only works on merged PRs where the head and base branch target the same repo. ;; Retarget child pull requests to the parent pull request branch target on merge of parent pull request. It only works on merged PRs where the head and base branch target the same repo.
;RETARGET_CHILDREN_ON_MERGE = true ;RETARGET_CHILDREN_ON_MERGE = true
;; ;;
;; Default source for the pull request title when opening a new PR.
;; "first-commit" uses the oldest commit's summary.
;; "auto" uses commit's summary if the PR only has one commit, normalizes the branch name if multiple commits.
;DEFAULT_TITLE_SOURCE = auto
;;
;; Delay mergeable check until page view or API access, for pull requests that have not been updated in the specified days when their base branches get updated. ;; Delay mergeable check until page view or API access, for pull requests that have not been updated in the specified days when their base branches get updated.
;; Use "-1" to always check all pull requests (old behavior). Use "0" to always delay the checks. ;; Use "-1" to always check all pull requests (old behavior). Use "0" to always delay the checks.
;DELAY_CHECK_FOR_INACTIVE_DAYS = 7 ;DELAY_CHECK_FOR_INACTIVE_DAYS = 7
+9
View File
@@ -18,6 +18,12 @@ const (
RepoCreatingPublic = "public" RepoCreatingPublic = "public"
) )
// enumerates the values for [repository.pull-request] DEFAULT_TITLE_SOURCE
const (
RepoPRTitleSourceFirstCommit = "first-commit"
RepoPRTitleSourceAuto = "auto"
)
// ItemsPerPage maximum items per page in forks, watchers and stars of a repo // ItemsPerPage maximum items per page in forks, watchers and stars of a repo
const ItemsPerPage = 40 const ItemsPerPage = 40
@@ -89,6 +95,7 @@ var (
RetargetChildrenOnMerge bool RetargetChildrenOnMerge bool
DelayCheckForInactiveDays int DelayCheckForInactiveDays int
DefaultDeleteBranchAfterMerge bool DefaultDeleteBranchAfterMerge bool
DefaultTitleSource string
} `ini:"repository.pull-request"` } `ini:"repository.pull-request"`
// Issue Setting // Issue Setting
@@ -213,6 +220,7 @@ var (
RetargetChildrenOnMerge bool RetargetChildrenOnMerge bool
DelayCheckForInactiveDays int DelayCheckForInactiveDays int
DefaultDeleteBranchAfterMerge bool DefaultDeleteBranchAfterMerge bool
DefaultTitleSource string
}{ }{
WorkInProgressPrefixes: []string{"WIP:", "[WIP]"}, WorkInProgressPrefixes: []string{"WIP:", "[WIP]"},
// Same as GitHub. See // Same as GitHub. See
@@ -229,6 +237,7 @@ var (
AddCoCommitterTrailers: true, AddCoCommitterTrailers: true,
RetargetChildrenOnMerge: true, RetargetChildrenOnMerge: true,
DelayCheckForInactiveDays: 7, DelayCheckForInactiveDays: 7,
DefaultTitleSource: RepoPRTitleSourceAuto,
}, },
// Issue settings // Issue settings
+38 -4
View File
@@ -13,6 +13,7 @@ import (
"path/filepath" "path/filepath"
"sort" "sort"
"strings" "strings"
"unicode"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git" git_model "code.gitea.io/gitea/models/git"
@@ -429,13 +430,46 @@ func ParseCompareInfo(ctx *context.Context) *git_service.CompareInfo {
return compareInfo return compareInfo
} }
func prepareNewPullRequestTitleContent(ci *git_service.CompareInfo, commits []*git_model.SignCommitWithStatuses) (title, content string) { // autoTitleFromBranchName humanizes a branch name into a PR title.
title = ci.HeadRef.ShortName() func autoTitleFromBranchName(name string) string {
var buf strings.Builder
var prevIsSpace bool
runes := []rune(name)
for i, r := range runes {
isSpace := unicode.IsSpace(r)
if r == '-' || r == '_' || isSpace {
if !prevIsSpace {
buf.WriteRune(' ')
}
prevIsSpace = true
continue
}
if !prevIsSpace && unicode.IsUpper(r) {
needSpace := i > 0 && unicode.IsLower(runes[i-1]) || i < len(runes)-1 && unicode.IsLower(runes[i+1])
if needSpace {
buf.WriteRune(' ')
}
}
buf.WriteRune(unicode.ToLower(r))
prevIsSpace = isSpace
}
out := strings.TrimSpace(buf.String())
if out == "" {
return out
}
outRunes := []rune(out)
outRunes[0] = unicode.ToUpper(outRunes[0])
return string(outRunes)
}
if len(commits) > 0 { func prepareNewPullRequestTitleContent(ci *git_service.CompareInfo, commits []*git_model.SignCommitWithStatuses, defaultTitleSource string) (title, content string) {
useFirstCommitAsTitle := len(commits) == 1 || (defaultTitleSource == setting.RepoPRTitleSourceFirstCommit && len(commits) > 0)
if useFirstCommitAsTitle {
// the "commits" are from "ShowPrettyFormatLogToList", which is ordered from newest to oldest, here take the oldest one // the "commits" are from "ShowPrettyFormatLogToList", which is ordered from newest to oldest, here take the oldest one
c := commits[len(commits)-1] c := commits[len(commits)-1]
title = strings.TrimSpace(c.UserCommit.Summary()) title = strings.TrimSpace(c.UserCommit.Summary())
} else {
title = autoTitleFromBranchName(ci.HeadRef.ShortName())
} }
if len(commits) == 1 { if len(commits) == 1 {
@@ -571,7 +605,7 @@ func PrepareCompareDiff(
ctx.Data["Commits"] = commits ctx.Data["Commits"] = commits
ctx.Data["CommitCount"] = len(commits) ctx.Data["CommitCount"] = len(commits)
ctx.Data["title"], ctx.Data["content"] = prepareNewPullRequestTitleContent(ci, commits) ctx.Data["title"], ctx.Data["content"] = prepareNewPullRequestTitleContent(ci, commits, setting.Repository.PullRequest.DefaultTitleSource)
ctx.Data["Username"] = ci.HeadRepo.OwnerName ctx.Data["Username"] = ci.HeadRepo.OwnerName
ctx.Data["Reponame"] = ci.HeadRepo.Name ctx.Data["Reponame"] = ci.HeadRepo.Name
+51 -15
View File
@@ -13,6 +13,7 @@ import (
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
git_service "code.gitea.io/gitea/services/git" git_service "code.gitea.io/gitea/services/git"
"code.gitea.io/gitea/services/gitdiff" "code.gitea.io/gitea/services/gitdiff"
@@ -61,31 +62,66 @@ func TestNewPullRequestTitleContent(t *testing.T) {
} }
} }
title, content := prepareNewPullRequestTitleContent(ci, nil) // no commit
assert.Equal(t, "head-branch", title) title, content := prepareNewPullRequestTitleContent(ci, nil, setting.RepoPRTitleSourceAuto)
assert.Equal(t, "Head branch", title)
assert.Empty(t, content) assert.Empty(t, content)
title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("title-only")}) title, content = prepareNewPullRequestTitleContent(ci, nil, setting.RepoPRTitleSourceFirstCommit)
assert.Equal(t, "title-only", title) assert.Equal(t, "Head branch", title)
assert.Empty(t, content) assert.Empty(t, content)
title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("title-" + strings.Repeat("a", 255))}) // single commit
assert.Equal(t, "title-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa…", title) title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("single-commit-title\nbody")}, setting.RepoPRTitleSourceAuto)
assert.Equal(t, "…aaaaaaaaa\n", content) assert.Equal(t, "single-commit-title", title)
title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("title\nbody")})
assert.Equal(t, "title", title)
assert.Equal(t, "body", content) assert.Equal(t, "body", content)
title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("a\xf0\xf0\xf0\nb\xf0\xf0\xf0")}) title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("single-commit-title\nbody")}, setting.RepoPRTitleSourceFirstCommit)
assert.Equal(t, "a?", title) assert.Equal(t, "single-commit-title", title)
assert.Equal(t, "b"+string(utf8.RuneError)+string(utf8.RuneError), content) assert.Equal(t, "body", content)
title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{ // multiple commits
commits := []*git_model.SignCommitWithStatuses{
// ordered from newest to oldest // ordered from newest to oldest
mockCommit("title2\nbody2"), mockCommit("title2\nbody2"),
mockCommit("title1\nbody1"), mockCommit("title1\nbody1"),
}) }
title, content = prepareNewPullRequestTitleContent(ci, commits, setting.RepoPRTitleSourceAuto)
assert.Equal(t, "Head branch", title)
assert.Empty(t, content)
title, content = prepareNewPullRequestTitleContent(ci, commits, setting.RepoPRTitleSourceFirstCommit)
assert.Equal(t, "title1", title) assert.Equal(t, "title1", title)
assert.Empty(t, content) assert.Empty(t, content)
// title string handling
title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("title-" + strings.Repeat("a", 255))}, setting.RepoPRTitleSourceFirstCommit)
assert.Equal(t, "title-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa…", title)
assert.Equal(t, "…aaaaaaaaa\n", content)
title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("a\xf0\xf0\xf0\nb\xf0\xf0\xf0")}, setting.RepoPRTitleSourceFirstCommit)
assert.Equal(t, "a?", title) // FIXME: GIT-COMMIT-MESSAGE-ENCODING: "title" doesn't use the same charset converting logic as "content"
assert.Equal(t, "b"+string(utf8.RuneError)+string(utf8.RuneError), content)
}
func TestAutoTitleFromBranchName(t *testing.T) {
cases := []struct {
branch string
want string
}{
{"fix/the-bug", "Fix/the bug"},
{"Already-Capitalized", "Already capitalized"},
{"ALL-CAPS-BRANCH", "All caps branch"},
{"FixHTMLBug", "Fix html bug"},
{"MixedCase-Name", "Mixed case name"},
{"fooBar-baz", "Foo bar baz"},
{"foo/BAR", "Foo/bar"},
{"_leading-underscore", "Leading underscore"},
{"CamelCase", "Camel case"},
{"foo--double-dash", "Foo double dash"},
{"123-fix", "123 fix"},
}
for _, c := range cases {
assert.Equal(t, c.want, autoTitleFromBranchName(c.branch), "branch: %q", c.branch)
}
} }