Repository: go-git/go-git

Issue:
EncodeWithoutSignature breaks signatures generated by Jujutsu
The canonical commit representation returned by [`EncodeWithoutSignature`](https://pkg.go.dev/github.com/go-git/go-git/v5/plumbing/object#Commit.EncodeWithoutSignature) will strip out any extra field it doesn't support.

[Jujutsu](https://jj-vcs.github.io) commits would add extra fields like:
```
tree eddf38b125ad5ed8fbd9438b33e0c41662f8739d
parent 7f92e7685ce2cac51a8348d38be665faf1af314d
parent 185bd1d7dbb2464cf744dc33c0993de1965685a6
author Arthur Gautier <...> 1753582512 +0000
committer Arthur Gautier <...> 1754670137 -0700
change-id xkmmqzonpyyrvxvtxsuutmmxrzyyvwon
gpgsig -----BEGIN PGP SIGNATURE-----

 -----END PGP SIGNATURE-----

Foobar
```
(notice the `change-id` which is jujutsu specific).

The canonical representation (the one to be signed) should be:
```
tree eddf38b125ad5ed8fbd9438b33e0c41662f8739d
parent 7f92e7685ce2cac51a8348d38be665faf1af314d
parent 185bd1d7dbb2464cf744dc33c0993de1965685a6
author Arthur Gautier <...> 1753582512 +0000
committer Arthur Gautier <...> 1754670137 -0700
change-id xkmmqzonpyyrvxvtxsuutmmxrzyyvwon

Foobar
```

but instead we get:
```
tree eddf38b125ad5ed8fbd9438b33e0c41662f8739d
parent 7f92e7685ce2cac51a8348d38be665faf1af314d
parent 185bd1d7dbb2464cf744dc33c0993de1965685a6
author Arthur Gautier <...> 1753582512 +0000
committer Arthur Gautier <...> 1754670137 -0700

Foobar
```

Which in effects breaks signatures.

The implementation should instead just filter out the gpgsig field and leave out the other ones.

Discussion:

Solution patch:
diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml
index 5e52614e..766880dc 100644
--- a/.github/workflows/cifuzz.yml
+++ b/.github/workflows/cifuzz.yml
@@ -34,7 +34,7 @@ jobs:
        path: ./out/artifacts
    - name: Upload Sarif
      if: always() && steps.build.outcome == 'success'
-     uses: github/codeql-action/upload-sarif@v3.30.1
+     uses: github/codeql-action/upload-sarif@v3.30.3
      with:
       # Path to SARIF file relative to the root of the repository
       sarif_file: cifuzz-sarif/results.sarif
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 9485fca8..ed61cdc8 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -32,7 +32,7 @@ jobs:
 
       # Initializes the CodeQL tools for scanning.
       - name: Initialize CodeQL
-        uses: github/codeql-action/init@aeaa720d65caa766c18788224dc84cddd1ba1c03 # v2.22.11
+        uses: github/codeql-action/init@aa90e97ad2ed17cde6a43e89f70138299e64f837 # v2.22.11
         with:
           languages: ${{ matrix.language }}
           # xref: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
@@ -43,6 +43,6 @@ jobs:
         run: go build ./...
 
       - name: Perform CodeQL Analysis
-        uses: github/codeql-action/analyze@aeaa720d65caa766c18788224dc84cddd1ba1c03 # v2.22.11
+        uses: github/codeql-action/analyze@aa90e97ad2ed17cde6a43e89f70138299e64f837 # v2.22.11
         with:
           category: "/language:${{matrix.language}}"
diff --git a/plumbing/object/commit.go b/plumbing/object/commit.go
index ca0b6e7e..97e52e59 100644
--- a/plumbing/object/commit.go
+++ b/plumbing/object/commit.go
@@ -62,10 +62,55 @@ type Commit struct {
 	ParentHashes []plumbing.Hash
 	// Encoding is the encoding of the commit.
 	Encoding MessageEncoding
+	// List of extra headers of the commit
+	ExtraHeaders []ExtraHeader
 
 	s storer.EncodedObjectStorer
 }
 
+// ExtraHeader holds any non-standard header
+type ExtraHeader struct {
+	// Header name
+	Key string
+	// Value of the header
+	Value string
+}
+
+// Implement fmt.Formatter for ExtraHeader
+func (h ExtraHeader) Format(f fmt.State, verb rune) {
+	switch verb {
+	case 'v':
+		fmt.Fprintf(f, "ExtraHeader{Key: %v, Value: %v}", h.Key, h.Value)
+	default:
+		fmt.Fprintf(f, "%s", h.Key)
+		if len(h.Value) > 0 {
+			fmt.Fprint(f, " ")
+			// Content may be spread on multiple lines, if so we need to
+			// prepend each of them with a space for "continuation".
+			value := strings.TrimSuffix(h.Value, "\n")
+			lines := strings.Split(value, "\n")
+			fmt.Fprint(f, strings.Join(lines, "\n "))
+		}
+	}
+}
+
+// Parse an extra header and indicate whether it may be continue on the next line
+func parseExtraHeader(line []byte) (ExtraHeader, bool) {
+	split := bytes.SplitN(line, []byte{' '}, 2)
+
+	out := ExtraHeader {
+		Key: string(bytes.TrimRight(split[0], "\n")),
+		Value: "",
+	}
+
+	if len(split) == 2 {
+		out.Value += string(split[1])
+		return out, true
+	} else {
+		return out, false
+	}
+}
+
 // GetCommit gets a commit from an object storer and decodes it.
 func GetCommit(s storer.EncodedObjectStorer, h plumbing.Hash) (*Commit, error) {
 	o, err := s.EncodedObject(plumbing.CommitObject, h)
@@ -204,6 +249,7 @@ func (c *Commit) Decode(o plumbing.EncodedObject) (err error) {
 	var mergetag bool
 	var pgpsig bool
 	var msgbuf bytes.Buffer
+	var extraheader *ExtraHeader = nil
 	for {
 		line, err := r.ReadBytes('\n')
 		if err != nil && err != io.EOF {
@@ -230,7 +276,19 @@ func (c *Commit) Decode(o plumbing.EncodedObject) (err error) {
 			}
 		}
 
+		if extraheader != nil {
+			if len(line) > 0 && line[0] == ' ' {
+				extraheader.Value += string(line[1:])
+				continue
+			} else {
+				extraheader.Value = strings.TrimRight(extraheader.Value, "\n")
+				c.ExtraHeaders = append(c.ExtraHeaders, *extraheader)
+				extraheader = nil
+			}
+		}
+
 		if !message {
+			original_line := line
 			line = bytes.TrimSpace(line)
 			if len(line) == 0 {
 				message = true
@@ -261,6 +319,13 @@ func (c *Commit) Decode(o plumbing.EncodedObject) (err error) {
 			case headerpgp:
 				c.PGPSignature += string(data) + "\n"
 				pgpsig = true
+			default:
+				h, maybecontinued := parseExtraHeader(original_line)
+				if maybecontinued {
+					extraheader = &h
+				} else {
+					c.ExtraHeaders = append(c.ExtraHeaders, h)
+				}
 			}
 		} else {
 			msgbuf.Write(line)
@@ -341,6 +406,13 @@ func (c *Commit) encode(o plumbing.EncodedObject, includeSig bool) (err error) {
 		}
 	}
 
+	for _, header := range c.ExtraHeaders {
+		
+		if _, err = fmt.Fprintf(w, "\n%s", header); err != nil {
+			return err
+		}
+	}
+
 	if c.PGPSignature != "" && includeSig {
 		if _, err = fmt.Fprint(w, "\n"+headerpgp+" "); err != nil {
 			return err



Test patch:
diff --git a/plumbing/object/commit_test.go b/plumbing/object/commit_test.go
index 30871007..91a25d2c 100644
--- a/plumbing/object/commit_test.go
+++ b/plumbing/object/commit_test.go
@@ -564,6 +564,114 @@ func (s *SuiteCommit) TestEncodeWithoutSignature() {
 		string(payload))
 }
 
+func (s *SuiteCommit) TestEncodeWithoutSignatureJujutsu() {
+	object := &plumbing.MemoryObject{}
+	object.SetType(plumbing.CommitObject)
+	object.Write([]byte(`tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
+author John Doe <john.doe@example.com> 1755280730 -0700
+committer John Doe <john.doe@example.com> 1755280730 -0700
+change-id wxmuynokkzxmuwxwvnnpnptoyuypknwv
+gpgsig -----BEGIN PGP SIGNATURE-----
+ 
+ iHUEABMIAB0WIQSZpnSpGKbQbDaLe5iiNQl48cTY5gUCaJ91XQAKCRCiNQl48cTY
+ 5vCYAP9Sf1yV9oUviRIxEA+4rsGIx0hI6kqFajJ/3TtBjyCTggD+PFnKOxdXeFL2
+ GLwcCzFIsmQmkLxuLypsg+vueDSLpsM=
+ =VucY
+ -----END PGP SIGNATURE-----
+
+initial commit
+
+Change-Id: I6a6a696432d51cbff02d53234ccaca6b151afc34
+`))
+
+	commit, err := DecodeCommit(s.Storer, object)
+	s.NoError(err)
+
+	// Similar to TestString since no signature
+	encoded := &plumbing.MemoryObject{}
+	err = commit.EncodeWithoutSignature(encoded)
+	er, err := encoded.Reader()
+	s.NoError(err)
+	payload, err := io.ReadAll(er)
+	s.NoError(err)
+
+	s.Equal(`tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
+author John Doe <john.doe@example.com> 1755280730 -0700
+committer John Doe <john.doe@example.com> 1755280730 -0700
+change-id wxmuynokkzxmuwxwvnnpnptoyuypknwv
+
+initial commit
+
+Change-Id: I6a6a696432d51cbff02d53234ccaca6b151afc34
+`, string(payload))
+}
+
+func (s *SuiteCommit) TestEncodeExtraHeaders() {
+	object := &plumbing.MemoryObject{}
+	object.SetType(plumbing.CommitObject)
+	object.Write([]byte(`tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
+author John Doe <john.doe@example.com> 1755280730 -0700
+committer John Doe <john.doe@example.com> 1755280730 -0700
+continuedheader to be
+ continued
+continuedheader to be
+ continued
+ on
+ more than
+ a single line
+simpleflag
+ value no key
+
+initial commit
+`))
+
+	commit, err := DecodeCommit(s.Storer, object)
+	s.NoError(err)
+
+	s.Equal(commit.ExtraHeaders, []ExtraHeader{
+		ExtraHeader {
+			Key: "continuedheader",
+			Value: "to be\ncontinued",
+		},
+		ExtraHeader {
+			Key: "continuedheader",
+			Value: "to be\ncontinued\non\nmore than\na single line",
+		},
+		ExtraHeader {
+			Key: "simpleflag",
+			Value: "",
+		},
+		ExtraHeader {
+			Key: "",
+			Value: "value no key",
+		},
+	})
+
+	// Similar to TestString since no signature
+	encoded := &plumbing.MemoryObject{}
+	err = commit.EncodeWithoutSignature(encoded)
+	er, err := encoded.Reader()
+	s.NoError(err)
+	payload, err := io.ReadAll(er)
+	s.NoError(err)
+
+	s.Equal(`tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
+author John Doe <john.doe@example.com> 1755280730 -0700
+committer John Doe <john.doe@example.com> 1755280730 -0700
+continuedheader to be
+ continued
+continuedheader to be
+ continued
+ on
+ more than
+ a single line
+simpleflag
+ value no key
+
+initial commit
+`, string(payload))
+}
+
 func (s *SuiteCommit) TestLess() {
 	when1 := time.Now()
 	when2 := when1.Add(time.Hour)