diff options
30 files changed, 1092 insertions, 131 deletions
diff --git a/_examples/clone/main.go b/_examples/clone/main.go index 7d173a6..bcdb6a9 100644 --- a/_examples/clone/main.go +++ b/_examples/clone/main.go @@ -14,11 +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, - Depth: 1, + URL: url, + RecurseSubmodules: git.DefaultSubmoduleRecursionDepth, }) CheckIfError(err) @@ -1,23 +1,8 @@ package git -import ( - "strings" +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 -} +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 diff --git a/config/config.go b/config/config.go index 866ae8e..259ebf9 100644 --- a/config/config.go +++ b/config/config.go @@ -32,14 +32,19 @@ 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 string } - // Remote 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, 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 // the parsed information from the original format, to avoid missing @@ -47,15 +52,16 @@ 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), - raw: format.New(), + Remotes: make(map[string]*RemoteConfig, 0), + Submodules: make(map[string]*Submodule, 0), + raw: format.New(), } } -// 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 { @@ -71,14 +77,16 @@ func (c *Config) Validate() error { } const ( - remoteSection = "remote" - coreSection = "core" - fetchKey = "fetch" - urlKey = "url" - bareKey = "bare" + remoteSection = "remote" + submoduleSection = "submodule" + coreSection = "core" + fetchKey = "fetch" + urlKey = "url" + bareKey = "bare" + 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) @@ -89,6 +97,7 @@ func (c *Config) Unmarshal(b []byte) error { } c.unmarshalCore() + c.unmarshalSubmodules() return c.unmarshalRemotes() } @@ -97,6 +106,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 { @@ -113,10 +124,21 @@ func (c *Config) unmarshalRemotes() error { return nil } -// Marshal returns Config encoded as a git-config file +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 { @@ -129,6 +151,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() { @@ -142,7 +168,22 @@ func (c *Config) marshalRemotes() { } } -// RemoteConfig contains the configuration for a given remote repository +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 Name string @@ -156,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/config_test.go b/config/config_test.go index 2bcefe4..cfab36d 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -9,9 +9,14 @@ 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/* +[submodule "qux"] + path = qux + url = https://github.com/foo/qux.git + branch = bar [branch "master"] remote = origin merge = refs/heads/master @@ -22,15 +27,51 @@ 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/*"}) + 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) { + output := []byte(`[core] + bare = true + worktree = bar +[remote "origin"] + url = git@github.com:mcuadros/go-git.git +[submodule "qux"] + url = https://github.com/foo/qux.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", + } + + cfg.Submodules["qux"] = &Submodule{ + Name: "qux", + URL: "https://github.com/foo/qux.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 diff --git a/config/modules.go b/config/modules.go index 4d98b16..3d01117 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 @@ -30,12 +30,11 @@ func NewModules() *Modules { } const ( - submoduleSection = "submodule" - pathKey = "path" - branchKey = "branch" + pathKey = "path" + 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) @@ -56,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)) @@ -75,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 @@ -89,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 dd68edc..9441df8 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) { @@ -9,36 +9,49 @@ import ( "srcd.works/go-git.v4/plumbing/transport" ) +// SubmoduleRescursivity defines how depth will affect any submodule recursive +// 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" + + // 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. + 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 @@ -55,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 @@ -65,11 +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. + 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 } @@ -94,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 @@ -117,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 @@ -148,3 +164,16 @@ func (o *PushOptions) Validate() error { return nil } + +// SubmoduleUpdateOptions describes how a submodule update should be performed. +type SubmoduleUpdateOptions struct { + // Init, if true initializes the submodules recorded in the index. + Init bool + // 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 + // 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/plumbing/format/index/encoder.go b/plumbing/format/index/encoder.go index e5de135..bdb10c1 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 914ee26..a6a0ea2 100644 --- a/plumbing/format/index/encoder_test.go +++ b/plumbing/format/index/encoder_test.go @@ -26,6 +26,11 @@ func (s *IndexSuite) TestEncode(c *C) { }, { CreatedAt: time.Now(), ModifiedAt: time.Now(), + Name: "bar", + Size: 82, + }, { + CreatedAt: time.Now(), + ModifiedAt: time.Now(), Name: strings.Repeat(" ", 20), Size: 82, }}, @@ -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 e5dc178..a95dba2 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 diff --git a/plumbing/object/file.go b/plumbing/object/file.go index 35e7f24..4866777 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 4c8bbb6..ff01c9f 100644 --- a/plumbing/object/file_test.go +++ b/plumbing/object/file_test.go @@ -247,3 +247,31 @@ func (s *FileSuite) TestFileIter(c *C) { c.Assert(count, Equals, 1) } + +func (s *FileSuite) TestFileIterSubmodule(c *C) { + 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") + 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 3bcd80a..27d8578 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 be721b9..8ea31bb 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,44 @@ func (s *TreeSuite) TestTreeWalkerNextNonRecursive(c *C) { c.Assert(count, Equals, 8) } +func (s *TreeSuite) TestTreeWalkerNextSubmodule(c *C) { + 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") + 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) + defer walker.Close() + + 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 }{{ @@ -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 1e34905..e13ef20 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 a8dd7ef..c065a26 100644 --- a/repository.go +++ b/repository.go @@ -4,12 +4,14 @@ import ( "errors" "fmt" "os" + "path/filepath" "srcd.works/go-git.v4/config" "srcd.works/go-git.v4/internal/revision" "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 +31,7 @@ var ( // Repository represents a git repository type Repository struct { - Storer Storer + Storer storage.Storer r map[string]*Remote wt billy.Filesystem @@ -38,7 +40,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 { @@ -56,9 +58,75 @@ func Init(s 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, @@ -66,7 +134,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 +159,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 +227,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 +315,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, @@ -286,9 +348,32 @@ func (r *Repository) clone(o *CloneOptions) error { return err } + if o.RecurseSubmodules != NoRecurseSubmodules && r.wt != nil { + if err := r.updateSubmodules(o.RecurseSubmodules); err != nil { + return err + } + } + return r.updateRemoteConfig(remote, o, c, head) } +func (r *Repository) updateSubmodules(recursion SubmoduleRescursivity) error { + w, err := r.Worktree() + if err != nil { + return err + } + + s, err := w.Submodules() + if err != nil { + return err + } + + return s.Update(&SubmoduleUpdateOptions{ + Init: true, + RecurseSubmodules: recursion, + }) +} + func (r *Repository) cloneRefSpec(o *CloneOptions, c *config.RemoteConfig) []config.RefSpec { @@ -462,7 +547,17 @@ func (r *Repository) Pull(o *PullOptions) error { return NoErrAlreadyUpToDate } - return r.updateWorktree() + if err := r.updateWorktree(); err != nil { + return err + } + + if o.RecurseSubmodules != NoRecurseSubmodules && 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 1b5b345..89ea188 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) @@ -246,6 +293,24 @@ 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: DefaultSubmoduleRecursionDepth, + }) + + 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{ @@ -515,27 +580,44 @@ 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: DefaultSubmoduleRecursionDepth, + }) + c.Assert(err, IsNil) + + cfg, err := r.Config() + c.Assert(cfg.Submodules, HasLen, 2) +} + 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 +628,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/storage/filesystem/internal/dotgit/dotgit.go b/storage/filesystem/internal/dotgit/dotgit.go index 6646e18..b46f827 100644 --- a/storage/filesystem/internal/dotgit/dotgit.go +++ b/storage/filesystem/internal/dotgit/dotgit.go @@ -21,13 +21,13 @@ const ( configPath = "config" indexPath = "index" shallowPath = "shallow" + modulePath = "module" + objectsPath = "objects" + packPath = "pack" + refsPath = "refs" tmpPackedRefsPrefix = "._packed-refs" - objectsPath = "objects" - packPath = "pack" - refsPath = "refs" - packExt = ".pack" idxExt = ".idx" ) @@ -454,6 +454,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/internal/dotgit/dotgit_test.go b/storage/filesystem/internal/dotgit/dotgit_test.go index 226b299..57dfb53 100644 --- a/storage/filesystem/internal/dotgit/dotgit_test.go +++ b/storage/filesystem/internal/dotgit/dotgit_test.go @@ -434,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/module.go b/storage/filesystem/module.go new file mode 100644 index 0000000..e8985d8 --- /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 7021d3a..9895507 100644 --- a/storage/filesystem/storage.go +++ b/storage/filesystem/storage.go @@ -11,11 +11,14 @@ 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 ShallowStorage ConfigStorage + ModuleStorage } // NewStorage returns a new Storage backed by a given `fs.Filesystem` @@ -27,10 +30,18 @@ func NewStorage(fs billy.Filesystem) (*Storage, error) { } return &Storage{ + fs: fs, + ObjectStorage: o, ReferenceStorage: ReferenceStorage{dir: dir}, IndexStorage: IndexStorage{dir: dir}, ShallowStorage: ShallowStorage{dir: dir}, ConfigStorage: ConfigStorage{dir: dir}, + 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 e398d22..7300de7 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) +} diff --git a/storage/memory/storage.go b/storage/memory/storage.go index 9b55b1f..92aeec9 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), } } @@ -232,3 +235,16 @@ 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 m, ok := s[name]; ok { + return m, nil + } + + m := NewStorage() + s[name] = m + + return m, nil +} diff --git a/storage/storer.go b/storage/storer.go new file mode 100644 index 0000000..d217209 --- /dev/null +++ b/storage/storer.go @@ -0,0 +1,26 @@ +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 +} + +// 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 e09a673..2a10c78 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,7 @@ type Storer interface { storer.ShallowStorer storer.IndexStorer config.ConfigStorer + storage.ModuleStorer } type TestObject struct { @@ -321,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) { @@ -353,6 +357,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() diff --git a/submodule.go b/submodule.go new file mode 100644 index 0000000..e329fda --- /dev/null +++ b/submodule.go @@ -0,0 +1,174 @@ +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 { + initialized bool + + c *config.Submodule + w *Worktree +} + +// Config returns the submodule config +func (s *Submodule) Config() *config.Submodule { + return s.c +} + +// Init initialize the submodule reading the recoreded Entry in the index for +// the given submodule +func (s *Submodule) Init() error { + cfg, err := s.w.r.Storer.Config() + if err != nil { + return err + } + + _, 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.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 + } + + 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) +} + +func (s *Submodule) doRecrusiveUpdate(r *Repository, o *SubmoduleUpdateOptions) error { + if o.RecurseSubmodules == NoRecurseSubmodules { + return nil + } + + w, err := r.Worktree() + if err != nil { + return err + } + + l, err := w.Submodules() + if err != nil { + return err + } + + new := &SubmoduleUpdateOptions{} + *new = *o + new.RecurseSubmodules-- + return l.Update(new) +} + +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 := r.Worktree() + if err != nil { + return err + } + + if err := w.Checkout(hash); err != nil { + return err + } + + head := plumbing.NewHashReference(plumbing.HEAD, hash) + return r.Storer.SetReference(head) +} + +// Submodules list of several submodules from the same repository +type Submodules []*Submodule + +// Init initializes the submodules in this list +func (s Submodules) Init() error { + for _, sub := range s { + if err := sub.Init(); err != nil { + return err + } + } + + 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 new file mode 100644 index 0000000..a933965 --- /dev/null +++ b/submodule_test.go @@ -0,0 +1,164 @@ +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 = dir +} + +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.Init() + c.Assert(err, IsNil) + + cfg, err := s.Repository.Config() + c.Assert(err, IsNil) + + 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) + + err = sm.Update(&SubmoduleUpdateOptions{ + Init: true, + }) + + c.Assert(err, IsNil) + + 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) TestUpdateWithoutInit(c *C) { + sm, err := s.Worktree.Submodule("basic") + c.Assert(err, IsNil) + + err = sm.Update(&SubmoduleUpdateOptions{}) + c.Assert(err, Equals, ErrSubmoduleNotInitialized) +} + +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() + 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(&SubmoduleUpdateOptions{}) + c.Assert(err, IsNil) + + 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, "b029517f6300c2da0f4b651b8642506cd6aaf45d") + +} + +func (s *SubmoduleSuite) TestSubmodulesInit(c *C) { + sm, err := s.Worktree.Submodules() + c.Assert(err, IsNil) + + err = sm.Init() + c.Assert(err, IsNil) + + sm, err = s.Worktree.Submodules() + c.Assert(err, IsNil) + + for _, m := range sm { + c.Assert(m.initialized, Equals, true) + } +} diff --git a/worktree.go b/worktree.go index 58e008e..2a4e5d8 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" @@ -14,6 +16,7 @@ import ( ) var ErrWorktreeNotClean = errors.New("worktree is not clean") +var ErrSubmoduleNotFound = errors.New("submodule not found") type Worktree struct { r *Repository @@ -35,29 +38,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 { + name, entry, err := walker.Next() + if err == io.EOF { + break + } + + if err != nil { + return err + } + + if err := w.checkoutEntry(name, &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(name string, e *object.TreeEntry, idx *index.Index) error { + if e.Mode == object.SubmoduleMode { + return w.addIndexFromTreeEntry(name, e, idx) + } + + if e.Mode.IsDir() { + return nil + } + + return w.checkoutFile(name, e, idx) +} + +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 + } + + 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(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, e.Mode.Perm()) if err != nil { return err } @@ -67,20 +98,30 @@ func (w *Worktree) checkoutFile(f *object.File, idx *index.Index) error { } defer to.Close() - return w.indexFile(f, idx) + return w.addIndexFromFile(name, e, idx) } var fillSystemInfo func(e *index.Entry, sys interface{}) -func (w *Worktree) indexFile(f *object.File, idx *index.Index) error { - fi, err := w.fs.Stat(f.Name) +func (w *Worktree) addIndexFromTreeEntry(name string, f *object.TreeEntry, idx *index.Index) error { + idx.Entries = append(idx.Entries, index.Entry{ + Hash: f.Hash, + Name: name, + Mode: object.SubmoduleMode, + }) + + return nil +} + +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()), @@ -167,6 +208,90 @@ func (w *Worktree) getMode(fi billy.FileInfo) os.FileMode { return object.FileMode } +const gitmodulesFile = ".gitmodules" + +// Submodule returns the submodule with the given name +func (w *Worktree) Submodule(name string) (*Submodule, error) { + l, err := w.Submodules() + if err != nil { + return nil, err + } + + for _, m := range l { + if m.Config().Name == name { + return m, nil + } + } + + return nil, ErrSubmoduleNotFound +} + +// Submodules returns all the available submodules +func (w *Worktree) Submodules() (Submodules, error) { + l := make(Submodules, 0) + m, err := w.readGitmodulesFile() + if err != nil || m == nil { + return l, err + } + + 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(fromModules, fromConfig *config.Submodule) *Submodule { + m := &Submodule{w: w} + m.initialized = fromConfig != nil + + if !m.initialized { + m.c = fromModules + return m + } + + m.c = fromConfig + m.c.Path = fromModules.Path + return m +} + +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) +} + +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 @@ -281,17 +406,25 @@ 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 } l, err := fs.ReadDir(path) if err != nil { + if os.IsNotExist(err) { + return nil + } + return err } 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 8ca3d4f..81d35b1 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" @@ -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) +} |