[autoroll] Add better handling for git trailers
Change-Id: I62b6998c1063d9b242b5187581bc359c79722474
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/373740
Commit-Queue: Eric Boren <borenet@google.com>
Reviewed-by: Ravi Mistry <rmistry@google.com>
diff --git a/autoroll/go/repo_manager/deps_repo_manager_test.go b/autoroll/go/repo_manager/deps_repo_manager_test.go
index 0f63fb3..ac7949e 100644
--- a/autoroll/go/repo_manager/deps_repo_manager_test.go
+++ b/autoroll/go/repo_manager/deps_repo_manager_test.go
@@ -41,7 +41,7 @@
blah blah blah
`
- fakeCommitMsgMock = fakeCommitMsg + "Change-Id: 123"
+ fakeCommitMsgMock = fakeCommitMsg + "\nChange-Id: 123"
)
var (
diff --git a/go/gerrit/change_edit_helpers.go b/go/gerrit/change_edit_helpers.go
index cb775c6..1ab523d 100644
--- a/go/gerrit/change_edit_helpers.go
+++ b/go/gerrit/change_edit_helpers.go
@@ -4,6 +4,7 @@
"context"
"strings"
+ "go.skia.org/infra/go/git"
"go.skia.org/infra/go/skerr"
)
@@ -36,7 +37,10 @@
if err != nil {
return nil, skerr.Wrapf(err, "failed to create change")
}
- commitMsg = strings.TrimSpace(commitMsg) + "\n" + "Change-Id: " + ci.ChangeId
+ commitMsg, err = git.AddTrailer(commitMsg, "Change-Id: "+ci.ChangeId)
+ if err != nil {
+ return nil, skerr.Wrap(err)
+ }
if err := EditChange(ctx, g, ci, func(ctx context.Context, g GerritInterface, ci *ChangeInfo) error {
if err := g.SetCommitMessage(ctx, ci, commitMsg); err != nil {
return skerr.Wrapf(err, "failed to set commit message to:\n\n%s\n\n", commitMsg)
diff --git a/go/git/util.go b/go/git/util.go
index 2c292ff..403d7c9 100644
--- a/go/git/util.go
+++ b/go/git/util.go
@@ -7,6 +7,7 @@
"os"
"path"
"path/filepath"
+ "regexp"
"strconv"
"strings"
"time"
@@ -31,6 +32,10 @@
DefaultRemoteBranch = git_common.DefaultRemoteBranch
)
+// This regex is taken from:
+// https://source.chromium.org/chromium/infra/infra/+/master:go/src/go.chromium.org/luci/common/git/footer/footer.go?q=%22%5E%5Cs*(%5B%5Cw-%5D%2B):%20*(.*)$%22&ss=chromium
+var trailerRegex = regexp.MustCompile(`^\s*([\w-]+): *(.*)$`)
+
// Types of git objects.
const (
ObjectTypeBlob ObjectType = "blob"
@@ -197,3 +202,70 @@
Sys: nil,
}.Get(), nil
}
+
+// SplitTrailers splits a commit message into a main commit message body and
+// trailers footer. Assumes that the commit message is already well-formed with
+// respect to trailers, ie. there is an empty line between the last body
+// paragraph and the single trailers paragraph, which contains only lines in
+// "key: value" format.
+func SplitTrailers(commitMsg string) ([]string, []string) {
+ // Split the commit message into paragraphs.
+ if commitMsg == "" {
+ return []string{}, []string{}
+ }
+ lines := strings.Split(strings.TrimSpace(commitMsg), "\n")
+ paragraphs := [][]string{}
+ var paragraph []string
+ for _, line := range lines {
+ paragraph = append(paragraph, line)
+ if line == "" {
+ if len(paragraph) > 0 {
+ paragraphs = append(paragraphs, paragraph)
+ }
+ paragraph = []string{}
+ }
+ }
+ if len(paragraph) > 0 {
+ paragraphs = append(paragraphs, paragraph)
+ }
+
+ // If the last paragraph consists of valid trailers, split off those lines,
+ // otherwise include them as part of the main commit message body.
+ if len(paragraphs) < 1 {
+ return lines, []string{}
+ }
+ trailerLines := paragraphs[len(paragraphs)-1]
+ for _, line := range trailerLines {
+ if !trailerRegex.MatchString(line) {
+ // At least one line in the last paragraph does not fit the trailer
+ // format; assume there are no trailers.
+ return lines, []string{}
+ }
+ }
+ bodyLines := make([]string, 0, len(paragraphs[0]))
+ for _, paragraph := range paragraphs[:len(paragraphs)-1] {
+ bodyLines = append(bodyLines, paragraph...)
+ }
+ return bodyLines, trailerLines
+}
+
+// JoinTrailers joins a main commit message body with a trailers footer.
+func JoinTrailers(bodyLines, trailers []string) string {
+ commitMsg := make([]string, 0, len(bodyLines)+len(trailers)+1)
+ commitMsg = append(commitMsg, bodyLines...)
+ if len(commitMsg) > 0 && commitMsg[len(commitMsg)-1] != "" {
+ commitMsg = append(commitMsg, "")
+ }
+ commitMsg = append(commitMsg, trailers...)
+ return strings.Join(commitMsg, "\n")
+}
+
+// AddTrailer adds a trailer to the given commit message.
+func AddTrailer(commitMsg, trailer string) (string, error) {
+ if !trailerRegex.MatchString(trailer) {
+ return "", skerr.Fmt("%q is not a valid git trailer", trailer)
+ }
+ body, trailers := SplitTrailers(commitMsg)
+ trailers = append(trailers, trailer)
+ return JoinTrailers(body, trailers), nil
+}
diff --git a/go/git/util_test.go b/go/git/util_test.go
index 3849751..213d3bb 100644
--- a/go/git/util_test.go
+++ b/go/git/util_test.go
@@ -25,3 +25,117 @@
require.NoError(t, err)
assert.Equal(t, "github.com/skia-dev/textfiles", normGitWithExt)
}
+
+func TestSplitTrailers(t *testing.T) {
+ unittest.SmallTest(t)
+
+ test := func(commitMsg string, expectBody, expectTrailers []string) {
+ actualBody, actualTrailers := SplitTrailers(commitMsg)
+ require.Equal(t, expectBody, actualBody)
+ require.Equal(t, expectTrailers, actualTrailers)
+ }
+
+ test("", []string{}, []string{})
+ test("Hello World", []string{"Hello World"}, []string{})
+ test(`Hello World
+ `, []string{"Hello World"}, []string{})
+ test(`Hello World
+
+Paragraph 2
+`, []string{"Hello World", "", "Paragraph 2"}, []string{})
+ test(`Hello World
+
+Trailer-Key: trailer-value`, []string{"Hello World", ""}, []string{"Trailer-Key: trailer-value"})
+ test(`Hello World
+
+Trailer-Key: trailer-value
+`, []string{"Hello World", ""}, []string{"Trailer-Key: trailer-value"})
+
+ test(`Hello World
+
+Paragraph 2
+
+Trailer-Key: trailer-value
+T2: V2
+Bug: 1234, chromium:5678
+`,
+ []string{"Hello World", "", "Paragraph 2", ""},
+ []string{"Trailer-Key: trailer-value", "T2: V2", "Bug: 1234, chromium:5678"})
+
+ test(`Trailer-Key: trailer-value`, []string{}, []string{"Trailer-Key: trailer-value"})
+}
+
+func TestJoinTrailers(t *testing.T) {
+ unittest.SmallTest(t)
+
+ test := func(body, trailers []string, expect string) {
+ actual := JoinTrailers(body, trailers)
+ require.Equal(t, expect, actual)
+ }
+
+ test([]string{}, []string{}, "")
+ test([]string{"Hello World"}, []string{}, "Hello World\n")
+ test([]string{"Hello World", ""}, []string{}, "Hello World\n")
+ test(
+ []string{"Hello World", "", "Paragraph 2", ""},
+ []string{},
+ `Hello World
+
+Paragraph 2
+`)
+ test([]string{}, []string{"Trailer-Key: trailer-value"}, "Trailer-Key: trailer-value")
+
+ test(
+ []string{"Hello World", "", "Paragraph 2", ""},
+ []string{"Trailer-Key: trailer-value"},
+ `Hello World
+
+Paragraph 2
+
+Trailer-Key: trailer-value`)
+}
+
+func TestAddTrailer(t *testing.T) {
+ unittest.SmallTest(t)
+
+ test := func(commitMsg, trailer, expect, expectErr string) {
+ actual, err := AddTrailer(commitMsg, trailer)
+ require.Equal(t, expect, actual)
+ if expectErr != "" {
+ require.NotNil(t, err)
+ require.Contains(t, err.Error(), expectErr)
+ }
+ }
+
+ test("", "", "", "\"\" is not a valid git trailer")
+ test("", "bogustrailer", "", "\"bogustrailer\" is not a valid git trailer")
+ test("", "Trailer-Key: trailer-value", "Trailer-Key: trailer-value", "")
+ test("Hello World", "Trailer-Key: trailer-value", `Hello World
+
+Trailer-Key: trailer-value`, "")
+
+ test(`Hello World
+
+Paragraph 2
+`,
+ "Trailer-Key: trailer-value",
+ `Hello World
+
+Paragraph 2
+
+Trailer-Key: trailer-value`, "")
+
+ test(`Hello World
+
+Paragraph 2
+
+K1: V1
+`,
+ "Trailer-Key: trailer-value",
+ `Hello World
+
+Paragraph 2
+
+K1: V1
+Trailer-Key: trailer-value`, "")
+}