From 498dbf7dc92e288641f1af1acc52704150e8a6c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1ximo=20Cuadros?= Date: Sun, 12 Feb 2017 23:03:30 +0100 Subject: [PATCH 01/11] storage: git.Storer move to storage.Storer and module handling --- storage/filesystem/internal/dotgit/dotgit.go | 13 +++++++---- storage/filesystem/module.go | 14 ++++++++++++ storage/filesystem/storage.go | 2 ++ storage/memory/storage.go | 13 +++++++++++ storage/storer.go | 23 ++++++++++++++++++++ storage/test/storage_suite.go | 13 +++++++++++ 6 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 storage/filesystem/module.go create mode 100644 storage/storer.go diff --git a/storage/filesystem/internal/dotgit/dotgit.go b/storage/filesystem/internal/dotgit/dotgit.go index accf9ca4c..360b3d101 100644 --- a/storage/filesystem/internal/dotgit/dotgit.go +++ b/storage/filesystem/internal/dotgit/dotgit.go @@ -20,10 +20,10 @@ const ( configPath = "config" indexPath = "index" shallowPath = "shallow" - - objectsPath = "objects" - packPath = "pack" - refsPath = "refs" + modulePath = "module" + objectsPath = "objects" + packPath = "pack" + refsPath = "refs" packExt = ".pack" idxExt = ".idx" @@ -390,6 +390,11 @@ func (d *DotGit) readReferenceFile(refsPath, refFile string) (ref *plumbing.Refe return plumbing.NewReferenceFromStrings(refFile, line), nil } +// Module return a billy.Filesystem poiting to the module folder +func (d *DotGit) Module(name string) billy.Filesystem { + return d.fs.Dir(d.fs.Join(modulePath, name)) +} + func isHex(s string) bool { for _, b := range []byte(s) { if isNum(b) { diff --git a/storage/filesystem/module.go b/storage/filesystem/module.go new file mode 100644 index 000000000..e8985d826 --- /dev/null +++ b/storage/filesystem/module.go @@ -0,0 +1,14 @@ +package filesystem + +import ( + "srcd.works/go-git.v4/storage" + "srcd.works/go-git.v4/storage/filesystem/internal/dotgit" +) + +type ModuleStorage struct { + dir *dotgit.DotGit +} + +func (s *ModuleStorage) Module(name string) (storage.Storer, error) { + return NewStorage(s.dir.Module(name)) +} diff --git a/storage/filesystem/storage.go b/storage/filesystem/storage.go index 7021d3a6a..27dd07f3a 100644 --- a/storage/filesystem/storage.go +++ b/storage/filesystem/storage.go @@ -16,6 +16,7 @@ type Storage struct { IndexStorage ShallowStorage ConfigStorage + ModuleStorage } // NewStorage returns a new Storage backed by a given `fs.Filesystem` @@ -32,5 +33,6 @@ func NewStorage(fs billy.Filesystem) (*Storage, error) { IndexStorage: IndexStorage{dir: dir}, ShallowStorage: ShallowStorage{dir: dir}, ConfigStorage: ConfigStorage{dir: dir}, + ModuleStorage: ModuleStorage{dir: dir}, }, nil } diff --git a/storage/memory/storage.go b/storage/memory/storage.go index 6cec47b68..fa9a4fa3d 100644 --- a/storage/memory/storage.go +++ b/storage/memory/storage.go @@ -8,6 +8,7 @@ import ( "srcd.works/go-git.v4/plumbing" "srcd.works/go-git.v4/plumbing/format/index" "srcd.works/go-git.v4/plumbing/storer" + "srcd.works/go-git.v4/storage" ) var ErrUnsupportedObjectType = fmt.Errorf("unsupported object type") @@ -22,6 +23,7 @@ type Storage struct { ShallowStorage IndexStorage ReferenceStorage + ModuleStorage } // NewStorage returns a new Storage base on memory @@ -37,6 +39,7 @@ func NewStorage() *Storage { Blobs: make(map[plumbing.Hash]plumbing.EncodedObject, 0), Tags: make(map[plumbing.Hash]plumbing.EncodedObject, 0), }, + ModuleStorage: make(ModuleStorage, 0), } } @@ -227,3 +230,13 @@ func (s *ShallowStorage) SetShallow(commits []plumbing.Hash) error { func (s ShallowStorage) Shallow() ([]plumbing.Hash, error) { return s, nil } + +type ModuleStorage map[string]*Storage + +func (s ModuleStorage) Module(name string) (storage.Storer, error) { + if _, ok := s[name]; !ok { + s[name] = NewStorage() + } + + return s[name], nil +} diff --git a/storage/storer.go b/storage/storer.go new file mode 100644 index 000000000..0a2c2566f --- /dev/null +++ b/storage/storer.go @@ -0,0 +1,23 @@ +package storage + +import ( + "srcd.works/go-git.v4/config" + "srcd.works/go-git.v4/plumbing/storer" +) + +// Storer is a generic storage of objects, references and any information +// related to a particular repository. The package srcd.works/go-git.v4/storage +// contains two implementation a filesystem base implementation (such as `.git`) +// and a memory implementations being ephemeral +type Storer interface { + storer.EncodedObjectStorer + storer.ReferenceStorer + storer.ShallowStorer + storer.IndexStorer + config.ConfigStorer + ModuleStorer +} + +type ModuleStorer interface { + Module(name string) (Storer, error) +} diff --git a/storage/test/storage_suite.go b/storage/test/storage_suite.go index 6fc29375c..d6c8afa75 100644 --- a/storage/test/storage_suite.go +++ b/storage/test/storage_suite.go @@ -11,6 +11,7 @@ import ( "srcd.works/go-git.v4/plumbing" "srcd.works/go-git.v4/plumbing/format/index" "srcd.works/go-git.v4/plumbing/storer" + "srcd.works/go-git.v4/storage" . "gopkg.in/check.v1" ) @@ -21,6 +22,8 @@ type Storer interface { storer.ShallowStorer storer.IndexStorer config.ConfigStorer + + storage.ModuleStorer } type TestObject struct { @@ -326,6 +329,16 @@ func (s *BaseStorageSuite) TestSetConfigInvalid(c *C) { c.Assert(err, NotNil) } +func (s *BaseStorageSuite) TestModule(c *C) { + storer, err := s.Storer.Module("foo") + c.Assert(err, IsNil) + c.Assert(storer, NotNil) + + storer, err = s.Storer.Module("foo") + c.Assert(err, IsNil) + c.Assert(storer, NotNil) +} + func objectEquals(a plumbing.EncodedObject, b plumbing.EncodedObject) error { ha := a.Hash() hb := b.Hash() From b3b6e51565dcdbf81546f1d8f0121874a4e4fce9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1ximo=20Cuadros?= Date: Sun, 12 Feb 2017 23:03:47 +0100 Subject: [PATCH 02/11] submodule init implementation --- _examples/clone/main.go | 5 ++-- common.go | 19 +------------ doc.go | 2 +- options.go | 4 +++ remote.go | 9 ++++-- remote_test.go | 3 +- repository.go | 40 ++++++++++++++++++-------- repository_test.go | 18 +++--------- submodule.go | 30 ++++++++++++++++++++ worktree.go | 62 +++++++++++++++++++++++++++++++++++++++++ 10 files changed, 142 insertions(+), 50 deletions(-) create mode 100644 submodule.go diff --git a/_examples/clone/main.go b/_examples/clone/main.go index 7d173a68e..18d8a5505 100644 --- a/_examples/clone/main.go +++ b/_examples/clone/main.go @@ -17,8 +17,9 @@ func main() { Info("git clone %s %s", url, directory) r, err := git.PlainClone(directory, false, &git.CloneOptions{ - URL: url, - Depth: 1, + URL: url, + RecursiveSubmodules: true, + Depth: 1, }) CheckIfError(err) diff --git a/common.go b/common.go index 1a05dc3e9..6174339a8 100644 --- a/common.go +++ b/common.go @@ -1,23 +1,6 @@ package git -import ( - "strings" - - "srcd.works/go-git.v4/config" - "srcd.works/go-git.v4/plumbing/storer" -) - -// Storer is a generic storage of objects, references and any information -// related to a particular repository. The package srcd.works/go-git.v4/storage -// contains two implementation a filesystem base implementation (such as `.git`) -// and a memory implementations being ephemeral -type Storer interface { - storer.EncodedObjectStorer - storer.ReferenceStorer - storer.ShallowStorer - storer.IndexStorer - config.ConfigStorer -} +import "strings" // countLines returns the number of lines in a string à la git, this is // The newline character is assumed to be '\n'. The empty string diff --git a/doc.go b/doc.go index 3d817fe9c..7f57cbdf5 100644 --- a/doc.go +++ b/doc.go @@ -7,4 +7,4 @@ // It is highly extensible, we have been following the open/close principle in // its design to facilitate extensions, mainly focusing the efforts on the // persistence of the objects. -package git +package git // import "srcd.works/go-git.v4" diff --git a/options.go b/options.go index 120c47279..1f83c363c 100644 --- a/options.go +++ b/options.go @@ -32,6 +32,10 @@ type CloneOptions struct { SingleBranch bool // Limit fetching to the specified number of commits Depth int + // RecursiveSubmodules after the clone is created, initialize all submodules + // within, using their default settings. This option is ignored if the + // cloned repository does not have a worktree + RecursiveSubmodules bool // Progress is where the human readable information sent by the server is // stored, if nil nothing is stored and the capability (if supported) // no-progress, is sent to the server to avoid send this information diff --git a/remote.go b/remote.go index 2d243e93e..a51946bf8 100644 --- a/remote.go +++ b/remote.go @@ -16,6 +16,7 @@ import ( "srcd.works/go-git.v4/plumbing/storer" "srcd.works/go-git.v4/plumbing/transport" "srcd.works/go-git.v4/plumbing/transport/client" + "srcd.works/go-git.v4/storage" "srcd.works/go-git.v4/storage/memory" "srcd.works/go-git.v4/utils/ioutil" ) @@ -25,10 +26,10 @@ var NoErrAlreadyUpToDate = errors.New("already up-to-date") // Remote represents a connection to a remote repository type Remote struct { c *config.RemoteConfig - s Storer + s storage.Storer } -func newRemote(s Storer, c *config.RemoteConfig) *Remote { +func newRemote(s storage.Storer, c *config.RemoteConfig) *Remote { return &Remote{s: s, c: c} } @@ -321,7 +322,9 @@ func getHaves(localRefs storer.ReferenceStorer) ([]plumbing.Hash, error) { return haves, nil } -func getWants(spec []config.RefSpec, localStorer Storer, remoteRefs storer.ReferenceStorer) ([]plumbing.Hash, error) { +func getWants( + spec []config.RefSpec, localStorer storage.Storer, remoteRefs storer.ReferenceStorer, +) ([]plumbing.Hash, error) { wantTags := true for _, s := range spec { if !s.IsWildcard() { diff --git a/remote_test.go b/remote_test.go index 1e3490519..e13ef2070 100644 --- a/remote_test.go +++ b/remote_test.go @@ -11,6 +11,7 @@ import ( "srcd.works/go-git.v4/config" "srcd.works/go-git.v4/plumbing" "srcd.works/go-git.v4/plumbing/storer" + "srcd.works/go-git.v4/storage" "srcd.works/go-git.v4/storage/filesystem" "srcd.works/go-git.v4/storage/memory" @@ -126,7 +127,7 @@ func (s *RemoteSuite) TestFetchWithProgress(c *C) { } type mockPackfileWriter struct { - Storer + storage.Storer PackfileWriterCalled bool } diff --git a/repository.go b/repository.go index a8dd7ef4f..2f5ff8216 100644 --- a/repository.go +++ b/repository.go @@ -10,6 +10,7 @@ import ( "srcd.works/go-git.v4/plumbing" "srcd.works/go-git.v4/plumbing/object" "srcd.works/go-git.v4/plumbing/storer" + "srcd.works/go-git.v4/storage" "srcd.works/go-git.v4/storage/filesystem" "srcd.works/go-billy.v1" @@ -29,7 +30,7 @@ var ( // Repository represents a git repository type Repository struct { - Storer Storer + Storer storage.Storer r map[string]*Remote wt billy.Filesystem @@ -38,7 +39,7 @@ type Repository struct { // Init creates an empty git repository, based on the given Storer and worktree. // The worktree Filesystem is optional, if nil a bare repository is created. If // the given storer is not empty ErrRepositoryAlreadyExists is returned -func Init(s Storer, worktree billy.Filesystem) (*Repository, error) { +func Init(s storage.Storer, worktree billy.Filesystem) (*Repository, error) { r := newRepository(s, worktree) _, err := r.Reference(plumbing.HEAD, false) switch err { @@ -66,7 +67,7 @@ func Init(s Storer, worktree billy.Filesystem) (*Repository, error) { // The worktree can be nil when the repository being opened is bare, if the // repository is a normal one (not bare) and worktree is nil the err // ErrWorktreeNotProvided is returned -func Open(s Storer, worktree billy.Filesystem) (*Repository, error) { +func Open(s storage.Storer, worktree billy.Filesystem) (*Repository, error) { _, err := s.Reference(plumbing.HEAD) if err == plumbing.ErrReferenceNotFound { return nil, ErrRepositoryNotExists @@ -91,7 +92,7 @@ func Open(s Storer, worktree billy.Filesystem) (*Repository, error) { // Clone a repository into the given Storer and worktree Filesystem with the // given options, if worktree is nil a bare repository is created. If the given // storer is not empty ErrRepositoryAlreadyExists is returned -func Clone(s Storer, worktree billy.Filesystem, o *CloneOptions) (*Repository, error) { +func Clone(s storage.Storer, worktree billy.Filesystem, o *CloneOptions) (*Repository, error) { r, err := Init(s, worktree) if err != nil { return nil, err @@ -159,7 +160,7 @@ func PlainClone(path string, isBare bool, o *CloneOptions) (*Repository, error) return r, r.clone(o) } -func newRepository(s Storer, worktree billy.Filesystem) *Repository { +func newRepository(s storage.Storer, worktree billy.Filesystem) *Repository { return &Repository{ Storer: s, wt: worktree, @@ -247,12 +248,6 @@ func (r *Repository) clone(o *CloneOptions) error { return err } - // marks the repository as bare in the config, until we have Worktree, all - // the repository are bare - if err := r.setIsBare(true); err != nil { - return err - } - c := &config.RemoteConfig{ Name: o.RemoteName, URL: o.URL, @@ -270,11 +265,13 @@ func (r *Repository) clone(o *CloneOptions) error { Progress: o.Progress, }) if err != nil { + return err } head, err := storer.ResolveReference(remoteRefs, o.ReferenceName) if err != nil { + return err } @@ -283,12 +280,33 @@ func (r *Repository) clone(o *CloneOptions) error { } if err := r.updateWorktree(); err != nil { + fmt.Println("q", err) return err } + if o.RecursiveSubmodules && r.wt != nil { + if err := r.initSubmodules(); err != nil { + return err + } + } + return r.updateRemoteConfig(remote, o, c, head) } +func (r *Repository) initSubmodules() error { + w, err := r.Worktree() + if err != nil { + return err + } + + s, err := w.Submodules() + if err != nil { + return err + } + + return s.Init() +} + func (r *Repository) cloneRefSpec(o *CloneOptions, c *config.RemoteConfig) []config.RefSpec { diff --git a/repository_test.go b/repository_test.go index 1b5b345c8..9fd48fce9 100644 --- a/repository_test.go +++ b/repository_test.go @@ -516,26 +516,21 @@ func (s *RepositorySuite) TestPullProgress(c *C) { } func (s *RepositorySuite) TestPullAdd(c *C) { - path := fixtures.Basic().One().Worktree().Base() + path := fixtures.Basic().ByTag("worktree").One().Worktree().Base() - r, _ := Init(memory.NewStorage(), nil) - err := r.clone(&CloneOptions{ + r, err := Clone(memory.NewStorage(), nil, &CloneOptions{ URL: fmt.Sprintf("file://%s", filepath.Join(path, ".git")), }) c.Assert(err, IsNil) storage := r.Storer.(*memory.Storage) - c.Assert(storage.Objects, HasLen, 31) + c.Assert(storage.Objects, HasLen, 28) branch, err := r.Reference("refs/heads/master", false) c.Assert(err, IsNil) c.Assert(branch.Hash().String(), Equals, "6ecf0ef2c2dffb796033e5a02219af86ec6584e5") - branch, err = r.Reference("refs/remotes/origin/branch", false) - c.Assert(err, IsNil) - c.Assert(branch.Hash().String(), Equals, "e8d3ffab552895c19b9fcf7aa264d277cde33881") - ExecuteOnPath(c, path, "touch foo", "git add foo", @@ -546,16 +541,11 @@ func (s *RepositorySuite) TestPullAdd(c *C) { c.Assert(err, IsNil) // the commit command has introduced a new commit, tree and blob - c.Assert(storage.Objects, HasLen, 34) + c.Assert(storage.Objects, HasLen, 31) branch, err = r.Reference("refs/heads/master", false) c.Assert(err, IsNil) c.Assert(branch.Hash().String(), Not(Equals), "6ecf0ef2c2dffb796033e5a02219af86ec6584e5") - - // the commit command, was in the local branch, so the remote should be read ok - branch, err = r.Reference("refs/remotes/origin/branch", false) - c.Assert(err, IsNil) - c.Assert(branch.Hash().String(), Equals, "e8d3ffab552895c19b9fcf7aa264d277cde33881") } func (s *RepositorySuite) TestPushToEmptyRepository(c *C) { diff --git a/submodule.go b/submodule.go new file mode 100644 index 000000000..32f730b83 --- /dev/null +++ b/submodule.go @@ -0,0 +1,30 @@ +package git + +import "srcd.works/go-git.v4/plumbing" + +type Submodule struct { + Name string + Branch string + URL string + + r *Repository +} + +func (s *Submodule) Init() error { + return s.r.clone(&CloneOptions{ + URL: s.URL, + ReferenceName: plumbing.ReferenceName(s.Branch), + }) +} + +type Submodules []*Submodule + +func (s Submodules) Init() error { + for _, sub := range s { + if err := sub.Init(); err != nil { + return err + } + } + + return nil +} diff --git a/worktree.go b/worktree.go index 58e008e72..8aee0dcf6 100644 --- a/worktree.go +++ b/worktree.go @@ -4,8 +4,10 @@ import ( "errors" "fmt" "io" + "io/ioutil" "os" + "srcd.works/go-git.v4/config" "srcd.works/go-git.v4/plumbing" "srcd.works/go-git.v4/plumbing/format/index" "srcd.works/go-git.v4/plumbing/object" @@ -104,6 +106,7 @@ func (w *Worktree) Status() (Status, error) { files, err := readDirAll(w.fs) if err != nil { + fmt.Println("ch", err) return nil, err } @@ -167,6 +170,61 @@ func (w *Worktree) getMode(fi billy.FileInfo) os.FileMode { return object.FileMode } +const gitmodulesFile = ".gitmodules" + +func (w *Worktree) Submodules() (Submodules, error) { + l := make(Submodules, 0) + m, err := w.readGitmodulesFile() + if err != nil || m == nil { + return l, err + } + + for _, c := range m.Submodules { + s, err := w.newSubmodule(c) + if err != nil { + return nil, err + } + + l = append(l, s) + } + + return l, nil +} + +func (w *Worktree) newSubmodule(m *config.Submodule) (*Submodule, error) { + s, err := w.r.Storer.Module(m.Name) + if err != nil { + return nil, err + } + + return &Submodule{ + Name: m.Name, + URL: m.URL, + + r: newRepository(s, w.fs.Dir(m.Path)), + }, nil +} + +func (w *Worktree) readGitmodulesFile() (*config.Modules, error) { + f, err := w.fs.Open(gitmodulesFile) + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } + + return nil, err + } + + input, err := ioutil.ReadAll(f) + if err != nil { + return nil, err + } + + m := config.NewModules() + return m, m.Unmarshal(input) + +} + // Status current status of a Worktree type Status map[string]*FileStatus @@ -287,6 +345,10 @@ func doReadDirAll(fs billy.Filesystem, path string, files map[string]billy.FileI l, err := fs.ReadDir(path) if err != nil { + if os.IsNotExist(err) { + return nil + } + return err } From b18d6490aefe8509791843136530e3426d0c8200 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1ximo=20Cuadros?= Date: Mon, 13 Feb 2017 00:15:19 +0100 Subject: [PATCH 03/11] plumbing/object: allow TreeIter return SubModule entries --- plumbing/object/file.go | 2 +- plumbing/object/file_test.go | 26 ++++++++++++++++++++++++ plumbing/object/tree.go | 5 ----- plumbing/object/tree_test.go | 38 ++++++++++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 6 deletions(-) diff --git a/plumbing/object/file.go b/plumbing/object/file.go index 35e7f244b..486677718 100644 --- a/plumbing/object/file.go +++ b/plumbing/object/file.go @@ -81,7 +81,7 @@ func (iter *FileIter) Next() (*File, error) { return nil, err } - if entry.Mode.IsDir() { + if entry.Mode.IsDir() || entry.Mode == SubmoduleMode { continue } diff --git a/plumbing/object/file_test.go b/plumbing/object/file_test.go index 4c8bbb63d..426fa8f03 100644 --- a/plumbing/object/file_test.go +++ b/plumbing/object/file_test.go @@ -247,3 +247,29 @@ func (s *FileSuite) TestFileIter(c *C) { c.Assert(count, Equals, 1) } + +func (s *FileSuite) TestFileIterSubmodule(c *C) { + st, err := filesystem.NewStorage(fixtures.ByTag("submodule").One().DotGit()) + c.Assert(err, IsNil) + + hash := plumbing.NewHash("a692ec699bff9117c1ed91752afbb7d9d272ebef") + commit, err := GetCommit(st, hash) + c.Assert(err, IsNil) + + tree, err := commit.Tree() + c.Assert(err, IsNil) + + expected := []string{ + ".gitmodules", + } + + var count int + i := tree.Files() + i.ForEach(func(f *File) error { + c.Assert(f.Name, Equals, expected[count]) + count++ + return nil + }) + + c.Assert(count, Equals, 1) +} diff --git a/plumbing/object/tree.go b/plumbing/object/tree.go index 3bcd80a55..27d8578b8 100644 --- a/plumbing/object/tree.go +++ b/plumbing/object/tree.go @@ -375,11 +375,6 @@ func (w *TreeWalker) Next() (name string, entry TreeEntry, err error) { return } - if entry.Mode == SubmoduleMode { - err = nil - continue - } - if entry.Mode.IsDir() { obj, err = GetTree(w.s, entry.Hash) } diff --git a/plumbing/object/tree_test.go b/plumbing/object/tree_test.go index be721b9a8..0ddf391b8 100644 --- a/plumbing/object/tree_test.go +++ b/plumbing/object/tree_test.go @@ -4,7 +4,10 @@ import ( "io" "os" + fixtures "github.com/src-d/go-git-fixtures" + "srcd.works/go-git.v4/plumbing" + "srcd.works/go-git.v4/storage/filesystem" . "gopkg.in/check.v1" "srcd.works/go-git.v4/plumbing/storer" @@ -262,6 +265,41 @@ func (s *TreeSuite) TestTreeWalkerNextNonRecursive(c *C) { c.Assert(count, Equals, 8) } +func (s *TreeSuite) TestTreeWalkerNextSubmodule(c *C) { + st, err := filesystem.NewStorage(fixtures.ByTag("submodule").One().DotGit()) + c.Assert(err, IsNil) + + hash := plumbing.NewHash("a692ec699bff9117c1ed91752afbb7d9d272ebef") + commit, err := GetCommit(st, hash) + c.Assert(err, IsNil) + + tree, err := commit.Tree() + c.Assert(err, IsNil) + + expected := []string{ + ".gitmodules", + "basic", + "itself", + } + + var count int + walker := NewTreeWalker(tree, true) + for { + name, entry, err := walker.Next() + if err == io.EOF { + break + } + + c.Assert(err, IsNil) + c.Assert(entry, NotNil) + c.Assert(name, Equals, expected[count]) + + count++ + } + + c.Assert(count, Equals, 3) +} + var treeWalkerExpects = []struct { Path, Mode, Name, Hash, Tree string }{{ From c551c29a93882658d4b34860b5300de0f3456059 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1ximo=20Cuadros?= Date: Mon, 13 Feb 2017 00:40:14 +0100 Subject: [PATCH 04/11] submodule init implementation --- repository_test.go | 4 ++-- submodule.go | 8 ++++++- worktree.go | 58 ++++++++++++++++++++++++++++++++++++++-------- worktree_test.go | 2 +- 4 files changed, 58 insertions(+), 14 deletions(-) diff --git a/repository_test.go b/repository_test.go index 9fd48fce9..84a722116 100644 --- a/repository_test.go +++ b/repository_test.go @@ -306,7 +306,7 @@ func (s *RepositorySuite) TestCloneDeep(c *C) { fi, err := fs.ReadDir("") c.Assert(err, IsNil) - c.Assert(fi, HasLen, 8) + c.Assert(fi, HasLen, 9) } func (s *RepositorySuite) TestCloneConfig(c *C) { @@ -431,7 +431,7 @@ func (s *RepositorySuite) TestPullCheckout(c *C) { fi, err := fs.ReadDir("") c.Assert(err, IsNil) - c.Assert(fi, HasLen, 8) + c.Assert(fi, HasLen, 9) } func (s *RepositorySuite) TestCloneWithProgress(c *C) { diff --git a/submodule.go b/submodule.go index 32f730b83..ea099f0e9 100644 --- a/submodule.go +++ b/submodule.go @@ -1,6 +1,10 @@ package git -import "srcd.works/go-git.v4/plumbing" +import ( + "fmt" + + "srcd.works/go-git.v4/plumbing" +) type Submodule struct { Name string @@ -21,7 +25,9 @@ type Submodules []*Submodule func (s Submodules) Init() error { for _, sub := range s { + fmt.Println("clone", sub.URL) if err := sub.Init(); err != nil { + fmt.Println(err) return err } } diff --git a/worktree.go b/worktree.go index 8aee0dcf6..473f933da 100644 --- a/worktree.go +++ b/worktree.go @@ -37,29 +37,57 @@ func (w *Worktree) Checkout(commit plumbing.Hash) error { return err } - files, err := c.Files() + t, err := c.Tree() if err != nil { return err } idx := &index.Index{Version: 2} - if err := files.ForEach(func(f *object.File) error { - return w.checkoutFile(f, idx) - }); err != nil { - return err + walker := object.NewTreeWalker(t, true) + + for { + _, entry, err := walker.Next() + if err == io.EOF { + break + } + + if err != nil { + return err + } + + if err := w.checkoutEntry(&entry, idx); err != nil { + return err + } } return w.r.Storer.SetIndex(idx) } -func (w *Worktree) checkoutFile(f *object.File, idx *index.Index) error { - from, err := f.Reader() +func (w *Worktree) checkoutEntry(e *object.TreeEntry, idx *index.Index) error { + if e.Mode == object.SubmoduleMode { + return w.indexEntry(e, idx) + } + + if e.Mode.IsDir() { + return nil + } + + return w.checkoutFile(e, idx) +} + +func (w *Worktree) checkoutFile(e *object.TreeEntry, idx *index.Index) error { + blob, err := object.GetBlob(w.r.Storer, e.Hash) + if err != nil { + return err + } + + from, err := blob.Reader() if err != nil { return err } defer from.Close() - to, err := w.fs.OpenFile(f.Name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode.Perm()) + to, err := w.fs.OpenFile(e.Name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, e.Mode.Perm()) if err != nil { return err } @@ -69,12 +97,22 @@ func (w *Worktree) checkoutFile(f *object.File, idx *index.Index) error { } defer to.Close() - return w.indexFile(f, idx) + return w.indexFile(e, idx) } var fillSystemInfo func(e *index.Entry, sys interface{}) -func (w *Worktree) indexFile(f *object.File, idx *index.Index) error { +func (w *Worktree) indexEntry(f *object.TreeEntry, idx *index.Index) error { + idx.Entries = append(idx.Entries, index.Entry{ + Hash: f.Hash, + Name: f.Name, + Mode: object.SubmoduleMode, + }) + + return nil +} + +func (w *Worktree) indexFile(f *object.TreeEntry, idx *index.Index) error { fi, err := w.fs.Stat(f.Name) if err != nil { return err diff --git a/worktree_test.go b/worktree_test.go index 8ca3d4fb9..306b56d04 100644 --- a/worktree_test.go +++ b/worktree_test.go @@ -40,7 +40,7 @@ func (s *WorktreeSuite) TestCheckout(c *C) { entries, err := fs.ReadDir("/") c.Assert(err, IsNil) - c.Assert(entries, HasLen, 8) + c.Assert(entries, HasLen, 9) ch, err := fs.Open("CHANGELOG") c.Assert(err, IsNil) From 940a16ca68457beded1eff551f23febeffb32f3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1ximo=20Cuadros?= Date: Mon, 13 Feb 2017 19:18:05 +0100 Subject: [PATCH 05/11] format/index: sort the Entries before encode --- plumbing/format/index/encoder.go | 9 +++++++++ plumbing/format/index/encoder_test.go | 10 ++++++++++ plumbing/format/index/index.go | 13 +++++++++---- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/plumbing/format/index/encoder.go b/plumbing/format/index/encoder.go index e5de1359e..4699d436d 100644 --- a/plumbing/format/index/encoder.go +++ b/plumbing/format/index/encoder.go @@ -6,6 +6,7 @@ import ( "errors" "hash" "io" + "sort" "time" "srcd.works/go-git.v4/utils/binary" @@ -61,6 +62,8 @@ func (e *Encoder) encodeHeader(idx *Index) error { } func (e *Encoder) encodeEntries(idx *Index) error { + sort.Sort(ByName(idx.Entries)) + for _, entry := range idx.Entries { if err := e.encodeEntry(&entry); err != nil { return err @@ -139,3 +142,9 @@ func (e *Encoder) padEntry(wrote int) error { func (e *Encoder) encodeFooter() error { return binary.Write(e.w, e.hash.Sum(nil)) } + +type ByName []Entry + +func (l ByName) Len() int { return len(l) } +func (l ByName) Swap(i, j int) { l[i], l[j] = l[j], l[i] } +func (l ByName) Less(i, j int) bool { return l[i].Name < l[j].Name } diff --git a/plumbing/format/index/encoder_test.go b/plumbing/format/index/encoder_test.go index 914ee2645..a6a0ea23e 100644 --- a/plumbing/format/index/encoder_test.go +++ b/plumbing/format/index/encoder_test.go @@ -23,6 +23,11 @@ func (s *IndexSuite) TestEncode(c *C) { Stage: TheirMode, Hash: plumbing.NewHash("e25b29c8946e0e192fae2edc1dabf7be71e8ecf3"), Name: "foo", + }, { + CreatedAt: time.Now(), + ModifiedAt: time.Now(), + Name: "bar", + Size: 82, }, { CreatedAt: time.Now(), ModifiedAt: time.Now(), @@ -42,6 +47,11 @@ func (s *IndexSuite) TestEncode(c *C) { c.Assert(err, IsNil) c.Assert(idx, DeepEquals, output) + + c.Assert(output.Entries[0].Name, Equals, strings.Repeat(" ", 20)) + c.Assert(output.Entries[1].Name, Equals, "bar") + c.Assert(output.Entries[2].Name, Equals, "foo") + } func (s *IndexSuite) TestEncodeUnsuportedVersion(c *C) { diff --git a/plumbing/format/index/index.go b/plumbing/format/index/index.go index e5dc178fb..a95dba202 100644 --- a/plumbing/format/index/index.go +++ b/plumbing/format/index/index.go @@ -36,9 +36,14 @@ const ( // in the worktree, having information about the working files. Changes in // worktree are detected using this Index. The Index is also used during merges type Index struct { - Version uint32 - Entries []Entry - Cache *Tree + // Version is index version + Version uint32 + // Entries collection of entries represented by this Index. The order of + // this collection is not guaranteed + Entries []Entry + // Cache represents the 'Cached tree' extension + Cache *Tree + // ResolveUndo represents the 'Resolve undo' extension ResolveUndo *ResolveUndo } @@ -84,7 +89,7 @@ type TreeEntry struct { // Path component (relative to its parent directory) Path string // Entries is the number of entries in the index that is covered by the tree - // this entry represents + // this entry represents. Entries int // Trees is the number that represents the number of subtrees this tree has Trees int From f8b5557875513c5b0aff24bb7b8e28f8f680976f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1ximo=20Cuadros?= Date: Mon, 13 Feb 2017 23:38:57 +0100 Subject: [PATCH 06/11] config: adding Config.Core.Worktree --- config/config.go | 9 +++++++++ config/config_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/config/config.go b/config/config.go index 866ae8ee0..b3a3fcc82 100644 --- a/config/config.go +++ b/config/config.go @@ -37,6 +37,8 @@ type Config struct { // IsBare if true this repository is assumed to be bare and has no // working directory associated with it IsBare bool + // Worktree is the path to the root of the working tree + Worktree string } // Remote list of repository remotes Remotes map[string]*RemoteConfig @@ -76,6 +78,7 @@ const ( fetchKey = "fetch" urlKey = "url" bareKey = "bare" + worktreeKey = "worktree" ) // Unmarshal parses a git-config file and stores it @@ -97,6 +100,8 @@ func (c *Config) unmarshalCore() { if s.Options.Get(bareKey) == "true" { c.Core.IsBare = true } + + c.Core.Worktree = s.Options.Get(worktreeKey) } func (c *Config) unmarshalRemotes() error { @@ -129,6 +134,10 @@ func (c *Config) Marshal() ([]byte, error) { func (c *Config) marshalCore() { s := c.raw.Section(coreSection) s.SetOption(bareKey, fmt.Sprintf("%t", c.Core.IsBare)) + + if c.Core.Worktree != "" { + s.SetOption(worktreeKey, c.Core.Worktree) + } } func (c *Config) marshalRemotes() { diff --git a/config/config_test.go b/config/config_test.go index 2bcefe4ab..313c96ce2 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -9,6 +9,7 @@ var _ = Suite(&ConfigSuite{}) func (s *ConfigSuite) TestUnmarshall(c *C) { input := []byte(`[core] bare = true + worktree = foo [remote "origin"] url = git@github.com:mcuadros/go-git.git fetch = +refs/heads/*:refs/remotes/origin/* @@ -22,15 +23,39 @@ func (s *ConfigSuite) TestUnmarshall(c *C) { c.Assert(err, IsNil) c.Assert(cfg.Core.IsBare, Equals, true) + c.Assert(cfg.Core.Worktree, Equals, "foo") c.Assert(cfg.Remotes, HasLen, 1) c.Assert(cfg.Remotes["origin"].Name, Equals, "origin") c.Assert(cfg.Remotes["origin"].URL, Equals, "git@github.com:mcuadros/go-git.git") c.Assert(cfg.Remotes["origin"].Fetch, DeepEquals, []RefSpec{"+refs/heads/*:refs/remotes/origin/*"}) } +func (s *ConfigSuite) TestMarshall(c *C) { + output := []byte(`[core] + bare = true + worktree = bar +[remote "origin"] + url = git@github.com:mcuadros/go-git.git +`) + + cfg := NewConfig() + cfg.Core.IsBare = true + cfg.Core.Worktree = "bar" + cfg.Remotes["origin"] = &RemoteConfig{ + Name: "origin", + URL: "git@github.com:mcuadros/go-git.git", + } + + b, err := cfg.Marshal() + c.Assert(err, IsNil) + + c.Assert(string(b), Equals, string(output)) +} + func (s *ConfigSuite) TestUnmarshallMarshall(c *C) { input := []byte(`[core] bare = true + worktree = foo custom = ignored [remote "origin"] url = git@github.com:mcuadros/go-git.git From 65351f835dcaa4b50dd44bce7bf3f2e31582dadc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1ximo=20Cuadros?= Date: Mon, 13 Feb 2017 23:59:49 +0100 Subject: [PATCH 07/11] Repository.Init now handles non-standard .git location --- repository.go | 70 +++++++++++++++++++++++++++++- repository_test.go | 51 +++++++++++++++++++++- storage/filesystem/storage.go | 9 ++++ storage/filesystem/storage_test.go | 9 ++++ 4 files changed, 135 insertions(+), 4 deletions(-) diff --git a/repository.go b/repository.go index 2f5ff8216..4c184abaf 100644 --- a/repository.go +++ b/repository.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "os" + "path/filepath" "srcd.works/go-git.v4/config" "srcd.works/go-git.v4/internal/revision" @@ -57,9 +58,75 @@ func Init(s storage.Storer, worktree billy.Filesystem) (*Repository, error) { if worktree == nil { r.setIsBare(true) + return r, nil } - return r, nil + return r, setWorktreeAndStoragePaths(r, worktree) +} + +func setWorktreeAndStoragePaths(r *Repository, worktree billy.Filesystem) error { + type fsBased interface { + Filesystem() billy.Filesystem + } + + // .git file is only created if the storage is file based and the file + // system is osfs.OS + fs, isFSBased := r.Storer.(fsBased) + if !isFSBased { + return nil + } + + _, isOS := fs.Filesystem().(*osfs.OS) + if !isOS { + return nil + } + + if err := createDotGitFile(worktree, fs.Filesystem()); err != nil { + return err + } + + return setConfigWorktree(r, worktree, fs.Filesystem()) +} + +func createDotGitFile(worktree, storage billy.Filesystem) error { + path, err := filepath.Rel(worktree.Base(), storage.Base()) + if err != nil { + path = storage.Base() + } + + if path == ".git" { + // not needed, since the folder is the default place + return nil + } + + f, err := worktree.Create(".git") + if err != nil { + return err + } + + defer f.Close() + _, err = fmt.Fprintf(f, "gitdir: %s\n", path) + return err +} + +func setConfigWorktree(r *Repository, worktree, storage billy.Filesystem) error { + path, err := filepath.Rel(storage.Base(), worktree.Base()) + if err != nil { + path = worktree.Base() + } + + if path == ".." { + // not needed, since the folder is the default place + return nil + } + + cfg, err := r.Storer.Config() + if err != nil { + return err + } + + cfg.Core.Worktree = path + return r.Storer.SetConfig(cfg) } // Open opens a git repository using the given Storer and worktree filesystem, @@ -280,7 +347,6 @@ func (r *Repository) clone(o *CloneOptions) error { } if err := r.updateWorktree(); err != nil { - fmt.Println("q", err) return err } diff --git a/repository_test.go b/repository_test.go index 84a722116..2c1d4a28b 100644 --- a/repository_test.go +++ b/repository_test.go @@ -18,6 +18,7 @@ import ( . "gopkg.in/check.v1" "srcd.works/go-billy.v1/memfs" + "srcd.works/go-billy.v1/osfs" ) type RepositorySuite struct { @@ -36,6 +37,52 @@ func (s *RepositorySuite) TestInit(c *C) { c.Assert(cfg.Core.IsBare, Equals, false) } +func (s *RepositorySuite) TestInitNonStandardDotGit(c *C) { + dir, err := ioutil.TempDir("", "init-non-standard") + c.Assert(err, IsNil) + c.Assert(os.RemoveAll(dir), IsNil) + + fs := osfs.New(dir) + storage, err := filesystem.NewStorage(fs.Dir("storage")) + c.Assert(err, IsNil) + + r, err := Init(storage, fs.Dir("worktree")) + c.Assert(err, IsNil) + c.Assert(r, NotNil) + + f, err := fs.Open("worktree/.git") + c.Assert(err, IsNil) + + all, err := ioutil.ReadAll(f) + c.Assert(err, IsNil) + c.Assert(string(all), Equals, "gitdir: ../storage\n") + + cfg, err := r.Config() + c.Assert(err, IsNil) + c.Assert(cfg.Core.Worktree, Equals, "../worktree") +} + +func (s *RepositorySuite) TestInitStandardDotGit(c *C) { + dir, err := ioutil.TempDir("", "init-standard") + c.Assert(err, IsNil) + c.Assert(os.RemoveAll(dir), IsNil) + + fs := osfs.New(dir) + storage, err := filesystem.NewStorage(fs.Dir(".git")) + c.Assert(err, IsNil) + + r, err := Init(storage, fs) + c.Assert(err, IsNil) + c.Assert(r, NotNil) + + l, err := fs.ReadDir(".git") + c.Assert(len(l) > 0, Equals, true) + + cfg, err := r.Config() + c.Assert(err, IsNil) + c.Assert(cfg.Core.Worktree, Equals, "") +} + func (s *RepositorySuite) TestInitBare(c *C) { r, err := Init(memory.NewStorage(), nil) c.Assert(err, IsNil) @@ -306,7 +353,7 @@ func (s *RepositorySuite) TestCloneDeep(c *C) { fi, err := fs.ReadDir("") c.Assert(err, IsNil) - c.Assert(fi, HasLen, 9) + c.Assert(fi, HasLen, 8) } func (s *RepositorySuite) TestCloneConfig(c *C) { @@ -431,7 +478,7 @@ func (s *RepositorySuite) TestPullCheckout(c *C) { fi, err := fs.ReadDir("") c.Assert(err, IsNil) - c.Assert(fi, HasLen, 9) + c.Assert(fi, HasLen, 8) } func (s *RepositorySuite) TestCloneWithProgress(c *C) { diff --git a/storage/filesystem/storage.go b/storage/filesystem/storage.go index 27dd07f3a..989550797 100644 --- a/storage/filesystem/storage.go +++ b/storage/filesystem/storage.go @@ -11,6 +11,8 @@ import ( // standard git format (this is, the .git directory). Zero values of this type // are not safe to use, see the NewStorage function below. type Storage struct { + fs billy.Filesystem + ObjectStorage ReferenceStorage IndexStorage @@ -28,6 +30,8 @@ func NewStorage(fs billy.Filesystem) (*Storage, error) { } return &Storage{ + fs: fs, + ObjectStorage: o, ReferenceStorage: ReferenceStorage{dir: dir}, IndexStorage: IndexStorage{dir: dir}, @@ -36,3 +40,8 @@ func NewStorage(fs billy.Filesystem) (*Storage, error) { ModuleStorage: ModuleStorage{dir: dir}, }, nil } + +// Filesystem returns the underlying filesystem +func (s *Storage) Filesystem() billy.Filesystem { + return s.fs +} diff --git a/storage/filesystem/storage_test.go b/storage/filesystem/storage_test.go index e398d2280..7300de7ba 100644 --- a/storage/filesystem/storage_test.go +++ b/storage/filesystem/storage_test.go @@ -6,6 +6,7 @@ import ( "srcd.works/go-git.v4/storage/test" . "gopkg.in/check.v1" + "srcd.works/go-billy.v1/memfs" "srcd.works/go-billy.v1/osfs" ) @@ -23,3 +24,11 @@ func (s *StorageSuite) SetUpTest(c *C) { s.BaseStorageSuite = test.NewBaseStorageSuite(storage) } + +func (s *StorageSuite) TestFilesystem(c *C) { + fs := memfs.New() + storage, err := NewStorage(fs) + c.Assert(err, IsNil) + + c.Assert(storage.Filesystem(), Equals, fs) +} From 7e990a811d9e23b5a3573c405b70f06a1be9e7b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1ximo=20Cuadros?= Date: Tue, 14 Feb 2017 00:25:09 +0100 Subject: [PATCH 08/11] submodule init and update implementation --- common.go | 2 + submodule.go | 68 ++++++++++++++++++++++----- submodule_test.go | 114 ++++++++++++++++++++++++++++++++++++++++++++++ worktree.go | 79 ++++++++++++++++++++++++-------- worktree_test.go | 32 ++++++++++++- 5 files changed, 263 insertions(+), 32 deletions(-) create mode 100644 submodule_test.go diff --git a/common.go b/common.go index 6174339a8..f837a2654 100644 --- a/common.go +++ b/common.go @@ -2,6 +2,8 @@ package git import "strings" +const defaultDotGitPath = ".git" + // countLines returns the number of lines in a string à la git, this is // The newline character is assumed to be '\n'. The empty string // contains 0 lines. If the last line of the string doesn't end with a diff --git a/submodule.go b/submodule.go index ea099f0e9..83c28b7fd 100644 --- a/submodule.go +++ b/submodule.go @@ -1,33 +1,79 @@ package git import ( - "fmt" - + "srcd.works/go-git.v4/config" "srcd.works/go-git.v4/plumbing" ) +// Submodule a submodule allows you to keep another Git repository in a +// subdirectory of your repository. type Submodule struct { - Name string - Branch string - URL string - + m *config.Submodule + w *Worktree + // r is the submodule repository r *Repository } +// Config returns the submodule config +func (s *Submodule) Config() *config.Submodule { + return s.m +} + +// Init initialize the submodule reading the recoreded Entry in the index for +// the given submodule func (s *Submodule) Init() error { - return s.r.clone(&CloneOptions{ - URL: s.URL, - ReferenceName: plumbing.ReferenceName(s.Branch), + e, err := s.w.readIndexEntry(s.m.Path) + if err != nil { + return err + } + + _, err = s.r.CreateRemote(&config.RemoteConfig{ + Name: DefaultRemoteName, + URL: s.m.URL, }) + + if err != nil { + return err + } + + return s.fetchAndCheckout(e.Hash) +} + +// Update the registered submodule to match what the superproject expects +func (s *Submodule) Update() error { + e, err := s.w.readIndexEntry(s.m.Path) + if err != nil { + return err + } + + return s.fetchAndCheckout(e.Hash) +} + +func (s *Submodule) fetchAndCheckout(hash plumbing.Hash) error { + if err := s.r.Fetch(&FetchOptions{}); err != nil && err != NoErrAlreadyUpToDate { + return err + } + + w, err := s.r.Worktree() + if err != nil { + return err + } + + if err := w.Checkout(hash); err != nil { + return err + } + + head := plumbing.NewHashReference(plumbing.HEAD, hash) + return s.r.Storer.SetReference(head) } +// Submodules list of several submodules from the same repository type Submodules []*Submodule +// Init initialize the submodule recorded in the index func (s Submodules) Init() error { for _, sub := range s { - fmt.Println("clone", sub.URL) if err := sub.Init(); err != nil { - fmt.Println(err) return err } } diff --git a/submodule_test.go b/submodule_test.go new file mode 100644 index 000000000..ed4992761 --- /dev/null +++ b/submodule_test.go @@ -0,0 +1,114 @@ +package git + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "github.com/src-d/go-git-fixtures" + + . "gopkg.in/check.v1" + "srcd.works/go-git.v4/plumbing" +) + +type SubmoduleSuite struct { + BaseSuite + Worktree *Worktree + path string +} + +var _ = Suite(&SubmoduleSuite{}) + +func (s *SubmoduleSuite) SetUpTest(c *C) { + path := fixtures.ByTag("submodule").One().Worktree().Base() + + dir, err := ioutil.TempDir("", "submodule") + c.Assert(err, IsNil) + + r, err := PlainClone(dir, false, &CloneOptions{ + URL: fmt.Sprintf("file://%s", filepath.Join(path)), + }) + + c.Assert(err, IsNil) + + s.Repository = r + s.Worktree, err = r.Worktree() + c.Assert(err, IsNil) + + s.path = path +} + +func (s *SubmoduleSuite) TearDownTest(c *C) { + err := os.RemoveAll(s.path) + c.Assert(err, IsNil) +} + +func (s *SubmoduleSuite) TestInit(c *C) { + sm, err := s.Worktree.Submodule("basic") + c.Assert(err, IsNil) + + _, err = sm.r.Reference(plumbing.HEAD, true) + c.Assert(err, Equals, plumbing.ErrReferenceNotFound) + + err = sm.Init() + c.Assert(err, IsNil) + + ref, err := sm.r.Reference(plumbing.HEAD, true) + c.Assert(err, IsNil) + c.Assert(ref.Hash().String(), Equals, "6ecf0ef2c2dffb796033e5a02219af86ec6584e5") + + w, err := sm.r.Worktree() + c.Assert(err, IsNil) + + status, err := w.Status() + c.Assert(err, IsNil) + c.Assert(status.IsClean(), Equals, true) +} + +func (s *SubmoduleSuite) TestUpdate(c *C) { + sm, err := s.Worktree.Submodule("basic") + c.Assert(err, IsNil) + + _, err = sm.r.Reference(plumbing.HEAD, true) + c.Assert(err, Equals, plumbing.ErrReferenceNotFound) + + err = sm.Init() + c.Assert(err, IsNil) + + idx, err := s.Repository.Storer.Index() + c.Assert(err, IsNil) + + for i, e := range idx.Entries { + if e.Name == "basic" { + e.Hash = plumbing.NewHash("b029517f6300c2da0f4b651b8642506cd6aaf45d") + } + + idx.Entries[i] = e + } + + err = s.Repository.Storer.SetIndex(idx) + c.Assert(err, IsNil) + + err = sm.Update() + c.Assert(err, IsNil) + + ref, err := sm.r.Reference(plumbing.HEAD, true) + c.Assert(err, IsNil) + c.Assert(ref.Hash().String(), Equals, "b029517f6300c2da0f4b651b8642506cd6aaf45d") + +} + +func (s *SubmoduleSuite) TestSubmodulesInit(c *C) { + sm, err := s.Worktree.Submodules() + c.Assert(err, IsNil) + + err = sm.Init() + c.Assert(err, IsNil) + + for _, m := range sm { + ref, err := m.r.Reference(plumbing.HEAD, true) + c.Assert(err, IsNil) + c.Assert(ref.Hash(), Not(Equals), plumbing.ZeroHash) + } +} diff --git a/worktree.go b/worktree.go index 473f933da..2a5b58a46 100644 --- a/worktree.go +++ b/worktree.go @@ -16,6 +16,7 @@ import ( ) var ErrWorktreeNotClean = errors.New("worktree is not clean") +var ErrSubmoduleNotFound = errors.New("submodule not found") type Worktree struct { r *Repository @@ -46,7 +47,7 @@ func (w *Worktree) Checkout(commit plumbing.Hash) error { walker := object.NewTreeWalker(t, true) for { - _, entry, err := walker.Next() + name, entry, err := walker.Next() if err == io.EOF { break } @@ -55,7 +56,7 @@ func (w *Worktree) Checkout(commit plumbing.Hash) error { return err } - if err := w.checkoutEntry(&entry, idx); err != nil { + if err := w.checkoutEntry(name, &entry, idx); err != nil { return err } } @@ -63,19 +64,19 @@ func (w *Worktree) Checkout(commit plumbing.Hash) error { return w.r.Storer.SetIndex(idx) } -func (w *Worktree) checkoutEntry(e *object.TreeEntry, idx *index.Index) error { +func (w *Worktree) checkoutEntry(name string, e *object.TreeEntry, idx *index.Index) error { if e.Mode == object.SubmoduleMode { - return w.indexEntry(e, idx) + return w.addIndexFromTreeEntry(name, e, idx) } if e.Mode.IsDir() { return nil } - return w.checkoutFile(e, idx) + return w.checkoutFile(name, e, idx) } -func (w *Worktree) checkoutFile(e *object.TreeEntry, idx *index.Index) error { +func (w *Worktree) checkoutFile(name string, e *object.TreeEntry, idx *index.Index) error { blob, err := object.GetBlob(w.r.Storer, e.Hash) if err != nil { return err @@ -87,7 +88,7 @@ func (w *Worktree) checkoutFile(e *object.TreeEntry, idx *index.Index) error { } defer from.Close() - to, err := w.fs.OpenFile(e.Name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, e.Mode.Perm()) + to, err := w.fs.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, e.Mode.Perm()) if err != nil { return err } @@ -97,30 +98,30 @@ func (w *Worktree) checkoutFile(e *object.TreeEntry, idx *index.Index) error { } defer to.Close() - return w.indexFile(e, idx) + return w.addIndexFromFile(name, e, idx) } var fillSystemInfo func(e *index.Entry, sys interface{}) -func (w *Worktree) indexEntry(f *object.TreeEntry, idx *index.Index) error { +func (w *Worktree) addIndexFromTreeEntry(name string, f *object.TreeEntry, idx *index.Index) error { idx.Entries = append(idx.Entries, index.Entry{ Hash: f.Hash, - Name: f.Name, + Name: name, Mode: object.SubmoduleMode, }) return nil } -func (w *Worktree) indexFile(f *object.TreeEntry, idx *index.Index) error { - fi, err := w.fs.Stat(f.Name) +func (w *Worktree) addIndexFromFile(name string, f *object.TreeEntry, idx *index.Index) error { + fi, err := w.fs.Stat(name) if err != nil { return err } e := index.Entry{ Hash: f.Hash, - Name: f.Name, + Name: name, Mode: w.getMode(fi), ModifiedAt: fi.ModTime(), Size: uint32(fi.Size()), @@ -144,7 +145,6 @@ func (w *Worktree) Status() (Status, error) { files, err := readDirAll(w.fs) if err != nil { - fmt.Println("ch", err) return nil, err } @@ -210,6 +210,22 @@ func (w *Worktree) getMode(fi billy.FileInfo) os.FileMode { const gitmodulesFile = ".gitmodules" +// Submodule returns the submodule with the given name +func (w *Worktree) Submodule(name string) (*Submodule, error) { + m, err := w.readGitmodulesFile() + if err != nil || m == nil { + return nil, err + } + + c, ok := m.Submodules[name] + if !ok { + return nil, ErrSubmoduleNotFound + } + + return w.newSubmodule(c) +} + +// Submodules returns all the available submodules func (w *Worktree) Submodules() (Submodules, error) { l := make(Submodules, 0) m, err := w.readGitmodulesFile() @@ -235,11 +251,15 @@ func (w *Worktree) newSubmodule(m *config.Submodule) (*Submodule, error) { return nil, err } - return &Submodule{ - Name: m.Name, - URL: m.URL, + r, err := Init(s, w.fs.Dir(m.Path)) + if err != nil { + return nil, err + } - r: newRepository(s, w.fs.Dir(m.Path)), + return &Submodule{ + m: m, + w: w, + r: r, }, nil } @@ -263,6 +283,23 @@ func (w *Worktree) readGitmodulesFile() (*config.Modules, error) { } +func (w *Worktree) readIndexEntry(path string) (index.Entry, error) { + var e index.Entry + + idx, err := w.r.Storer.Index() + if err != nil { + return e, err + } + + for _, e := range idx.Entries { + if e.Name == path { + return e, nil + } + } + + return e, fmt.Errorf("unable to find %q entry in the index", path) +} + // Status current status of a Worktree type Status map[string]*FileStatus @@ -377,7 +414,7 @@ func readDirAll(filesystem billy.Filesystem) (map[string]billy.FileInfo, error) } func doReadDirAll(fs billy.Filesystem, path string, files map[string]billy.FileInfo) error { - if path == ".git" { + if path == defaultDotGitPath { return nil } @@ -392,6 +429,10 @@ func doReadDirAll(fs billy.Filesystem, path string, files map[string]billy.FileI for _, info := range l { file := fs.Join(path, info.Name()) + if file == defaultDotGitPath { + continue + } + if !info.IsDir() { files[file] = info continue diff --git a/worktree_test.go b/worktree_test.go index 306b56d04..81d35b198 100644 --- a/worktree_test.go +++ b/worktree_test.go @@ -7,6 +7,7 @@ import ( "srcd.works/go-git.v4/plumbing/format/index" "srcd.works/go-git.v4/plumbing/object" + "github.com/src-d/go-git-fixtures" . "gopkg.in/check.v1" "srcd.works/go-billy.v1/memfs" "srcd.works/go-billy.v1/osfs" @@ -40,7 +41,7 @@ func (s *WorktreeSuite) TestCheckout(c *C) { entries, err := fs.ReadDir("/") c.Assert(err, IsNil) - c.Assert(entries, HasLen, 9) + c.Assert(entries, HasLen, 8) ch, err := fs.Open("CHANGELOG") c.Assert(err, IsNil) @@ -116,7 +117,6 @@ func (s *WorktreeSuite) TestCheckoutIndexOS(c *C) { } func (s *WorktreeSuite) TestStatus(c *C) { - h, err := s.Repository.Head() c.Assert(err, IsNil) @@ -164,3 +164,31 @@ func (s *WorktreeSuite) TestStatusModified(c *C) { c.Assert(err, IsNil) c.Assert(status.IsClean(), Equals, false) } + +func (s *WorktreeSuite) TestSubmodule(c *C) { + path := fixtures.ByTag("submodule").One().Worktree().Base() + r, err := PlainOpen(path) + c.Assert(err, IsNil) + + w, err := r.Worktree() + c.Assert(err, IsNil) + + m, err := w.Submodule("basic") + c.Assert(err, IsNil) + + c.Assert(m.Config().Name, Equals, "basic") +} + +func (s *WorktreeSuite) TestSubmodules(c *C) { + path := fixtures.ByTag("submodule").One().Worktree().Base() + r, err := PlainOpen(path) + c.Assert(err, IsNil) + + w, err := r.Worktree() + c.Assert(err, IsNil) + + l, err := w.Submodules() + c.Assert(err, IsNil) + + c.Assert(l, HasLen, 2) +} From 09110d8e6d1ddb6f6a22867dcedeebd8f2262780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1ximo=20Cuadros?= Date: Tue, 14 Feb 2017 15:56:32 +0100 Subject: [PATCH 09/11] config: added Config.Submodules --- config/config.go | 49 +++++++++++++++++++++++++++++++++++-------- config/config_test.go | 16 ++++++++++++++ config/modules.go | 5 ++--- 3 files changed, 58 insertions(+), 12 deletions(-) diff --git a/config/config.go b/config/config.go index b3a3fcc82..65b51eb00 100644 --- a/config/config.go +++ b/config/config.go @@ -40,8 +40,10 @@ type Config struct { // Worktree is the path to the root of the working tree Worktree string } - // Remote list of repository remotes + // Remotes list of repository remotes Remotes map[string]*RemoteConfig + // Submodules list of repository submodules + Submodules map[string]*Submodule // contains the raw information of a config file, the main goal is preserve // the parsed information from the original format, to avoid missing @@ -52,8 +54,9 @@ type Config struct { // NewConfig returns a new empty Config func NewConfig() *Config { return &Config{ - Remotes: make(map[string]*RemoteConfig, 0), - raw: format.New(), + Remotes: make(map[string]*RemoteConfig, 0), + Submodules: make(map[string]*Submodule, 0), + raw: format.New(), } } @@ -73,12 +76,13 @@ func (c *Config) Validate() error { } const ( - remoteSection = "remote" - coreSection = "core" - fetchKey = "fetch" - urlKey = "url" - bareKey = "bare" - worktreeKey = "worktree" + remoteSection = "remote" + submoduleSection = "submodule" + coreSection = "core" + fetchKey = "fetch" + urlKey = "url" + bareKey = "bare" + worktreeKey = "worktree" ) // Unmarshal parses a git-config file and stores it @@ -92,6 +96,7 @@ func (c *Config) Unmarshal(b []byte) error { } c.unmarshalCore() + c.unmarshalSubmodules() return c.unmarshalRemotes() } @@ -118,10 +123,21 @@ func (c *Config) unmarshalRemotes() error { return nil } +func (c *Config) unmarshalSubmodules() { + s := c.raw.Section(submoduleSection) + for _, sub := range s.Subsections { + m := &Submodule{} + m.unmarshal(sub) + + c.Submodules[m.Name] = m + } +} + // Marshal returns Config encoded as a git-config file func (c *Config) Marshal() ([]byte, error) { c.marshalCore() c.marshalRemotes() + c.marshalSubmodules() buf := bytes.NewBuffer(nil) if err := format.NewEncoder(buf).Encode(c.raw); err != nil { @@ -151,6 +167,21 @@ func (c *Config) marshalRemotes() { } } +func (c *Config) marshalSubmodules() { + s := c.raw.Section(submoduleSection) + s.Subsections = make(format.Subsections, len(c.Submodules)) + + var i int + for _, r := range c.Submodules { + section := r.marshal() + // the submodule section at config is a subset of the .gitmodule file + // we should remove the non-valid options for the config file. + section.RemoveOption(pathKey) + s.Subsections[i] = section + i++ + } +} + // RemoteConfig contains the configuration for a given remote repository type RemoteConfig struct { // Name of the remote diff --git a/config/config_test.go b/config/config_test.go index 313c96ce2..cfab36dc3 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -13,6 +13,10 @@ func (s *ConfigSuite) TestUnmarshall(c *C) { [remote "origin"] url = git@github.com:mcuadros/go-git.git fetch = +refs/heads/*:refs/remotes/origin/* +[submodule "qux"] + path = qux + url = https://github.com/foo/qux.git + branch = bar [branch "master"] remote = origin merge = refs/heads/master @@ -28,6 +32,11 @@ func (s *ConfigSuite) TestUnmarshall(c *C) { c.Assert(cfg.Remotes["origin"].Name, Equals, "origin") c.Assert(cfg.Remotes["origin"].URL, Equals, "git@github.com:mcuadros/go-git.git") c.Assert(cfg.Remotes["origin"].Fetch, DeepEquals, []RefSpec{"+refs/heads/*:refs/remotes/origin/*"}) + c.Assert(cfg.Submodules, HasLen, 1) + c.Assert(cfg.Submodules["qux"].Name, Equals, "qux") + c.Assert(cfg.Submodules["qux"].URL, Equals, "https://github.com/foo/qux.git") + c.Assert(cfg.Submodules["qux"].Branch, Equals, "bar") + } func (s *ConfigSuite) TestMarshall(c *C) { @@ -36,6 +45,8 @@ func (s *ConfigSuite) TestMarshall(c *C) { worktree = bar [remote "origin"] url = git@github.com:mcuadros/go-git.git +[submodule "qux"] + url = https://github.com/foo/qux.git `) cfg := NewConfig() @@ -46,6 +57,11 @@ func (s *ConfigSuite) TestMarshall(c *C) { URL: "git@github.com:mcuadros/go-git.git", } + cfg.Submodules["qux"] = &Submodule{ + Name: "qux", + URL: "https://github.com/foo/qux.git", + } + b, err := cfg.Marshal() c.Assert(err, IsNil) diff --git a/config/modules.go b/config/modules.go index 4d98b16e0..a17cc273f 100644 --- a/config/modules.go +++ b/config/modules.go @@ -30,9 +30,8 @@ func NewModules() *Modules { } const ( - submoduleSection = "submodule" - pathKey = "path" - branchKey = "branch" + pathKey = "path" + branchKey = "branch" ) // Unmarshal parses a git-config file and stores it From d6a6decd1be0515faf36256ce06c58c7d662bbd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1ximo=20Cuadros?= Date: Tue, 14 Feb 2017 19:26:32 +0100 Subject: [PATCH 10/11] submodule update implementation --- _examples/clone/main.go | 7 +-- options.go | 29 ++++++++- repository.go | 23 ++++++-- repository_test.go | 41 +++++++++++++ submodule.go | 128 ++++++++++++++++++++++++++++++++++------ submodule_test.go | 86 +++++++++++++++++++++------ worktree.go | 45 ++++++-------- 7 files changed, 286 insertions(+), 73 deletions(-) diff --git a/_examples/clone/main.go b/_examples/clone/main.go index 18d8a5505..d4d38808a 100644 --- a/_examples/clone/main.go +++ b/_examples/clone/main.go @@ -14,12 +14,11 @@ func main() { directory := os.Args[2] // Clone the given repository to the given directory - Info("git clone %s %s", url, directory) + Info("git clone %s %s --recursive", url, directory) r, err := git.PlainClone(directory, false, &git.CloneOptions{ - URL: url, - RecursiveSubmodules: true, - Depth: 1, + URL: url, + RecurseSubmodules: git.DefaultRecursivity, }) CheckIfError(err) diff --git a/options.go b/options.go index 1f83c363c..db694b812 100644 --- a/options.go +++ b/options.go @@ -9,9 +9,18 @@ import ( "srcd.works/go-git.v4/plumbing/transport" ) +// SubmoduleRescursivity defines how depth will affect any submodule recursive +// operation +type SubmoduleRescursivity int + const ( // DefaultRemoteName name of the default Remote, just like git command DefaultRemoteName = "origin" + + // NoRecursivity disables the recursion for a submodule operation + NoRecursivity SubmoduleRescursivity = 0 + // DefaultRecursivity allow recursion in a submodule operation + DefaultRecursivity SubmoduleRescursivity = 10 ) var ( @@ -32,10 +41,10 @@ type CloneOptions struct { SingleBranch bool // Limit fetching to the specified number of commits Depth int - // RecursiveSubmodules after the clone is created, initialize all submodules + // RecurseSubmodules after the clone is created, initialize all submodules // within, using their default settings. This option is ignored if the // cloned repository does not have a worktree - RecursiveSubmodules bool + RecurseSubmodules SubmoduleRescursivity // Progress is where the human readable information sent by the server is // stored, if nil nothing is stored and the capability (if supported) // no-progress, is sent to the server to avoid send this information @@ -71,6 +80,9 @@ type PullOptions struct { Depth int // Auth credentials, if required, to use with the remote repository Auth transport.AuthMethod + // RecurseSubmodules controls if new commits of all populated submodules + // should be fetched too + RecurseSubmodules SubmoduleRescursivity // Progress is where the human readable information sent by the server is // stored, if nil nothing is stored and the capability (if supported) // no-progress, is sent to the server to avoid send this information @@ -152,3 +164,16 @@ func (o *PushOptions) Validate() error { return nil } + +// SubmoduleUpdateOptions describes how a submodule update should be performed +type SubmoduleUpdateOptions struct { + // Init initializes the submodules recorded in the index + Init bool + // NoFetch tell to the update command to don’t fetch new objects from the + // remote site. + NoFetch bool + // RecurseSubmodules the update is performed not only in the submodules of + // the current repository but also in any nested submodules inside those + // submodules (and so on). Until the SubmoduleRescursivity is reached. + RecurseSubmodules SubmoduleRescursivity +} diff --git a/repository.go b/repository.go index 4c184abaf..9969b8643 100644 --- a/repository.go +++ b/repository.go @@ -350,8 +350,8 @@ func (r *Repository) clone(o *CloneOptions) error { return err } - if o.RecursiveSubmodules && r.wt != nil { - if err := r.initSubmodules(); err != nil { + if o.RecurseSubmodules != NoRecursivity && r.wt != nil { + if err := r.updateSubmodules(o.RecurseSubmodules); err != nil { return err } } @@ -359,7 +359,7 @@ func (r *Repository) clone(o *CloneOptions) error { return r.updateRemoteConfig(remote, o, c, head) } -func (r *Repository) initSubmodules() error { +func (r *Repository) updateSubmodules(recursion SubmoduleRescursivity) error { w, err := r.Worktree() if err != nil { return err @@ -370,7 +370,10 @@ func (r *Repository) initSubmodules() error { return err } - return s.Init() + return s.Update(&SubmoduleUpdateOptions{ + Init: true, + RecurseSubmodules: recursion, + }) } func (r *Repository) cloneRefSpec(o *CloneOptions, @@ -546,7 +549,17 @@ func (r *Repository) Pull(o *PullOptions) error { return NoErrAlreadyUpToDate } - return r.updateWorktree() + if err := r.updateWorktree(); err != nil { + return err + } + + if o.RecurseSubmodules != NoRecursivity && r.wt != nil { + if err := r.updateSubmodules(o.RecurseSubmodules); err != nil { + return err + } + } + + return nil } func (r *Repository) updateWorktree() error { diff --git a/repository_test.go b/repository_test.go index 2c1d4a28b..6a9c14a4b 100644 --- a/repository_test.go +++ b/repository_test.go @@ -293,6 +293,25 @@ func (s *RepositorySuite) TestPlainClone(c *C) { c.Assert(remotes, HasLen, 1) } +func (s *RepositorySuite) TestPlainCloneWithRecurseSubmodules(c *C) { + dir, err := ioutil.TempDir("", "plain-clone-submodule") + c.Assert(err, IsNil) + defer os.RemoveAll(dir) + + path := fixtures.ByTag("submodule").One().Worktree().Base() + + r, err := PlainClone(dir, false, &CloneOptions{ + URL: fmt.Sprintf("file://%s", path), + RecurseSubmodules: DefaultRecursivity, + }) + + c.Assert(err, IsNil) + + cfg, err := r.Config() + c.Assert(cfg.Remotes, HasLen, 1) + c.Assert(cfg.Submodules, HasLen, 2) +} + func (s *RepositorySuite) TestFetch(c *C) { r, _ := Init(memory.NewStorage(), nil) _, err := r.CreateRemote(&config.RemoteConfig{ @@ -562,6 +581,28 @@ func (s *RepositorySuite) TestPullProgress(c *C) { c.Assert(buf.Len(), Not(Equals), 0) } +func (s *RepositorySuite) TestPullProgressWithRecursion(c *C) { + path := fixtures.ByTag("submodule").One().Worktree().Base() + + dir, err := ioutil.TempDir("", "plain-clone-submodule") + c.Assert(err, IsNil) + defer os.RemoveAll(dir) + + r, _ := PlainInit(dir, false) + r.CreateRemote(&config.RemoteConfig{ + Name: DefaultRemoteName, + URL: fmt.Sprintf("file://%s", path), + }) + + err = r.Pull(&PullOptions{ + RecurseSubmodules: DefaultRecursivity, + }) + c.Assert(err, IsNil) + + cfg, err := r.Config() + c.Assert(cfg.Submodules, HasLen, 2) +} + func (s *RepositorySuite) TestPullAdd(c *C) { path := fixtures.Basic().ByTag("worktree").One().Worktree().Base() diff --git a/submodule.go b/submodule.go index 83c28b7fd..b6cc045b7 100644 --- a/submodule.go +++ b/submodule.go @@ -1,60 +1,141 @@ package git import ( + "errors" + "srcd.works/go-git.v4/config" "srcd.works/go-git.v4/plumbing" ) +var ( + ErrSubmoduleAlreadyInitialized = errors.New("submodule already initialized") + ErrSubmoduleNotInitialized = errors.New("submodule not initialized") +) + // Submodule a submodule allows you to keep another Git repository in a // subdirectory of your repository. type Submodule struct { - m *config.Submodule + initialized bool + + c *config.Submodule w *Worktree - // r is the submodule repository - r *Repository } // Config returns the submodule config func (s *Submodule) Config() *config.Submodule { - return s.m + return s.c } // Init initialize the submodule reading the recoreded Entry in the index for // the given submodule func (s *Submodule) Init() error { - e, err := s.w.readIndexEntry(s.m.Path) + cfg, err := s.w.r.Storer.Config() if err != nil { return err } - _, err = s.r.CreateRemote(&config.RemoteConfig{ + _, ok := cfg.Submodules[s.c.Name] + if ok { + return ErrSubmoduleAlreadyInitialized + } + + s.initialized = true + + cfg.Submodules[s.c.Name] = s.c + return s.w.r.Storer.SetConfig(cfg) +} + +// Repository returns the Repository represented by this submodule +func (s *Submodule) Repository() (*Repository, error) { + storer, err := s.w.r.Storer.Module(s.c.Name) + if err != nil { + return nil, err + } + + _, err = storer.Reference(plumbing.HEAD) + if err != nil && err != plumbing.ErrReferenceNotFound { + return nil, err + } + + worktree := s.w.fs.Dir(s.c.Path) + if err == nil { + return Open(storer, worktree) + } + + r, err := Init(storer, worktree) + if err != nil { + return nil, err + } + + _, err = r.CreateRemote(&config.RemoteConfig{ Name: DefaultRemoteName, - URL: s.m.URL, + URL: s.c.URL, }) + return r, err +} + +// Update the registered submodule to match what the superproject expects, the +// submodule should be initilized first calling the Init method or setting in +// the options SubmoduleUpdateOptions.Init equals true +func (s *Submodule) Update(o *SubmoduleUpdateOptions) error { + if !s.initialized && !o.Init { + return ErrSubmoduleNotInitialized + } + + if !s.initialized && o.Init { + if err := s.Init(); err != nil { + return err + } + } + + e, err := s.w.readIndexEntry(s.c.Path) if err != nil { return err } - return s.fetchAndCheckout(e.Hash) + r, err := s.Repository() + if err != nil { + return err + } + + if err := s.fetchAndCheckout(r, o, e.Hash); err != nil { + return err + } + + return s.doRecrusiveUpdate(r, o) } -// Update the registered submodule to match what the superproject expects -func (s *Submodule) Update() error { - e, err := s.w.readIndexEntry(s.m.Path) +func (s *Submodule) doRecrusiveUpdate(r *Repository, o *SubmoduleUpdateOptions) error { + if o.RecurseSubmodules == NoRecursivity { + return nil + } + + w, err := r.Worktree() if err != nil { return err } - return s.fetchAndCheckout(e.Hash) + l, err := w.Submodules() + if err != nil { + return err + } + + new := &SubmoduleUpdateOptions{} + *new = *o + new.RecurseSubmodules-- + return l.Update(new) } -func (s *Submodule) fetchAndCheckout(hash plumbing.Hash) error { - if err := s.r.Fetch(&FetchOptions{}); err != nil && err != NoErrAlreadyUpToDate { - return err +func (s *Submodule) fetchAndCheckout(r *Repository, o *SubmoduleUpdateOptions, hash plumbing.Hash) error { + if !o.NoFetch { + err := r.Fetch(&FetchOptions{}) + if err != nil && err != NoErrAlreadyUpToDate { + return err + } } - w, err := s.r.Worktree() + w, err := r.Worktree() if err != nil { return err } @@ -64,13 +145,13 @@ func (s *Submodule) fetchAndCheckout(hash plumbing.Hash) error { } head := plumbing.NewHashReference(plumbing.HEAD, hash) - return s.r.Storer.SetReference(head) + return r.Storer.SetReference(head) } // Submodules list of several submodules from the same repository type Submodules []*Submodule -// Init initialize the submodule recorded in the index +// Init initializes the submodules in this list func (s Submodules) Init() error { for _, sub := range s { if err := sub.Init(); err != nil { @@ -80,3 +161,14 @@ func (s Submodules) Init() error { return nil } + +// Update updates all the submodules in this list +func (s Submodules) Update(o *SubmoduleUpdateOptions) error { + for _, sub := range s { + if err := sub.Update(o); err != nil { + return err + } + } + + return nil +} diff --git a/submodule_test.go b/submodule_test.go index ed4992761..a933965a3 100644 --- a/submodule_test.go +++ b/submodule_test.go @@ -36,7 +36,7 @@ func (s *SubmoduleSuite) SetUpTest(c *C) { s.Worktree, err = r.Worktree() c.Assert(err, IsNil) - s.path = path + s.path = dir } func (s *SubmoduleSuite) TearDownTest(c *C) { @@ -48,32 +48,78 @@ func (s *SubmoduleSuite) TestInit(c *C) { sm, err := s.Worktree.Submodule("basic") c.Assert(err, IsNil) - _, err = sm.r.Reference(plumbing.HEAD, true) - c.Assert(err, Equals, plumbing.ErrReferenceNotFound) - err = sm.Init() c.Assert(err, IsNil) - ref, err := sm.r.Reference(plumbing.HEAD, true) + cfg, err := s.Repository.Config() c.Assert(err, IsNil) - c.Assert(ref.Hash().String(), Equals, "6ecf0ef2c2dffb796033e5a02219af86ec6584e5") - w, err := sm.r.Worktree() + c.Assert(cfg.Submodules, HasLen, 1) + c.Assert(cfg.Submodules["basic"], NotNil) +} + +func (s *SubmoduleSuite) TestUpdate(c *C) { + sm, err := s.Worktree.Submodule("basic") c.Assert(err, IsNil) - status, err := w.Status() + err = sm.Update(&SubmoduleUpdateOptions{ + Init: true, + }) + c.Assert(err, IsNil) - c.Assert(status.IsClean(), Equals, true) + + r, err := sm.Repository() + c.Assert(err, IsNil) + + ref, err := r.Reference(plumbing.HEAD, true) + c.Assert(err, IsNil) + c.Assert(ref.Hash().String(), Equals, "6ecf0ef2c2dffb796033e5a02219af86ec6584e5") + } -func (s *SubmoduleSuite) TestUpdate(c *C) { +func (s *SubmoduleSuite) TestUpdateWithoutInit(c *C) { sm, err := s.Worktree.Submodule("basic") c.Assert(err, IsNil) - _, err = sm.r.Reference(plumbing.HEAD, true) - c.Assert(err, Equals, plumbing.ErrReferenceNotFound) + err = sm.Update(&SubmoduleUpdateOptions{}) + c.Assert(err, Equals, ErrSubmoduleNotInitialized) +} - err = sm.Init() +func (s *SubmoduleSuite) TestUpdateWithNotFetch(c *C) { + sm, err := s.Worktree.Submodule("basic") + c.Assert(err, IsNil) + + err = sm.Update(&SubmoduleUpdateOptions{ + Init: true, + NoFetch: true, + }) + + // Since we are not fetching, the object is not there + c.Assert(err, Equals, plumbing.ErrObjectNotFound) +} + +func (s *SubmoduleSuite) TestUpdateWithRecursion(c *C) { + sm, err := s.Worktree.Submodule("itself") + c.Assert(err, IsNil) + + err = sm.Update(&SubmoduleUpdateOptions{ + Init: true, + RecurseSubmodules: 2, + }) + + c.Assert(err, IsNil) + + _, err = s.Worktree.fs.Stat("itself/basic/LICENSE") + c.Assert(err, IsNil) +} + +func (s *SubmoduleSuite) TestUpdateWithInitAndUpdate(c *C) { + sm, err := s.Worktree.Submodule("basic") + c.Assert(err, IsNil) + + err = sm.Update(&SubmoduleUpdateOptions{ + Init: true, + }) c.Assert(err, IsNil) idx, err := s.Repository.Storer.Index() @@ -90,10 +136,13 @@ func (s *SubmoduleSuite) TestUpdate(c *C) { err = s.Repository.Storer.SetIndex(idx) c.Assert(err, IsNil) - err = sm.Update() + err = sm.Update(&SubmoduleUpdateOptions{}) + c.Assert(err, IsNil) + + r, err := sm.Repository() c.Assert(err, IsNil) - ref, err := sm.r.Reference(plumbing.HEAD, true) + ref, err := r.Reference(plumbing.HEAD, true) c.Assert(err, IsNil) c.Assert(ref.Hash().String(), Equals, "b029517f6300c2da0f4b651b8642506cd6aaf45d") @@ -106,9 +155,10 @@ func (s *SubmoduleSuite) TestSubmodulesInit(c *C) { err = sm.Init() c.Assert(err, IsNil) + sm, err = s.Worktree.Submodules() + c.Assert(err, IsNil) + for _, m := range sm { - ref, err := m.r.Reference(plumbing.HEAD, true) - c.Assert(err, IsNil) - c.Assert(ref.Hash(), Not(Equals), plumbing.ZeroHash) + c.Assert(m.initialized, Equals, true) } } diff --git a/worktree.go b/worktree.go index 2a5b58a46..2514a0c9f 100644 --- a/worktree.go +++ b/worktree.go @@ -212,17 +212,18 @@ const gitmodulesFile = ".gitmodules" // Submodule returns the submodule with the given name func (w *Worktree) Submodule(name string) (*Submodule, error) { - m, err := w.readGitmodulesFile() - if err != nil || m == nil { + l, err := w.Submodules() + if err != nil { return nil, err } - c, ok := m.Submodules[name] - if !ok { - return nil, ErrSubmoduleNotFound + for _, m := range l { + if m.Config().Name == name { + return m, nil + } } - return w.newSubmodule(c) + return nil, ErrSubmoduleNotFound } // Submodules returns all the available submodules @@ -233,34 +234,26 @@ func (w *Worktree) Submodules() (Submodules, error) { return l, err } - for _, c := range m.Submodules { - s, err := w.newSubmodule(c) - if err != nil { - return nil, err - } - - l = append(l, s) + c, err := w.r.Config() + for _, s := range m.Submodules { + l = append(l, w.newSubmodule(s, c.Submodules[s.Name])) } return l, nil } -func (w *Worktree) newSubmodule(m *config.Submodule) (*Submodule, error) { - s, err := w.r.Storer.Module(m.Name) - if err != nil { - return nil, err - } +func (w *Worktree) newSubmodule(fromModules, fromConfig *config.Submodule) *Submodule { + m := &Submodule{w: w} + m.initialized = fromConfig != nil - r, err := Init(s, w.fs.Dir(m.Path)) - if err != nil { - return nil, err + if !m.initialized { + m.c = fromModules + return m } - return &Submodule{ - m: m, - w: w, - r: r, - }, nil + m.c = fromConfig + m.c.Path = fromModules.Path + return m } func (w *Worktree) readGitmodulesFile() (*config.Modules, error) { From ed288b30de1ac3dcb3ce675c4b9af89eb4e6fcba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1ximo=20Cuadros?= Date: Tue, 21 Feb 2017 16:03:39 +0100 Subject: [PATCH 11/11] documentation and API improvements --- _examples/clone/main.go | 2 +- config/config.go | 23 +- config/modules.go | 14 +- config/refspec.go | 12 +- doc.go | 2 +- options.go | 60 +- plumbing/difftree/difftree.go | 246 +------- plumbing/difftree/difftree_test.go | 523 ++++++++---------- plumbing/format/index/encoder.go | 10 +- plumbing/object/file_test.go | 4 +- plumbing/object/tree_test.go | 5 +- plumbing/storer/reference.go | 1 + plumbing/transport/file/common_test.go | 17 +- plumbing/transport/file/server_test.go | 49 +- repository.go | 6 +- repository_test.go | 5 +- storage/filesystem/internal/dotgit/dotgit.go | 92 ++- .../filesystem/internal/dotgit/dotgit_test.go | 99 ++++ storage/filesystem/reference.go | 4 + storage/memory/storage.go | 14 +- storage/storer.go | 3 + storage/test/storage_suite.go | 32 +- submodule.go | 2 +- utils/merkletrie/doc.go | 22 +- utils/merkletrie/internal/frame/frame_test.go | 11 +- utils/merkletrie/iter_test.go | 14 +- worktree.go | 1 - 27 files changed, 565 insertions(+), 708 deletions(-) diff --git a/_examples/clone/main.go b/_examples/clone/main.go index d4d38808a..bcdb6a9f4 100644 --- a/_examples/clone/main.go +++ b/_examples/clone/main.go @@ -18,7 +18,7 @@ func main() { r, err := git.PlainClone(directory, false, &git.CloneOptions{ URL: url, - RecurseSubmodules: git.DefaultRecursivity, + RecurseSubmodules: git.DefaultSubmoduleRecursionDepth, }) CheckIfError(err) diff --git a/config/config.go b/config/config.go index 65b51eb00..259ebf967 100644 --- a/config/config.go +++ b/config/config.go @@ -32,17 +32,18 @@ var ( // Config contains the repository configuration // ftp://www.kernel.org/pub/software/scm/git/docs/git-config.html#FILES type Config struct { - // Core variables Core struct { // IsBare if true this repository is assumed to be bare and has no - // working directory associated with it + // working directory associated with it. IsBare bool - // Worktree is the path to the root of the working tree + // Worktree is the path to the root of the working tree. Worktree string } - // Remotes list of repository remotes + // Remotes list of repository remotes, the key of the map is the name + // of the remote, should equal to RemoteConfig.Name. Remotes map[string]*RemoteConfig - // Submodules list of repository submodules + // Submodules list of repository submodules, the key of the map is the name + // of the submodule, should equal to Submodule.Name. Submodules map[string]*Submodule // contains the raw information of a config file, the main goal is preserve @@ -51,7 +52,7 @@ type Config struct { raw *format.Config } -// NewConfig returns a new empty Config +// NewConfig returns a new empty Config. func NewConfig() *Config { return &Config{ Remotes: make(map[string]*RemoteConfig, 0), @@ -60,7 +61,7 @@ func NewConfig() *Config { } } -// Validate validates the fields and sets the default values +// Validate validates the fields and sets the default values. func (c *Config) Validate() error { for name, r := range c.Remotes { if r.Name != name { @@ -85,7 +86,7 @@ const ( worktreeKey = "worktree" ) -// Unmarshal parses a git-config file and stores it +// Unmarshal parses a git-config file and stores it. func (c *Config) Unmarshal(b []byte) error { r := bytes.NewBuffer(b) d := format.NewDecoder(r) @@ -133,7 +134,7 @@ func (c *Config) unmarshalSubmodules() { } } -// Marshal returns Config encoded as a git-config file +// Marshal returns Config encoded as a git-config file. func (c *Config) Marshal() ([]byte, error) { c.marshalCore() c.marshalRemotes() @@ -182,7 +183,7 @@ func (c *Config) marshalSubmodules() { } } -// RemoteConfig contains the configuration for a given remote repository +// RemoteConfig contains the configuration for a given remote repository. type RemoteConfig struct { // Name of the remote Name string @@ -196,7 +197,7 @@ type RemoteConfig struct { raw *format.Subsection } -// Validate validates the fields and sets the default values +// Validate validates the fields and sets the default values. func (c *RemoteConfig) Validate() error { if c.Name == "" { return ErrRemoteConfigEmptyName diff --git a/config/modules.go b/config/modules.go index a17cc273f..3d01117c7 100644 --- a/config/modules.go +++ b/config/modules.go @@ -15,7 +15,7 @@ var ( // Modules defines the submodules properties, represents a .gitmodules file // https://www.kernel.org/pub/software/scm/git/docs/gitmodules.html type Modules struct { - // Submodules is a map of submodules being the key the name of the submodule + // Submodules is a map of submodules being the key the name of the submodule. Submodules map[string]*Submodule raw *format.Config @@ -34,7 +34,7 @@ const ( branchKey = "branch" ) -// Unmarshal parses a git-config file and stores it +// Unmarshal parses a git-config file and stores it. func (m *Modules) Unmarshal(b []byte) error { r := bytes.NewBuffer(b) d := format.NewDecoder(r) @@ -55,7 +55,7 @@ func (m *Modules) Unmarshal(b []byte) error { return nil } -// Marshal returns Modules encoded as a git-config file +// Marshal returns Modules encoded as a git-config file. func (m *Modules) Marshal() ([]byte, error) { s := m.raw.Section(submoduleSection) s.Subsections = make(format.Subsections, len(m.Submodules)) @@ -74,12 +74,12 @@ func (m *Modules) Marshal() ([]byte, error) { return buf.Bytes(), nil } -// Submodule defines a submodule +// Submodule defines a submodule. type Submodule struct { // Name module name Name string // Path defines the path, relative to the top-level directory of the Git - // working tree, + // working tree. Path string // URL defines a URL from which the submodule repository can be cloned. URL string @@ -88,11 +88,11 @@ type Submodule struct { Branch string // raw representation of the subsection, filled by marshal or unmarshal are - // called + // called. raw *format.Subsection } -// Validate validates the fields and sets the default values +// Validate validates the fields and sets the default values. func (m *Submodule) Validate() error { if m.Path == "" { return ErrModuleEmptyPath diff --git a/config/refspec.go b/config/refspec.go index dd68edcb6..9441df801 100644 --- a/config/refspec.go +++ b/config/refspec.go @@ -49,7 +49,7 @@ func (s RefSpec) Validate() error { return ErrRefSpecMalformedWildcard } -// IsForceUpdate returns if update is allowed in non fast-forward merges +// IsForceUpdate returns if update is allowed in non fast-forward merges. func (s RefSpec) IsForceUpdate() bool { if s[0] == refSpecForce[0] { return true @@ -67,7 +67,7 @@ func (s RefSpec) IsDelete() bool { return false } -// Src return the src side +// Src return the src side. func (s RefSpec) Src() string { spec := string(s) start := strings.Index(spec, refSpecForce) + 1 @@ -76,7 +76,7 @@ func (s RefSpec) Src() string { return spec[start:end] } -// Match match the given plumbing.ReferenceName against the source +// Match match the given plumbing.ReferenceName against the source. func (s RefSpec) Match(n plumbing.ReferenceName) bool { if !s.IsWildcard() { return s.matchExact(n) @@ -85,7 +85,7 @@ func (s RefSpec) Match(n plumbing.ReferenceName) bool { return s.matchGlob(n) } -// IsWildcard returns true if the RefSpec contains a wildcard +// IsWildcard returns true if the RefSpec contains a wildcard. func (s RefSpec) IsWildcard() bool { return strings.Index(string(s), refSpecWildcard) != -1 } @@ -110,7 +110,7 @@ func (s RefSpec) matchGlob(n plumbing.ReferenceName) bool { strings.HasSuffix(name, suffix) } -// Dst returns the destination for the given remote reference +// Dst returns the destination for the given remote reference. func (s RefSpec) Dst(n plumbing.ReferenceName) plumbing.ReferenceName { spec := string(s) start := strings.Index(spec, refSpecSeparator) + 1 @@ -133,7 +133,7 @@ func (s RefSpec) String() string { return string(s) } -// MatchAny returns true if any of the RefSpec match with the given ReferenceName +// MatchAny returns true if any of the RefSpec match with the given ReferenceName. func MatchAny(l []RefSpec, n plumbing.ReferenceName) bool { for _, r := range l { if r.Match(n) { diff --git a/doc.go b/doc.go index 7f57cbdf5..3d817fe9c 100644 --- a/doc.go +++ b/doc.go @@ -7,4 +7,4 @@ // It is highly extensible, we have been following the open/close principle in // its design to facilitate extensions, mainly focusing the efforts on the // persistence of the objects. -package git // import "srcd.works/go-git.v4" +package git diff --git a/options.go b/options.go index db694b812..d018356d6 100644 --- a/options.go +++ b/options.go @@ -10,48 +10,48 @@ import ( ) // SubmoduleRescursivity defines how depth will affect any submodule recursive -// operation -type SubmoduleRescursivity int +// operation. +type SubmoduleRescursivity uint const ( - // DefaultRemoteName name of the default Remote, just like git command + // DefaultRemoteName name of the default Remote, just like git command. DefaultRemoteName = "origin" - // NoRecursivity disables the recursion for a submodule operation - NoRecursivity SubmoduleRescursivity = 0 - // DefaultRecursivity allow recursion in a submodule operation - DefaultRecursivity SubmoduleRescursivity = 10 + // NoRecurseSubmodules disables the recursion for a submodule operation. + NoRecurseSubmodules SubmoduleRescursivity = 0 + // DefaultSubmoduleRecursionDepth allow recursion in a submodule operation. + DefaultSubmoduleRecursionDepth SubmoduleRescursivity = 10 ) var ( ErrMissingURL = errors.New("URL field is required") ) -// CloneOptions describes how a clone should be performed +// CloneOptions describes how a clone should be performed. type CloneOptions struct { - // The (possibly remote) repository URL to clone from + // The (possibly remote) repository URL to clone from. URL string - // Auth credentials, if required, to use with the remote repository + // Auth credentials, if required, to use with the remote repository. Auth transport.AuthMethod - // Name of the remote to be added, by default `origin` + // Name of the remote to be added, by default `origin`. RemoteName string - // Remote branch to clone + // Remote branch to clone. ReferenceName plumbing.ReferenceName - // Fetch only ReferenceName if true + // Fetch only ReferenceName if true. SingleBranch bool - // Limit fetching to the specified number of commits + // Limit fetching to the specified number of commits. Depth int // RecurseSubmodules after the clone is created, initialize all submodules // within, using their default settings. This option is ignored if the - // cloned repository does not have a worktree + // cloned repository does not have a worktree. RecurseSubmodules SubmoduleRescursivity // Progress is where the human readable information sent by the server is // stored, if nil nothing is stored and the capability (if supported) - // no-progress, is sent to the server to avoid send this information + // no-progress, is sent to the server to avoid send this information. Progress sideband.Progress } -// Validate validates the fields and sets the default values +// Validate validates the fields and sets the default values. func (o *CloneOptions) Validate() error { if o.URL == "" { return ErrMissingURL @@ -68,7 +68,7 @@ func (o *CloneOptions) Validate() error { return nil } -// PullOptions describes how a pull should be performed +// PullOptions describes how a pull should be performed. type PullOptions struct { // Name of the remote to be pulled. If empty, uses the default. RemoteName string @@ -78,14 +78,14 @@ type PullOptions struct { SingleBranch bool // Limit fetching to the specified number of commits. Depth int - // Auth credentials, if required, to use with the remote repository + // Auth credentials, if required, to use with the remote repository. Auth transport.AuthMethod // RecurseSubmodules controls if new commits of all populated submodules - // should be fetched too + // should be fetched too. RecurseSubmodules SubmoduleRescursivity // Progress is where the human readable information sent by the server is // stored, if nil nothing is stored and the capability (if supported) - // no-progress, is sent to the server to avoid send this information + // no-progress, is sent to the server to avoid send this information. Progress sideband.Progress } @@ -110,15 +110,15 @@ type FetchOptions struct { // Depth limit fetching to the specified number of commits from the tip of // each remote branch history. Depth int - // Auth credentials, if required, to use with the remote repository + // Auth credentials, if required, to use with the remote repository. Auth transport.AuthMethod // Progress is where the human readable information sent by the server is // stored, if nil nothing is stored and the capability (if supported) - // no-progress, is sent to the server to avoid send this information + // no-progress, is sent to the server to avoid send this information. Progress sideband.Progress } -// Validate validates the fields and sets the default values +// Validate validates the fields and sets the default values. func (o *FetchOptions) Validate() error { if o.RemoteName == "" { o.RemoteName = DefaultRemoteName @@ -133,18 +133,18 @@ func (o *FetchOptions) Validate() error { return nil } -// PushOptions describes how a push should be performed +// PushOptions describes how a push should be performed. type PushOptions struct { // RemoteName is the name of the remote to be pushed to. RemoteName string // RefSpecs specify what destination ref to update with what source // object. A refspec with empty src can be used to delete a reference. RefSpecs []config.RefSpec - // Auth credentials, if required, to use with the remote repository + // Auth credentials, if required, to use with the remote repository. Auth transport.AuthMethod } -// Validate validates the fields and sets the default values +// Validate validates the fields and sets the default values. func (o *PushOptions) Validate() error { if o.RemoteName == "" { o.RemoteName = DefaultRemoteName @@ -165,11 +165,11 @@ func (o *PushOptions) Validate() error { return nil } -// SubmoduleUpdateOptions describes how a submodule update should be performed +// SubmoduleUpdateOptions describes how a submodule update should be performed. type SubmoduleUpdateOptions struct { - // Init initializes the submodules recorded in the index + // Init, if true initializes the submodules recorded in the index. Init bool - // NoFetch tell to the update command to don’t fetch new objects from the + // NoFetch tell to the update command to not fetch new objects from the // remote site. NoFetch bool // RecurseSubmodules the update is performed not only in the submodules of diff --git a/plumbing/difftree/difftree.go b/plumbing/difftree/difftree.go index 869f496a3..76c5f2797 100644 --- a/plumbing/difftree/difftree.go +++ b/plumbing/difftree/difftree.go @@ -2,252 +2,24 @@ package difftree import ( "bytes" - "fmt" - "io" - "sort" - "strings" - "srcd.works/go-git.v4/plumbing" "srcd.works/go-git.v4/plumbing/object" + "srcd.works/go-git.v4/utils/merkletrie" + "srcd.works/go-git.v4/utils/merkletrie/noder" ) -type Action int - -func (a Action) String() string { - switch a { - case Insert: - return "Insert" - case Delete: - return "Delete" - case Modify: - return "Modify" - default: - panic(fmt.Sprintf("unsupported action: %d", a)) - } -} - -const ( - Insert Action = iota - Delete - Modify -) - -type Change struct { - Action - From ChangeEntry - To ChangeEntry -} - -type ChangeEntry struct { - Name string - Tree *object.Tree - TreeEntry object.TreeEntry -} - -func (c *Change) Files() (from, to *object.File, err error) { - if c.Action == Insert || c.Action == Modify { - to, err = c.To.Tree.TreeEntryFile(&c.To.TreeEntry) - if err != nil { - return - } - - } - - if c.Action == Delete || c.Action == Modify { - from, err = c.From.Tree.TreeEntryFile(&c.From.TreeEntry) - if err != nil { - return - } - } - - return -} - -func (c *Change) String() string { - return fmt.Sprintf("", c.Action, c.name()) -} - -func (c *Change) name() string { - if c.From.Name != "" { - return c.From.Name - } - - return c.To.Name -} - -type Changes []*Change - -func newEmpty() Changes { - return make([]*Change, 0, 0) -} - func DiffTree(a, b *object.Tree) ([]*Change, error) { - if a == b { - return newEmpty(), nil - } - - if a == nil || b == nil { - return newWithEmpty(a, b) - } - - return newDiffTree(a, b) -} - -func (c Changes) Len() int { - return len(c) -} - -func (c Changes) Swap(i, j int) { - c[i], c[j] = c[j], c[i] -} - -func (c Changes) Less(i, j int) bool { - return strings.Compare(c[i].name(), c[j].name()) < 0 -} - -func (c Changes) String() string { - var buffer bytes.Buffer - buffer.WriteString("[") - comma := "" - for _, v := range c { - buffer.WriteString(comma) - buffer.WriteString(v.String()) - comma = ", " - } - buffer.WriteString("]") - - return buffer.String() -} - -func newWithEmpty(a, b *object.Tree) (Changes, error) { - changes := newEmpty() - - var action Action - var tree *object.Tree - if a == nil { - action = Insert - tree = b - } else { - action = Delete - tree = a - } - - w := object.NewTreeWalker(tree, true) - defer w.Close() - - for { - path, entry, err := w.Next() - if err == io.EOF { - break - } else if err != nil { - return nil, fmt.Errorf("cannot get next file: %s", err) - } - - if entry.Mode.IsDir() { - continue - } - - c := &Change{Action: action} - - if action == Insert { - c.To.Name = path - c.To.TreeEntry = entry - c.To.Tree = tree - } else { - c.From.Name = path - c.From.TreeEntry = entry - c.From.Tree = tree - } - - changes = append(changes, c) - } - - return changes, nil -} - -// FIXME: this is very inefficient, but correct. -// The proper way to do this is to implement a diff-tree algorithm, -// while taking advantage of the tree hashes to avoid traversing -// subtrees when the hash is equal in both inputs. -func newDiffTree(a, b *object.Tree) ([]*Change, error) { - var result []*Change - - aChanges, err := newWithEmpty(a, nil) - if err != nil { - return nil, fmt.Errorf("cannot create nil-diff of source tree: %s", err) - } - sort.Sort(aChanges) + from := newTreeNoder(a) + to := newTreeNoder(b) - bChanges, err := newWithEmpty(nil, b) - if err != nil { - return nil, fmt.Errorf("cannot create nil-diff of destination tree: %s", err) - } - sort.Sort(bChanges) - - for len(aChanges) > 0 && len(bChanges) > 0 { - switch comp := strings.Compare(aChanges[0].name(), bChanges[0].name()); { - case comp == 0: // append as "Modify" or ignore if not changed - modified, err := hasChange(a, b, aChanges[0].name()) - if err != nil { - return nil, err - } - - if modified { - c := mergeInsertAndDeleteIntoModify(aChanges[0], bChanges[0]) - result = append(result, c) - } - - aChanges = aChanges[1:] - bChanges = bChanges[1:] - case comp < 0: // delete first a change - result = append(result, aChanges[0]) - aChanges = aChanges[1:] - case comp > 0: // insert first b change - result = append(result, bChanges[0]) - bChanges = bChanges[1:] - } + hashEqual := func(a, b noder.Hasher) bool { + return bytes.Equal(a.Hash(), b.Hash()) } - // append all remaining changes in aChanges, if any, as deletes - // append all remaining changes in bChanges, if any, as inserts - result = append(result, aChanges...) - result = append(result, bChanges...) - - return result, nil -} - -func mergeInsertAndDeleteIntoModify(a, b *Change) *Change { - c := &Change{Action: Modify} - c.From.Name = a.From.Name - c.From.Tree = a.From.Tree - c.From.TreeEntry = a.From.TreeEntry - c.To.Name = b.To.Name - c.To.Tree = b.To.Tree - c.To.TreeEntry = b.To.TreeEntry - - return c -} - -func hasChange(a, b *object.Tree, path string) (bool, error) { - ha, err := hash(a, path) - if err != nil { - return false, err - } - - hb, err := hash(b, path) - if err != nil { - return false, err - } - - return ha != hb, nil -} - -func hash(tree *object.Tree, path string) (plumbing.Hash, error) { - file, err := tree.File(path) + merkletrieChanges, err := merkletrie.DiffTree(from, to, hashEqual) if err != nil { - var empty plumbing.Hash - return empty, fmt.Errorf("cannot find file %s in tree: %s", path, err) + return nil, err } - return file.Hash, nil + return newChanges(merkletrieChanges) } diff --git a/plumbing/difftree/difftree_test.go b/plumbing/difftree/difftree_test.go index 7679d0f90..e2519b343 100644 --- a/plumbing/difftree/difftree_test.go +++ b/plumbing/difftree/difftree_test.go @@ -4,14 +4,15 @@ import ( "sort" "testing" - "github.com/src-d/go-git-fixtures" "srcd.works/go-git.v4/plumbing" "srcd.works/go-git.v4/plumbing/format/packfile" "srcd.works/go-git.v4/plumbing/object" "srcd.works/go-git.v4/plumbing/storer" "srcd.works/go-git.v4/storage/filesystem" "srcd.works/go-git.v4/storage/memory" + "srcd.works/go-git.v4/utils/merkletrie" + "github.com/src-d/go-git-fixtures" . "gopkg.in/check.v1" ) @@ -33,12 +34,6 @@ func (s *DiffTreeSuite) SetUpSuite(c *C) { s.cache = make(map[string]storer.EncodedObjectStorer) } -func (s *DiffTreeSuite) tree(c *C, h plumbing.Hash) *object.Tree { - t, err := object.GetTree(s.Storer, h) - c.Assert(err, IsNil) - return t -} - func (s *DiffTreeSuite) commitFromStorer(c *C, sto storer.EncodedObjectStorer, h plumbing.Hash) *object.Commit { @@ -76,108 +71,48 @@ func (s *DiffTreeSuite) storageFromPackfile(f *fixtures.Fixture) storer.EncodedO var _ = Suite(&DiffTreeSuite{}) -func (s *DiffTreeSuite) TestActionString(c *C) { - expected := "Insert" - action := Insert - obtained := action.String() - c.Assert(obtained, Equals, expected) - - expected = "Delete" - action = Delete - obtained = action.String() - c.Assert(obtained, Equals, expected) - - expected = "Modify" - action = Modify - obtained = action.String() - c.Assert(obtained, Equals, expected) - - action = 37 - c.Assert(func() { _ = action.String() }, - PanicMatches, "unsupported action: 37") -} - -func (s *DiffTreeSuite) TestChangeFilesInsert(c *C) { - tree := s.tree(c, plumbing.NewHash("a8d315b2b1c615d43042c3a62402b8a54288cf5c")) - - change := &Change{Action: Insert} - change.To.Name = "json/long.json" - change.To.Tree = tree - change.To.TreeEntry.Hash = plumbing.NewHash("49c6bb89b17060d7b4deacb7b338fcc6ea2352a9") - - from, to, err := change.Files() - c.Assert(err, IsNil) - c.Assert(from, IsNil) - c.Assert(to.ID(), Equals, change.To.TreeEntry.Hash) -} - -func (s *DiffTreeSuite) TestChangeFilesDelete(c *C) { - tree := s.tree(c, plumbing.NewHash("a8d315b2b1c615d43042c3a62402b8a54288cf5c")) - - change := &Change{Action: Delete} - change.From.Name = "json/long.json" - change.From.Tree = tree - change.From.TreeEntry.Hash = plumbing.NewHash("49c6bb89b17060d7b4deacb7b338fcc6ea2352a9") - - from, to, err := change.Files() - c.Assert(err, IsNil) - c.Assert(to, IsNil) - c.Assert(from.ID(), Equals, change.From.TreeEntry.Hash) -} - -func (s *DiffTreeSuite) TestChangeFilesModify(c *C) { - tree := s.tree(c, plumbing.NewHash("a8d315b2b1c615d43042c3a62402b8a54288cf5c")) - - change := &Change{Action: Modify} - change.To.Name = "json/long.json" - change.To.Tree = tree - change.To.TreeEntry.Hash = plumbing.NewHash("49c6bb89b17060d7b4deacb7b338fcc6ea2352a9") - change.From.Name = "json/long.json" - change.From.Tree = tree - change.From.TreeEntry.Hash = plumbing.NewHash("9a48f23120e880dfbe41f7c9b7b708e9ee62a492") - - from, to, err := change.Files() - c.Assert(err, IsNil) - c.Assert(to.ID(), Equals, change.To.TreeEntry.Hash) - c.Assert(from.ID(), Equals, change.From.TreeEntry.Hash) +type expectChange struct { + Action merkletrie.Action + Name string } -func (s *DiffTreeSuite) TestChangeString(c *C) { - expected := "" - change := &Change{Action: Insert} - change.From.Name = "foo" - - obtained := change.String() - c.Assert(obtained, Equals, expected) +func assertChanges(a Changes, c *C) { + for _, changes := range a { + action, err := changes.Action() + c.Assert(err, IsNil) + switch action { + case merkletrie.Insert: + c.Assert(changes.From.Tree, IsNil) + c.Assert(changes.To.Tree, NotNil) + case merkletrie.Delete: + c.Assert(changes.From.Tree, NotNil) + c.Assert(changes.To.Tree, IsNil) + case merkletrie.Modify: + c.Assert(changes.From.Tree, NotNil) + c.Assert(changes.To.Tree, NotNil) + default: + c.Fatalf("unknown action: %d", action) + } + } } -func (s *DiffTreeSuite) TestChangesString(c *C) { - expected := "[]" - changes := newEmpty() - obtained := changes.String() - c.Assert(obtained, Equals, expected) - - expected = "[]" - changes = make([]*Change, 1) - changes[0] = &Change{Action: Modify} - changes[0].From.Name = "bla" +func equalChanges(a Changes, b []expectChange, c *C) bool { + if len(a) != len(b) { + return false + } - obtained = changes.String() - c.Assert(obtained, Equals, expected) + sort.Sort(a) - expected = "[, ]" - changes = make([]*Change, 2) - changes[0] = &Change{Action: Modify} - changes[0].From.Name = "bla" - changes[1] = &Change{Action: Insert} - changes[1].From.Name = "foo/bar" - obtained = changes.String() - c.Assert(obtained, Equals, expected) -} + for i, va := range a { + vb := b[i] + action, err := va.Action() + c.Assert(err, IsNil) + if action != vb.Action || va.name() != vb.Name { + return false + } + } -type expectChange struct { - Action Action - Name string + return true } func (s *DiffTreeSuite) TestDiffTree(c *C) { @@ -186,186 +121,209 @@ func (s *DiffTreeSuite) TestDiffTree(c *C) { commit1 string // the commit of the first tree commit2 string // the commit of the second tree expected []expectChange // the expected list of []changeExpect - }{{ - "https://github.com/dezfowler/LiteMock.git", - "", - "", - []expectChange{}, - }, { - "https://github.com/dezfowler/LiteMock.git", - "b7965eaa2c4f245d07191fe0bcfe86da032d672a", - "b7965eaa2c4f245d07191fe0bcfe86da032d672a", - []expectChange{}, - }, { - "https://github.com/dezfowler/LiteMock.git", - "", - "b7965eaa2c4f245d07191fe0bcfe86da032d672a", - []expectChange{ - {Action: Insert, Name: "README"}, + }{ + { + "https://github.com/dezfowler/LiteMock.git", + "", + "", + []expectChange{}, + }, + { + "https://github.com/dezfowler/LiteMock.git", + "b7965eaa2c4f245d07191fe0bcfe86da032d672a", + "b7965eaa2c4f245d07191fe0bcfe86da032d672a", + []expectChange{}, + }, + { + "https://github.com/dezfowler/LiteMock.git", + "", + "b7965eaa2c4f245d07191fe0bcfe86da032d672a", + []expectChange{ + {Action: merkletrie.Insert, Name: "README"}, + }, }, - }, { - "https://github.com/dezfowler/LiteMock.git", - "b7965eaa2c4f245d07191fe0bcfe86da032d672a", - "", - []expectChange{ - {Action: Delete, Name: "README"}, + { + "https://github.com/dezfowler/LiteMock.git", + "b7965eaa2c4f245d07191fe0bcfe86da032d672a", + "", + []expectChange{ + {Action: merkletrie.Delete, Name: "README"}, + }, }, - }, { - "https://github.com/githubtraining/example-branches.git", - "", - "f0eb272cc8f77803478c6748103a1450aa1abd37", - []expectChange{ - {Action: Insert, Name: "README.md"}, + { + "https://github.com/githubtraining/example-branches.git", + "", + "f0eb272cc8f77803478c6748103a1450aa1abd37", + []expectChange{ + {Action: merkletrie.Insert, Name: "README.md"}, + }, }, - }, { - "https://github.com/githubtraining/example-branches.git", - "f0eb272cc8f77803478c6748103a1450aa1abd37", - "", - []expectChange{ - {Action: Delete, Name: "README.md"}, + { + "https://github.com/githubtraining/example-branches.git", + "f0eb272cc8f77803478c6748103a1450aa1abd37", + "", + []expectChange{ + {Action: merkletrie.Delete, Name: "README.md"}, + }, }, - }, { - "https://github.com/githubtraining/example-branches.git", - "f0eb272cc8f77803478c6748103a1450aa1abd37", - "f0eb272cc8f77803478c6748103a1450aa1abd37", - []expectChange{}, - }, { - "https://github.com/github/gem-builder.git", - "", - "9608eed92b3839b06ebf72d5043da547de10ce85", - []expectChange{ - {Action: Insert, Name: "README"}, - {Action: Insert, Name: "gem_builder.rb"}, - {Action: Insert, Name: "gem_eval.rb"}, + { + "https://github.com/githubtraining/example-branches.git", + "f0eb272cc8f77803478c6748103a1450aa1abd37", + "f0eb272cc8f77803478c6748103a1450aa1abd37", + []expectChange{}, }, - }, { - "https://github.com/github/gem-builder.git", - "9608eed92b3839b06ebf72d5043da547de10ce85", - "", - []expectChange{ - {Action: Delete, Name: "README"}, - {Action: Delete, Name: "gem_builder.rb"}, - {Action: Delete, Name: "gem_eval.rb"}, + { + "https://github.com/github/gem-builder.git", + "", + "9608eed92b3839b06ebf72d5043da547de10ce85", + []expectChange{ + {Action: merkletrie.Insert, Name: "README"}, + {Action: merkletrie.Insert, Name: "gem_builder.rb"}, + {Action: merkletrie.Insert, Name: "gem_eval.rb"}, + }, }, - }, { - "https://github.com/github/gem-builder.git", - "9608eed92b3839b06ebf72d5043da547de10ce85", - "9608eed92b3839b06ebf72d5043da547de10ce85", - []expectChange{}, - }, { - "https://github.com/toqueteos/ts3.git", - "", - "764e914b75d6d6df1fc5d832aa9840f590abf1bb", - []expectChange{ - {Action: Insert, Name: "README.markdown"}, - {Action: Insert, Name: "examples/bot.go"}, - {Action: Insert, Name: "examples/raw_shell.go"}, - {Action: Insert, Name: "helpers.go"}, - {Action: Insert, Name: "ts3.go"}, + { + "https://github.com/github/gem-builder.git", + "9608eed92b3839b06ebf72d5043da547de10ce85", + "", + []expectChange{ + {Action: merkletrie.Delete, Name: "README"}, + {Action: merkletrie.Delete, Name: "gem_builder.rb"}, + {Action: merkletrie.Delete, Name: "gem_eval.rb"}, + }, }, - }, { - "https://github.com/toqueteos/ts3.git", - "764e914b75d6d6df1fc5d832aa9840f590abf1bb", - "", - []expectChange{ - {Action: Delete, Name: "README.markdown"}, - {Action: Delete, Name: "examples/bot.go"}, - {Action: Delete, Name: "examples/raw_shell.go"}, - {Action: Delete, Name: "helpers.go"}, - {Action: Delete, Name: "ts3.go"}, + { + "https://github.com/github/gem-builder.git", + "9608eed92b3839b06ebf72d5043da547de10ce85", + "9608eed92b3839b06ebf72d5043da547de10ce85", + []expectChange{}, }, - }, { - "https://github.com/toqueteos/ts3.git", - "764e914b75d6d6df1fc5d832aa9840f590abf1bb", - "764e914b75d6d6df1fc5d832aa9840f590abf1bb", - []expectChange{}, - }, { - "https://github.com/github/gem-builder.git", - "9608eed92b3839b06ebf72d5043da547de10ce85", - "6c41e05a17e19805879689414026eb4e279f7de0", - []expectChange{ - {Action: Modify, Name: "gem_eval.rb"}, + { + "https://github.com/toqueteos/ts3.git", + "", + "764e914b75d6d6df1fc5d832aa9840f590abf1bb", + []expectChange{ + {Action: merkletrie.Insert, Name: "README.markdown"}, + {Action: merkletrie.Insert, Name: "examples/bot.go"}, + {Action: merkletrie.Insert, Name: "examples/raw_shell.go"}, + {Action: merkletrie.Insert, Name: "helpers.go"}, + {Action: merkletrie.Insert, Name: "ts3.go"}, + }, }, - }, { - "https://github.com/github/gem-builder.git", - "6c41e05a17e19805879689414026eb4e279f7de0", - "89be3aac2f178719c12953cc9eaa23441f8d9371", - []expectChange{ - {Action: Modify, Name: "gem_eval.rb"}, - {Action: Insert, Name: "gem_eval_test.rb"}, - {Action: Insert, Name: "security.rb"}, - {Action: Insert, Name: "security_test.rb"}, + { + "https://github.com/toqueteos/ts3.git", + "764e914b75d6d6df1fc5d832aa9840f590abf1bb", + "", + []expectChange{ + {Action: merkletrie.Delete, Name: "README.markdown"}, + {Action: merkletrie.Delete, Name: "examples/bot.go"}, + {Action: merkletrie.Delete, Name: "examples/raw_shell.go"}, + {Action: merkletrie.Delete, Name: "helpers.go"}, + {Action: merkletrie.Delete, Name: "ts3.go"}, + }, }, - }, { - "https://github.com/github/gem-builder.git", - "89be3aac2f178719c12953cc9eaa23441f8d9371", - "597240b7da22d03ad555328f15abc480b820acc0", - []expectChange{ - {Action: Modify, Name: "gem_eval.rb"}, + { + "https://github.com/toqueteos/ts3.git", + "764e914b75d6d6df1fc5d832aa9840f590abf1bb", + "764e914b75d6d6df1fc5d832aa9840f590abf1bb", + []expectChange{}, }, - }, { - "https://github.com/github/gem-builder.git", - "597240b7da22d03ad555328f15abc480b820acc0", - "0260380e375d2dd0e1a8fcab15f91ce56dbe778e", - []expectChange{ - {Action: Modify, Name: "gem_eval.rb"}, - {Action: Modify, Name: "gem_eval_test.rb"}, - {Action: Insert, Name: "lazy_dir.rb"}, - {Action: Insert, Name: "lazy_dir_test.rb"}, - {Action: Modify, Name: "security.rb"}, - {Action: Modify, Name: "security_test.rb"}, + { + "https://github.com/github/gem-builder.git", + "9608eed92b3839b06ebf72d5043da547de10ce85", + "6c41e05a17e19805879689414026eb4e279f7de0", + []expectChange{ + {Action: merkletrie.Modify, Name: "gem_eval.rb"}, + }, }, - }, { - "https://github.com/github/gem-builder.git", - "0260380e375d2dd0e1a8fcab15f91ce56dbe778e", - "597240b7da22d03ad555328f15abc480b820acc0", - []expectChange{ - {Action: Modify, Name: "gem_eval.rb"}, - {Action: Modify, Name: "gem_eval_test.rb"}, - {Action: Delete, Name: "lazy_dir.rb"}, - {Action: Delete, Name: "lazy_dir_test.rb"}, - {Action: Modify, Name: "security.rb"}, - {Action: Modify, Name: "security_test.rb"}, + { + "https://github.com/github/gem-builder.git", + "6c41e05a17e19805879689414026eb4e279f7de0", + "89be3aac2f178719c12953cc9eaa23441f8d9371", + []expectChange{ + {Action: merkletrie.Modify, Name: "gem_eval.rb"}, + {Action: merkletrie.Insert, Name: "gem_eval_test.rb"}, + {Action: merkletrie.Insert, Name: "security.rb"}, + {Action: merkletrie.Insert, Name: "security_test.rb"}, + }, }, - }, { - "https://github.com/github/gem-builder.git", - "0260380e375d2dd0e1a8fcab15f91ce56dbe778e", - "ca9fd470bacb6262eb4ca23ee48bb2f43711c1ff", - []expectChange{ - {Action: Modify, Name: "gem_eval.rb"}, - {Action: Modify, Name: "security.rb"}, - {Action: Modify, Name: "security_test.rb"}, + { + "https://github.com/github/gem-builder.git", + "89be3aac2f178719c12953cc9eaa23441f8d9371", + "597240b7da22d03ad555328f15abc480b820acc0", + []expectChange{ + {Action: merkletrie.Modify, Name: "gem_eval.rb"}, + }, }, - }, { - "https://github.com/github/gem-builder.git", - "fe3c86745f887c23a0d38c85cfd87ca957312f86", - "b7e3f636febf7a0cd3ab473b6d30081786d2c5b6", - []expectChange{ - {Action: Modify, Name: "gem_eval.rb"}, - {Action: Modify, Name: "gem_eval_test.rb"}, - {Action: Insert, Name: "git_mock"}, - {Action: Modify, Name: "lazy_dir.rb"}, - {Action: Modify, Name: "lazy_dir_test.rb"}, - {Action: Modify, Name: "security.rb"}, + { + "https://github.com/github/gem-builder.git", + "597240b7da22d03ad555328f15abc480b820acc0", + "0260380e375d2dd0e1a8fcab15f91ce56dbe778e", + []expectChange{ + {Action: merkletrie.Modify, Name: "gem_eval.rb"}, + {Action: merkletrie.Modify, Name: "gem_eval_test.rb"}, + {Action: merkletrie.Insert, Name: "lazy_dir.rb"}, + {Action: merkletrie.Insert, Name: "lazy_dir_test.rb"}, + {Action: merkletrie.Modify, Name: "security.rb"}, + {Action: merkletrie.Modify, Name: "security_test.rb"}, + }, }, - }, { - "https://github.com/rumpkernel/rumprun-xen.git", - "1831e47b0c6db750714cd0e4be97b5af17fb1eb0", - "51d8515578ea0c88cc8fc1a057903675cf1fc16c", - []expectChange{ - {Action: Modify, Name: "Makefile"}, - {Action: Modify, Name: "netbsd_init.c"}, - {Action: Modify, Name: "rumphyper_stubs.c"}, - {Action: Delete, Name: "sysproxy.c"}, + { + "https://github.com/github/gem-builder.git", + "0260380e375d2dd0e1a8fcab15f91ce56dbe778e", + "597240b7da22d03ad555328f15abc480b820acc0", + []expectChange{ + {Action: merkletrie.Modify, Name: "gem_eval.rb"}, + {Action: merkletrie.Modify, Name: "gem_eval_test.rb"}, + {Action: merkletrie.Delete, Name: "lazy_dir.rb"}, + {Action: merkletrie.Delete, Name: "lazy_dir_test.rb"}, + {Action: merkletrie.Modify, Name: "security.rb"}, + {Action: merkletrie.Modify, Name: "security_test.rb"}, + }, }, - }, { - "https://github.com/rumpkernel/rumprun-xen.git", - "1831e47b0c6db750714cd0e4be97b5af17fb1eb0", - "e13e678f7ee9badd01b120889e0ec5fdc8ae3802", - []expectChange{ - {Action: Modify, Name: "app-tools/rumprun"}, + { + "https://github.com/github/gem-builder.git", + "0260380e375d2dd0e1a8fcab15f91ce56dbe778e", + "ca9fd470bacb6262eb4ca23ee48bb2f43711c1ff", + []expectChange{ + {Action: merkletrie.Modify, Name: "gem_eval.rb"}, + {Action: merkletrie.Modify, Name: "security.rb"}, + {Action: merkletrie.Modify, Name: "security_test.rb"}, + }, }, - }} { + { + "https://github.com/github/gem-builder.git", + "fe3c86745f887c23a0d38c85cfd87ca957312f86", + "b7e3f636febf7a0cd3ab473b6d30081786d2c5b6", + []expectChange{ + {Action: merkletrie.Modify, Name: "gem_eval.rb"}, + {Action: merkletrie.Modify, Name: "gem_eval_test.rb"}, + {Action: merkletrie.Insert, Name: "git_mock"}, + {Action: merkletrie.Modify, Name: "lazy_dir.rb"}, + {Action: merkletrie.Modify, Name: "lazy_dir_test.rb"}, + {Action: merkletrie.Modify, Name: "security.rb"}, + }, + }, + { + "https://github.com/rumpkernel/rumprun-xen.git", + "1831e47b0c6db750714cd0e4be97b5af17fb1eb0", + "51d8515578ea0c88cc8fc1a057903675cf1fc16c", + []expectChange{ + {Action: merkletrie.Modify, Name: "Makefile"}, + {Action: merkletrie.Modify, Name: "netbsd_init.c"}, + {Action: merkletrie.Modify, Name: "rumphyper_stubs.c"}, + {Action: merkletrie.Delete, Name: "sysproxy.c"}, + }, + }, + { + "https://github.com/rumpkernel/rumprun-xen.git", + "1831e47b0c6db750714cd0e4be97b5af17fb1eb0", + "e13e678f7ee9badd01b120889e0ec5fdc8ae3802", + []expectChange{ + {Action: merkletrie.Modify, Name: "app-tools/rumprun"}, + }, + }, + } { f := fixtures.ByURL(t.repository).One() sto := s.storageFromPackfile(f) @@ -388,43 +346,10 @@ func (s *DiffTreeSuite) TestDiffTree(c *C) { obtained, err := DiffTree(tree1, tree2) c.Assert(err, IsNil, Commentf("subtest %d: unable to calculate difftree: %s", i, err)) - c.Assert(equalChanges(obtained, t.expected), Equals, true, + c.Assert(equalChanges(obtained, t.expected, c), Equals, true, Commentf("subtest:%d\nrepo=%s\ncommit1=%s\ncommit2=%s\nexpected=%s\nobtained=%s", i, t.repository, t.commit1, t.commit2, t.expected, obtained)) assertChanges(obtained, c) } } - -func assertChanges(a Changes, c *C) { - for _, changes := range a { - switch changes.Action { - case Insert: - c.Assert(changes.From.Tree, IsNil) - c.Assert(changes.To.Tree, NotNil) - case Delete: - c.Assert(changes.From.Tree, NotNil) - c.Assert(changes.To.Tree, IsNil) - case Modify: - c.Assert(changes.From.Tree, NotNil) - c.Assert(changes.To.Tree, NotNil) - } - } -} - -func equalChanges(a Changes, b []expectChange) bool { - if len(a) != len(b) { - return false - } - - sort.Sort(a) - - for i, va := range a { - vb := b[i] - if va.Action != vb.Action || va.name() != vb.Name { - return false - } - } - - return true -} diff --git a/plumbing/format/index/encoder.go b/plumbing/format/index/encoder.go index 4699d436d..bdb10c1d2 100644 --- a/plumbing/format/index/encoder.go +++ b/plumbing/format/index/encoder.go @@ -62,7 +62,7 @@ func (e *Encoder) encodeHeader(idx *Index) error { } func (e *Encoder) encodeEntries(idx *Index) error { - sort.Sort(ByName(idx.Entries)) + sort.Sort(byName(idx.Entries)) for _, entry := range idx.Entries { if err := e.encodeEntry(&entry); err != nil { @@ -143,8 +143,8 @@ func (e *Encoder) encodeFooter() error { return binary.Write(e.w, e.hash.Sum(nil)) } -type ByName []Entry +type byName []Entry -func (l ByName) Len() int { return len(l) } -func (l ByName) Swap(i, j int) { l[i], l[j] = l[j], l[i] } -func (l ByName) Less(i, j int) bool { return l[i].Name < l[j].Name } +func (l byName) Len() int { return len(l) } +func (l byName) Swap(i, j int) { l[i], l[j] = l[j], l[i] } +func (l byName) Less(i, j int) bool { return l[i].Name < l[j].Name } diff --git a/plumbing/object/file_test.go b/plumbing/object/file_test.go index 426fa8f03..ff01c9f76 100644 --- a/plumbing/object/file_test.go +++ b/plumbing/object/file_test.go @@ -249,7 +249,9 @@ func (s *FileSuite) TestFileIter(c *C) { } func (s *FileSuite) TestFileIterSubmodule(c *C) { - st, err := filesystem.NewStorage(fixtures.ByTag("submodule").One().DotGit()) + dotgit := fixtures.ByURL("https://github.com/git-fixtures/submodule.git").One().DotGit() + st, err := filesystem.NewStorage(dotgit) + c.Assert(err, IsNil) hash := plumbing.NewHash("a692ec699bff9117c1ed91752afbb7d9d272ebef") diff --git a/plumbing/object/tree_test.go b/plumbing/object/tree_test.go index 0ddf391b8..8ea31bbcb 100644 --- a/plumbing/object/tree_test.go +++ b/plumbing/object/tree_test.go @@ -266,7 +266,8 @@ func (s *TreeSuite) TestTreeWalkerNextNonRecursive(c *C) { } func (s *TreeSuite) TestTreeWalkerNextSubmodule(c *C) { - st, err := filesystem.NewStorage(fixtures.ByTag("submodule").One().DotGit()) + dotgit := fixtures.ByURL("https://github.com/git-fixtures/submodule.git").One().DotGit() + st, err := filesystem.NewStorage(dotgit) c.Assert(err, IsNil) hash := plumbing.NewHash("a692ec699bff9117c1ed91752afbb7d9d272ebef") @@ -284,6 +285,8 @@ func (s *TreeSuite) TestTreeWalkerNextSubmodule(c *C) { var count int walker := NewTreeWalker(tree, true) + defer walker.Close() + for { name, entry, err := walker.Next() if err == io.EOF { diff --git a/plumbing/storer/reference.go b/plumbing/storer/reference.go index 40474f9b8..692fe88db 100644 --- a/plumbing/storer/reference.go +++ b/plumbing/storer/reference.go @@ -18,6 +18,7 @@ type ReferenceStorer interface { SetReference(*plumbing.Reference) error Reference(plumbing.ReferenceName) (*plumbing.Reference, error) IterReferences() (ReferenceIter, error) + RemoveReference(plumbing.ReferenceName) error } // ReferenceIter is a generic closable interface for iterating over references diff --git a/plumbing/transport/file/common_test.go b/plumbing/transport/file/common_test.go index 3dc4500b4..4f3ae8fe2 100644 --- a/plumbing/transport/file/common_test.go +++ b/plumbing/transport/file/common_test.go @@ -1,6 +1,7 @@ package file import ( + "io/ioutil" "os" "os/exec" "path/filepath" @@ -8,13 +9,13 @@ import ( "github.com/src-d/go-git-fixtures" . "gopkg.in/check.v1" - "io/ioutil" ) type CommonSuite struct { fixtures.Suite ReceivePackBin string UploadPackBin string + tmpDir string // to be removed at teardown } var _ = Suite(&CommonSuite{}) @@ -26,14 +27,20 @@ func (s *CommonSuite) SetUpSuite(c *C) { c.Skip("git command not found") } - binDir, err := ioutil.TempDir(os.TempDir(), "") + var err error + s.tmpDir, err = ioutil.TempDir("", "") c.Assert(err, IsNil) - s.ReceivePackBin = filepath.Join(binDir, "git-receive-pack") - s.UploadPackBin = filepath.Join(binDir, "git-upload-pack") - bin := filepath.Join(binDir, "go-git") + s.ReceivePackBin = filepath.Join(s.tmpDir, "git-receive-pack") + s.UploadPackBin = filepath.Join(s.tmpDir, "git-upload-pack") + bin := filepath.Join(s.tmpDir, "go-git") cmd := exec.Command("go", "build", "-o", bin, "../../../cli/go-git/...") c.Assert(cmd.Run(), IsNil) c.Assert(os.Symlink(bin, s.ReceivePackBin), IsNil) c.Assert(os.Symlink(bin, s.UploadPackBin), IsNil) } + +func (s *CommonSuite) TearDownSuite(c *C) { + defer s.Suite.TearDownSuite(c) + c.Assert(os.RemoveAll(s.tmpDir), IsNil) +} diff --git a/plumbing/transport/file/server_test.go b/plumbing/transport/file/server_test.go index 775b031bb..a7b4e34c1 100644 --- a/plumbing/transport/file/server_test.go +++ b/plumbing/transport/file/server_test.go @@ -2,8 +2,6 @@ package file import ( "fmt" - "io" - "io/ioutil" "os" "os/exec" @@ -48,8 +46,8 @@ func (s *ServerSuite) TestPush(c *C) { cmd.Dir = s.SrcPath cmd.Env = os.Environ() cmd.Env = append(cmd.Env, "GIT_TRACE=true", "GIT_TRACE_PACKET=true") - stdout, stderr, err := execAndGetOutput(c, cmd) - c.Assert(err, IsNil, Commentf("STDOUT:\n%s\nSTDERR:\n%s\n", stdout, stderr)) + out, err := cmd.CombinedOutput() + c.Assert(err, IsNil, Commentf("combined stdout and stderr:\n%s\n", out)) } func (s *ServerSuite) TestClone(c *C) { @@ -61,45 +59,6 @@ func (s *ServerSuite) TestClone(c *C) { ) cmd.Env = os.Environ() cmd.Env = append(cmd.Env, "GIT_TRACE=true", "GIT_TRACE_PACKET=true") - stdout, stderr, err := execAndGetOutput(c, cmd) - c.Assert(err, IsNil, Commentf("STDOUT:\n%s\nSTDERR:\n%s\n", stdout, stderr)) -} - -func execAndGetOutput(c *C, cmd *exec.Cmd) (stdout, stderr string, err error) { - sout, err := cmd.StdoutPipe() - c.Assert(err, IsNil) - serr, err := cmd.StderrPipe() - c.Assert(err, IsNil) - - outChan, outErr := readAllAsync(sout) - errChan, errErr := readAllAsync(serr) - - c.Assert(cmd.Start(), IsNil) - - if err = cmd.Wait(); err != nil { - return <-outChan, <-errChan, err - } - - if err := <-outErr; err != nil { - return <-outChan, <-errChan, err - } - - return <-outChan, <-errChan, <-errErr -} - -func readAllAsync(r io.Reader) (out chan string, err chan error) { - out = make(chan string, 1) - err = make(chan error, 1) - go func() { - b, e := ioutil.ReadAll(r) - if e != nil { - err <- e - } else { - err <- nil - } - - out <- string(b) - }() - - return out, err + out, err := cmd.CombinedOutput() + c.Assert(err, IsNil, Commentf("combined stdout and stderr:\n%s\n", out)) } diff --git a/repository.go b/repository.go index 9969b8643..c065a266c 100644 --- a/repository.go +++ b/repository.go @@ -332,13 +332,11 @@ func (r *Repository) clone(o *CloneOptions) error { Progress: o.Progress, }) if err != nil { - return err } head, err := storer.ResolveReference(remoteRefs, o.ReferenceName) if err != nil { - return err } @@ -350,7 +348,7 @@ func (r *Repository) clone(o *CloneOptions) error { return err } - if o.RecurseSubmodules != NoRecursivity && r.wt != nil { + if o.RecurseSubmodules != NoRecurseSubmodules && r.wt != nil { if err := r.updateSubmodules(o.RecurseSubmodules); err != nil { return err } @@ -553,7 +551,7 @@ func (r *Repository) Pull(o *PullOptions) error { return err } - if o.RecurseSubmodules != NoRecursivity && r.wt != nil { + if o.RecurseSubmodules != NoRecurseSubmodules && r.wt != nil { if err := r.updateSubmodules(o.RecurseSubmodules); err != nil { return err } diff --git a/repository_test.go b/repository_test.go index 6a9c14a4b..89ea188c9 100644 --- a/repository_test.go +++ b/repository_test.go @@ -299,10 +299,9 @@ func (s *RepositorySuite) TestPlainCloneWithRecurseSubmodules(c *C) { defer os.RemoveAll(dir) path := fixtures.ByTag("submodule").One().Worktree().Base() - r, err := PlainClone(dir, false, &CloneOptions{ URL: fmt.Sprintf("file://%s", path), - RecurseSubmodules: DefaultRecursivity, + RecurseSubmodules: DefaultSubmoduleRecursionDepth, }) c.Assert(err, IsNil) @@ -595,7 +594,7 @@ func (s *RepositorySuite) TestPullProgressWithRecursion(c *C) { }) err = r.Pull(&PullOptions{ - RecurseSubmodules: DefaultRecursivity, + RecurseSubmodules: DefaultSubmoduleRecursionDepth, }) c.Assert(err, IsNil) diff --git a/storage/filesystem/internal/dotgit/dotgit.go b/storage/filesystem/internal/dotgit/dotgit.go index 360b3d101..b46f8271c 100644 --- a/storage/filesystem/internal/dotgit/dotgit.go +++ b/storage/filesystem/internal/dotgit/dotgit.go @@ -5,11 +5,12 @@ import ( "bufio" "errors" "fmt" - "io/ioutil" + stdioutil "io/ioutil" "os" "strings" "srcd.works/go-git.v4/plumbing" + "srcd.works/go-git.v4/utils/ioutil" "srcd.works/go-billy.v1" ) @@ -25,6 +26,8 @@ const ( packPath = "pack" refsPath = "refs" + tmpPackedRefsPrefix = "._packed-refs" + packExt = ".pack" idxExt = ".idx" ) @@ -269,6 +272,21 @@ func (d *DotGit) Ref(name plumbing.ReferenceName) (*plumbing.Reference, error) { return nil, plumbing.ErrReferenceNotFound } +// RemoveRef removes a reference by name. +func (d *DotGit) RemoveRef(name plumbing.ReferenceName) error { + path := d.fs.Join(".", name.String()) + _, err := d.fs.Stat(path) + if err == nil { + return d.fs.Remove(path) + } + + if err != nil && !os.IsNotExist(err) { + return err + } + + return d.rewritePackedRefsWithoutRef(name) +} + func (d *DotGit) addRefsFromPackedRefs(refs *[]*plumbing.Reference) (err error) { f, err := d.fs.Open(packedRefsPath) if err != nil { @@ -277,12 +295,7 @@ func (d *DotGit) addRefsFromPackedRefs(refs *[]*plumbing.Reference) (err error) } return err } - - defer func() { - if errClose := f.Close(); err == nil { - err = errClose - } - }() + defer ioutil.CheckClose(f, &err) s := bufio.NewScanner(f) for s.Scan() { @@ -299,8 +312,64 @@ func (d *DotGit) addRefsFromPackedRefs(refs *[]*plumbing.Reference) (err error) return s.Err() } +func (d *DotGit) rewritePackedRefsWithoutRef(name plumbing.ReferenceName) (err error) { + f, err := d.fs.Open(packedRefsPath) + if err != nil { + if os.IsNotExist(err) { + return nil + } + + return err + } + defer ioutil.CheckClose(f, &err) + + // Creating the temp file in the same directory as the target file + // improves our chances for rename operation to be atomic. + tmp, err := d.fs.TempFile("", tmpPackedRefsPrefix) + if err != nil { + return err + } + + tmpPath := tmp.Filename() + defer ioutil.CheckClose(tmp, &err) + defer d.fs.Remove(tmpPath) + + s := bufio.NewScanner(f) + found := false + for s.Scan() { + line := s.Text() + ref, err := d.processLine(line) + if err != nil { + return err + } + + if ref != nil && ref.Name() == name { + found = true + continue + } + + if _, err := fmt.Fprintln(tmp, line); err != nil { + return err + } + } + + if err := s.Err(); err != nil { + return err + } + + if !found { + return nil + } + + return d.fs.Rename(tmpPath, packedRefsPath) +} + // process lines from a packed-refs file func (d *DotGit) processLine(line string) (*plumbing.Reference, error) { + if len(line) == 0 { + return nil, nil + } + switch line[0] { case '#': // comment - ignore return nil, nil @@ -374,14 +443,9 @@ func (d *DotGit) readReferenceFile(refsPath, refFile string) (ref *plumbing.Refe if err != nil { return nil, err } + defer ioutil.CheckClose(f, &err) - defer func() { - if errClose := f.Close(); err == nil { - err = errClose - } - }() - - b, err := ioutil.ReadAll(f) + b, err := stdioutil.ReadAll(f) if err != nil { return nil, err } diff --git a/storage/filesystem/internal/dotgit/dotgit_test.go b/storage/filesystem/internal/dotgit/dotgit_test.go index a335e5f0d..57dfb5343 100644 --- a/storage/filesystem/internal/dotgit/dotgit_test.go +++ b/storage/filesystem/internal/dotgit/dotgit_test.go @@ -1,6 +1,7 @@ package dotgit import ( + "bufio" "io/ioutil" "os" "path/filepath" @@ -108,6 +109,96 @@ func (s *SuiteDotGit) TestRefsFromReferenceFile(c *C) { } +func (s *SuiteDotGit) TestRemoveRefFromReferenceFile(c *C) { + fs := fixtures.Basic().ByTag(".git").One().DotGit() + dir := New(fs) + + name := plumbing.ReferenceName("refs/remotes/origin/HEAD") + err := dir.RemoveRef(name) + c.Assert(err, IsNil) + + refs, err := dir.Refs() + c.Assert(err, IsNil) + + ref := findReference(refs, string(name)) + c.Assert(ref, IsNil) +} + +func (s *SuiteDotGit) TestRemoveRefFromPackedRefs(c *C) { + fs := fixtures.Basic().ByTag(".git").One().DotGit() + dir := New(fs) + + name := plumbing.ReferenceName("refs/remotes/origin/master") + err := dir.RemoveRef(name) + c.Assert(err, IsNil) + + b, err := ioutil.ReadFile(filepath.Join(fs.Base(), packedRefsPath)) + c.Assert(err, IsNil) + + c.Assert(string(b), Equals, ""+ + "# pack-refs with: peeled fully-peeled \n"+ + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 refs/heads/master\n"+ + "e8d3ffab552895c19b9fcf7aa264d277cde33881 refs/remotes/origin/branch\n") +} + +func (s *SuiteDotGit) TestRemoveRefNonExistent(c *C) { + fs := fixtures.Basic().ByTag(".git").One().DotGit() + dir := New(fs) + + packedRefs := filepath.Join(fs.Base(), packedRefsPath) + before, err := ioutil.ReadFile(packedRefs) + c.Assert(err, IsNil) + + name := plumbing.ReferenceName("refs/heads/nonexistent") + err = dir.RemoveRef(name) + c.Assert(err, IsNil) + + after, err := ioutil.ReadFile(packedRefs) + c.Assert(err, IsNil) + + c.Assert(string(before), Equals, string(after)) +} + +func (s *SuiteDotGit) TestRemoveRefInvalidPackedRefs(c *C) { + fs := fixtures.Basic().ByTag(".git").One().DotGit() + dir := New(fs) + + packedRefs := filepath.Join(fs.Base(), packedRefsPath) + brokenContent := "BROKEN STUFF REALLY BROKEN" + + err := ioutil.WriteFile(packedRefs, []byte(brokenContent), os.FileMode(0755)) + c.Assert(err, IsNil) + + name := plumbing.ReferenceName("refs/heads/nonexistent") + err = dir.RemoveRef(name) + c.Assert(err, NotNil) + + after, err := ioutil.ReadFile(filepath.Join(fs.Base(), packedRefsPath)) + c.Assert(err, IsNil) + + c.Assert(brokenContent, Equals, string(after)) +} + +func (s *SuiteDotGit) TestRemoveRefInvalidPackedRefs2(c *C) { + fs := fixtures.Basic().ByTag(".git").One().DotGit() + dir := New(fs) + + packedRefs := filepath.Join(fs.Base(), packedRefsPath) + brokenContent := strings.Repeat("a", bufio.MaxScanTokenSize*2) + + err := ioutil.WriteFile(packedRefs, []byte(brokenContent), os.FileMode(0755)) + c.Assert(err, IsNil) + + name := plumbing.ReferenceName("refs/heads/nonexistent") + err = dir.RemoveRef(name) + c.Assert(err, NotNil) + + after, err := ioutil.ReadFile(filepath.Join(fs.Base(), packedRefsPath)) + c.Assert(err, IsNil) + + c.Assert(brokenContent, Equals, string(after)) +} + func (s *SuiteDotGit) TestRefsFromHEADFile(c *C) { fs := fixtures.Basic().ByTag(".git").One().DotGit() dir := New(fs) @@ -343,3 +434,11 @@ func (s *SuiteDotGit) TestObjectNotFound(c *C) { c.Assert(err, NotNil) c.Assert(file, IsNil) } + +func (s *SuiteDotGit) TestSubmodules(c *C) { + fs := fixtures.ByTag("submodule").One().DotGit() + dir := New(fs) + + m := dir.Module("basic") + c.Assert(strings.HasSuffix(m.Base(), ".git/module/basic"), Equals, true) +} diff --git a/storage/filesystem/reference.go b/storage/filesystem/reference.go index ee87830fc..cff00c915 100644 --- a/storage/filesystem/reference.go +++ b/storage/filesystem/reference.go @@ -26,3 +26,7 @@ func (r *ReferenceStorage) IterReferences() (storer.ReferenceIter, error) { return storer.NewReferenceSliceIter(refs), nil } + +func (r *ReferenceStorage) RemoveReference(n plumbing.ReferenceName) error { + return r.dir.RemoveRef(n) +} diff --git a/storage/memory/storage.go b/storage/memory/storage.go index fa9a4fa3d..92aeec9a7 100644 --- a/storage/memory/storage.go +++ b/storage/memory/storage.go @@ -220,6 +220,11 @@ func (r ReferenceStorage) IterReferences() (storer.ReferenceIter, error) { return storer.NewReferenceSliceIter(refs), nil } +func (r ReferenceStorage) RemoveReference(n plumbing.ReferenceName) error { + delete(r, n) + return nil +} + type ShallowStorage []plumbing.Hash func (s *ShallowStorage) SetShallow(commits []plumbing.Hash) error { @@ -234,9 +239,12 @@ func (s ShallowStorage) Shallow() ([]plumbing.Hash, error) { type ModuleStorage map[string]*Storage func (s ModuleStorage) Module(name string) (storage.Storer, error) { - if _, ok := s[name]; !ok { - s[name] = NewStorage() + if m, ok := s[name]; ok { + return m, nil } - return s[name], nil + m := NewStorage() + s[name] = m + + return m, nil } diff --git a/storage/storer.go b/storage/storer.go index 0a2c2566f..d21720971 100644 --- a/storage/storer.go +++ b/storage/storer.go @@ -18,6 +18,9 @@ type Storer interface { ModuleStorer } +// ModuleStorer allows interact with the modules' Storers type ModuleStorer interface { + // Module returns a Storer reprensting a submodule, if not exists returns a + // new empty Storer is returned Module(name string) (Storer, error) } diff --git a/storage/test/storage_suite.go b/storage/test/storage_suite.go index d6c8afa75..2a10c7836 100644 --- a/storage/test/storage_suite.go +++ b/storage/test/storage_suite.go @@ -22,7 +22,6 @@ type Storer interface { storer.ShallowStorer storer.IndexStorer config.ConfigStorer - storage.ModuleStorer } @@ -245,6 +244,33 @@ func (s *BaseStorageSuite) TestSetReferenceAndGetReference(c *C) { c.Assert(e.Hash().String(), Equals, "bc9968d75e48de59f0870ffb71f5e160bbbdcf52") } +func (s *BaseStorageSuite) TestRemoveReference(c *C) { + err := s.Storer.SetReference( + plumbing.NewReferenceFromStrings("foo", "bc9968d75e48de59f0870ffb71f5e160bbbdcf52"), + ) + c.Assert(err, IsNil) + + err = s.Storer.RemoveReference(plumbing.ReferenceName("foo")) + c.Assert(err, IsNil) + + _, err = s.Storer.Reference(plumbing.ReferenceName("foo")) + c.Assert(err, Equals, plumbing.ErrReferenceNotFound) +} + +func (s *BaseStorageSuite) TestRemoveReferenceNonExistent(c *C) { + err := s.Storer.SetReference( + plumbing.NewReferenceFromStrings("foo", "bc9968d75e48de59f0870ffb71f5e160bbbdcf52"), + ) + c.Assert(err, IsNil) + + err = s.Storer.RemoveReference(plumbing.ReferenceName("nonexistent")) + c.Assert(err, IsNil) + + e, err := s.Storer.Reference(plumbing.ReferenceName("foo")) + c.Assert(err, IsNil) + c.Assert(e.Hash().String(), Equals, "bc9968d75e48de59f0870ffb71f5e160bbbdcf52") +} + func (s *BaseStorageSuite) TestGetReferenceNotFound(c *C) { r, err := s.Storer.Reference(plumbing.ReferenceName("bar")) c.Assert(err, Equals, plumbing.ErrReferenceNotFound) @@ -297,7 +323,9 @@ func (s *BaseStorageSuite) TestSetConfigAndConfig(c *C) { cfg, err := s.Storer.Config() c.Assert(err, IsNil) - c.Assert(cfg, DeepEquals, expected) + + c.Assert(cfg.Core.IsBare, DeepEquals, expected.Core.IsBare) + c.Assert(cfg.Remotes, DeepEquals, expected.Remotes) } func (s *BaseStorageSuite) TestIndex(c *C) { diff --git a/submodule.go b/submodule.go index b6cc045b7..e329fda0e 100644 --- a/submodule.go +++ b/submodule.go @@ -107,7 +107,7 @@ func (s *Submodule) Update(o *SubmoduleUpdateOptions) error { } func (s *Submodule) doRecrusiveUpdate(r *Repository, o *SubmoduleUpdateOptions) error { - if o.RecurseSubmodules == NoRecursivity { + if o.RecurseSubmodules == NoRecurseSubmodules { return nil } diff --git a/utils/merkletrie/doc.go b/utils/merkletrie/doc.go index 28ece3eea..5204024ad 100644 --- a/utils/merkletrie/doc.go +++ b/utils/merkletrie/doc.go @@ -1,20 +1,11 @@ /* Package merkletrie provides support for n-ary trees that are at the same -time Merkle trees and Radix trees (tries), and provides an efficient -tree comparison algorithm for them. +time Merkle trees and Radix trees (tries). Git trees are Radix n-ary trees in virtue of the names of their tree entries. At the same time, git trees are Merkle trees thanks to their hashes. -When comparing git trees, the simple approach of alphabetically sorting -their elements and comparing the resulting lists is too slow as it -depends linearly on the number of files in the trees: When a directory -has lots of files but none of them has been modified, this approach is -very expensive. We can do better by prunning whole directories that -have not change, just by looking at their hashes. This package provides -the tools to do exactly that. - This package defines Merkle tries as nodes that should have: - a hash: the Merkle part of the Merkle trie @@ -28,5 +19,16 @@ their children, which is good for testing purposes. Nodes in the Merkle trie are abstracted by the Noder interface. The intended use is that git trees implements this interface, either directly or using a simple wrapper. + +This package provides an iterator for merkletries that can skip whole +directory-like noders and an efficient merkletrie comparison algorithm. + +When comparing git trees, the simple approach of alphabetically sorting +their elements and comparing the resulting lists is too slow as it +depends linearly on the number of files in the trees: When a directory +has lots of files but none of them has been modified, this approach is +very expensive. We can do better by prunning whole directories that +have not change, just by looking at their hashes. This package provides +the tools to do exactly that. */ package merkletrie diff --git a/utils/merkletrie/internal/frame/frame_test.go b/utils/merkletrie/internal/frame/frame_test.go index 9cc0994c5..516d78b1d 100644 --- a/utils/merkletrie/internal/frame/frame_test.go +++ b/utils/merkletrie/internal/frame/frame_test.go @@ -89,20 +89,13 @@ func checkFirstAndDrop(c *C, f *Frame, expectedNodeName string, expectedOK bool) } // a mock noder that returns error when Children() is called -type errorNoder struct{} +type errorNoder struct{ noder.Noder } -func (e *errorNoder) Hash() []byte { return nil } -func (e *errorNoder) Name() string { return "" } -func (e *errorNoder) String() string { return "" } -func (e *errorNoder) IsDir() bool { return true } func (e *errorNoder) Children() ([]noder.Noder, error) { return nil, fmt.Errorf("mock error") } -func (e *errorNoder) NumChildren() (int, error) { - return 0, fmt.Errorf("mock error") -} func (s *FrameSuite) TestNewFrameErrors(c *C) { _, err := New(&errorNoder{}) - c.Assert(err, Not(IsNil)) + c.Assert(err, ErrorMatches, "mock error") } diff --git a/utils/merkletrie/iter_test.go b/utils/merkletrie/iter_test.go index 52d567a0e..fa7c5f510 100644 --- a/utils/merkletrie/iter_test.go +++ b/utils/merkletrie/iter_test.go @@ -4,7 +4,6 @@ import ( "fmt" "io" "strings" - "testing" "srcd.works/go-git.v4/utils/merkletrie" "srcd.works/go-git.v4/utils/merkletrie/internal/fsnoder" @@ -13,8 +12,6 @@ import ( . "gopkg.in/check.v1" ) -func Test(t *testing.T) { TestingT(t) } - type IterSuite struct{} var _ = Suite(&IterSuite{}) @@ -443,20 +440,13 @@ func find(c *C, tree noder.Noder, name string) noder.Path { } } -type errorNoder struct{} +type errorNoder struct{ noder.Noder } -func (e *errorNoder) Name() string { return "" } -func (e *errorNoder) String() string { return "" } -func (e *errorNoder) Hash() []byte { return nil } -func (e *errorNoder) IsDir() bool { return true } func (e *errorNoder) Children() ([]noder.Noder, error) { return nil, fmt.Errorf("mock error") } -func (e *errorNoder) NumChildren() (int, error) { - return 0, fmt.Errorf("mock error") -} func (s *IterSuite) TestNewIterFailsOnChildrenErrors(c *C) { _, err := merkletrie.NewIter(&errorNoder{}) - c.Assert(err, Not(IsNil)) + c.Assert(err, ErrorMatches, "mock error") } diff --git a/worktree.go b/worktree.go index 2514a0c9f..2a4e5d857 100644 --- a/worktree.go +++ b/worktree.go @@ -273,7 +273,6 @@ func (w *Worktree) readGitmodulesFile() (*config.Modules, error) { m := config.NewModules() return m, m.Unmarshal(input) - } func (w *Worktree) readIndexEntry(path string) (index.Entry, error) {