From c3c841090facce33faa2c80ca475ee673442b4a5 Mon Sep 17 00:00:00 2001 From: Paulo Gomes Date: Mon, 14 Apr 2025 18:05:46 +0100 Subject: [PATCH 1/6] Merge pull request #1492 from onee-only/fix-sparse-checkout-status utils: merkletrie, Fix diff on sparse-checkout index. Fixes #1406 --- utils/merkletrie/change.go | 4 +++- utils/merkletrie/difftree.go | 43 +++++++++++++++++------------------- worktree_test.go | 17 ++++++++++++++ 3 files changed, 40 insertions(+), 24 deletions(-) diff --git a/utils/merkletrie/change.go b/utils/merkletrie/change.go index 450feb4ba..562ff8902 100644 --- a/utils/merkletrie/change.go +++ b/utils/merkletrie/change.go @@ -131,7 +131,9 @@ func (l *Changes) addRecursive(root noder.Path, ctor noderToChangeFn) error { } if !root.IsDir() { - l.Add(ctor(root)) + if !root.Skip() { + l.Add(ctor(root)) + } return nil } diff --git a/utils/merkletrie/difftree.go b/utils/merkletrie/difftree.go index 4ef2d9907..7fc8d02d7 100644 --- a/utils/merkletrie/difftree.go +++ b/utils/merkletrie/difftree.go @@ -297,18 +297,16 @@ func DiffTreeContext(ctx context.Context, fromTree, toTree noder.Noder, case noMoreNoders: return ret, nil case onlyFromRemains: - if err = ret.AddRecursiveDelete(from); err != nil { - return nil, err + if !from.Skip() { + if err = ret.AddRecursiveDelete(from); err != nil { + return nil, err + } } if err = ii.nextFrom(); err != nil { return nil, err } case onlyToRemains: - if to.Skip() { - if err = ret.AddRecursiveDelete(to); err != nil { - return nil, err - } - } else { + if !to.Skip() { if err = ret.AddRecursiveInsert(to); err != nil { return nil, err } @@ -317,26 +315,25 @@ func DiffTreeContext(ctx context.Context, fromTree, toTree noder.Noder, return nil, err } case bothHaveNodes: - if from.Skip() { - if err = ret.AddRecursiveDelete(from); err != nil { - return nil, err - } - if err := ii.nextBoth(); err != nil { - return nil, err + var err error + switch { + case from.Skip(): + if from.Name() == to.Name() { + err = ii.nextBoth() + } else { + err = ii.nextFrom() } - break - } - if to.Skip() { - if err = ret.AddRecursiveDelete(to); err != nil { - return nil, err - } - if err := ii.nextBoth(); err != nil { - return nil, err + case to.Skip(): + if from.Name() == to.Name() { + err = ii.nextBoth() + } else { + err = ii.nextTo() } - break + default: + err = diffNodes(&ret, ii) } - if err = diffNodes(&ret, ii); err != nil { + if err != nil { return nil, err } default: diff --git a/worktree_test.go b/worktree_test.go index a3dbcfeb3..db2fd7cde 100644 --- a/worktree_test.go +++ b/worktree_test.go @@ -1359,7 +1359,24 @@ func (s *WorktreeSuite) TestStatusAfterCheckout(c *C) { status, err := w.Status() c.Assert(err, IsNil) c.Assert(status.IsClean(), Equals, true) +} + +func (s *WorktreeSuite) TestStatusAfterSparseCheckout(c *C) { + fs := memfs.New() + w := &Worktree{ + r: s.Repository, + Filesystem: fs, + } + err := w.Checkout(&CheckoutOptions{ + SparseCheckoutDirectories: []string{"php"}, + Force: true, + }) + c.Assert(err, IsNil) + + status, err := w.Status() + c.Assert(err, IsNil) + c.Assert(status.IsClean(), Equals, true) } func (s *WorktreeSuite) TestStatusModified(c *C) { From 4f35ebab20f1034df1df943044903e7044e6cdf4 Mon Sep 17 00:00:00 2001 From: Paulo Gomes Date: Sun, 13 Apr 2025 16:48:21 +0100 Subject: [PATCH 2/6] Merge pull request #1484 from patricsss/patricsss/fix-1455 utils: fix diff so subpaths work for sparse checkouts, fixes 1455 --- utils/merkletrie/change.go | 2 +- utils/merkletrie/index/node.go | 10 ++++++++- utils/merkletrie/index/node_test.go | 34 +++++++++++++++++++++++++---- 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/utils/merkletrie/change.go b/utils/merkletrie/change.go index 562ff8902..d32b5a7d4 100644 --- a/utils/merkletrie/change.go +++ b/utils/merkletrie/change.go @@ -150,7 +150,7 @@ func (l *Changes) addRecursive(root noder.Path, ctor noderToChangeFn) error { } return err } - if current.IsDir() { + if current.IsDir() || current.Skip() { continue } l.Add(ctor(current)) diff --git a/utils/merkletrie/index/node.go b/utils/merkletrie/index/node.go index c1809f7ec..5bc63f8b2 100644 --- a/utils/merkletrie/index/node.go +++ b/utils/merkletrie/index/node.go @@ -36,7 +36,15 @@ func NewRootNode(idx *index.Index) noder.Noder { parent := fullpath fullpath = path.Join(fullpath, part) - if _, ok := m[fullpath]; ok { + // It's possible that the first occurrence of subdirectory is skipped. + // The parent node can be created with SkipWorktree set to true, but + // if any future children do not skip their subtree, the entire lineage + // of the tree needs to have this value set to false so that subdirectories + // are not ignored. + if parentNode, ok := m[fullpath]; ok { + if e.SkipWorktree == false { + parentNode.skip = false + } continue } diff --git a/utils/merkletrie/index/node_test.go b/utils/merkletrie/index/node_test.go index cc5600dcb..53f8a9f45 100644 --- a/utils/merkletrie/index/node_test.go +++ b/utils/merkletrie/index/node_test.go @@ -2,7 +2,7 @@ package index import ( "bytes" - "path/filepath" + "path" "testing" "github.com/go-git/go-git/v5/plumbing" @@ -46,14 +46,14 @@ func (s *NoderSuite) TestDiff(c *C) { func (s *NoderSuite) TestDiffChange(c *C) { indexA := &index.Index{ Entries: []*index.Entry{{ - Name: filepath.Join("bar", "baz", "bar"), + Name: path.Join("bar", "baz", "bar"), Hash: plumbing.NewHash("8ab686eafeb1f44702738c8b0f24f2567c36da6d"), }}, } indexB := &index.Index{ Entries: []*index.Entry{{ - Name: filepath.Join("bar", "baz", "foo"), + Name: path.Join("bar", "baz", "foo"), Hash: plumbing.NewHash("8ab686eafeb1f44702738c8b0f24f2567c36da6d"), }}, } @@ -63,6 +63,32 @@ func (s *NoderSuite) TestDiffChange(c *C) { c.Assert(ch, HasLen, 2) } +func (s *NoderSuite) TestDiffSkipIssue1455(c *C) { + indexA := &index.Index{ + Entries: []*index.Entry{ + { + Name: path.Join("bar", "baz", "bar"), + Hash: plumbing.NewHash("8ab686eafeb1f44702738c8b0f24f2567c36da6d"), + SkipWorktree: true, + }, + { + Name: path.Join("bar", "biz", "bat"), + Hash: plumbing.NewHash("8ab686eafeb1f44702738c8b0f24f2567c36da6d"), + SkipWorktree: false, + }, + }, + } + + indexB := &index.Index{} + + ch, err := merkletrie.DiffTree(NewRootNode(indexB), NewRootNode(indexA), isEquals) + c.Assert(err, IsNil) + c.Assert(ch, HasLen, 1) + a, err := ch[0].Action() + c.Assert(err, IsNil) + c.Assert(a, Equals, merkletrie.Insert) +} + func (s *NoderSuite) TestDiffDir(c *C) { indexA := &index.Index{ Entries: []*index.Entry{{ @@ -73,7 +99,7 @@ func (s *NoderSuite) TestDiffDir(c *C) { indexB := &index.Index{ Entries: []*index.Entry{{ - Name: filepath.Join("foo", "bar"), + Name: path.Join("foo", "bar"), Hash: plumbing.NewHash("8ab686eafeb1f44702738c8b0f24f2567c36da6d"), }}, } From b4862019696d8dc00b9aeb86394951e5c1b9076a Mon Sep 17 00:00:00 2001 From: Paulo Gomes Date: Fri, 5 Sep 2025 17:56:05 +0100 Subject: [PATCH 3/6] internal: Expand regex to fix build Signed-off-by: Paulo Gomes --- plumbing/transport/test/receive_pack.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plumbing/transport/test/receive_pack.go b/plumbing/transport/test/receive_pack.go index d4d2b1070..b72a42a1a 100644 --- a/plumbing/transport/test/receive_pack.go +++ b/plumbing/transport/test/receive_pack.go @@ -206,11 +206,12 @@ func (s *ReceivePackSuite) TestSendPackOnNonEmptyWithReportStatusWithError(c *C) report, err := s.receivePackNoCheck(c, endpoint, req, fixture, full) //XXX: Recent git versions return "failed to update ref", while older // (>=1.9) return "failed to lock". - c.Assert(err, ErrorMatches, ".*(failed to update ref|failed to lock).*") + // More recent versions: command error on : reference already exists + c.Assert(err, ErrorMatches, ".*(failed to update ref|failed to lock|reference already exists).*") c.Assert(report.UnpackStatus, Equals, "ok") c.Assert(len(report.CommandStatuses), Equals, 1) c.Assert(report.CommandStatuses[0].ReferenceName, Equals, plumbing.ReferenceName("refs/heads/master")) - c.Assert(report.CommandStatuses[0].Status, Matches, "(failed to update ref|failed to lock)") + c.Assert(report.CommandStatuses[0].Status, Matches, "(failed to update ref|failed to lock|reference already exists)") s.checkRemoteHead(c, endpoint, plumbing.NewHash(fixture.Head)) } From 15d46ceb597e4092783314c48c782097f436aa66 Mon Sep 17 00:00:00 2001 From: Arthur Gautier Date: Sun, 7 Sep 2025 23:56:41 +0000 Subject: [PATCH 4/6] build: raise timeouts for windows CI tests --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0c61f1b26..861421d3b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,4 +36,4 @@ jobs: run: make test-coverage - name: Test Examples - run: go test -timeout 30s -v -run '^TestExamples$' github.com/go-git/go-git/v5/_examples --examples + run: go test -timeout ${{ matrix.platform == 'windows-latest' && '60s' || '30s' }} -v -run '^TestExamples$' github.com/go-git/go-git/v5/_examples --examples From 111f37418f4e164e114b094728ca87733dae1779 Mon Sep 17 00:00:00 2001 From: Arthur Gautier Date: Mon, 8 Sep 2025 01:40:50 +0000 Subject: [PATCH 5/6] build: disable fuzzing on maintenance branch --- .github/workflows/cifuzz.yml | 35 ----------------------------------- 1 file changed, 35 deletions(-) delete mode 100644 .github/workflows/cifuzz.yml diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml deleted file mode 100644 index f3b67df1d..000000000 --- a/.github/workflows/cifuzz.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: CIFuzz -on: [pull_request] -permissions: {} -jobs: - Fuzzing: - runs-on: ubuntu-latest - permissions: - security-events: write - steps: - - name: Build Fuzzers - id: build - uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master - with: - oss-fuzz-project-name: 'go-git' - language: go - - name: Run Fuzzers - uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master - with: - oss-fuzz-project-name: 'go-git' - language: go - fuzz-seconds: 300 - output-sarif: true - - name: Upload Crash - uses: actions/upload-artifact@v4 - if: failure() && steps.build.outcome == 'success' - with: - name: artifacts - path: ./out/artifacts - - name: Upload Sarif - if: always() && steps.build.outcome == 'success' - uses: github/codeql-action/upload-sarif@v3.28.1 - with: - # Path to SARIF file relative to the root of the repository - sarif_file: cifuzz-sarif/results.sarif - checkout_path: cifuzz-sarif From f2c3467492033820145d5230dc681b5f976eb1c0 Mon Sep 17 00:00:00 2001 From: Arthur Gautier Date: Fri, 15 Aug 2025 11:00:38 -0700 Subject: [PATCH 6/6] plumbing: support extra headers, support jujutsu signed commit [5.x] This adds support for extra headers. While git has a set of ["standard headers"] (`tree`, `parent`, `author`, `committer`, and `encoding`), it will also support [extra headers] when serializing. The extra headers must come after the standard ones, but they are otherwise freetyped. [Jujutsu] takes advantage of that to store its own identifier (`change-id`) as an extra header. Because signatures will cover the hash of the whole commit (standard headers, extra headers and the message. Everything but the signature itself), if we deserialize a commit and then `EncodeWithoutSignature` to get back the "canonical" representation of a commit, if we don't serialize back the extra headers, the hash will no longer match and the signature will fail to verify. This adds support for parsing and reencoding the extra headers from the original commit and it's expected to fix support for Jujutsu signed commits. Fixes #1626 ["standard headers"]: https://github.com/git/git/blob/724518f3884d8707c5f51428ba98c115818229b8/commit.c#L1450 [extra headers]: https://github.com/git/git/blob/724518f3884d8707c5f51428ba98c115818229b8/commit.c#L1690 [Jujutsu]: https://github.com/jj-vcs/jj --- plumbing/object/commit.go | 72 ++++++++++++++++++++++ plumbing/object/commit_test.go | 108 +++++++++++++++++++++++++++++++++ 2 files changed, 180 insertions(+) diff --git a/plumbing/object/commit.go b/plumbing/object/commit.go index 3d096e18b..78627b065 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 diff --git a/plumbing/object/commit_test.go b/plumbing/object/commit_test.go index a0489269a..a6bed1ead 100644 --- a/plumbing/object/commit_test.go +++ b/plumbing/object/commit_test.go @@ -553,6 +553,114 @@ func (s *SuiteCommit) TestEncodeWithoutSignature(c *C) { "Merge branch 'master' of github.com:tyba/git-fixture\n") } +func (s *SuiteCommit) TestEncodeWithoutSignatureJujutsu(c *C) { + object := &plumbing.MemoryObject{} + object.SetType(plumbing.CommitObject) + object.Write([]byte(`tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904 +author John Doe 1755280730 -0700 +committer John Doe 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) + c.Assert(err, IsNil) + + // Similar to TestString since no signature + encoded := &plumbing.MemoryObject{} + err = commit.EncodeWithoutSignature(encoded) + er, err := encoded.Reader() + c.Assert(err, IsNil) + payload, err := io.ReadAll(er) + c.Assert(err, IsNil) + + c.Assert(string(payload), Equals, `tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904 +author John Doe 1755280730 -0700 +committer John Doe 1755280730 -0700 +change-id wxmuynokkzxmuwxwvnnpnptoyuypknwv + +initial commit + +Change-Id: I6a6a696432d51cbff02d53234ccaca6b151afc34 +`) +} + +func (s *SuiteCommit) TestEncodeExtraHeaders(c *C) { + object := &plumbing.MemoryObject{} + object.SetType(plumbing.CommitObject) + object.Write([]byte(`tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904 +author John Doe 1755280730 -0700 +committer John Doe 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) + c.Assert(err, IsNil) + + c.Assert(commit.ExtraHeaders, DeepEquals, []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() + c.Assert(err, IsNil) + payload, err := io.ReadAll(er) + c.Assert(err, IsNil) + + c.Assert(string(payload), Equals, `tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904 +author John Doe 1755280730 -0700 +committer John Doe 1755280730 -0700 +continuedheader to be + continued +continuedheader to be + continued + on + more than + a single line +simpleflag + value no key + +initial commit +`) +} + func (s *SuiteCommit) TestLess(c *C) { when1 := time.Now() when2 := when1.Add(time.Hour)