diff options
-rw-r--r-- | config/branch.go | 71 | ||||
-rw-r--r-- | config/branch_test.go | 76 | ||||
-rw-r--r-- | config/config.go | 66 | ||||
-rw-r--r-- | config/config_test.go | 76 | ||||
-rw-r--r-- | repository.go | 79 | ||||
-rw-r--r-- | repository_test.go | 146 |
6 files changed, 507 insertions, 7 deletions
diff --git a/config/branch.go b/config/branch.go new file mode 100644 index 0000000..e18073c --- /dev/null +++ b/config/branch.go @@ -0,0 +1,71 @@ +package config + +import ( + "errors" + + "gopkg.in/src-d/go-git.v4/plumbing" + format "gopkg.in/src-d/go-git.v4/plumbing/format/config" +) + +var ( + errBranchEmptyName = errors.New("branch config: empty name") + errBranchInvalidMerge = errors.New("branch config: invalid merge") +) + +// Branch contains information on the +// local branches and which remote to track +type Branch struct { + // Name of branch + Name string + // Remote name of remote to track + Remote string + // Merge is the local refspec for the branch + Merge plumbing.ReferenceName + + raw *format.Subsection +} + +// Validate validates fields of branch +func (b *Branch) Validate() error { + if b.Name == "" { + return errBranchEmptyName + } + + if b.Merge != "" && !b.Merge.IsBranch() { + return errBranchInvalidMerge + } + + return nil +} + +func (b *Branch) marshal() *format.Subsection { + if b.raw == nil { + b.raw = &format.Subsection{} + } + + b.raw.Name = b.Name + + if b.Remote == "" { + b.raw.RemoveOption(remoteSection) + } else { + b.raw.SetOption(remoteSection, b.Remote) + } + + if b.Merge == "" { + b.raw.RemoveOption(mergeKey) + } else { + b.raw.SetOption(mergeKey, string(b.Merge)) + } + + return b.raw +} + +func (b *Branch) unmarshal(s *format.Subsection) error { + b.raw = s + + b.Name = b.raw.Name + b.Remote = b.raw.Options.Get(remoteSection) + b.Merge = plumbing.ReferenceName(b.raw.Options.Get(mergeKey)) + + return b.Validate() +} diff --git a/config/branch_test.go b/config/branch_test.go new file mode 100644 index 0000000..d74122e --- /dev/null +++ b/config/branch_test.go @@ -0,0 +1,76 @@ +package config + +import ( + . "gopkg.in/check.v1" + "gopkg.in/src-d/go-git.v4/plumbing" +) + +type BranchSuite struct{} + +var _ = Suite(&BranchSuite{}) + +func (b *BranchSuite) TestValidateName(c *C) { + goodBranch := Branch{ + Name: "master", + Remote: "some_remote", + Merge: "refs/heads/master", + } + badBranch := Branch{ + Remote: "some_remote", + Merge: "refs/heads/master", + } + c.Assert(goodBranch.Validate(), IsNil) + c.Assert(badBranch.Validate(), NotNil) +} + +func (b *BranchSuite) TestValidateMerge(c *C) { + goodBranch := Branch{ + Name: "master", + Remote: "some_remote", + Merge: "refs/heads/master", + } + badBranch := Branch{ + Name: "master", + Remote: "some_remote", + Merge: "blah", + } + c.Assert(goodBranch.Validate(), IsNil) + c.Assert(badBranch.Validate(), NotNil) +} + +func (b *BranchSuite) TestMarshall(c *C) { + expected := []byte(`[core] + bare = false +[branch "branch-tracking-on-clone"] + remote = fork + merge = refs/heads/branch-tracking-on-clone +`) + + cfg := NewConfig() + cfg.Branches["branch-tracking-on-clone"] = &Branch{ + Name: "branch-tracking-on-clone", + Remote: "fork", + Merge: plumbing.ReferenceName("refs/heads/branch-tracking-on-clone"), + } + + actual, err := cfg.Marshal() + c.Assert(err, IsNil) + c.Assert(string(actual), Equals, string(expected)) +} + +func (b *BranchSuite) TestUnmarshall(c *C) { + input := []byte(`[core] + bare = false +[branch "branch-tracking-on-clone"] + remote = fork + merge = refs/heads/branch-tracking-on-clone +`) + + cfg := NewConfig() + err := cfg.Unmarshal(input) + c.Assert(err, IsNil) + branch := cfg.Branches["branch-tracking-on-clone"] + c.Assert(branch.Name, Equals, "branch-tracking-on-clone") + c.Assert(branch.Remote, Equals, "fork") + c.Assert(branch.Merge, Equals, plumbing.ReferenceName("refs/heads/branch-tracking-on-clone")) +} diff --git a/config/config.go b/config/config.go index 87a847d..c730015 100644 --- a/config/config.go +++ b/config/config.go @@ -25,7 +25,7 @@ type ConfigStorer interface { } var ( - ErrInvalid = errors.New("config invalid remote") + ErrInvalid = errors.New("config invalid key in remote or branch") ErrRemoteConfigNotFound = errors.New("remote config not found") ErrRemoteConfigEmptyURL = errors.New("remote config: empty URL") ErrRemoteConfigEmptyName = errors.New("remote config: empty name") @@ -55,7 +55,9 @@ type Config struct { // 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 - + // Branches list of branches, the key is the branch name and should + // equal Branch.Name + Branches map[string]*Branch // Raw contains the raw information of a config file. The main goal is // preserve the parsed information from the original format, to avoid // dropping unsupported fields. @@ -67,6 +69,7 @@ func NewConfig() *Config { config := &Config{ Remotes: make(map[string]*RemoteConfig), Submodules: make(map[string]*Submodule), + Branches: make(map[string]*Branch), Raw: format.New(), } @@ -87,12 +90,23 @@ func (c *Config) Validate() error { } } + for name, b := range c.Branches { + if b.Name != name { + return ErrInvalid + } + + if err := b.Validate(); err != nil { + return err + } + } + return nil } const ( remoteSection = "remote" submoduleSection = "submodule" + branchSection = "branch" coreSection = "core" packSection = "pack" fetchKey = "fetch" @@ -100,6 +114,7 @@ const ( bareKey = "bare" worktreeKey = "worktree" windowKey = "window" + mergeKey = "merge" // DefaultPackWindow holds the number of previous objects used to // generate deltas. The value 10 is the same used by git command. @@ -121,6 +136,11 @@ func (c *Config) Unmarshal(b []byte) error { return err } c.unmarshalSubmodules() + + if err := c.unmarshalBranches(); err != nil { + return err + } + return c.unmarshalRemotes() } @@ -172,12 +192,27 @@ func (c *Config) unmarshalSubmodules() { } } +func (c *Config) unmarshalBranches() error { + bs := c.Raw.Section(branchSection) + for _, sub := range bs.Subsections { + b := &Branch{} + + if err := b.unmarshal(sub); err != nil { + return err + } + + c.Branches[b.Name] = b + } + return nil +} + // Marshal returns Config encoded as a git-config file. func (c *Config) Marshal() ([]byte, error) { c.marshalCore() c.marshalPack() c.marshalRemotes() c.marshalSubmodules() + c.marshalBranches() buf := bytes.NewBuffer(nil) if err := format.NewEncoder(buf).Encode(c.Raw); err != nil { @@ -245,6 +280,33 @@ func (c *Config) marshalSubmodules() { } } +func (c *Config) marshalBranches() { + s := c.Raw.Section(branchSection) + newSubsections := make(format.Subsections, 0, len(c.Branches)) + added := make(map[string]bool) + for _, subsection := range s.Subsections { + if branch, ok := c.Branches[subsection.Name]; ok { + newSubsections = append(newSubsections, branch.marshal()) + added[subsection.Name] = true + } + } + + branchNames := make([]string, 0, len(c.Branches)) + for name := range c.Branches { + branchNames = append(branchNames, name) + } + + sort.Strings(branchNames) + + for _, name := range branchNames { + if !added[name] { + newSubsections = append(newSubsections, c.Branches[name].marshal()) + } + } + + s.Subsections = newSubsections +} + // RemoteConfig contains the configuration for a given remote repository. type RemoteConfig struct { // Name of the remote diff --git a/config/config_test.go b/config/config_test.go index 1f120c0..5cd713e 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1,6 +1,9 @@ package config -import . "gopkg.in/check.v1" +import ( + . "gopkg.in/check.v1" + "gopkg.in/src-d/go-git.v4/plumbing" +) type ConfigSuite struct{} @@ -47,7 +50,8 @@ func (s *ConfigSuite) TestUnmarshall(c *C) { 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") - + c.Assert(cfg.Branches["master"].Remote, Equals, "origin") + c.Assert(cfg.Branches["master"].Merge, Equals, plumbing.ReferenceName("refs/heads/master")) } func (s *ConfigSuite) TestMarshall(c *C) { @@ -65,6 +69,9 @@ func (s *ConfigSuite) TestMarshall(c *C) { url = git@github.com:mcuadros/go-git.git [submodule "qux"] url = https://github.com/foo/qux.git +[branch "master"] + remote = origin + merge = refs/heads/master `) cfg := NewConfig() @@ -87,6 +94,12 @@ func (s *ConfigSuite) TestMarshall(c *C) { URL: "https://github.com/foo/qux.git", } + cfg.Branches["master"] = &Branch{ + Name: "master", + Remote: "origin", + Merge: "refs/heads/master", + } + b, err := cfg.Marshal() c.Assert(err, IsNil) @@ -118,6 +131,29 @@ func (s *ConfigSuite) TestUnmarshallMarshall(c *C) { c.Assert(string(output), DeepEquals, string(input)) } +func (s *ConfigSuite) TestValidateConfig(c *C) { + config := &Config{ + Remotes: map[string]*RemoteConfig{ + "bar": { + Name: "bar", + URLs: []string{"http://foo/bar"}, + }, + }, + Branches: map[string]*Branch{ + "bar": { + Name: "bar", + }, + "foo": { + Name: "foo", + Remote: "origin", + Merge: plumbing.ReferenceName("refs/heads/foo"), + }, + }, + } + + c.Assert(config.Validate(), IsNil) +} + func (s *ConfigSuite) TestValidateInvalidRemote(c *C) { config := &Config{ Remotes: map[string]*RemoteConfig{ @@ -128,7 +164,7 @@ func (s *ConfigSuite) TestValidateInvalidRemote(c *C) { c.Assert(config.Validate(), Equals, ErrRemoteConfigEmptyURL) } -func (s *ConfigSuite) TestValidateInvalidKey(c *C) { +func (s *ConfigSuite) TestValidateInvalidRemoteKey(c *C) { config := &Config{ Remotes: map[string]*RemoteConfig{ "bar": {Name: "foo"}, @@ -157,10 +193,44 @@ func (s *ConfigSuite) TestRemoteConfigValidateDefault(c *C) { c.Assert(fetch[0].String(), Equals, "+refs/heads/*:refs/remotes/foo/*") } +func (s *ConfigSuite) TestValidateInvalidBranchKey(c *C) { + config := &Config{ + Branches: map[string]*Branch{ + "foo": { + Name: "bar", + Remote: "origin", + Merge: plumbing.ReferenceName("refs/heads/bar"), + }, + }, + } + + c.Assert(config.Validate(), Equals, ErrInvalid) +} + +func (s *ConfigSuite) TestValidateInvalidBranch(c *C) { + config := &Config{ + Branches: map[string]*Branch{ + "bar": { + Name: "bar", + Remote: "origin", + Merge: plumbing.ReferenceName("refs/heads/bar"), + }, + "foo": { + Name: "foo", + Remote: "origin", + Merge: plumbing.ReferenceName("baz"), + }, + }, + } + + c.Assert(config.Validate(), Equals, errBranchInvalidMerge) +} + func (s *ConfigSuite) TestRemoteConfigDefaultValues(c *C) { config := NewConfig() c.Assert(config.Remotes, HasLen, 0) + c.Assert(config.Branches, HasLen, 0) c.Assert(config.Submodules, HasLen, 0) c.Assert(config.Raw, NotNil) c.Assert(config.Pack.Window, Equals, DefaultPackWindow) diff --git a/repository.go b/repository.go index 53c545c..35780e2 100644 --- a/repository.go +++ b/repository.go @@ -25,11 +25,15 @@ import ( ) var ( + // ErrBranchExists an error stating the specified branch already exists + ErrBranchExists = errors.New("branch already exists") + // ErrBranchNotFound an error stating the specified branch does not exist + ErrBranchNotFound = errors.New("branch not found") ErrInvalidReference = errors.New("invalid reference, should be a tag or a branch") ErrRepositoryNotExists = errors.New("repository does not exist") ErrRepositoryAlreadyExists = errors.New("repository already exists") ErrRemoteNotFound = errors.New("remote not found") - ErrRemoteExists = errors.New("remote already exists ") + ErrRemoteExists = errors.New("remote already exists") ErrWorktreeNotProvided = errors.New("worktree should be provided") ErrIsBareRepository = errors.New("worktree not available in a bare repository") ErrUnableToResolveCommit = errors.New("unable to resolve commit") @@ -428,6 +432,55 @@ func (r *Repository) DeleteRemote(name string) error { return r.Storer.SetConfig(cfg) } +// Branch return a Branch if exists +func (r *Repository) Branch(name string) (*config.Branch, error) { + cfg, err := r.Storer.Config() + if err != nil { + return nil, err + } + + b, ok := cfg.Branches[name] + if !ok { + return nil, ErrBranchNotFound + } + + return b, nil +} + +// CreateBranch creates a new Branch +func (r *Repository) CreateBranch(c *config.Branch) error { + if err := c.Validate(); err != nil { + return err + } + + cfg, err := r.Storer.Config() + if err != nil { + return err + } + + if _, ok := cfg.Branches[c.Name]; ok { + return ErrBranchExists + } + + cfg.Branches[c.Name] = c + return r.Storer.SetConfig(cfg) +} + +// DeleteBranch delete a Branch from the repository and delete the config +func (r *Repository) DeleteBranch(name string) error { + cfg, err := r.Storer.Config() + if err != nil { + return err + } + + if _, ok := cfg.Branches[name]; !ok { + return ErrBranchNotFound + } + + delete(cfg.Branches, name) + return r.Storer.SetConfig(cfg) +} + func (r *Repository) resolveToCommitHash(h plumbing.Hash) (plumbing.Hash, error) { obj, err := r.Storer.EncodedObject(plumbing.AnyObject, h) if err != nil { @@ -501,7 +554,29 @@ func (r *Repository) clone(ctx context.Context, o *CloneOptions) error { } } - return r.updateRemoteConfigIfNeeded(o, c, ref) + if err := r.updateRemoteConfigIfNeeded(o, c, ref); err != nil { + return err + } + + if ref.Name().IsBranch() { + branchRef := ref.Name() + branchName := strings.Split(string(branchRef), "refs/heads/")[1] + + b := &config.Branch{ + Name: branchName, + Merge: branchRef, + } + if o.RemoteName == "" { + b.Remote = "origin" + } else { + b.Remote = o.RemoteName + } + if err := r.CreateBranch(b); err != nil { + return err + } + } + + return nil } const ( diff --git a/repository_test.go b/repository_test.go index 4765a53..c98e2ac 100644 --- a/repository_test.go +++ b/repository_test.go @@ -244,6 +244,119 @@ func (s *RepositorySuite) TestDeleteRemote(c *C) { c.Assert(alt, IsNil) } +func (s *RepositorySuite) TestCreateBranchAndBranch(c *C) { + r, _ := Init(memory.NewStorage(), nil) + testBranch := &config.Branch{ + Name: "foo", + Remote: "origin", + Merge: "refs/heads/foo", + } + err := r.CreateBranch(testBranch) + + c.Assert(err, IsNil) + cfg, err := r.Config() + c.Assert(err, IsNil) + c.Assert(len(cfg.Branches), Equals, 1) + branch := cfg.Branches["foo"] + c.Assert(branch.Name, Equals, testBranch.Name) + c.Assert(branch.Remote, Equals, testBranch.Remote) + c.Assert(branch.Merge, Equals, testBranch.Merge) + + branch, err = r.Branch("foo") + c.Assert(err, IsNil) + c.Assert(branch.Name, Equals, testBranch.Name) + c.Assert(branch.Remote, Equals, testBranch.Remote) + c.Assert(branch.Merge, Equals, testBranch.Merge) +} + +func (s *RepositorySuite) TestCreateBranchUnmarshal(c *C) { + r, _ := Init(memory.NewStorage(), nil) + + expected := []byte(`[core] + bare = true +[remote "foo"] + url = http://foo/foo.git + fetch = +refs/heads/*:refs/remotes/foo/* +[branch "foo"] + remote = origin + merge = refs/heads/foo +[branch "master"] + remote = origin + merge = refs/heads/master +`) + + _, err := r.CreateRemote(&config.RemoteConfig{ + Name: "foo", + URLs: []string{"http://foo/foo.git"}, + }) + c.Assert(err, IsNil) + testBranch1 := &config.Branch{ + Name: "master", + Remote: "origin", + Merge: "refs/heads/master", + } + testBranch2 := &config.Branch{ + Name: "foo", + Remote: "origin", + Merge: "refs/heads/foo", + } + err = r.CreateBranch(testBranch1) + err = r.CreateBranch(testBranch2) + + c.Assert(err, IsNil) + cfg, err := r.Config() + c.Assert(err, IsNil) + marshaled, err := cfg.Marshal() + c.Assert(string(expected), Equals, string(marshaled)) +} + +func (s *RepositorySuite) TestBranchInvalid(c *C) { + r, _ := Init(memory.NewStorage(), nil) + branch, err := r.Branch("foo") + + c.Assert(err, NotNil) + c.Assert(branch, IsNil) +} + +func (s *RepositorySuite) TestCreateBranchInvalid(c *C) { + r, _ := Init(memory.NewStorage(), nil) + err := r.CreateBranch(&config.Branch{}) + + c.Assert(err, NotNil) + + testBranch := &config.Branch{ + Name: "foo", + Remote: "origin", + Merge: "refs/heads/foo", + } + err = r.CreateBranch(testBranch) + c.Assert(err, IsNil) + err = r.CreateBranch(testBranch) + c.Assert(err, NotNil) +} + +func (s *RepositorySuite) TestDeleteBranch(c *C) { + r, _ := Init(memory.NewStorage(), nil) + testBranch := &config.Branch{ + Name: "foo", + Remote: "origin", + Merge: "refs/heads/foo", + } + err := r.CreateBranch(testBranch) + + c.Assert(err, IsNil) + + err = r.DeleteBranch("foo") + c.Assert(err, IsNil) + + b, err := r.Branch("foo") + c.Assert(err, Equals, ErrBranchNotFound) + c.Assert(b, IsNil) + + err = r.DeleteBranch("foo") + c.Assert(err, Equals, ErrBranchNotFound) +} + func (s *RepositorySuite) TestPlainInit(c *C) { dir, err := ioutil.TempDir("", "plain-init") c.Assert(err, IsNil) @@ -447,6 +560,10 @@ func (s *RepositorySuite) TestPlainClone(c *C) { remotes, err := r.Remotes() c.Assert(err, IsNil) c.Assert(remotes, HasLen, 1) + cfg, err := r.Config() + c.Assert(err, IsNil) + c.Assert(cfg.Branches, HasLen, 1) + c.Assert(cfg.Branches["master"].Name, Equals, "master") } func (s *RepositorySuite) TestPlainCloneContext(c *C) { @@ -480,6 +597,7 @@ func (s *RepositorySuite) TestPlainCloneWithRecurseSubmodules(c *C) { cfg, err := r.Config() c.Assert(err, IsNil) c.Assert(cfg.Remotes, HasLen, 1) + c.Assert(cfg.Branches, HasLen, 1) c.Assert(cfg.Submodules, HasLen, 2) } @@ -615,6 +733,8 @@ func (s *RepositorySuite) TestCloneConfig(c *C) { c.Assert(cfg.Remotes, HasLen, 1) c.Assert(cfg.Remotes["origin"].Name, Equals, "origin") c.Assert(cfg.Remotes["origin"].URLs, HasLen, 1) + c.Assert(cfg.Branches, HasLen, 1) + c.Assert(cfg.Branches["master"].Name, Equals, "master") } func (s *RepositorySuite) TestCloneSingleBranchAndNonHEAD(c *C) { @@ -636,6 +756,13 @@ func (s *RepositorySuite) TestCloneSingleBranchAndNonHEAD(c *C) { c.Assert(err, IsNil) c.Assert(remotes, HasLen, 1) + cfg, err := r.Config() + c.Assert(err, IsNil) + c.Assert(cfg.Branches, HasLen, 1) + c.Assert(cfg.Branches["branch"].Name, Equals, "branch") + c.Assert(cfg.Branches["branch"].Remote, Equals, "origin") + c.Assert(cfg.Branches["branch"].Merge, Equals, plumbing.ReferenceName("refs/heads/branch")) + head, err = r.Reference(plumbing.HEAD, false) c.Assert(err, IsNil) c.Assert(head, NotNil) @@ -672,6 +799,13 @@ func (s *RepositorySuite) TestCloneSingleBranch(c *C) { c.Assert(err, IsNil) c.Assert(remotes, HasLen, 1) + cfg, err := r.Config() + c.Assert(err, IsNil) + c.Assert(cfg.Branches, HasLen, 1) + c.Assert(cfg.Branches["master"].Name, Equals, "master") + c.Assert(cfg.Branches["master"].Remote, Equals, "origin") + c.Assert(cfg.Branches["master"].Merge, Equals, plumbing.ReferenceName("refs/heads/master")) + head, err = r.Reference(plumbing.HEAD, false) c.Assert(err, IsNil) c.Assert(head, NotNil) @@ -698,6 +832,10 @@ func (s *RepositorySuite) TestCloneDetachedHEAD(c *C) { }) c.Assert(err, IsNil) + cfg, err := r.Config() + c.Assert(err, IsNil) + c.Assert(cfg.Branches, HasLen, 0) + head, err := r.Reference(plumbing.HEAD, false) c.Assert(err, IsNil) c.Assert(head, NotNil) @@ -721,6 +859,10 @@ func (s *RepositorySuite) TestCloneDetachedHEADAndShallow(c *C) { c.Assert(err, IsNil) + cfg, err := r.Config() + c.Assert(err, IsNil) + c.Assert(cfg.Branches, HasLen, 0) + head, err := r.Reference(plumbing.HEAD, false) c.Assert(err, IsNil) c.Assert(head, NotNil) @@ -742,6 +884,10 @@ func (s *RepositorySuite) TestCloneDetachedHEADAnnotatedTag(c *C) { }) c.Assert(err, IsNil) + cfg, err := r.Config() + c.Assert(err, IsNil) + c.Assert(cfg.Branches, HasLen, 0) + head, err := r.Reference(plumbing.HEAD, false) c.Assert(err, IsNil) c.Assert(head, NotNil) |