Skip to content

Commit ed977d9

Browse files
authored
Use GitHub-style commit message for squash merge (#35987)
1 parent 62d750e commit ed977d9

File tree

3 files changed

+195
-44
lines changed

3 files changed

+195
-44
lines changed

services/pull/pull.go

Lines changed: 42 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"regexp"
1414
"strings"
1515
"time"
16+
"unicode/utf8"
1617

1718
"code.gitea.io/gitea/models/db"
1819
git_model "code.gitea.io/gitea/models/git"
@@ -838,64 +839,66 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequ
838839
stringBuilder := strings.Builder{}
839840

840841
if !setting.Repository.PullRequest.PopulateSquashCommentWithCommitMessages {
842+
// use PR's title and description as squash commit message
841843
message := strings.TrimSpace(pr.Issue.Content)
842844
stringBuilder.WriteString(message)
843845
if stringBuilder.Len() > 0 {
844846
stringBuilder.WriteRune('\n')
845847
if !commitMessageTrailersPattern.MatchString(message) {
848+
// TODO: this trailer check doesn't work with the separator line added below for the co-authors
846849
stringBuilder.WriteRune('\n')
847850
}
848851
}
849-
}
850-
851-
// commits list is in reverse chronological order
852-
first := true
853-
for i := len(commits) - 1; i >= 0; i-- {
854-
commit := commits[i]
855-
856-
if setting.Repository.PullRequest.PopulateSquashCommentWithCommitMessages {
857-
maxSize := setting.Repository.PullRequest.DefaultMergeMessageSize
858-
if maxSize < 0 || stringBuilder.Len() < maxSize {
859-
var toWrite []byte
860-
if first {
861-
first = false
862-
toWrite = []byte(strings.TrimPrefix(commit.CommitMessage, pr.Issue.Title))
863-
} else {
864-
toWrite = []byte(commit.CommitMessage)
865-
}
866-
867-
if len(toWrite) > maxSize-stringBuilder.Len() && maxSize > -1 {
868-
toWrite = append(toWrite[:maxSize-stringBuilder.Len()], "..."...)
869-
}
870-
if _, err := stringBuilder.Write(toWrite); err != nil {
871-
log.Error("Unable to write commit message Error: %v", err)
872-
return ""
873-
}
852+
} else {
853+
// use PR's commit messages as squash commit message
854+
// commits list is in reverse chronological order
855+
maxMsgSize := setting.Repository.PullRequest.DefaultMergeMessageSize
856+
for i := len(commits) - 1; i >= 0; i-- {
857+
commit := commits[i]
858+
msg := strings.TrimSpace(commit.CommitMessage)
859+
if msg == "" {
860+
continue
861+
}
874862

875-
if _, err := stringBuilder.WriteRune('\n'); err != nil {
876-
log.Error("Unable to write commit message Error: %v", err)
877-
return ""
863+
// This format follows GitHub's squash commit message style,
864+
// even if there are other "* " in the commit message body, they are written as-is.
865+
// Maybe, ideally, we should indent those lines too.
866+
_, _ = fmt.Fprintf(&stringBuilder, "* %s\n\n", msg)
867+
if maxMsgSize > 0 && stringBuilder.Len() >= maxMsgSize {
868+
tmp := stringBuilder.String()
869+
wasValidUtf8 := utf8.ValidString(tmp)
870+
tmp = tmp[:maxMsgSize] + "..."
871+
if wasValidUtf8 {
872+
// If the message was valid UTF-8 before truncation, ensure it remains valid after truncation
873+
// For non-utf8 messages, we can't do much about it, end users should use utf-8 as much as possible
874+
tmp = strings.ToValidUTF8(tmp, "")
878875
}
876+
stringBuilder.Reset()
877+
stringBuilder.WriteString(tmp)
878+
break
879879
}
880880
}
881+
}
881882

883+
// collect co-authors
884+
for _, commit := range commits {
882885
authorString := commit.Author.String()
883886
if uniqueAuthors.Add(authorString) && authorString != posterSig {
884887
// Compare use account as well to avoid adding the same author multiple times
885-
// times when email addresses are private or multiple emails are used.
888+
// when email addresses are private or multiple emails are used.
886889
commitUser, _ := user_model.GetUserByEmail(ctx, commit.Author.Email)
887890
if commitUser == nil || commitUser.ID != pr.Issue.Poster.ID {
888891
authors = append(authors, authorString)
889892
}
890893
}
891894
}
892895

893-
// Consider collecting the remaining authors
896+
// collect the remaining authors
894897
if limit >= 0 && setting.Repository.PullRequest.DefaultMergeMessageAllAuthors {
895898
skip := limit
896899
limit = 30
897900
for {
898-
commits, err := gitRepo.CommitsBetweenLimit(headCommit, mergeBase, limit, skip)
901+
commits, err = gitRepo.CommitsBetweenLimit(headCommit, mergeBase, limit, skip)
899902
if err != nil {
900903
log.Error("Unable to get commits between: %s %s Error: %v", pr.HeadBranch, pr.MergeBase, err)
901904
return ""
@@ -916,19 +919,15 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequ
916919
}
917920
}
918921

922+
if stringBuilder.Len() > 0 && len(authors) > 0 {
923+
// TODO: this separator line doesn't work with the trailer check (commitMessageTrailersPattern) above
924+
stringBuilder.WriteString("---------\n\n")
925+
}
926+
919927
for _, author := range authors {
920-
if _, err := stringBuilder.WriteString("Co-authored-by: "); err != nil {
921-
log.Error("Unable to write to string builder Error: %v", err)
922-
return ""
923-
}
924-
if _, err := stringBuilder.WriteString(author); err != nil {
925-
log.Error("Unable to write to string builder Error: %v", err)
926-
return ""
927-
}
928-
if _, err := stringBuilder.WriteRune('\n'); err != nil {
929-
log.Error("Unable to write to string builder Error: %v", err)
930-
return ""
931-
}
928+
stringBuilder.WriteString("Co-authored-by: ")
929+
stringBuilder.WriteString(author)
930+
stringBuilder.WriteRune('\n')
932931
}
933932

934933
return stringBuilder.String()

tests/integration/api_repo_file_helpers.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ import (
1919

2020
type createFileInBranchOptions struct {
2121
OldBranch, NewBranch string
22+
CommitMessage string
23+
CommitterName string
24+
CommitterEmail string
2225
}
2326

2427
func testCreateFileInBranch(t *testing.T, user *user_model.User, repo *repo_model.Repository, createOpts createFileInBranchOptions, files map[string]string) *api.FilesResponse {
@@ -29,7 +32,17 @@ func testCreateFileInBranch(t *testing.T, user *user_model.User, repo *repo_mode
2932

3033
func createFileInBranch(user *user_model.User, repo *repo_model.Repository, createOpts createFileInBranchOptions, files map[string]string) (*api.FilesResponse, error) {
3134
ctx := context.TODO()
32-
opts := &files_service.ChangeRepoFilesOptions{OldBranch: createOpts.OldBranch, NewBranch: createOpts.NewBranch}
35+
opts := &files_service.ChangeRepoFilesOptions{
36+
OldBranch: createOpts.OldBranch,
37+
NewBranch: createOpts.NewBranch,
38+
Message: createOpts.CommitMessage,
39+
}
40+
if createOpts.CommitterName != "" || createOpts.CommitterEmail != "" {
41+
opts.Committer = &files_service.IdentityOptions{
42+
GitUserName: createOpts.CommitterName,
43+
GitUserEmail: createOpts.CommitterEmail,
44+
}
45+
}
3346
for path, content := range files {
3447
opts.Files = append(opts.Files, &files_service.ChangeRepoFile{
3548
Operation: "create",

tests/integration/pull_merge_test.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333
api "code.gitea.io/gitea/modules/structs"
3434
"code.gitea.io/gitea/modules/test"
3535
"code.gitea.io/gitea/modules/translation"
36+
"code.gitea.io/gitea/modules/util"
3637
"code.gitea.io/gitea/services/automerge"
3738
"code.gitea.io/gitea/services/automergequeue"
3839
pull_service "code.gitea.io/gitea/services/pull"
@@ -41,6 +42,7 @@ import (
4142
files_service "code.gitea.io/gitea/services/repository/files"
4243

4344
"github.com/stretchr/testify/assert"
45+
"github.com/stretchr/testify/require"
4446
)
4547

4648
type MergeOptions struct {
@@ -1212,3 +1214,140 @@ func TestPullSquashMergeEmpty(t *testing.T) {
12121214
})
12131215
})
12141216
}
1217+
1218+
func TestPullSquashMessage(t *testing.T) {
1219+
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
1220+
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
1221+
user2Session := loginUser(t, user2.Name)
1222+
1223+
defer test.MockVariableValue(&setting.Repository.PullRequest.PopulateSquashCommentWithCommitMessages, true)()
1224+
defer test.MockVariableValue(&setting.Repository.PullRequest.DefaultMergeMessageSize, 80)()
1225+
1226+
repo, err := repo_service.CreateRepository(t.Context(), user2, user2, repo_service.CreateRepoOptions{
1227+
Name: "squash-message-test",
1228+
Description: "Test squash message",
1229+
AutoInit: true,
1230+
Readme: "Default",
1231+
DefaultBranch: "main",
1232+
})
1233+
require.NoError(t, err)
1234+
1235+
type commitInfo struct {
1236+
userName string
1237+
commitMessage string
1238+
}
1239+
1240+
testCases := []struct {
1241+
name string
1242+
commitInfos []*commitInfo
1243+
expectedMessage string
1244+
}{
1245+
{
1246+
name: "Single-line messages",
1247+
commitInfos: []*commitInfo{
1248+
{
1249+
userName: user2.Name,
1250+
commitMessage: "commit msg 1",
1251+
},
1252+
{
1253+
userName: user2.Name,
1254+
commitMessage: "commit msg 2",
1255+
},
1256+
},
1257+
expectedMessage: `* commit msg 1
1258+
1259+
* commit msg 2
1260+
1261+
`,
1262+
},
1263+
{
1264+
name: "Multiple-line messages",
1265+
commitInfos: []*commitInfo{
1266+
{
1267+
userName: user2.Name,
1268+
commitMessage: `commit msg 1
1269+
1270+
Commit description.`,
1271+
},
1272+
{
1273+
userName: user2.Name,
1274+
commitMessage: `commit msg 2
1275+
1276+
- Detail 1
1277+
- Detail 2`,
1278+
},
1279+
},
1280+
expectedMessage: `* commit msg 1
1281+
1282+
Commit description.
1283+
1284+
* commit msg 2
1285+
1286+
- Detail 1
1287+
- Detail 2
1288+
1289+
`,
1290+
},
1291+
{
1292+
name: "Too long message",
1293+
commitInfos: []*commitInfo{
1294+
{
1295+
userName: user2.Name,
1296+
commitMessage: `loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong message`,
1297+
},
1298+
},
1299+
expectedMessage: `* looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo...`,
1300+
},
1301+
{
1302+
name: "Test Co-authored-by",
1303+
commitInfos: []*commitInfo{
1304+
{
1305+
userName: user2.Name,
1306+
commitMessage: "commit msg 1",
1307+
},
1308+
{
1309+
userName: "user4",
1310+
commitMessage: "commit msg 2",
1311+
},
1312+
},
1313+
expectedMessage: `* commit msg 1
1314+
1315+
* commit msg 2
1316+
1317+
---------
1318+
1319+
Co-authored-by: user4 <[email protected]>
1320+
`,
1321+
},
1322+
}
1323+
1324+
for tcNum, tc := range testCases {
1325+
t.Run(tc.name, func(t *testing.T) {
1326+
branchName := "test-branch-" + strconv.Itoa(tcNum)
1327+
for infoIdx, info := range tc.commitInfos {
1328+
createFileOpts := createFileInBranchOptions{
1329+
CommitMessage: info.commitMessage,
1330+
CommitterName: info.userName,
1331+
CommitterEmail: util.Iif(info.userName != "", info.userName+"@example.com", ""),
1332+
OldBranch: util.Iif(infoIdx == 0, "main", branchName),
1333+
NewBranch: branchName,
1334+
}
1335+
testCreateFileInBranch(t, user2, repo, createFileOpts, map[string]string{"dummy-file-" + strconv.Itoa(infoIdx): "dummy content"})
1336+
}
1337+
resp := testPullCreateDirectly(t, user2Session, createPullRequestOptions{
1338+
BaseRepoOwner: user2.Name,
1339+
BaseRepoName: repo.Name,
1340+
BaseBranch: repo.DefaultBranch,
1341+
HeadBranch: branchName,
1342+
Title: "Pull for " + branchName,
1343+
})
1344+
elems := strings.Split(test.RedirectURL(resp), "/")
1345+
pullIndex, err := strconv.ParseInt(elems[4], 10, 64)
1346+
assert.NoError(t, err)
1347+
pullRequest := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, Index: pullIndex})
1348+
squashMergeCommitMessage := pull_service.GetSquashMergeCommitMessages(t.Context(), pullRequest)
1349+
assert.Equal(t, tc.expectedMessage, squashMergeCommitMessage)
1350+
})
1351+
}
1352+
})
1353+
}

0 commit comments

Comments
 (0)