Skip to content

Commit

Permalink
simplified sparse checkout
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
john-cai authored and John Cai committed Nov 9, 2021
1 parent e4fcd07 commit f92011d
Show file tree
Hide file tree
Showing 14 changed files with 160 additions and 10 deletions.
2 changes: 2 additions & 0 deletions options.go
Expand Up @@ -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.
Expand Down
18 changes: 18 additions & 0 deletions plumbing/format/index/index.go
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"path/filepath"
"strings"
"time"

"github.com/go-git/go-git/v5/plumbing"
Expand Down Expand Up @@ -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
}
}
}
4 changes: 4 additions & 0 deletions plumbing/object/treenoder.go
Expand Up @@ -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 == ""
}
Expand Down
31 changes: 31 additions & 0 deletions repository_test.go
Expand Up @@ -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{
Expand Down
29 changes: 27 additions & 2 deletions utils/merkletrie/difftree.go
Expand Up @@ -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
}
Expand Down
4 changes: 4 additions & 0 deletions utils/merkletrie/filesystem/node.go
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion utils/merkletrie/index/node.go
Expand Up @@ -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,
Expand All @@ -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 {
Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions utils/merkletrie/internal/fsnoder/dir.go
Expand Up @@ -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 = ')'
Expand Down
4 changes: 4 additions & 0 deletions utils/merkletrie/internal/fsnoder/file.go
Expand Up @@ -55,6 +55,10 @@ func (f *file) NumChildren() (int, error) {
return 0, nil
}

func (f *file) Skip() bool {
return false
}

const (
fileStartMark = '<'
fileEndMark = '>'
Expand Down
1 change: 1 addition & 0 deletions utils/merkletrie/noder/noder.go
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions utils/merkletrie/noder/noder_test.go
Expand Up @@ -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:
Expand Down
8 changes: 8 additions & 0 deletions utils/merkletrie/noder/path.go
Expand Up @@ -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 {
Expand Down
25 changes: 18 additions & 7 deletions worktree.go
Expand Up @@ -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"
Expand All @@ -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 (
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
}
Expand All @@ -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
}
Expand Down
32 changes: 32 additions & 0 deletions worktree_test.go
Expand Up @@ -10,6 +10,7 @@ import (
"path/filepath"
"regexp"
"runtime"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -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")
Expand Down

0 comments on commit f92011d

Please sign in to comment.