diff options
author | Santiago M. Mola <santi@mola.io> | 2016-12-19 23:36:44 +0100 |
---|---|---|
committer | Máximo Cuadros <mcuadros@gmail.com> | 2016-12-19 23:36:44 +0100 |
commit | 90d67bb648ae32d5b1a0f7b1af011da6dfb24315 (patch) | |
tree | fc8c14e82974be6ff49e842328ec3206ebf1b4c2 | |
parent | 725ade0de6f60549e65cc4d94094b1f5ed48587f (diff) | |
download | go-git-90d67bb648ae32d5b1a0f7b1af011da6dfb24315.tar.gz |
remote: add Push (#178)
* remote: add Push.
* add Push method to Remote.
* add method Push to Repository.
* examples: add push example.
* requested changes
* add tests, fixes
-rw-r--r-- | config/config.go | 8 | ||||
-rw-r--r-- | examples/common_test.go | 23 | ||||
-rw-r--r-- | examples/push/main.go | 22 | ||||
-rw-r--r-- | options.go | 30 | ||||
-rw-r--r-- | plumbing/protocol/packp/report_status.go | 29 | ||||
-rw-r--r-- | plumbing/protocol/packp/report_status_test.go | 19 | ||||
-rw-r--r-- | plumbing/transport/internal/common/common.go | 4 | ||||
-rw-r--r-- | plumbing/transport/test/send_pack.go | 53 | ||||
-rw-r--r-- | remote.go | 233 | ||||
-rw-r--r-- | remote_test.go | 109 | ||||
-rw-r--r-- | repository.go | 14 | ||||
-rw-r--r-- | repository_test.go | 54 |
12 files changed, 565 insertions, 33 deletions
diff --git a/config/config.go b/config/config.go index dc76571..a2b5012 100644 --- a/config/config.go +++ b/config/config.go @@ -7,8 +7,10 @@ import ( ) const ( - // DefaultRefSpec is the default refspec used, when none is given - DefaultRefSpec = "+refs/heads/*:refs/remotes/%s/*" + // DefaultFetchRefSpec is the default refspec used for fetch. + DefaultFetchRefSpec = "+refs/heads/*:refs/remotes/%s/*" + // DefaultPushRefSpec is the default refspec used for push. + DefaultPushRefSpec = "refs/heads/*:refs/heads/*" ) // ConfigStorer generic storage of Config object @@ -72,7 +74,7 @@ func (c *RemoteConfig) Validate() error { } if len(c.Fetch) == 0 { - c.Fetch = []RefSpec{RefSpec(fmt.Sprintf(DefaultRefSpec, c.Name))} + c.Fetch = []RefSpec{RefSpec(fmt.Sprintf(DefaultFetchRefSpec, c.Name))} } return nil diff --git a/examples/common_test.go b/examples/common_test.go index e75b492..9ec1f05 100644 --- a/examples/common_test.go +++ b/examples/common_test.go @@ -2,6 +2,7 @@ package examples import ( "flag" + "fmt" "go/build" "io/ioutil" "os" @@ -20,6 +21,7 @@ var args = map[string][]string{ "clone": []string{defaultURL, tempFolder()}, "progress": []string{defaultURL, tempFolder()}, "open": []string{filepath.Join(cloneRepository(defaultURL, tempFolder()), ".git")}, + "push": []string{setEmptyRemote(filepath.Join(cloneRepository(defaultURL, tempFolder()), ".git"))}, } var ignored = map[string]bool{ @@ -85,6 +87,27 @@ func cloneRepository(url, folder string) string { return folder } +func createBareRepository(dir string) string { + cmd := exec.Command("git", "init", "--bare", dir) + err := cmd.Run() + CheckIfError(err) + + return dir +} + +func setEmptyRemote(dir string) string { + remote := createBareRepository(tempFolder()) + setRemote(dir, fmt.Sprintf("file://%s", remote)) + return dir +} + +func setRemote(local, remote string) { + cmd := exec.Command("git", "remote", "set-url", "origin", remote) + cmd.Dir = local + err := cmd.Run() + CheckIfError(err) +} + func testExample(t *testing.T, name, example string) { cmd := exec.Command("go", append([]string{ "run", filepath.Join(example), diff --git a/examples/push/main.go b/examples/push/main.go new file mode 100644 index 0000000..9a30599 --- /dev/null +++ b/examples/push/main.go @@ -0,0 +1,22 @@ +package main + +import ( + "fmt" + "os" + + "gopkg.in/src-d/go-git.v4" + . "gopkg.in/src-d/go-git.v4/examples" +) + +func main() { + CheckArgs("<repo path>") + repoPath := os.Args[1] + + repo, err := git.NewFilesystemRepository(repoPath) + CheckIfError(err) + + err = repo.Push(&git.PushOptions{}) + CheckIfError(err) + + fmt.Print("pushed") +} @@ -100,3 +100,33 @@ func (o *FetchOptions) Validate() error { return nil } + +// PushOptions describe 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 +} + +// Validate validate the fields and set the default values +func (o *PushOptions) Validate() error { + if o.RemoteName == "" { + o.RemoteName = DefaultRemoteName + } + + if len(o.RefSpecs) == 0 { + o.RefSpecs = []config.RefSpec{ + config.RefSpec(config.DefaultPushRefSpec), + } + } + + for _, r := range o.RefSpecs { + if !r.IsValid() { + return ErrInvalidRefSpec + } + } + + return nil +} diff --git a/plumbing/protocol/packp/report_status.go b/plumbing/protocol/packp/report_status.go index ead4bb6..29c1a4c 100644 --- a/plumbing/protocol/packp/report_status.go +++ b/plumbing/protocol/packp/report_status.go @@ -26,9 +26,19 @@ func NewReportStatus() *ReportStatus { return &ReportStatus{} } -// Ok returns true if the report status reported no error. -func (s *ReportStatus) Ok() bool { - return s.UnpackStatus == ok +// Error returns the first error if any. +func (s *ReportStatus) Error() error { + if s.UnpackStatus != ok { + return fmt.Errorf("unpack error: %s", s.UnpackStatus) + } + + for _, s := range s.CommandStatuses { + if err := s.Error(); err != nil { + return err + } + } + + return nil } // Encode writes the report status to a writer. @@ -135,14 +145,19 @@ type CommandStatus struct { Status string } -// Ok returns true if the command status reported no error. -func (s *CommandStatus) Ok() bool { - return s.Status == ok +// Error returns the error, if any. +func (s *CommandStatus) Error() error { + if s.Status == ok { + return nil + } + + return fmt.Errorf("command error on %s: %s", + s.ReferenceName.String(), s.Status) } func (s *CommandStatus) encode(w io.Writer) error { e := pktline.NewEncoder(w) - if s.Ok() { + if s.Error() == nil { return e.Encodef("ok %s\n", s.ReferenceName.String()) } diff --git a/plumbing/protocol/packp/report_status_test.go b/plumbing/protocol/packp/report_status_test.go index 168d25b..1c3fa81 100644 --- a/plumbing/protocol/packp/report_status_test.go +++ b/plumbing/protocol/packp/report_status_test.go @@ -13,22 +13,25 @@ type ReportStatusSuite struct{} var _ = Suite(&ReportStatusSuite{}) -func (s *ReportStatusSuite) TestOk(c *C) { +func (s *ReportStatusSuite) TestError(c *C) { rs := NewReportStatus() rs.UnpackStatus = "ok" - c.Assert(rs.Ok(), Equals, true) + c.Assert(rs.Error(), IsNil) rs.UnpackStatus = "OK" - c.Assert(rs.Ok(), Equals, false) + c.Assert(rs.Error(), ErrorMatches, "unpack error: OK") rs.UnpackStatus = "" - c.Assert(rs.Ok(), Equals, false) + c.Assert(rs.Error(), ErrorMatches, "unpack error: ") + + cs := &CommandStatus{ReferenceName: plumbing.ReferenceName("ref")} + rs.UnpackStatus = "ok" + rs.CommandStatuses = append(rs.CommandStatuses, cs) - cs := &CommandStatus{} cs.Status = "ok" - c.Assert(cs.Ok(), Equals, true) + c.Assert(rs.Error(), IsNil) cs.Status = "OK" - c.Assert(cs.Ok(), Equals, false) + c.Assert(rs.Error(), ErrorMatches, "command error on ref: OK") cs.Status = "" - c.Assert(cs.Ok(), Equals, false) + c.Assert(rs.Error(), ErrorMatches, "command error on ref: ") } func (s *ReportStatusSuite) testEncodeDecodeOk(c *C, rs *ReportStatus, lines ...string) { diff --git a/plumbing/transport/internal/common/common.go b/plumbing/transport/internal/common/common.go index f6d1249..17c473e 100644 --- a/plumbing/transport/internal/common/common.go +++ b/plumbing/transport/internal/common/common.go @@ -281,8 +281,8 @@ func (s *session) SendPack(req *packp.ReferenceUpdateRequest) (*packp.ReportStat return nil, err } - if !report.Ok() { - return report, fmt.Errorf("report status: %s", report.UnpackStatus) + if err := report.Error(); err != nil { + return report, err } return report, s.Command.Wait() diff --git a/plumbing/transport/test/send_pack.go b/plumbing/transport/test/send_pack.go index 8cb549d..f880588 100644 --- a/plumbing/transport/test/send_pack.go +++ b/plumbing/transport/test/send_pack.go @@ -131,7 +131,7 @@ func (s *SendPackSuite) TestFullSendPackOnNonEmpty(c *C) { fixture := fixtures.Basic().ByTag("packfile").One() req := packp.NewReferenceUpdateRequest() req.Commands = []*packp.Command{ - {"refs/heads/master", plumbing.ZeroHash, fixture.Head}, + {"refs/heads/master", fixture.Head, fixture.Head}, } s.sendPack(c, endpoint, req, fixture, full) s.checkRemoteHead(c, endpoint, fixture.Head) @@ -143,7 +143,7 @@ func (s *SendPackSuite) TestSendPackOnNonEmpty(c *C) { fixture := fixtures.Basic().ByTag("packfile").One() req := packp.NewReferenceUpdateRequest() req.Commands = []*packp.Command{ - {"refs/heads/master", plumbing.ZeroHash, fixture.Head}, + {"refs/heads/master", fixture.Head, fixture.Head}, } s.sendPack(c, endpoint, req, fixture, full) s.checkRemoteHead(c, endpoint, fixture.Head) @@ -155,7 +155,7 @@ func (s *SendPackSuite) TestSendPackOnNonEmptyWithReportStatus(c *C) { fixture := fixtures.Basic().ByTag("packfile").One() req := packp.NewReferenceUpdateRequest() req.Commands = []*packp.Command{ - {"refs/heads/master", plumbing.ZeroHash, fixture.Head}, + {"refs/heads/master", fixture.Head, fixture.Head}, } req.Capabilities.Set(capability.ReportStatus) @@ -163,10 +163,30 @@ func (s *SendPackSuite) TestSendPackOnNonEmptyWithReportStatus(c *C) { s.checkRemoteHead(c, endpoint, fixture.Head) } -func (s *SendPackSuite) sendPack(c *C, ep transport.Endpoint, - req *packp.ReferenceUpdateRequest, fixture *fixtures.Fixture, - callAdvertisedReferences bool) { +func (s *SendPackSuite) TestSendPackOnNonEmptyWithReportStatusWithError(c *C) { + endpoint := s.Endpoint + full := false + fixture := fixtures.Basic().ByTag("packfile").One() + req := packp.NewReferenceUpdateRequest() + req.Commands = []*packp.Command{ + {"refs/heads/master", plumbing.ZeroHash, fixture.Head}, + } + req.Capabilities.Set(capability.ReportStatus) + + report, err := s.sendPackNoCheck(c, endpoint, req, fixture, full) + //XXX: Recent git versions return "failed to update ref", while older + // (>=1.9) return "failed to lock". + c.Assert(err, ErrorMatches, ".*(failed to update ref|failed to lock).*") + c.Assert(report.UnpackStatus, Equals, "ok") + c.Assert(len(report.CommandStatuses), Equals, 1) + c.Assert(report.CommandStatuses[0].ReferenceName, Equals, plumbing.ReferenceName("refs/heads/master")) + c.Assert(report.CommandStatuses[0].Status, Matches, "(failed to update ref|failed to lock)") + s.checkRemoteHead(c, endpoint, fixture.Head) +} +func (s *SendPackSuite) sendPackNoCheck(c *C, ep transport.Endpoint, + req *packp.ReferenceUpdateRequest, fixture *fixtures.Fixture, + callAdvertisedReferences bool) (*packp.ReportStatus, error) { url := "" if fixture != nil { url = fixture.URL @@ -193,11 +213,28 @@ func (s *SendPackSuite) sendPack(c *C, ep transport.Endpoint, req.Packfile = s.emptyPackfile() } - report, err := r.SendPack(req) + return r.SendPack(req) +} + +func (s *SendPackSuite) sendPack(c *C, ep transport.Endpoint, + req *packp.ReferenceUpdateRequest, fixture *fixtures.Fixture, + callAdvertisedReferences bool) { + + url := "" + if fixture != nil { + url = fixture.URL + } + + comment := Commentf( + "failed with ep=%s fixture=%s callAdvertisedReferences=%s", + ep.String(), url, callAdvertisedReferences, + ) + report, err := s.sendPackNoCheck(c, ep, req, fixture, callAdvertisedReferences) + c.Assert(err, IsNil, comment) if req.Capabilities.Supports(capability.ReportStatus) { c.Assert(report, NotNil, comment) - c.Assert(report.Ok(), Equals, true, comment) + c.Assert(report.Error(), IsNil, comment) } else { c.Assert(report, IsNil, comment) } @@ -8,9 +8,11 @@ import ( "gopkg.in/src-d/go-git.v4/config" "gopkg.in/src-d/go-git.v4/plumbing" "gopkg.in/src-d/go-git.v4/plumbing/format/packfile" + "gopkg.in/src-d/go-git.v4/plumbing/object" "gopkg.in/src-d/go-git.v4/plumbing/protocol/packp" "gopkg.in/src-d/go-git.v4/plumbing/protocol/packp/capability" "gopkg.in/src-d/go-git.v4/plumbing/protocol/packp/sideband" + "gopkg.in/src-d/go-git.v4/plumbing/revlist" "gopkg.in/src-d/go-git.v4/plumbing/storer" "gopkg.in/src-d/go-git.v4/plumbing/transport" "gopkg.in/src-d/go-git.v4/plumbing/transport/client" @@ -49,6 +51,72 @@ func (r *Remote) Fetch(o *FetchOptions) error { return err } +// Push performs a push to the remote. Returns NoErrAlreadyUpToDate if the +// remote was already up-to-date. +// +// TODO: Support deletes. +// TODO: Support pushing tags. +// TODO: Check if force update is given, otherwise reject non-fast forward. +func (r *Remote) Push(o *PushOptions) (err error) { + if o.RemoteName == "" { + o.RemoteName = r.c.Name + } + + if err := o.Validate(); err != nil { + return err + } + + if o.RemoteName != r.c.Name { + return fmt.Errorf("remote names don't match: %s != %s", o.RemoteName, r.c.Name) + } + + s, err := newSendPackSession(r.c.URL) + if err != nil { + return err + } + + ar, err := s.AdvertisedReferences() + if err != nil { + return err + } + + remoteRefs, err := ar.AllReferences() + if err != nil { + return err + } + + req := packp.NewReferenceUpdateRequestFromCapabilities(ar.Capabilities) + if err := r.addReferencesToUpdate(o.RefSpecs, remoteRefs, req); err != nil { + return err + } + + if len(req.Commands) == 0 { + return NoErrAlreadyUpToDate + } + + commits, err := commitsToPush(r.s, req.Commands) + if err != nil { + return err + } + + haves, err := referencesToHashes(remoteRefs) + if err != nil { + return err + } + + hashesToPush, err := revlist.Objects(r.s, commits, haves) + if err != nil { + return err + } + + rs, err := pushHashes(s, r.s, req, hashesToPush) + if err != nil { + return err + } + + return rs.Error() +} + func (r *Remote) fetch(o *FetchOptions) (refs storer.ReferenceStorer, err error) { if o.RemoteName == "" { o.RemoteName = r.c.Name @@ -62,7 +130,7 @@ func (r *Remote) fetch(o *FetchOptions) (refs storer.ReferenceStorer, err error) o.RefSpecs = r.c.Fetch } - s, err := r.newFetchPackSession() + s, err := newFetchPackSession(r.c.URL) if err != nil { return nil, err } @@ -105,18 +173,36 @@ func (r *Remote) fetch(o *FetchOptions) (refs storer.ReferenceStorer, err error) return remoteRefs, err } -func (r *Remote) newFetchPackSession() (transport.FetchPackSession, error) { - ep, err := transport.NewEndpoint(r.c.URL) +func newFetchPackSession(url string) (transport.FetchPackSession, error) { + c, ep, err := newClient(url) if err != nil { return nil, err } - c, err := client.NewClient(ep) + return c.NewFetchPackSession(ep) +} + +func newSendPackSession(url string) (transport.SendPackSession, error) { + c, ep, err := newClient(url) if err != nil { return nil, err } - return c.NewFetchPackSession(ep) + return c.NewSendPackSession(ep) +} + +func newClient(url string) (transport.Client, transport.Endpoint, error) { + ep, err := transport.NewEndpoint(url) + if err != nil { + return nil, transport.Endpoint{}, err + } + + c, err := client.NewClient(ep) + if err != nil { + return nil, transport.Endpoint{}, err + } + + return c, ep, err } func (r *Remote) fetchPack(o *FetchOptions, s transport.FetchPackSession, @@ -142,6 +228,75 @@ func (r *Remote) fetchPack(o *FetchOptions, s transport.FetchPackSession, return err } +func (r *Remote) addReferencesToUpdate(refspecs []config.RefSpec, + remoteRefs storer.ReferenceStorer, + req *packp.ReferenceUpdateRequest) error { + + for _, rs := range refspecs { + iter, err := r.s.IterReferences() + if err != nil { + return err + } + + err = iter.ForEach(func(ref *plumbing.Reference) error { + return r.addReferenceIfRefSpecMatches( + rs, remoteRefs, ref, req, + ) + }) + if err != nil { + return err + } + } + + return nil +} + +func (r *Remote) addReferenceIfRefSpecMatches(rs config.RefSpec, + remoteRefs storer.ReferenceStorer, localRef *plumbing.Reference, + req *packp.ReferenceUpdateRequest) error { + + if localRef.Type() != plumbing.HashReference { + return nil + } + + if !rs.Match(localRef.Name()) { + return nil + } + + dstName := rs.Dst(localRef.Name()) + oldHash := plumbing.ZeroHash + newHash := localRef.Hash() + + iter, err := remoteRefs.IterReferences() + if err != nil { + return err + } + + err = iter.ForEach(func(remoteRef *plumbing.Reference) error { + if remoteRef.Type() != plumbing.HashReference { + return nil + } + + if dstName != remoteRef.Name() { + return nil + } + + oldHash = remoteRef.Hash() + return nil + }) + + if oldHash == newHash { + return nil + } + + req.Commands = append(req.Commands, &packp.Command{ + Name: dstName, + Old: oldHash, + New: newHash, + }) + return nil +} + func getHaves(localRefs storer.ReferenceStorer) ([]plumbing.Hash, error) { iter, err := localRefs.IterReferences() if err != nil { @@ -337,6 +492,74 @@ func (r *Remote) buildFetchedTags(refs storer.ReferenceStorer) error { }) } +func commitsToPush(s storer.EncodedObjectStorer, commands []*packp.Command) ([]*object.Commit, error) { + var commits []*object.Commit + for _, cmd := range commands { + if cmd.New == plumbing.ZeroHash { + continue + } + + c, err := object.GetCommit(s, cmd.New) + if err != nil { + return nil, err + } + + commits = append(commits, c) + } + + return commits, nil +} + +func referencesToHashes(refs storer.ReferenceStorer) ([]plumbing.Hash, error) { + iter, err := refs.IterReferences() + if err != nil { + return nil, err + } + + var hs []plumbing.Hash + err = iter.ForEach(func(ref *plumbing.Reference) error { + if ref.Type() != plumbing.HashReference { + return nil + } + + hs = append(hs, ref.Hash()) + return nil + }) + if err != nil { + return nil, err + } + + return hs, nil +} + +func pushHashes(sess transport.SendPackSession, sto storer.EncodedObjectStorer, + req *packp.ReferenceUpdateRequest, hs []plumbing.Hash) (*packp.ReportStatus, error) { + + rd, wr := io.Pipe() + req.Packfile = rd + done := make(chan error) + go func() { + e := packfile.NewEncoder(wr, sto, false) + if _, err := e.Encode(hs); err != nil { + done <- wr.CloseWithError(err) + return + } + + done <- wr.Close() + }() + + rs, err := sess.SendPack(req) + if err != nil { + return nil, err + } + + if err := <-done; err != nil { + return nil, err + } + + return rs, nil +} + func (r *Remote) updateShallow(o *FetchOptions, resp *packp.UploadPackResponse) error { if o.Depth == 0 { return nil diff --git a/remote_test.go b/remote_test.go index 47c8180..02ff690 100644 --- a/remote_test.go +++ b/remote_test.go @@ -2,11 +2,13 @@ package git import ( "bytes" + "fmt" "io" "io/ioutil" "os" "gopkg.in/src-d/go-git.v4/config" + "gopkg.in/src-d/go-git.v4/fixtures" "gopkg.in/src-d/go-git.v4/plumbing" "gopkg.in/src-d/go-git.v4/plumbing/storer" "gopkg.in/src-d/go-git.v4/storage/filesystem" @@ -194,3 +196,110 @@ func (s *RemoteSuite) TestString(c *C) { "foo\thttps://github.com/git-fixtures/basic.git (push)", ) } + +func (s *RemoteSuite) TestPushToEmptyRepository(c *C) { + srcFs := fixtures.Basic().One().DotGit() + sto, err := filesystem.NewStorage(srcFs) + c.Assert(err, IsNil) + + dstFs := fixtures.ByTag("empty").One().DotGit() + url := fmt.Sprintf("file://%s", dstFs.Base()) + + r := newRemote(sto, nil, &config.RemoteConfig{ + Name: DefaultRemoteName, + URL: url, + }) + + rs := config.RefSpec("refs/heads/*:refs/heads/*") + err = r.Push(&PushOptions{ + RefSpecs: []config.RefSpec{rs}, + }) + c.Assert(err, IsNil) + + dstSto, err := filesystem.NewStorage(dstFs) + c.Assert(err, IsNil) + dstRepo, err := NewRepository(dstSto) + c.Assert(err, IsNil) + + iter, err := sto.IterReferences() + c.Assert(err, IsNil) + err = iter.ForEach(func(ref *plumbing.Reference) error { + if !ref.IsBranch() { + return nil + } + + dstRef, err := dstRepo.Reference(ref.Name(), true) + c.Assert(err, IsNil, Commentf("ref: %s", ref.String())) + c.Assert(dstRef, DeepEquals, ref) + + return nil + }) + c.Assert(err, IsNil) +} + +func (s *RemoteSuite) TestPushNoErrAlreadyUpToDate(c *C) { + f := fixtures.Basic().One() + sto, err := filesystem.NewStorage(f.DotGit()) + c.Assert(err, IsNil) + url := fmt.Sprintf("file://%s", f.DotGit().Base()) + r := newRemote(sto, nil, &config.RemoteConfig{ + Name: DefaultRemoteName, + URL: url, + }) + + rs := config.RefSpec("refs/heads/*:refs/heads/*") + err = r.Push(&PushOptions{ + RefSpecs: []config.RefSpec{rs}, + }) + c.Assert(err, Equals, NoErrAlreadyUpToDate) +} + +func (s *RemoteSuite) TestPushInvalidEndpoint(c *C) { + r := newRemote(nil, nil, &config.RemoteConfig{Name: "foo", URL: "qux"}) + err := r.Push(&PushOptions{}) + c.Assert(err, ErrorMatches, ".*invalid endpoint.*") +} + +func (s *RemoteSuite) TestPushNonExistentEndpoint(c *C) { + r := newRemote(nil, nil, &config.RemoteConfig{Name: "foo", URL: "ssh://non-existent/foo.git"}) + err := r.Push(&PushOptions{}) + c.Assert(err, NotNil) +} + +func (s *RemoteSuite) TestPushInvalidSchemaEndpoint(c *C) { + r := newRemote(nil, nil, &config.RemoteConfig{Name: "foo", URL: "qux://foo"}) + err := r.Push(&PushOptions{}) + c.Assert(err, ErrorMatches, ".*unsupported scheme.*") +} + +func (s *RemoteSuite) TestPushInvalidFetchOptions(c *C) { + r := newRemote(nil, nil, &config.RemoteConfig{Name: "foo", URL: "qux://foo"}) + invalid := config.RefSpec("^*$ñ") + err := r.Push(&PushOptions{RefSpecs: []config.RefSpec{invalid}}) + c.Assert(err, Equals, ErrInvalidRefSpec) +} + +func (s *RemoteSuite) TestPushInvalidRefSpec(c *C) { + r := newRemote(nil, nil, &config.RemoteConfig{ + Name: DefaultRemoteName, + URL: "file:///some-url", + }) + + rs := config.RefSpec("^*$**") + err := r.Push(&PushOptions{ + RefSpecs: []config.RefSpec{rs}, + }) + c.Assert(err, ErrorMatches, ".*invalid.*") +} + +func (s *RemoteSuite) TestPushWrongRemoteName(c *C) { + r := newRemote(nil, nil, &config.RemoteConfig{ + Name: DefaultRemoteName, + URL: "file:///some-url", + }) + + err := r.Push(&PushOptions{ + RemoteName: "other-remote", + }) + c.Assert(err, ErrorMatches, ".*remote names don't match.*") +} diff --git a/repository.go b/repository.go index b9afb9a..3c77188 100644 --- a/repository.go +++ b/repository.go @@ -346,6 +346,20 @@ func (r *Repository) Fetch(o *FetchOptions) error { return remote.Fetch(o) } +// Push pushes changes to a remote. +func (r *Repository) Push(o *PushOptions) error { + if err := o.Validate(); err != nil { + return err + } + + remote, err := r.Remote(o.RemoteName) + if err != nil { + return err + } + + return remote.Push(o) +} + // object.Commit return the commit with the given hash func (r *Repository) Commit(h plumbing.Hash) (*object.Commit, error) { return object.GetCommit(r.s, h) diff --git a/repository_test.go b/repository_test.go index 4d17dce..12ae858 100644 --- a/repository_test.go +++ b/repository_test.go @@ -15,6 +15,7 @@ import ( "gopkg.in/src-d/go-git.v4/storage/memory" . "gopkg.in/check.v1" + "gopkg.in/src-d/go-git.v4/storage/filesystem" ) type RepositorySuite struct { @@ -331,6 +332,59 @@ func (s *RepositorySuite) TestPullA(c *C) { c.Assert(branch.Hash().String(), Equals, "e8d3ffab552895c19b9fcf7aa264d277cde33881") } +func (s *RepositorySuite) TestPushToEmptyRepository(c *C) { + srcFs := fixtures.Basic().One().DotGit() + sto, err := filesystem.NewStorage(srcFs) + c.Assert(err, IsNil) + + dstFs := fixtures.ByTag("empty").One().DotGit() + url := fmt.Sprintf("file://%s", dstFs.Base()) + + r, err := NewRepository(sto) + c.Assert(err, IsNil) + + _, err = r.CreateRemote(&config.RemoteConfig{ + Name: "myremote", + URL: url, + }) + c.Assert(err, IsNil) + + err = r.Push(&PushOptions{RemoteName: "myremote"}) + c.Assert(err, IsNil) + + sto, err = filesystem.NewStorage(dstFs) + c.Assert(err, IsNil) + dstRepo, err := NewRepository(sto) + c.Assert(err, IsNil) + + iter, err := sto.IterReferences() + c.Assert(err, IsNil) + err = iter.ForEach(func(ref *plumbing.Reference) error { + if !ref.IsBranch() { + return nil + } + + dstRef, err := dstRepo.Reference(ref.Name(), true) + c.Assert(err, IsNil) + c.Assert(dstRef, DeepEquals, ref) + + return nil + }) + c.Assert(err, IsNil) +} + +func (s *RepositorySuite) TestPushNonExistentRemote(c *C) { + srcFs := fixtures.Basic().One().DotGit() + sto, err := filesystem.NewStorage(srcFs) + c.Assert(err, IsNil) + + r, err := NewRepository(sto) + c.Assert(err, IsNil) + + err = r.Push(&PushOptions{RemoteName: "myremote"}) + c.Assert(err, ErrorMatches, ".*remote not found.*") +} + func (s *RepositorySuite) TestIsEmpty(c *C) { r := NewMemoryRepository() |