Skip to content

Commit

Permalink
Merge pull request #410 from john-cai/jc-sparse
Browse files Browse the repository at this point in the history
Worktree: Checkout, simplified sparse checkout
  • Loading branch information
mcuadros committed Dec 10, 2021
2 parents c71074e + f92011d commit 39f97ab
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 @@ -306,6 +306,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 39f97ab

Please sign in to comment.