Skip to content

Commit 930b3ff

Browse files
committed
commit.go: support multi-line header continuations
When Git wishes to continue one or more of a commit's extra headers on more than a single line, it writes out the following: parent: <SHA-1> tree: <SHA-1> gpgsig: -----BEGIN PGP SIGNATURE----- <signature> -----END PGP SIGNATURE----- Our current parsing implementation does not handle this correctly, based on a misunderstanding that one line is equivalent to one extra header, and vice versa. In fact, the situation presently is even more dire than not parsing the 'gpgsig' header incorrectly: we'll split the signature end ending line into their own "headers" and in doing so trim off the leading whitespace. In practice, this means that we can corrupt commits when round-tripping them in many interesting ways [1]. To address the situation, we do two things: 1. Teach gitobj that when we are parsing extra headers for a commit, _and_ a header line begins with a single whitespace character, we are in fact continuing the last known header. 2. Likewise, teach gitobj that when encoding a commit which has an extra header whose value contains a LF character, replace each LF with a leading space, to round trip commits of this form successfully. Together, (1) and (2) means that we parse the 'gpgsig' header in the above example as a _single_ entry in the commit's 'ExtraHeaders' field, as expected. [1]: git-lfs/git-lfs#3530
1 parent f9ae4a7 commit 930b3ff

File tree

2 files changed

+61
-5
lines changed

2 files changed

+61
-5
lines changed

commit.go

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -132,10 +132,23 @@ func (c *Commit) Decode(from io.Reader, size int64) (n int, err error) {
132132
c.Committer = ""
133133
}
134134
default:
135-
c.ExtraHeaders = append(c.ExtraHeaders, &ExtraHeader{
136-
K: fields[0],
137-
V: strings.Join(fields[1:], " "),
138-
})
135+
if strings.HasPrefix(s.Text(), " ") {
136+
idx := len(c.ExtraHeaders) - 1
137+
hdr := c.ExtraHeaders[idx]
138+
139+
// Append the line of text (removing the
140+
// leading space) to the last header
141+
// that we parsed, adding a newline
142+
// between the two.
143+
hdr.V = strings.Join(append(
144+
[]string{hdr.V}, s.Text()[1:],
145+
), "\n")
146+
} else {
147+
c.ExtraHeaders = append(c.ExtraHeaders, &ExtraHeader{
148+
K: fields[0],
149+
V: strings.Join(fields[1:], " "),
150+
})
151+
}
139152
}
140153
} else {
141154
messageParts = append(messageParts, s.Text())
@@ -177,7 +190,8 @@ func (c *Commit) Encode(to io.Writer) (n int, err error) {
177190
n = n + n2
178191

179192
for _, hdr := range c.ExtraHeaders {
180-
n3, err := fmt.Fprintf(to, "%s %s\n", hdr.K, hdr.V)
193+
n3, err := fmt.Fprintf(to, "%s %s\n",
194+
hdr.K, strings.Replace(hdr.V, "\n", "\n ", -1))
181195
if err != nil {
182196
return n, err
183197
}

commit_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"time"
1111

1212
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
1314
)
1415

1516
func TestCommitReturnsCorrectObjectType(t *testing.T) {
@@ -20,6 +21,8 @@ func TestCommitEncoding(t *testing.T) {
2021
author := &Signature{Name: "John Doe", Email: "john@example.com", When: time.Now()}
2122
committer := &Signature{Name: "Jane Doe", Email: "jane@example.com", When: time.Now()}
2223

24+
sig := "-----BEGIN PGP SIGNATURE-----\n<signature>\n-----END PGP SIGNATURE-----"
25+
2326
c := &Commit{
2427
Author: author.String(),
2528
Committer: committer.String(),
@@ -29,6 +32,7 @@ func TestCommitEncoding(t *testing.T) {
2932
TreeID: []byte("cccccccccccccccccccc"),
3033
ExtraHeaders: []*ExtraHeader{
3134
{"foo", "bar"},
35+
{"gpgsig", sig},
3236
},
3337
Message: "initial commit",
3438
}
@@ -44,6 +48,9 @@ func TestCommitEncoding(t *testing.T) {
4448
assertLine(t, buf, "author %s", author.String())
4549
assertLine(t, buf, "committer %s", committer.String())
4650
assertLine(t, buf, "foo bar")
51+
assertLine(t, buf, "gpgsig -----BEGIN PGP SIGNATURE-----")
52+
assertLine(t, buf, " <signature>")
53+
assertLine(t, buf, " -----END PGP SIGNATURE-----")
4754
assertLine(t, buf, "")
4855
assertLine(t, buf, "initial commit")
4956

@@ -164,6 +171,41 @@ func TestCommitDecodingWithWhitespace(t *testing.T) {
164171
assert.Equal(t, "tree <- initial commit", commit.Message)
165172
}
166173

174+
func TestCommitDecodingMultilineHeader(t *testing.T) {
175+
author := &Signature{Name: "", Email: "john@example.com", When: time.Now()}
176+
committer := &Signature{Name: "", Email: "jane@example.com", When: time.Now()}
177+
178+
treeId := []byte("cccccccccccccccccccc")
179+
180+
from := new(bytes.Buffer)
181+
182+
fmt.Fprintf(from, "author %s\n", author)
183+
fmt.Fprintf(from, "committer %s\n", committer)
184+
fmt.Fprintf(from, "tree %s\n", hex.EncodeToString(treeId))
185+
fmt.Fprintf(from, "gpgsig -----BEGIN PGP SIGNATURE-----\n")
186+
fmt.Fprintf(from, " <signature>\n")
187+
fmt.Fprintf(from, " -----END PGP SIGNATURE-----\n")
188+
fmt.Fprintf(from, "\ninitial commit\n")
189+
190+
flen := from.Len()
191+
192+
commit := new(Commit)
193+
n, err := commit.Decode(from, int64(flen))
194+
195+
require.Nil(t, err)
196+
require.Equal(t, flen, n)
197+
require.Len(t, commit.ExtraHeaders, 1)
198+
199+
hdr := commit.ExtraHeaders[0]
200+
201+
assert.Equal(t, "gpgsig", hdr.K)
202+
assert.EqualValues(t, []string{
203+
"-----BEGIN PGP SIGNATURE-----",
204+
"<signature>",
205+
"-----END PGP SIGNATURE-----"},
206+
strings.Split(hdr.V, "\n"))
207+
}
208+
167209
func assertLine(t *testing.T, buf *bytes.Buffer, wanted string, args ...interface{}) {
168210
got, err := buf.ReadString('\n')
169211
if err == io.EOF {

0 commit comments

Comments
 (0)