0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-06-02 01:42:16 +02:00

fix(pull): preserve squash message trailers and additional commit messages

When a PR description already ends with git trailers (e.g. Issue: X,
Signed-off-by:), the co-author separator line (---------)  was still
inserted before the Co-authored-by lines, breaking the trailer block.
messageHasTrailers now skips the separator so co-authors are appended
directly into the existing trailer block.

In PR-description mode (PopulateSquashCommentWithCommitMessages=false),
commit messages beyond the oldest were silently dropped. They are now
appended as bullet points after the PR description, consistent with the
commit-message mode format.

The commit-message loop is extracted into formatSquashMergeCommitMessages.
Callers that want to skip the oldest commit pass a trimmed slice
(commits[:max(0, len(commits)-1)]) instead of a skipFirst bool flag.

Co-Authored-By: Nicolas <nicolas@bircks.eu>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Nicolas 2026-05-31 12:12:36 +02:00
parent a39b2775ed
commit f5d0e1633d
No known key found for this signature in database
GPG Key ID: 9BA6A5FDF1283D78
2 changed files with 63 additions and 31 deletions

View File

@ -831,45 +831,27 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequ
authors := make([]string, 0, len(commits))
stringBuilder := strings.Builder{}
messageHasTrailers := false
if !setting.Repository.PullRequest.PopulateSquashCommentWithCommitMessages {
// use PR's title and description as squash commit message
message := strings.TrimSpace(pr.Issue.Content)
messageHasTrailers = commitMessageTrailersPattern.MatchString(message)
stringBuilder.WriteString(message)
if stringBuilder.Len() > 0 {
additionalCommitMessages := formatSquashMergeCommitMessages(commits[:max(0, len(commits)-1)])
if additionalCommitMessages != "" {
if stringBuilder.Len() > 0 {
stringBuilder.WriteString("\n\n")
}
stringBuilder.WriteString(additionalCommitMessages)
} else if stringBuilder.Len() > 0 {
stringBuilder.WriteRune('\n')
if !commitMessageTrailersPattern.MatchString(message) {
// TODO: this trailer check doesn't work with the separator line added below for the co-authors
if !messageHasTrailers {
stringBuilder.WriteRune('\n')
}
}
} else {
// use PR's commit messages as squash commit message
// commits list is in reverse chronological order
maxMsgSize := setting.Repository.PullRequest.DefaultMergeMessageSize
for _, commit := range slices.Backward(commits) {
msg := strings.TrimSpace(commit.MessageUTF8())
if msg == "" {
continue
}
// This format follows GitHub's squash commit message style,
// even if there are other "* " in the commit message body, they are written as-is.
// Maybe, ideally, we should indent those lines too.
_, _ = fmt.Fprintf(&stringBuilder, "* %s\n\n", msg)
if maxMsgSize > 0 && stringBuilder.Len() >= maxMsgSize {
tmp := stringBuilder.String()
wasValidUtf8 := utf8.ValidString(tmp)
tmp = tmp[:maxMsgSize] + "..."
if wasValidUtf8 {
// If the message was valid UTF-8 before truncation, ensure it remains valid after truncation
// For non-utf8 messages, we can't do much about it, end users should use utf-8 as much as possible
tmp = strings.ToValidUTF8(tmp, "")
}
stringBuilder.Reset()
stringBuilder.WriteString(tmp)
break
}
}
stringBuilder.WriteString(formatSquashMergeCommitMessages(commits))
}
// collect co-authors
@ -911,8 +893,7 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequ
}
}
if stringBuilder.Len() > 0 && len(authors) > 0 {
// TODO: this separator line doesn't work with the trailer check (commitMessageTrailersPattern) above
if stringBuilder.Len() > 0 && len(authors) > 0 && !messageHasTrailers {
stringBuilder.WriteString("---------\n\n")
}
@ -925,6 +906,37 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequ
return stringBuilder.String()
}
func formatSquashMergeCommitMessages(commits []*git.Commit) string {
// commits list is in reverse chronological order
maxMsgSize := setting.Repository.PullRequest.DefaultMergeMessageSize
stringBuilder := strings.Builder{}
for _, commit := range slices.Backward(commits) {
msg := strings.TrimSpace(commit.MessageUTF8())
if msg == "" {
continue
}
// This format follows GitHub's squash commit message style,
// even if there are other "* " in the commit message body, they are written as-is.
// Maybe, ideally, we should indent those lines too.
_, _ = fmt.Fprintf(&stringBuilder, "* %s\n\n", msg)
if maxMsgSize > 0 && stringBuilder.Len() >= maxMsgSize {
tmp := stringBuilder.String()
wasValidUtf8 := utf8.ValidString(tmp)
tmp = tmp[:maxMsgSize] + "..."
if wasValidUtf8 {
// If the message was valid UTF-8 before truncation, ensure it remains valid after truncation
// For non-utf8 messages, we can't do much about it, end users should use utf-8 as much as possible
tmp = strings.ToValidUTF8(tmp, "")
}
stringBuilder.Reset()
stringBuilder.WriteString(tmp)
break
}
}
return stringBuilder.String()
}
// GetIssuesAllCommitStatus returns a map of issue ID to a list of all statuses for the most recent commit as well as a map of issue ID to only the commit's latest status
func GetIssuesAllCommitStatus(ctx context.Context, issues issues_model.IssueList) (map[int64][]*git_model.CommitStatus, map[int64]*git_model.CommitStatus, error) {
if err := issues.LoadPullRequests(ctx); err != nil {

View File

@ -11,7 +11,9 @@ import (
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
"gitea.dev/models/unittest"
"gitea.dev/modules/git"
"gitea.dev/modules/gitrepo"
"gitea.dev/modules/setting"
"github.com/stretchr/testify/assert"
)
@ -35,6 +37,24 @@ func TestPullRequest_CommitMessageTrailersPattern(t *testing.T) {
assert.True(t, commitMessageTrailersPattern.MatchString("Folded value.\n\nFolded-trailer: This is\n a folded\n trailer value\nOther-Trailer: Value"))
}
func TestPullRequest_FormatSquashMergeCommitMessages(t *testing.T) {
oldest := &git.Commit{CommitMessage: git.CommitMessage{MessageRaw: "commit msg 1"}}
newest := &git.Commit{CommitMessage: git.CommitMessage{MessageRaw: "commit msg 2\n\nCommit description."}}
defer func(old int) { setting.Repository.PullRequest.DefaultMergeMessageSize = old }(
setting.Repository.PullRequest.DefaultMergeMessageSize,
)
setting.Repository.PullRequest.DefaultMergeMessageSize = 0
// all commits
assert.Equal(t, "* commit msg 1\n\n* commit msg 2\n\nCommit description.\n\n",
formatSquashMergeCommitMessages([]*git.Commit{newest, oldest}))
// PR-description mode: pass all-but-oldest so the oldest is not duplicated
assert.Equal(t, "* commit msg 2\n\nCommit description.\n\n",
formatSquashMergeCommitMessages([]*git.Commit{newest}))
}
func TestPullRequest_GetDefaultMergeMessage_InternalTracker(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})