Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Worktree: Checkout, simplified sparse checkout #410

Merged
merged 1 commit into from Dec 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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