mirror of
https://github.com/go-gitea/gitea.git
synced 2026-05-07 01:43:24 +02:00
Add DEFAULT_TITLE_SOURCE setting for pull request title default behavior (#37465)
Adds a new `DEFAULT_TITLE_SOURCE` option under `[repository.pull-request]` with three values: - `first-commit` (default): uses the oldest commit summary, current behavior since v1.26 - `auto`: normalizes branch name as title for multi-commit PRs (just like GitHub), use commit summary for single-commit PRs Closes: #37463 Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: Giteabot <teabot@gitea.io> Co-authored-by: Nicolas <bircni@icloud.com>
This commit is contained in:
parent
deec2b0929
commit
0ba862cb97
@ -1169,6 +1169,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_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 = first-commit
|
||||
;;
|
||||
;; 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.
|
||||
;DELAY_CHECK_FOR_INACTIVE_DAYS = 7
|
||||
|
||||
@ -18,6 +18,12 @@ const (
|
||||
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
|
||||
const ItemsPerPage = 40
|
||||
|
||||
@ -89,6 +95,7 @@ var (
|
||||
RetargetChildrenOnMerge bool
|
||||
DelayCheckForInactiveDays int
|
||||
DefaultDeleteBranchAfterMerge bool
|
||||
DefaultTitleSource string
|
||||
} `ini:"repository.pull-request"`
|
||||
|
||||
// Issue Setting
|
||||
@ -213,6 +220,7 @@ var (
|
||||
RetargetChildrenOnMerge bool
|
||||
DelayCheckForInactiveDays int
|
||||
DefaultDeleteBranchAfterMerge bool
|
||||
DefaultTitleSource string
|
||||
}{
|
||||
WorkInProgressPrefixes: []string{"WIP:", "[WIP]"},
|
||||
// Same as GitHub. See
|
||||
@ -229,6 +237,7 @@ var (
|
||||
AddCoCommitterTrailers: true,
|
||||
RetargetChildrenOnMerge: true,
|
||||
DelayCheckForInactiveDays: 7,
|
||||
DefaultTitleSource: RepoPRTitleSourceFirstCommit,
|
||||
},
|
||||
|
||||
// Issue settings
|
||||
|
||||
@ -13,6 +13,7 @@ import (
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
@ -349,13 +350,46 @@ func parseCompareInfo(ctx *context.Context) (*git_service.CompareInfo, error) {
|
||||
return &compareInfo, nil
|
||||
}
|
||||
|
||||
func prepareNewPullRequestTitleContent(ci *git_service.CompareInfo, commits []*git_model.SignCommitWithStatuses) (title, content string) {
|
||||
title = ci.HeadRef.ShortName()
|
||||
// autoTitleFromBranchName humanizes a branch name into a PR title.
|
||||
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
|
||||
c := commits[len(commits)-1]
|
||||
title = strings.TrimSpace(c.UserCommit.Summary())
|
||||
} else {
|
||||
title = autoTitleFromBranchName(ci.HeadRef.ShortName())
|
||||
}
|
||||
|
||||
if len(commits) == 1 {
|
||||
@ -491,7 +525,10 @@ func prepareCompareDiff(ctx *context.Context, ci *git_service.CompareInfo, white
|
||||
ctx.Data["Commits"] = 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["Reponame"] = ci.HeadRepo.Name
|
||||
|
||||
setCompareContext(ctx, beforeCommit, headCommit, ci.HeadRepo.OwnerName, repo.Name)
|
||||
|
||||
return false
|
||||
|
||||
@ -13,6 +13,7 @@ import (
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
git_service "code.gitea.io/gitea/services/git"
|
||||
"code.gitea.io/gitea/services/gitdiff"
|
||||
|
||||
@ -61,31 +62,66 @@ func TestNewPullRequestTitleContent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
title, content := prepareNewPullRequestTitleContent(ci, nil)
|
||||
assert.Equal(t, "head-branch", title)
|
||||
// no commit
|
||||
title, content := prepareNewPullRequestTitleContent(ci, nil, setting.RepoPRTitleSourceAuto)
|
||||
assert.Equal(t, "Head branch", title)
|
||||
assert.Empty(t, content)
|
||||
|
||||
title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("title-only")})
|
||||
assert.Equal(t, "title-only", title)
|
||||
title, content = prepareNewPullRequestTitleContent(ci, nil, setting.RepoPRTitleSourceFirstCommit)
|
||||
assert.Equal(t, "Head branch", title)
|
||||
assert.Empty(t, content)
|
||||
|
||||
title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("title-" + strings.Repeat("a", 255))})
|
||||
assert.Equal(t, "title-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa…", title)
|
||||
assert.Equal(t, "…aaaaaaaaa\n", content)
|
||||
|
||||
title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("title\nbody")})
|
||||
assert.Equal(t, "title", title)
|
||||
// single commit
|
||||
title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("single-commit-title\nbody")}, setting.RepoPRTitleSourceAuto)
|
||||
assert.Equal(t, "single-commit-title", title)
|
||||
assert.Equal(t, "body", content)
|
||||
|
||||
title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("a\xf0\xf0\xf0\nb\xf0\xf0\xf0")})
|
||||
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)
|
||||
title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("single-commit-title\nbody")}, setting.RepoPRTitleSourceFirstCommit)
|
||||
assert.Equal(t, "single-commit-title", title)
|
||||
assert.Equal(t, "body", content)
|
||||
|
||||
title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{
|
||||
// multiple commits
|
||||
commits := []*git_model.SignCommitWithStatuses{
|
||||
// ordered from newest to oldest
|
||||
mockCommit("title2\nbody2"),
|
||||
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.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)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user