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 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 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) 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)) } diff --git a/utils/merkletrie/change.go b/utils/merkletrie/change.go index 450feb4ba..d32b5a7d4 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 } @@ -148,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/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/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"), }}, } 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) {