aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSantiago M. Mola <santi@mola.io>2016-12-19 23:36:44 +0100
committerMáximo Cuadros <mcuadros@gmail.com>2016-12-19 23:36:44 +0100
commit90d67bb648ae32d5b1a0f7b1af011da6dfb24315 (patch)
treefc8c14e82974be6ff49e842328ec3206ebf1b4c2
parent725ade0de6f60549e65cc4d94094b1f5ed48587f (diff)
downloadgo-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.go8
-rw-r--r--examples/common_test.go23
-rw-r--r--examples/push/main.go22
-rw-r--r--options.go30
-rw-r--r--plumbing/protocol/packp/report_status.go29
-rw-r--r--plumbing/protocol/packp/report_status_test.go19
-rw-r--r--plumbing/transport/internal/common/common.go4
-rw-r--r--plumbing/transport/test/send_pack.go53
-rw-r--r--remote.go233
-rw-r--r--remote_test.go109
-rw-r--r--repository.go14
-rw-r--r--repository_test.go54
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")
+}
diff --git a/options.go b/options.go
index 31ff6e8..95584c2 100644
--- a/options.go
+++ b/options.go
@@ -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)
}
diff --git a/remote.go b/remote.go
index f0fb82c..102a764 100644
--- a/remote.go
+++ b/remote.go
@@ -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()