aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMáximo Cuadros <mcuadros@gmail.com>2017-02-21 16:07:25 +0100
committerGitHub <noreply@github.com>2017-02-21 16:07:25 +0100
commit867b10692e5f8a34a82cc0a783bdb63e2b5ff398 (patch)
tree3ddb2f430ee3c958f0650cb7db6a9df44b1e1361
parent0b8b8da617d5a077f282e57d0300dc106a604236 (diff)
parent790fbdaddc3c9a434f2ad97d9eb56db9b6c99495 (diff)
downloadgo-git-867b10692e5f8a34a82cc0a783bdb63e2b5ff398.tar.gz
Merge pull request #270 from mcuadros/submodules-init
Submodules init and update
-rw-r--r--_examples/clone/main.go6
-rw-r--r--common.go19
-rw-r--r--config/config.go73
-rw-r--r--config/config_test.go41
-rw-r--r--config/modules.go19
-rw-r--r--config/refspec.go12
-rw-r--r--options.go67
-rw-r--r--plumbing/format/index/encoder.go9
-rw-r--r--plumbing/format/index/encoder_test.go10
-rw-r--r--plumbing/format/index/index.go13
-rw-r--r--plumbing/object/file.go2
-rw-r--r--plumbing/object/file_test.go28
-rw-r--r--plumbing/object/tree.go5
-rw-r--r--plumbing/object/tree_test.go41
-rw-r--r--remote.go9
-rw-r--r--remote_test.go3
-rw-r--r--repository.go121
-rw-r--r--repository_test.go105
-rw-r--r--storage/filesystem/internal/dotgit/dotgit.go13
-rw-r--r--storage/filesystem/internal/dotgit/dotgit_test.go8
-rw-r--r--storage/filesystem/module.go14
-rw-r--r--storage/filesystem/storage.go11
-rw-r--r--storage/filesystem/storage_test.go9
-rw-r--r--storage/memory/storage.go16
-rw-r--r--storage/storer.go26
-rw-r--r--storage/test/storage_suite.go16
-rw-r--r--submodule.go174
-rw-r--r--submodule_test.go164
-rw-r--r--worktree.go159
-rw-r--r--worktree_test.go30
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)
diff --git a/common.go b/common.go
index 1a05dc3..f837a26 100644
--- a/common.go
+++ b/common.go
@@ -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) {
diff --git a/options.go b/options.go
index 120c472..d018356 100644
--- a/options.go
+++ b/options.go
@@ -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
}{{
diff --git a/remote.go b/remote.go
index 2d243e9..a51946b 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 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)
+}