From 7db545b14827462679760b2d584782d69695acf4 Mon Sep 17 00:00:00 2001 From: John Cai Date: Mon, 1 Nov 2021 18:11:08 -0400 Subject: [PATCH 1/9] Add ForceWithLease Push Option --force-with-lease allows a push to force push with some safety measures. If the ref on the remote is what we expect, then the force push is allowed to happen. See https://git-scm.com/docs/git-push#Documentation/git-push.txt---force-with-leaseltrefnamegt for more information --- options.go | 15 ++++++ remote.go | 43 ++++++++++++++--- remote_test.go | 127 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 179 insertions(+), 6 deletions(-) diff --git a/options.go b/options.go index e54889f80..fee2e16cf 100644 --- a/options.go +++ b/options.go @@ -228,10 +228,25 @@ type PushOptions struct { // FollowTags will send any annotated tags with a commit target reachable from // the refs already being pushed FollowTags bool + // ForceWithLease allows a force push as long as the remote ref adheres to a "lease" + ForceWithLease *ForceWithLease // PushOptions sets options to be transferred to the server during push. Options map[string]string } +// ForceWithLease sets fields on the lease +// If neither RefName nor Hash are set, ForceWithLease protects +// all refs in the refspec by ensuring the ref of the remote in the local repsitory +// matches the one in the ref advertisement. +type ForceWithLease struct { + // RefName, when set will protect the ref by ensuring it matches the + // hash in the ref advertisement. + RefName plumbing.ReferenceName + // Hash is the expected object id of RefName. The push will be rejected unless this + // matches the corresponding object id of RefName in the refs advertisement. + Hash plumbing.Hash +} + // Validate validates the fields and sets the default values. func (o *PushOptions) Validate() error { if o.RemoteName == "" { diff --git a/remote.go b/remote.go index 426bde928..fecdebf4e 100644 --- a/remote.go +++ b/remote.go @@ -326,7 +326,7 @@ func (r *Remote) newReferenceUpdateRequest( } } - if err := r.addReferencesToUpdate(o.RefSpecs, localRefs, remoteRefs, req, o.Prune); err != nil { + if err := r.addReferencesToUpdate(o.RefSpecs, localRefs, remoteRefs, req, o.Prune, o.ForceWithLease); err != nil { return nil, err } @@ -568,6 +568,7 @@ func (r *Remote) addReferencesToUpdate( remoteRefs storer.ReferenceStorer, req *packp.ReferenceUpdateRequest, prune bool, + forceWithLease *ForceWithLease, ) error { // This references dictionary will be used to search references by name. refsDict := make(map[string]*plumbing.Reference) @@ -581,7 +582,7 @@ func (r *Remote) addReferencesToUpdate( return err } } else { - err := r.addOrUpdateReferences(rs, localRefs, refsDict, remoteRefs, req) + err := r.addOrUpdateReferences(rs, localRefs, refsDict, remoteRefs, req, forceWithLease) if err != nil { return err } @@ -603,6 +604,7 @@ func (r *Remote) addOrUpdateReferences( refsDict map[string]*plumbing.Reference, remoteRefs storer.ReferenceStorer, req *packp.ReferenceUpdateRequest, + forceWithLease *ForceWithLease, ) error { // If it is not a wilcard refspec we can directly search for the reference // in the references dictionary. @@ -616,11 +618,11 @@ func (r *Remote) addOrUpdateReferences( return nil } - return r.addReferenceIfRefSpecMatches(rs, remoteRefs, ref, req) + return r.addReferenceIfRefSpecMatches(rs, remoteRefs, ref, req, forceWithLease) } for _, ref := range localRefs { - err := r.addReferenceIfRefSpecMatches(rs, remoteRefs, ref, req) + err := r.addReferenceIfRefSpecMatches(rs, remoteRefs, ref, req, forceWithLease) if err != nil { return err } @@ -706,7 +708,7 @@ func (r *Remote) addCommit(rs config.RefSpec, func (r *Remote) addReferenceIfRefSpecMatches(rs config.RefSpec, remoteRefs storer.ReferenceStorer, localRef *plumbing.Reference, - req *packp.ReferenceUpdateRequest) error { + req *packp.ReferenceUpdateRequest, forceWithLease *ForceWithLease) error { if localRef.Type() != plumbing.HashReference { return nil @@ -738,7 +740,11 @@ func (r *Remote) addReferenceIfRefSpecMatches(rs config.RefSpec, return nil } - if !rs.IsForceUpdate() { + if forceWithLease != nil { + if err = r.checkForceWithLease(localRef, cmd, forceWithLease); err != nil { + return err + } + } else if !rs.IsForceUpdate() { if err := checkFastForwardUpdate(r.s, remoteRefs, cmd); err != nil { return err } @@ -748,6 +754,31 @@ func (r *Remote) addReferenceIfRefSpecMatches(rs config.RefSpec, return nil } +func (r *Remote) checkForceWithLease(localRef *plumbing.Reference, cmd *packp.Command, forceWithLease *ForceWithLease) error { + remotePrefix := fmt.Sprintf("refs/remotes/%s/", r.Config().Name) + + ref, err := storer.ResolveReference( + r.s, + plumbing.ReferenceName(remotePrefix+strings.Replace(localRef.Name().String(), "refs/heads/", "", -1))) + if err != nil { + return err + } + + if forceWithLease.RefName.String() == "" || (forceWithLease.RefName == cmd.Name) { + expectedOID := ref.Hash() + + if !forceWithLease.Hash.IsZero() { + expectedOID = forceWithLease.Hash + } + + if cmd.Old != expectedOID { + return fmt.Errorf("non-fast-forward update: %s", cmd.Name.String()) + } + } + + return nil +} + func (r *Remote) references() ([]*plumbing.Reference, error) { var localRefs []*plumbing.Reference diff --git a/remote_test.go b/remote_test.go index df07c082d..ebe9a5580 100644 --- a/remote_test.go +++ b/remote_test.go @@ -816,6 +816,133 @@ func (s *RemoteSuite) TestPushForceWithOption(c *C) { c.Assert(newRef, Not(DeepEquals), oldRef) } +func (s *RemoteSuite) TestPushForceWithLease_success(c *C) { + testCases := []struct { + desc string + forceWithLease ForceWithLease + }{ + { + desc: "no arguments", + forceWithLease: ForceWithLease{}, + }, + { + desc: "ref name", + forceWithLease: ForceWithLease{ + RefName: plumbing.ReferenceName("refs/heads/branch"), + }, + }, + { + desc: "ref name and sha", + forceWithLease: ForceWithLease{ + RefName: plumbing.ReferenceName("refs/heads/branch"), + Hash: plumbing.NewHash("e8d3ffab552895c19b9fcf7aa264d277cde33881"), + }, + }, + } + + for _, tc := range testCases { + c.Log("Executing test cases:", tc.desc) + + f := fixtures.Basic().One() + sto := filesystem.NewStorage(f.DotGit(), cache.NewObjectLRUDefault()) + dstFs := f.DotGit() + dstSto := filesystem.NewStorage(dstFs, cache.NewObjectLRUDefault()) + + newCommit := plumbing.NewHashReference( + "refs/heads/branch", plumbing.NewHash("35e85108805c84807bc66a02d91535e1e24b38b9"), + ) + c.Assert(sto.SetReference(newCommit), IsNil) + + ref, err := sto.Reference("refs/heads/branch") + c.Log(ref.String()) + + url := dstFs.Root() + r := NewRemote(sto, &config.RemoteConfig{ + Name: DefaultRemoteName, + URLs: []string{url}, + }) + + oldRef, err := dstSto.Reference("refs/heads/branch") + c.Assert(err, IsNil) + c.Assert(oldRef, NotNil) + + c.Assert(r.Push(&PushOptions{ + RefSpecs: []config.RefSpec{"refs/heads/branch:refs/heads/branch"}, + ForceWithLease: &ForceWithLease{}, + }), IsNil) + + newRef, err := dstSto.Reference("refs/heads/branch") + c.Assert(err, IsNil) + c.Assert(newRef, DeepEquals, newCommit) + } +} + +func (s *RemoteSuite) TestPushForceWithLease_failure(c *C) { + testCases := []struct { + desc string + forceWithLease ForceWithLease + }{ + { + desc: "no arguments", + forceWithLease: ForceWithLease{}, + }, + { + desc: "ref name", + forceWithLease: ForceWithLease{ + RefName: plumbing.ReferenceName("refs/heads/branch"), + }, + }, + { + desc: "ref name and sha", + forceWithLease: ForceWithLease{ + RefName: plumbing.ReferenceName("refs/heads/branch"), + Hash: plumbing.NewHash("152175bf7e5580299fa1f0ba41ef6474cc043b70"), + }, + }, + } + + for _, tc := range testCases { + c.Log("Executing test cases:", tc.desc) + + f := fixtures.Basic().One() + sto := filesystem.NewStorage(f.DotGit(), cache.NewObjectLRUDefault()) + c.Assert(sto.SetReference( + plumbing.NewHashReference( + "refs/heads/branch", plumbing.NewHash("35e85108805c84807bc66a02d91535e1e24b38b9"), + ), + ), IsNil) + + dstFs := f.DotGit() + dstSto := filesystem.NewStorage(dstFs, cache.NewObjectLRUDefault()) + c.Assert(dstSto.SetReference( + plumbing.NewHashReference( + "refs/heads/branch", plumbing.NewHash("ad7897c0fb8e7d9a9ba41fa66072cf06095a6cfc"), + ), + ), IsNil) + + url := dstFs.Root() + r := NewRemote(sto, &config.RemoteConfig{ + Name: DefaultRemoteName, + URLs: []string{url}, + }) + + oldRef, err := dstSto.Reference("refs/heads/branch") + c.Assert(err, IsNil) + c.Assert(oldRef, NotNil) + + err = r.Push(&PushOptions{ + RefSpecs: []config.RefSpec{"refs/heads/branch:refs/heads/branch"}, + ForceWithLease: &ForceWithLease{}, + }) + + c.Assert(err, DeepEquals, errors.New("non-fast-forward update: refs/heads/branch")) + + newRef, err := dstSto.Reference("refs/heads/branch") + c.Assert(err, IsNil) + c.Assert(newRef, Not(DeepEquals), plumbing.NewHash("35e85108805c84807bc66a02d91535e1e24b38b9")) + } +} + func (s *RemoteSuite) TestPushPrune(c *C) { fs := fixtures.Basic().One().DotGit() From a04ddf534f941bb531459dbdf98c875495272069 Mon Sep 17 00:00:00 2001 From: John Cai Date: Fri, 5 Nov 2021 14:57:38 -0400 Subject: [PATCH 2/9] Support v3 index Currently the index encoder does not support the v3 index format. This change adds support to the encoder. This helps to unlock sparse checkout. --- plumbing/format/index/encoder.go | 34 ++++++++++++++++++++------- plumbing/format/index/encoder_test.go | 26 ++++++++++++++++---- 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/plumbing/format/index/encoder.go b/plumbing/format/index/encoder.go index 00d4e7a31..2c94d93fc 100644 --- a/plumbing/format/index/encoder.go +++ b/plumbing/format/index/encoder.go @@ -14,7 +14,7 @@ import ( var ( // EncodeVersionSupported is the range of supported index versions - EncodeVersionSupported uint32 = 2 + EncodeVersionSupported uint32 = 3 // ErrInvalidTimestamp is returned by Encode if a Index with a Entry with // negative timestamp values @@ -36,9 +36,9 @@ func NewEncoder(w io.Writer) *Encoder { // Encode writes the Index to the stream of the encoder. func (e *Encoder) Encode(idx *Index) error { - // TODO: support versions v3 and v4 + // TODO: support v4 // TODO: support extensions - if idx.Version != EncodeVersionSupported { + if idx.Version > EncodeVersionSupported { return ErrUnsupportedVersion } @@ -68,8 +68,12 @@ func (e *Encoder) encodeEntries(idx *Index) error { if err := e.encodeEntry(entry); err != nil { return err } + entryLength := entryHeaderLength + if entry.IntentToAdd || entry.SkipWorktree { + entryLength += 2 + } - wrote := entryHeaderLength + len(entry.Name) + wrote := entryLength + len(entry.Name) if err := e.padEntry(wrote); err != nil { return err } @@ -79,10 +83,6 @@ func (e *Encoder) encodeEntries(idx *Index) error { } func (e *Encoder) encodeEntry(entry *Entry) error { - if entry.IntentToAdd || entry.SkipWorktree { - return ErrUnsupportedVersion - } - sec, nsec, err := e.timeToUint32(&entry.CreatedAt) if err != nil { return err @@ -110,9 +110,25 @@ func (e *Encoder) encodeEntry(entry *Entry) error { entry.GID, entry.Size, entry.Hash[:], - flags, } + flagsFlow := []interface{}{flags} + + if entry.IntentToAdd || entry.SkipWorktree { + var extendedFlags uint16 + + if entry.IntentToAdd { + extendedFlags |= intentToAddMask + } + if entry.SkipWorktree { + extendedFlags |= skipWorkTreeMask + } + + flagsFlow = []interface{}{flags | entryExtended, extendedFlags} + } + + flow = append(flow, flagsFlow...) + if err := binary.Write(e.w, flow...); err != nil { return err } diff --git a/plumbing/format/index/encoder_test.go b/plumbing/format/index/encoder_test.go index b7a73cb14..25c24f14f 100644 --- a/plumbing/format/index/encoder_test.go +++ b/plumbing/format/index/encoder_test.go @@ -57,7 +57,7 @@ func (s *IndexSuite) TestEncode(c *C) { } func (s *IndexSuite) TestEncodeUnsupportedVersion(c *C) { - idx := &Index{Version: 3} + idx := &Index{Version: 4} buf := bytes.NewBuffer(nil) e := NewEncoder(buf) @@ -67,24 +67,40 @@ func (s *IndexSuite) TestEncodeUnsupportedVersion(c *C) { func (s *IndexSuite) TestEncodeWithIntentToAddUnsupportedVersion(c *C) { idx := &Index{ - Version: 2, + Version: 3, Entries: []*Entry{{IntentToAdd: true}}, } buf := bytes.NewBuffer(nil) e := NewEncoder(buf) err := e.Encode(idx) - c.Assert(err, Equals, ErrUnsupportedVersion) + c.Assert(err, IsNil) + + output := &Index{} + d := NewDecoder(buf) + err = d.Decode(output) + c.Assert(err, IsNil) + + c.Assert(cmp.Equal(idx, output), Equals, true) + c.Assert(output.Entries[0].IntentToAdd, Equals, true) } func (s *IndexSuite) TestEncodeWithSkipWorktreeUnsupportedVersion(c *C) { idx := &Index{ - Version: 2, + Version: 3, Entries: []*Entry{{SkipWorktree: true}}, } buf := bytes.NewBuffer(nil) e := NewEncoder(buf) err := e.Encode(idx) - c.Assert(err, Equals, ErrUnsupportedVersion) + c.Assert(err, IsNil) + + output := &Index{} + d := NewDecoder(buf) + err = d.Decode(output) + c.Assert(err, IsNil) + + c.Assert(cmp.Equal(idx, output), Equals, true) + c.Assert(output.Entries[0].SkipWorktree, Equals, true) } From b939cf8471d8c1aac1960a833c7275e5b79ed013 Mon Sep 17 00:00:00 2001 From: merlin Date: Sun, 7 Nov 2021 20:39:51 +0300 Subject: [PATCH 3/9] config: add support for branch description --- config/branch.go | 23 +++++++++++++++++++++++ config/config.go | 1 + 2 files changed, 24 insertions(+) diff --git a/config/branch.go b/config/branch.go index fe86cf542..a8fe0b5ab 100644 --- a/config/branch.go +++ b/config/branch.go @@ -2,6 +2,7 @@ package config import ( "errors" + "strings" "github.com/go-git/go-git/v5/plumbing" format "github.com/go-git/go-git/v5/plumbing/format/config" @@ -26,6 +27,12 @@ type Branch struct { // "true" and "interactive". "false" is undocumented and // typically represented by the non-existence of this field Rebase string + // Description explains what the branch is for. + // Multi-line explanations may be used. + // + // Original git command to edit: + // git branch --edit-description + Description string raw *format.Subsection } @@ -75,9 +82,20 @@ func (b *Branch) marshal() *format.Subsection { b.raw.SetOption(rebaseKey, b.Rebase) } + if b.Description == "" { + b.raw.RemoveOption(descriptionKey) + } else { + desc := quoteDescription(b.Description) + b.raw.SetOption(descriptionKey, desc) + } + return b.raw } +func quoteDescription(desc string) string { + return strings.ReplaceAll(desc, "\n", `\n`) +} + func (b *Branch) unmarshal(s *format.Subsection) error { b.raw = s @@ -85,6 +103,11 @@ func (b *Branch) unmarshal(s *format.Subsection) error { b.Remote = b.raw.Options.Get(remoteSection) b.Merge = plumbing.ReferenceName(b.raw.Options.Get(mergeKey)) b.Rebase = b.raw.Options.Get(rebaseKey) + b.Description = unquoteDescription(b.raw.Options.Get(descriptionKey)) return b.Validate() } + +func unquoteDescription(desc string) string { + return strings.ReplaceAll(desc, `\n`, "\n") +} diff --git a/config/config.go b/config/config.go index 1aee25a4c..a16a5e5f4 100644 --- a/config/config.go +++ b/config/config.go @@ -247,6 +247,7 @@ const ( rebaseKey = "rebase" nameKey = "name" emailKey = "email" + descriptionKey = "description" defaultBranchKey = "defaultBranch" // DefaultPackWindow holds the number of previous objects used to From e0567bda08fcee09a2c24010d63c10d5f933faad Mon Sep 17 00:00:00 2001 From: merlin Date: Mon, 8 Nov 2021 00:25:06 +0300 Subject: [PATCH 4/9] config: add tests for branch desc marshaling and unmarshaling --- config/config_test.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/config/config_test.go b/config/config_test.go index 6f0242d96..91f7df2ae 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -50,6 +50,7 @@ func (s *ConfigSuite) TestUnmarshal(c *C) { [branch "master"] remote = origin merge = refs/heads/master + description = "Add support for branch description.\\n\\nEdit branch description: git branch --edit-description\\n" [init] defaultBranch = main [url "ssh://git@github.com/"] @@ -86,6 +87,7 @@ func (s *ConfigSuite) TestUnmarshal(c *C) { c.Assert(cfg.Submodules["qux"].Branch, Equals, "bar") c.Assert(cfg.Branches["master"].Remote, Equals, "origin") c.Assert(cfg.Branches["master"].Merge, Equals, plumbing.ReferenceName("refs/heads/master")) + c.Assert(cfg.Branches["master"].Description, Equals, "Add support for branch description.\n\nEdit branch description: git branch --edit-description\n") c.Assert(cfg.Init.DefaultBranch, Equals, "main") } @@ -111,6 +113,7 @@ func (s *ConfigSuite) TestMarshal(c *C) { [branch "master"] remote = origin merge = refs/heads/master + description = "Add support for branch description.\\n\\nEdit branch description: git branch --edit-description\\n" [url "ssh://git@github.com/"] insteadOf = https://github.com/ [init] @@ -149,9 +152,10 @@ func (s *ConfigSuite) TestMarshal(c *C) { } cfg.Branches["master"] = &Branch{ - Name: "master", - Remote: "origin", - Merge: "refs/heads/master", + Name: "master", + Remote: "origin", + Merge: "refs/heads/master", + Description: "Add support for branch description.\n\nEdit branch description: git branch --edit-description\n", } cfg.URLs["ssh://git@github.com/"] = &URL{ @@ -364,4 +368,5 @@ func (s *ConfigSuite) TestRemoveUrlOptions(c *C) { if strings.Contains(string(buf), "url") { c.Fatal("conifg should not contain any url sections") } + c.Assert(err, IsNil) } From efc74e7730b7cfd72ae66602815e6acd67b2c01a Mon Sep 17 00:00:00 2001 From: merlin Date: Mon, 8 Nov 2021 00:33:17 +0300 Subject: [PATCH 5/9] config: describe reason for custom quote functions --- config/branch.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/config/branch.go b/config/branch.go index a8fe0b5ab..652270a28 100644 --- a/config/branch.go +++ b/config/branch.go @@ -92,6 +92,13 @@ func (b *Branch) marshal() *format.Subsection { return b.raw } +// hack to trigger conditional quoting in the +// plumbing/format/config/Encoder.encodeOptions +// +// Current Encoder implementation uses Go %q format if value contains a backslash character, +// which is not consistent with reference git implementation. +// git just replaces newline characters with \n, while Encoder prints them directly. +// Until value quoting fix, we should escape description value by replacing newline characters with \n. func quoteDescription(desc string) string { return strings.ReplaceAll(desc, "\n", `\n`) } @@ -108,6 +115,9 @@ func (b *Branch) unmarshal(s *format.Subsection) error { return b.Validate() } +// hack to enable conditional quoting in the +// plumbing/format/config/Encoder.encodeOptions +// goto quoteDescription for details. func unquoteDescription(desc string) string { return strings.ReplaceAll(desc, `\n`, "\n") } From f92011d95f98f5deea4959c7d432704a4300d3a8 Mon Sep 17 00:00:00 2001 From: John Cai Date: Thu, 4 Nov 2021 15:02:00 -0400 Subject: [PATCH 6/9] simplified sparse checkout This is the initial logic to support a simple sparse checkout where directories to be included can be specified in CheckoutOptions. This change doesn't fully support the sparse patterns, nor does this change include the optimization to collapse flie entries in ithe index that are excluded via the sparse checkout directory patterns included under the parent directory. --- options.go | 2 ++ plumbing/format/index/index.go | 18 +++++++++++++ plumbing/object/treenoder.go | 4 +++ repository_test.go | 31 ++++++++++++++++++++++ utils/merkletrie/difftree.go | 29 ++++++++++++++++++-- utils/merkletrie/filesystem/node.go | 4 +++ utils/merkletrie/index/node.go | 7 ++++- utils/merkletrie/internal/fsnoder/dir.go | 4 +++ utils/merkletrie/internal/fsnoder/file.go | 4 +++ utils/merkletrie/noder/noder.go | 1 + utils/merkletrie/noder/noder_test.go | 1 + utils/merkletrie/noder/path.go | 8 ++++++ worktree.go | 25 +++++++++++++----- worktree_test.go | 32 +++++++++++++++++++++++ 14 files changed, 160 insertions(+), 10 deletions(-) diff --git a/options.go b/options.go index e54889f80..4b18ec1b2 100644 --- a/options.go +++ b/options.go @@ -291,6 +291,8 @@ type CheckoutOptions struct { // target branch. Force and Keep are mutually exclusive, should not be both // set to true. Keep bool + // SparseCheckoutDirectories + SparseCheckoutDirectories []string } // Validate validates the fields and sets the default values. diff --git a/plumbing/format/index/index.go b/plumbing/format/index/index.go index 649416a2b..f4c7647d3 100644 --- a/plumbing/format/index/index.go +++ b/plumbing/format/index/index.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "path/filepath" + "strings" "time" "github.com/go-git/go-git/v5/plumbing" @@ -211,3 +212,20 @@ type EndOfIndexEntry struct { // their contents). Hash plumbing.Hash } + +// SkipUnless applies patterns in the form of A, A/B, A/B/C +// to the index to prevent the files from being checked out +func (i *Index) SkipUnless(patterns []string) { + for _, e := range i.Entries { + var include bool + for _, pattern := range patterns { + if strings.HasPrefix(e.Name, pattern) { + include = true + break + } + } + if !include { + e.SkipWorktree = true + } + } +} diff --git a/plumbing/object/treenoder.go b/plumbing/object/treenoder.go index b4891b957..6e7b334cb 100644 --- a/plumbing/object/treenoder.go +++ b/plumbing/object/treenoder.go @@ -38,6 +38,10 @@ func NewTreeRootNode(t *Tree) noder.Noder { } } +func (t *treeNoder) Skip() bool { + return false +} + func (t *treeNoder) isRoot() bool { return t.name == "" } diff --git a/repository_test.go b/repository_test.go index 2bc5c902c..668828e45 100644 --- a/repository_test.go +++ b/repository_test.go @@ -210,6 +210,37 @@ func (s *RepositorySuite) TestCloneWithTags(c *C) { c.Assert(count, Equals, 3) } +func (s *RepositorySuite) TestCloneSparse(c *C) { + fs := memfs.New() + r, err := Clone(memory.NewStorage(), fs, &CloneOptions{ + URL: s.GetBasicLocalRepositoryURL(), + }) + c.Assert(err, IsNil) + + w, err := r.Worktree() + c.Assert(err, IsNil) + + sparseCheckoutDirectories := []string{"go", "json", "php"} + c.Assert(w.Checkout(&CheckoutOptions{ + Branch: "refs/heads/master", + SparseCheckoutDirectories: sparseCheckoutDirectories, + }), IsNil) + + fis, err := fs.ReadDir(".") + c.Assert(err, IsNil) + for _, fi := range fis { + c.Assert(fi.IsDir(), Equals, true) + var oneOfSparseCheckoutDirs bool + + for _, sparseCheckoutDirectory := range sparseCheckoutDirectories { + if strings.HasPrefix(fi.Name(), sparseCheckoutDirectory) { + oneOfSparseCheckoutDirs = true + } + } + c.Assert(oneOfSparseCheckoutDirs, Equals, true) + } +} + func (s *RepositorySuite) TestCreateRemoteAndRemote(c *C) { r, _ := Init(memory.NewStorage(), nil) remote, err := r.CreateRemote(&config.RemoteConfig{ diff --git a/utils/merkletrie/difftree.go b/utils/merkletrie/difftree.go index bd084b2ab..9f5145a26 100644 --- a/utils/merkletrie/difftree.go +++ b/utils/merkletrie/difftree.go @@ -304,13 +304,38 @@ func DiffTreeContext(ctx context.Context, fromTree, toTree noder.Noder, return nil, err } case onlyToRemains: - if err = ret.AddRecursiveInsert(to); err != nil { - return nil, err + if to.Skip() { + if err = ret.AddRecursiveDelete(to); err != nil { + return nil, err + } + } else { + if err = ret.AddRecursiveInsert(to); err != nil { + return nil, err + } } if err = ii.nextTo(); err != nil { 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 + } + break + } + if to.Skip() { + if err = ret.AddRecursiveDelete(to); err != nil { + return nil, err + } + if err := ii.nextBoth(); err != nil { + return nil, err + } + break + } + if err = diffNodes(&ret, ii); err != nil { return nil, err } diff --git a/utils/merkletrie/filesystem/node.go b/utils/merkletrie/filesystem/node.go index 2fc3d7a63..ad169ff4a 100644 --- a/utils/merkletrie/filesystem/node.go +++ b/utils/merkletrie/filesystem/node.go @@ -61,6 +61,10 @@ func (n *node) IsDir() bool { return n.isDir } +func (n *node) Skip() bool { + return false +} + func (n *node) Children() ([]noder.Noder, error) { if err := n.calculateChildren(); err != nil { return nil, err diff --git a/utils/merkletrie/index/node.go b/utils/merkletrie/index/node.go index d05b0c694..c1809f7ec 100644 --- a/utils/merkletrie/index/node.go +++ b/utils/merkletrie/index/node.go @@ -19,6 +19,7 @@ type node struct { entry *index.Entry children []noder.Noder isDir bool + skip bool } // NewRootNode returns the root node of a computed tree from a index.Index, @@ -39,7 +40,7 @@ func NewRootNode(idx *index.Index) noder.Noder { continue } - n := &node{path: fullpath} + n := &node{path: fullpath, skip: e.SkipWorktree} if fullpath == e.Name { n.entry = e } else { @@ -58,6 +59,10 @@ func (n *node) String() string { return n.path } +func (n *node) Skip() bool { + return n.skip +} + // Hash the hash of a filesystem is a 24-byte slice, is the result of // concatenating the computed plumbing.Hash of the file as a Blob and its // plumbing.FileMode; that way the difftree algorithm will detect changes in the diff --git a/utils/merkletrie/internal/fsnoder/dir.go b/utils/merkletrie/internal/fsnoder/dir.go index 20a2aeebb..3a4c2424e 100644 --- a/utils/merkletrie/internal/fsnoder/dir.go +++ b/utils/merkletrie/internal/fsnoder/dir.go @@ -112,6 +112,10 @@ func (d *dir) NumChildren() (int, error) { return len(d.children), nil } +func (d *dir) Skip() bool { + return false +} + const ( dirStartMark = '(' dirEndMark = ')' diff --git a/utils/merkletrie/internal/fsnoder/file.go b/utils/merkletrie/internal/fsnoder/file.go index d53643f1a..0bb908b7a 100644 --- a/utils/merkletrie/internal/fsnoder/file.go +++ b/utils/merkletrie/internal/fsnoder/file.go @@ -55,6 +55,10 @@ func (f *file) NumChildren() (int, error) { return 0, nil } +func (f *file) Skip() bool { + return false +} + const ( fileStartMark = '<' fileEndMark = '>' diff --git a/utils/merkletrie/noder/noder.go b/utils/merkletrie/noder/noder.go index d6b3de4ad..6d22b8c14 100644 --- a/utils/merkletrie/noder/noder.go +++ b/utils/merkletrie/noder/noder.go @@ -53,6 +53,7 @@ type Noder interface { // implement NumChildren in O(1) while Children is usually more // complex. NumChildren() (int, error) + Skip() bool } // NoChildren represents the children of a noder without children. diff --git a/utils/merkletrie/noder/noder_test.go b/utils/merkletrie/noder/noder_test.go index 5e014fe9b..ccebdc9ee 100644 --- a/utils/merkletrie/noder/noder_test.go +++ b/utils/merkletrie/noder/noder_test.go @@ -25,6 +25,7 @@ func (n noderMock) Name() string { return n.name } func (n noderMock) IsDir() bool { return n.isDir } func (n noderMock) Children() ([]Noder, error) { return n.children, nil } func (n noderMock) NumChildren() (int, error) { return len(n.children), nil } +func (n noderMock) Skip() bool { return false } // Returns a sequence with the noders 3, 2, and 1 from the // following diagram: diff --git a/utils/merkletrie/noder/path.go b/utils/merkletrie/noder/path.go index 1c7ef54ee..6c1d36332 100644 --- a/utils/merkletrie/noder/path.go +++ b/utils/merkletrie/noder/path.go @@ -15,6 +15,14 @@ import ( // not be used. type Path []Noder +func (p Path) Skip() bool { + if len(p) > 0 { + return p.Last().Skip() + } + + return false +} + // String returns the full path of the final noder as a string, using // "/" as the separator. func (p Path) String() string { diff --git a/worktree.go b/worktree.go index 362d10e65..c974aed24 100644 --- a/worktree.go +++ b/worktree.go @@ -11,6 +11,8 @@ import ( "strings" "sync" + "github.com/go-git/go-billy/v5" + "github.com/go-git/go-billy/v5/util" "github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/filemode" @@ -20,9 +22,6 @@ import ( "github.com/go-git/go-git/v5/plumbing/storer" "github.com/go-git/go-git/v5/utils/ioutil" "github.com/go-git/go-git/v5/utils/merkletrie" - - "github.com/go-git/go-billy/v5" - "github.com/go-git/go-billy/v5/util" ) var ( @@ -183,6 +182,10 @@ func (w *Worktree) Checkout(opts *CheckoutOptions) error { return err } + if len(opts.SparseCheckoutDirectories) > 0 { + return w.ResetSparsely(ro, opts.SparseCheckoutDirectories) + } + return w.Reset(ro) } func (w *Worktree) createBranch(opts *CheckoutOptions) error { @@ -263,8 +266,7 @@ func (w *Worktree) setHEADToBranch(branch plumbing.ReferenceName, commit plumbin return w.r.Storer.SetReference(head) } -// Reset the worktree to a specified state. -func (w *Worktree) Reset(opts *ResetOptions) error { +func (w *Worktree) ResetSparsely(opts *ResetOptions, dirs []string) error { if err := opts.Validate(w.r); err != nil { return err } @@ -294,7 +296,7 @@ func (w *Worktree) Reset(opts *ResetOptions) error { } if opts.Mode == MixedReset || opts.Mode == MergeReset || opts.Mode == HardReset { - if err := w.resetIndex(t); err != nil { + if err := w.resetIndex(t, dirs); err != nil { return err } } @@ -308,8 +310,17 @@ func (w *Worktree) Reset(opts *ResetOptions) error { return nil } -func (w *Worktree) resetIndex(t *object.Tree) error { +// Reset the worktree to a specified state. +func (w *Worktree) Reset(opts *ResetOptions) error { + return w.ResetSparsely(opts, nil) +} + +func (w *Worktree) resetIndex(t *object.Tree, dirs []string) error { idx, err := w.r.Storer.Index() + if len(dirs) > 0 { + idx.SkipUnless(dirs) + } + if err != nil { return err } diff --git a/worktree_test.go b/worktree_test.go index 79cbefd77..a8f318797 100644 --- a/worktree_test.go +++ b/worktree_test.go @@ -10,6 +10,7 @@ import ( "path/filepath" "regexp" "runtime" + "strings" "testing" "time" @@ -417,6 +418,37 @@ func (s *WorktreeSuite) TestCheckoutSymlink(c *C) { c.Assert(err, IsNil) } +func (s *WorktreeSuite) TestCheckoutSparse(c *C) { + fs := memfs.New() + r, err := Clone(memory.NewStorage(), fs, &CloneOptions{ + URL: s.GetBasicLocalRepositoryURL(), + }) + c.Assert(err, IsNil) + + w, err := r.Worktree() + c.Assert(err, IsNil) + + sparseCheckoutDirectories := []string{"go", "json", "php"} + c.Assert(w.Checkout(&CheckoutOptions{ + SparseCheckoutDirectories: sparseCheckoutDirectories, + }), IsNil) + + fis, err := fs.ReadDir("/") + c.Assert(err, IsNil) + + for _, fi := range fis { + c.Assert(fi.IsDir(), Equals, true) + var oneOfSparseCheckoutDirs bool + + for _, sparseCheckoutDirectory := range sparseCheckoutDirectories { + if strings.HasPrefix(fi.Name(), sparseCheckoutDirectory) { + oneOfSparseCheckoutDirs = true + } + } + c.Assert(oneOfSparseCheckoutDirs, Equals, true) + } +} + func (s *WorktreeSuite) TestFilenameNormalization(c *C) { if runtime.GOOS == "windows" { c.Skip("windows paths may contain non utf-8 sequences") From fe308ea0d0ff6c31f2a218f8b47d8ace124ea679 Mon Sep 17 00:00:00 2001 From: Abhinav Gupta Date: Sat, 27 Nov 2021 16:51:55 -0800 Subject: [PATCH 7/9] packp: Actions should have type Action Per the [Go Spec](https://go.dev/ref/spec#Constant_declarations), the following yields the type `Action` for `Bar` and `Baz` only if there is no `=`. const ( Foo Action = ... Bar Baz ) The following has the type `Action` for the first item, but not the rest. Those are untyped constants of the corresponding type. const ( Foo Action = ... Bar = ... Baz = ... ) This means that `packp.{Update, Delete, Invalid}` are currently untyped string constants, and not `Action` constants as was intended here. This change fixes these. --- plumbing/protocol/packp/updreq.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plumbing/protocol/packp/updreq.go b/plumbing/protocol/packp/updreq.go index 46ad6fdc9..5dbd8ac73 100644 --- a/plumbing/protocol/packp/updreq.go +++ b/plumbing/protocol/packp/updreq.go @@ -87,9 +87,9 @@ type Action string const ( Create Action = "create" - Update = "update" - Delete = "delete" - Invalid = "invalid" + Update Action = "update" + Delete Action = "delete" + Invalid Action = "invalid" ) type Command struct { From f438ca3483c785f8649f6dbd96f1a1db3c6a2eaa Mon Sep 17 00:00:00 2001 From: NeP <93044008+nep-1@users.noreply.github.com> Date: Fri, 10 Dec 2021 13:39:50 +0800 Subject: [PATCH 8/9] examples: remote fix typo (#408) --- _examples/remotes/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_examples/remotes/main.go b/_examples/remotes/main.go index b1a91a9ef..d09957eae 100644 --- a/_examples/remotes/main.go +++ b/_examples/remotes/main.go @@ -33,7 +33,7 @@ func main() { CheckIfError(err) // List remotes from a repository - Info("git remotes -v") + Info("git remote -v") list, err := r.Remotes() CheckIfError(err) From 1a8f803f55879b5d0cb40e88cb5981d64555c1aa Mon Sep 17 00:00:00 2001 From: Abhinav Gupta Date: Thu, 9 Dec 2021 21:42:38 -0800 Subject: [PATCH 9/9] storage: filesystem, switch from os.SEEK_* to io.Seek* (#421) The `os.SEEK_*` constants have been deprecated since Go 1.7. It is now recommended to use the equivalent `io.Seek*` constants. Switch `os.SEEK_CUR` to `io.SeekCurrent`, and `os.SEEK_SET` to `io.SeekStart`. --- storage/filesystem/dotgit/dotgit_test.go | 7 ++++--- storage/filesystem/object_test.go | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/storage/filesystem/dotgit/dotgit_test.go b/storage/filesystem/dotgit/dotgit_test.go index 4c2ae941c..1a09fde74 100644 --- a/storage/filesystem/dotgit/dotgit_test.go +++ b/storage/filesystem/dotgit/dotgit_test.go @@ -3,6 +3,7 @@ package dotgit import ( "bufio" "encoding/hex" + "io" "io/ioutil" "os" "path/filepath" @@ -510,13 +511,13 @@ func (s *SuiteDotGit) TestObjectPackWithKeepDescriptors(c *C) { c.Assert(filepath.Ext(pack.Name()), Equals, ".pack") // Move to an specific offset - pack.Seek(42, os.SEEK_SET) + pack.Seek(42, io.SeekStart) pack2, err := dir.ObjectPack(plumbing.NewHash(f.PackfileHash)) c.Assert(err, IsNil) // If the file is the same the offset should be the same - offset, err := pack2.Seek(0, os.SEEK_CUR) + offset, err := pack2.Seek(0, io.SeekCurrent) c.Assert(err, IsNil) c.Assert(offset, Equals, int64(42)) @@ -527,7 +528,7 @@ func (s *SuiteDotGit) TestObjectPackWithKeepDescriptors(c *C) { c.Assert(err, IsNil) // If the file is opened again its offset should be 0 - offset, err = pack2.Seek(0, os.SEEK_CUR) + offset, err = pack2.Seek(0, io.SeekCurrent) c.Assert(err, IsNil) c.Assert(offset, Equals, int64(0)) diff --git a/storage/filesystem/object_test.go b/storage/filesystem/object_test.go index 59b40d3c2..1c3267b68 100644 --- a/storage/filesystem/object_test.go +++ b/storage/filesystem/object_test.go @@ -71,7 +71,7 @@ func (s *FsSuite) TestGetFromPackfileKeepDescriptors(c *C) { pack1, err := dg.ObjectPack(packfiles[0]) c.Assert(err, IsNil) - pack1.Seek(42, os.SEEK_SET) + pack1.Seek(42, io.SeekStart) err = o.Close() c.Assert(err, IsNil) @@ -79,7 +79,7 @@ func (s *FsSuite) TestGetFromPackfileKeepDescriptors(c *C) { pack2, err := dg.ObjectPack(packfiles[0]) c.Assert(err, IsNil) - offset, err := pack2.Seek(0, os.SEEK_CUR) + offset, err := pack2.Seek(0, io.SeekCurrent) c.Assert(err, IsNil) c.Assert(offset, Equals, int64(0))