aboutsummaryrefslogtreecommitdiffstats
path: root/plumbing/client
diff options
context:
space:
mode:
authorMáximo Cuadros <mcuadros@gmail.com>2016-11-08 23:46:38 +0100
committerGitHub <noreply@github.com>2016-11-08 23:46:38 +0100
commitac095bb12c4d29722b60ba9f20590fa7cfa6bc7d (patch)
tree223f36f336ba3414b1e45cac8af6c4744a5d7ef6 /plumbing/client
parente523701393598f4fa241dd407af9ff8925507a1a (diff)
downloadgo-git-ac095bb12c4d29722b60ba9f20590fa7cfa6bc7d.tar.gz
new plumbing package (#118)
* plumbing: now core was renamed to core, and formats and clients moved inside
Diffstat (limited to 'plumbing/client')
-rw-r--r--plumbing/client/common.go48
-rw-r--r--plumbing/client/common/common.go219
-rw-r--r--plumbing/client/common/common_test.go126
-rw-r--r--plumbing/client/common_test.go85
-rw-r--r--plumbing/client/http/common.go77
-rw-r--r--plumbing/client/http/common_test.go52
-rw-r--r--plumbing/client/http/git_upload_pack.go186
-rw-r--r--plumbing/client/http/git_upload_pack_test.go135
-rw-r--r--plumbing/client/ssh/auth_method.go159
-rw-r--r--plumbing/client/ssh/auth_method_test.go94
-rw-r--r--plumbing/client/ssh/git_upload_pack.go315
-rw-r--r--plumbing/client/ssh/git_upload_pack_test.go144
12 files changed, 1640 insertions, 0 deletions
diff --git a/plumbing/client/common.go b/plumbing/client/common.go
new file mode 100644
index 0000000..6a99339
--- /dev/null
+++ b/plumbing/client/common.go
@@ -0,0 +1,48 @@
+// Package clients includes the implementation for diferent transport protocols
+//
+// go-git needs the packfile and the refs of the repo. The
+// `NewGitUploadPackService` function returns an object that allows to
+// download them.
+//
+// go-git supports HTTP and SSH (see `Protocols`) for downloading the packfile
+// and the refs, but you can also install your own protocols (see
+// `InstallProtocol` below).
+//
+// Each protocol has its own implementation of
+// `NewGitUploadPackService`, but you should generally not use them
+// directly, use this package's `NewGitUploadPackService` instead.
+package clients
+
+import (
+ "fmt"
+
+ "gopkg.in/src-d/go-git.v4/plumbing/client/common"
+ "gopkg.in/src-d/go-git.v4/plumbing/client/http"
+ "gopkg.in/src-d/go-git.v4/plumbing/client/ssh"
+)
+
+type GitUploadPackServiceFactory func(common.Endpoint) common.GitUploadPackService
+
+// Protocols are the protocols supported by default.
+var Protocols = map[string]GitUploadPackServiceFactory{
+ "http": http.NewGitUploadPackService,
+ "https": http.NewGitUploadPackService,
+ "ssh": ssh.NewGitUploadPackService,
+}
+
+// InstallProtocol adds or modifies an existing protocol.
+func InstallProtocol(scheme string, f GitUploadPackServiceFactory) {
+ Protocols[scheme] = f
+}
+
+// NewGitUploadPackService returns the appropriate upload pack service
+// among of the set of known protocols: HTTP, SSH. See `InstallProtocol`
+// to add or modify protocols.
+func NewGitUploadPackService(endpoint common.Endpoint) (common.GitUploadPackService, error) {
+ f, ok := Protocols[endpoint.Scheme]
+ if !ok {
+ return nil, fmt.Errorf("unsupported scheme %q", endpoint.Scheme)
+ }
+
+ return f(endpoint), nil
+}
diff --git a/plumbing/client/common/common.go b/plumbing/client/common/common.go
new file mode 100644
index 0000000..97f78c4
--- /dev/null
+++ b/plumbing/client/common/common.go
@@ -0,0 +1,219 @@
+// Package common contains interfaces and non-specific protocol entities
+package common
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net/url"
+ "regexp"
+ "strings"
+
+ "gopkg.in/src-d/go-git.v4/plumbing"
+ "gopkg.in/src-d/go-git.v4/plumbing/format/packp"
+ "gopkg.in/src-d/go-git.v4/plumbing/format/packp/advrefs"
+ "gopkg.in/src-d/go-git.v4/plumbing/format/packp/pktline"
+ "gopkg.in/src-d/go-git.v4/plumbing/storer"
+ "gopkg.in/src-d/go-git.v4/storage/memory"
+)
+
+var (
+ ErrRepositoryNotFound = errors.New("repository not found")
+ ErrAuthorizationRequired = errors.New("authorization required")
+ ErrEmptyGitUploadPack = errors.New("empty git-upload-pack given")
+ ErrInvalidAuthMethod = errors.New("invalid auth method")
+)
+
+const GitUploadPackServiceName = "git-upload-pack"
+
+type GitUploadPackService interface {
+ Connect() error
+ SetAuth(AuthMethod) error
+ Info() (*GitUploadPackInfo, error)
+ Fetch(*GitUploadPackRequest) (io.ReadCloser, error)
+ Disconnect() error
+}
+
+type AuthMethod interface {
+ Name() string
+ String() string
+}
+
+type Endpoint url.URL
+
+var (
+ isSchemeRegExp = regexp.MustCompile("^[^:]+://")
+ scpLikeUrlRegExp = regexp.MustCompile("^(?P<user>[^@]+@)?(?P<host>[^:]+):/?(?P<path>.+)$")
+)
+
+func NewEndpoint(endpoint string) (Endpoint, error) {
+ endpoint = transformSCPLikeIfNeeded(endpoint)
+
+ u, err := url.Parse(endpoint)
+ if err != nil {
+ return Endpoint{}, plumbing.NewPermanentError(err)
+ }
+
+ if !u.IsAbs() {
+ return Endpoint{}, plumbing.NewPermanentError(fmt.Errorf(
+ "invalid endpoint: %s", endpoint,
+ ))
+ }
+
+ return Endpoint(*u), nil
+}
+
+func transformSCPLikeIfNeeded(endpoint string) string {
+ if !isSchemeRegExp.MatchString(endpoint) && scpLikeUrlRegExp.MatchString(endpoint) {
+ m := scpLikeUrlRegExp.FindStringSubmatch(endpoint)
+ return fmt.Sprintf("ssh://%s%s/%s", m[1], m[2], m[3])
+ }
+
+ return endpoint
+}
+
+func (e *Endpoint) String() string {
+ u := url.URL(*e)
+ return u.String()
+}
+
+type GitUploadPackInfo struct {
+ Capabilities *packp.Capabilities
+ Refs memory.ReferenceStorage
+}
+
+func NewGitUploadPackInfo() *GitUploadPackInfo {
+ return &GitUploadPackInfo{
+ Capabilities: packp.NewCapabilities(),
+ Refs: make(memory.ReferenceStorage, 0),
+ }
+}
+
+func (i *GitUploadPackInfo) Decode(r io.Reader) error {
+ d := advrefs.NewDecoder(r)
+ ar := advrefs.New()
+ if err := d.Decode(ar); err != nil {
+ if err == advrefs.ErrEmpty {
+ return plumbing.NewPermanentError(err)
+ }
+ return plumbing.NewUnexpectedError(err)
+ }
+
+ i.Capabilities = ar.Capabilities
+
+ if err := i.addRefs(ar); err != nil {
+ return plumbing.NewUnexpectedError(err)
+ }
+
+ return nil
+}
+
+func (i *GitUploadPackInfo) addRefs(ar *advrefs.AdvRefs) error {
+ for name, hash := range ar.References {
+ ref := plumbing.NewReferenceFromStrings(name, hash.String())
+ i.Refs.SetReference(ref)
+ }
+
+ return i.addSymbolicRefs(ar)
+}
+
+func (i *GitUploadPackInfo) addSymbolicRefs(ar *advrefs.AdvRefs) error {
+ if !hasSymrefs(ar) {
+ return nil
+ }
+
+ for _, symref := range ar.Capabilities.Get("symref").Values {
+ chunks := strings.Split(symref, ":")
+ if len(chunks) != 2 {
+ err := fmt.Errorf("bad number of `:` in symref value (%q)", symref)
+ return plumbing.NewUnexpectedError(err)
+ }
+ name := plumbing.ReferenceName(chunks[0])
+ target := plumbing.ReferenceName(chunks[1])
+ ref := plumbing.NewSymbolicReference(name, target)
+ i.Refs.SetReference(ref)
+ }
+
+ return nil
+}
+
+func hasSymrefs(ar *advrefs.AdvRefs) bool {
+ return ar.Capabilities.Supports("symref")
+}
+
+func (i *GitUploadPackInfo) Head() *plumbing.Reference {
+ ref, _ := storer.ResolveReference(i.Refs, plumbing.HEAD)
+ return ref
+}
+
+func (i *GitUploadPackInfo) String() string {
+ return string(i.Bytes())
+}
+
+func (i *GitUploadPackInfo) Bytes() []byte {
+ var buf bytes.Buffer
+ e := pktline.NewEncoder(&buf)
+
+ _ = e.EncodeString("# service=git-upload-pack\n")
+
+ // inserting a flush-pkt here violates the protocol spec, but some
+ // servers do it, like Github.com
+ e.Flush()
+
+ _ = e.Encodef("%s HEAD\x00%s\n", i.Head().Hash(), i.Capabilities.String())
+
+ for _, ref := range i.Refs {
+ if ref.Type() != plumbing.HashReference {
+ continue
+ }
+
+ _ = e.Encodef("%s %s\n", ref.Hash(), ref.Name())
+ }
+
+ e.Flush()
+
+ return buf.Bytes()
+}
+
+type GitUploadPackRequest struct {
+ Wants []plumbing.Hash
+ Haves []plumbing.Hash
+ Depth int
+}
+
+func (r *GitUploadPackRequest) Want(h ...plumbing.Hash) {
+ r.Wants = append(r.Wants, h...)
+}
+
+func (r *GitUploadPackRequest) Have(h ...plumbing.Hash) {
+ r.Haves = append(r.Haves, h...)
+}
+
+func (r *GitUploadPackRequest) String() string {
+ b, _ := ioutil.ReadAll(r.Reader())
+ return string(b)
+}
+
+func (r *GitUploadPackRequest) Reader() *strings.Reader {
+ var buf bytes.Buffer
+ e := pktline.NewEncoder(&buf)
+
+ for _, want := range r.Wants {
+ _ = e.Encodef("want %s\n", want)
+ }
+
+ for _, have := range r.Haves {
+ _ = e.Encodef("have %s\n", have)
+ }
+
+ if r.Depth != 0 {
+ _ = e.Encodef("deepen %d\n", r.Depth)
+ }
+
+ _ = e.Flush()
+ _ = e.EncodeString("done\n")
+
+ return strings.NewReader(buf.String())
+}
diff --git a/plumbing/client/common/common_test.go b/plumbing/client/common/common_test.go
new file mode 100644
index 0000000..cf4d871
--- /dev/null
+++ b/plumbing/client/common/common_test.go
@@ -0,0 +1,126 @@
+package common
+
+import (
+ "bytes"
+ "encoding/base64"
+ "testing"
+
+ "gopkg.in/src-d/go-git.v4/plumbing"
+ "gopkg.in/src-d/go-git.v4/plumbing/format/packp"
+
+ . "gopkg.in/check.v1"
+)
+
+func Test(t *testing.T) { TestingT(t) }
+
+type SuiteCommon struct{}
+
+var _ = Suite(&SuiteCommon{})
+
+func (s *SuiteCommon) TestNewEndpoint(c *C) {
+ e, err := NewEndpoint("ssh://git@github.com/user/repository.git")
+ c.Assert(err, IsNil)
+ c.Assert(e.String(), Equals, "ssh://git@github.com/user/repository.git")
+}
+
+func (s *SuiteCommon) TestNewEndpointSCPLike(c *C) {
+ e, err := NewEndpoint("git@github.com:user/repository.git")
+ c.Assert(err, IsNil)
+ c.Assert(e.String(), Equals, "ssh://git@github.com/user/repository.git")
+}
+
+func (s *SuiteCommon) TestNewEndpointWrongForgat(c *C) {
+ e, err := NewEndpoint("foo")
+ c.Assert(err, Not(IsNil))
+ c.Assert(e.Host, Equals, "")
+}
+
+const CapabilitiesFixture = "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEADmulti_ack thin-pack side-band side-band-64k ofs-delta shallow no-progress include-tag multi_ack_detailed no-done symref=HEAD:refs/heads/master agent=git/2:2.4.8~dbussink-fix-enterprise-tokens-compilation-1167-gc7006cf"
+
+func (s *SuiteCommon) TestCapabilitiesSymbolicReference(c *C) {
+ cap := packp.NewCapabilities()
+ cap.Decode(CapabilitiesFixture)
+ c.Assert(cap.SymbolicReference("HEAD"), Equals, "refs/heads/master")
+}
+
+const GitUploadPackInfoFixture = "MDAxZSMgc2VydmljZT1naXQtdXBsb2FkLXBhY2sKMDAwMDAxMGM2ZWNmMGVmMmMyZGZmYjc5NjAzM2U1YTAyMjE5YWY4NmVjNjU4NGU1IEhFQUQAbXVsdGlfYWNrIHRoaW4tcGFjayBzaWRlLWJhbmQgc2lkZS1iYW5kLTY0ayBvZnMtZGVsdGEgc2hhbGxvdyBuby1wcm9ncmVzcyBpbmNsdWRlLXRhZyBtdWx0aV9hY2tfZGV0YWlsZWQgbm8tZG9uZSBzeW1yZWY9SEVBRDpyZWZzL2hlYWRzL21hc3RlciBhZ2VudD1naXQvMjoyLjQuOH5kYnVzc2luay1maXgtZW50ZXJwcmlzZS10b2tlbnMtY29tcGlsYXRpb24tMTE2Ny1nYzcwMDZjZgowMDNmZThkM2ZmYWI1NTI4OTVjMTliOWZjZjdhYTI2NGQyNzdjZGUzMzg4MSByZWZzL2hlYWRzL2JyYW5jaAowMDNmNmVjZjBlZjJjMmRmZmI3OTYwMzNlNWEwMjIxOWFmODZlYzY1ODRlNSByZWZzL2hlYWRzL21hc3RlcgowMDNlYjhlNDcxZjU4YmNiY2E2M2IwN2JkYTIwZTQyODE5MDQwOWMyZGI0NyByZWZzL3B1bGwvMS9oZWFkCjAwMDA="
+
+func (s *SuiteCommon) TestGitUploadPackInfo(c *C) {
+ b, _ := base64.StdEncoding.DecodeString(GitUploadPackInfoFixture)
+
+ i := NewGitUploadPackInfo()
+ err := i.Decode(bytes.NewBuffer(b))
+ c.Assert(err, IsNil)
+
+ name := i.Capabilities.SymbolicReference("HEAD")
+ c.Assert(name, Equals, "refs/heads/master")
+ c.Assert(i.Refs, HasLen, 4)
+
+ ref := i.Refs[plumbing.ReferenceName(name)]
+ c.Assert(ref, NotNil)
+ c.Assert(ref.Hash().String(), Equals, "6ecf0ef2c2dffb796033e5a02219af86ec6584e5")
+
+ ref = i.Refs[plumbing.HEAD]
+ c.Assert(ref, NotNil)
+ c.Assert(ref.Target(), Equals, plumbing.ReferenceName(name))
+}
+
+const GitUploadPackInfoNoHEADFixture = "MDAxZSMgc2VydmljZT1naXQtdXBsb2FkLXBhY2sKMDAwMDAwYmNkN2UxZmVlMjYxMjM0YmIzYTQzYzA5NmY1NTg3NDhhNTY5ZDc5ZWZmIHJlZnMvaGVhZHMvdjQAbXVsdGlfYWNrIHRoaW4tcGFjayBzaWRlLWJhbmQgc2lkZS1iYW5kLTY0ayBvZnMtZGVsdGEgc2hhbGxvdyBuby1wcm9ncmVzcyBpbmNsdWRlLXRhZyBtdWx0aV9hY2tfZGV0YWlsZWQgbm8tZG9uZSBhZ2VudD1naXQvMS45LjEKMDAwMA=="
+
+func (s *SuiteCommon) TestGitUploadPackInfoNoHEAD(c *C) {
+ b, _ := base64.StdEncoding.DecodeString(GitUploadPackInfoNoHEADFixture)
+
+ i := NewGitUploadPackInfo()
+ err := i.Decode(bytes.NewBuffer(b))
+ c.Assert(err, IsNil)
+
+ name := i.Capabilities.SymbolicReference("HEAD")
+ c.Assert(name, Equals, "")
+ c.Assert(i.Refs, HasLen, 1)
+
+ ref := i.Refs["refs/heads/v4"]
+ c.Assert(ref, NotNil)
+ c.Assert(ref.Hash().String(), Equals, "d7e1fee261234bb3a43c096f558748a569d79eff")
+}
+
+func (s *SuiteCommon) TestGitUploadPackInfoEmpty(c *C) {
+ b := bytes.NewBuffer(nil)
+
+ i := NewGitUploadPackInfo()
+ err := i.Decode(b)
+ c.Assert(err, ErrorMatches, "permanent.*empty.*")
+}
+
+func (s *SuiteCommon) TestGitUploadPackEncode(c *C) {
+ info := NewGitUploadPackInfo()
+ info.Capabilities.Add("symref", "HEAD:refs/heads/master")
+
+ ref := plumbing.ReferenceName("refs/heads/master")
+ hash := plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5")
+ info.Refs = map[plumbing.ReferenceName]*plumbing.Reference{
+ plumbing.HEAD: plumbing.NewSymbolicReference(plumbing.HEAD, ref),
+ ref: plumbing.NewHashReference(ref, hash),
+ }
+
+ c.Assert(info.Head(), NotNil)
+ c.Assert(info.String(), Equals,
+ "001e# service=git-upload-pack\n"+
+ "000000506ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00symref=HEAD:refs/heads/master\n"+
+ "003f6ecf0ef2c2dffb796033e5a02219af86ec6584e5 refs/heads/master\n"+
+ "0000",
+ )
+}
+
+func (s *SuiteCommon) TestGitUploadPackRequest(c *C) {
+ r := &GitUploadPackRequest{}
+ r.Want(plumbing.NewHash("d82f291cde9987322c8a0c81a325e1ba6159684c"))
+ r.Want(plumbing.NewHash("2b41ef280fdb67a9b250678686a0c3e03b0a9989"))
+ r.Have(plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5"))
+
+ c.Assert(r.String(), Equals,
+ "0032want d82f291cde9987322c8a0c81a325e1ba6159684c\n"+
+ "0032want 2b41ef280fdb67a9b250678686a0c3e03b0a9989\n"+
+ "0032have 6ecf0ef2c2dffb796033e5a02219af86ec6584e5\n0000"+
+ "0009done\n",
+ )
+}
diff --git a/plumbing/client/common_test.go b/plumbing/client/common_test.go
new file mode 100644
index 0000000..058c4d3
--- /dev/null
+++ b/plumbing/client/common_test.go
@@ -0,0 +1,85 @@
+package clients
+
+import (
+ "fmt"
+ "io"
+ "testing"
+
+ "gopkg.in/src-d/go-git.v4/plumbing/client/common"
+
+ . "gopkg.in/check.v1"
+)
+
+func Test(t *testing.T) { TestingT(t) }
+
+type SuiteCommon struct{}
+
+var _ = Suite(&SuiteCommon{})
+
+func (s *SuiteCommon) TestNewGitUploadPackServiceHTTP(c *C) {
+ e, err := common.NewEndpoint("http://github.com/src-d/go-git")
+ c.Assert(err, IsNil)
+
+ output, err := NewGitUploadPackService(e)
+ c.Assert(err, IsNil)
+ c.Assert(typeAsString(output), Equals, "*http.GitUploadPackService")
+
+ e, err = common.NewEndpoint("https://github.com/src-d/go-git")
+ c.Assert(err, IsNil)
+
+ output, err = NewGitUploadPackService(e)
+ c.Assert(err, IsNil)
+ c.Assert(typeAsString(output), Equals, "*http.GitUploadPackService")
+}
+
+func (s *SuiteCommon) TestNewGitUploadPackServiceSSH(c *C) {
+ e, err := common.NewEndpoint("ssh://github.com/src-d/go-git")
+ c.Assert(err, IsNil)
+
+ output, err := NewGitUploadPackService(e)
+ c.Assert(err, IsNil)
+ c.Assert(typeAsString(output), Equals, "*ssh.GitUploadPackService")
+}
+
+func (s *SuiteCommon) TestNewGitUploadPackServiceUnknown(c *C) {
+ e, err := common.NewEndpoint("unknown://github.com/src-d/go-git")
+ c.Assert(err, IsNil)
+
+ _, err = NewGitUploadPackService(e)
+ c.Assert(err, NotNil)
+}
+
+func (s *SuiteCommon) TestInstallProtocol(c *C) {
+ InstallProtocol("newscheme", newDummyProtocolService)
+ c.Assert(Protocols["newscheme"], NotNil)
+}
+
+type dummyProtocolService struct{}
+
+func newDummyProtocolService(common.Endpoint) common.GitUploadPackService {
+ return &dummyProtocolService{}
+}
+
+func (s *dummyProtocolService) Connect() error {
+ return nil
+}
+
+func (s *dummyProtocolService) SetAuth(auth common.AuthMethod) error {
+ return nil
+}
+
+func (s *dummyProtocolService) Info() (*common.GitUploadPackInfo, error) {
+ return nil, nil
+}
+
+func (s *dummyProtocolService) Fetch(r *common.GitUploadPackRequest) (io.ReadCloser, error) {
+ return nil, nil
+}
+
+func (s *dummyProtocolService) Disconnect() error {
+ return nil
+}
+
+func typeAsString(v interface{}) string {
+ return fmt.Sprintf("%T", v)
+}
diff --git a/plumbing/client/http/common.go b/plumbing/client/http/common.go
new file mode 100644
index 0000000..4c07876
--- /dev/null
+++ b/plumbing/client/http/common.go
@@ -0,0 +1,77 @@
+// Package http implements a HTTP client for go-git.
+package http
+
+import (
+ "fmt"
+ "net/http"
+
+ "gopkg.in/src-d/go-git.v4/plumbing"
+ "gopkg.in/src-d/go-git.v4/plumbing/client/common"
+)
+
+// HTTPAuthMethod concrete implementation of common.AuthMethod for HTTP services
+type HTTPAuthMethod interface {
+ common.AuthMethod
+ setAuth(r *http.Request)
+}
+
+// BasicAuth represent a HTTP basic auth
+type BasicAuth struct {
+ username, password string
+}
+
+// NewBasicAuth returns a BasicAuth base on the given user and password
+func NewBasicAuth(username, password string) *BasicAuth {
+ return &BasicAuth{username, password}
+}
+
+func (a *BasicAuth) setAuth(r *http.Request) {
+ r.SetBasicAuth(a.username, a.password)
+}
+
+// Name name of the auth
+func (a *BasicAuth) Name() string {
+ return "http-basic-auth"
+}
+
+func (a *BasicAuth) String() string {
+ masked := "*******"
+ if a.password == "" {
+ masked = "<empty>"
+ }
+
+ return fmt.Sprintf("%s - %s:%s", a.Name(), a.username, masked)
+}
+
+// HTTPError a dedicated error to return errors bases on status codes
+type HTTPError struct {
+ Response *http.Response
+}
+
+// NewHTTPError returns a new HTTPError based on a http response
+func NewHTTPError(r *http.Response) error {
+ if r.StatusCode >= 200 && r.StatusCode < 300 {
+ return nil
+ }
+
+ switch r.StatusCode {
+ case 401:
+ return common.ErrAuthorizationRequired
+ case 404:
+ return common.ErrRepositoryNotFound
+ }
+
+ err := &HTTPError{r}
+ return plumbing.NewUnexpectedError(err)
+}
+
+// StatusCode returns the status code of the response
+func (e *HTTPError) StatusCode() int {
+ return e.Response.StatusCode
+}
+
+func (e *HTTPError) Error() string {
+ return fmt.Sprintf("unexpected requesting %q status code: %d",
+ e.Response.Request.URL, e.Response.StatusCode,
+ )
+}
diff --git a/plumbing/client/http/common_test.go b/plumbing/client/http/common_test.go
new file mode 100644
index 0000000..287897d
--- /dev/null
+++ b/plumbing/client/http/common_test.go
@@ -0,0 +1,52 @@
+package http
+
+import (
+ "net/http"
+ "testing"
+
+ . "gopkg.in/check.v1"
+)
+
+func Test(t *testing.T) { TestingT(t) }
+
+type SuiteCommon struct{}
+
+var _ = Suite(&SuiteCommon{})
+
+func (s *SuiteCommon) TestNewBasicAuth(c *C) {
+ a := NewBasicAuth("foo", "qux")
+
+ c.Assert(a.Name(), Equals, "http-basic-auth")
+ c.Assert(a.String(), Equals, "http-basic-auth - foo:*******")
+}
+
+func (s *SuiteCommon) TestNewHTTPError200(c *C) {
+ res := &http.Response{StatusCode: 200}
+ res.StatusCode = 200
+ err := NewHTTPError(res)
+ c.Assert(err, IsNil)
+}
+
+func (s *SuiteCommon) TestNewHTTPError401(c *C) {
+ s.testNewHTTPError(c, 401, "authorization required")
+}
+
+func (s *SuiteCommon) TestNewHTTPError404(c *C) {
+ s.testNewHTTPError(c, 404, "repository not found")
+}
+
+func (s *SuiteCommon) TestNewHTTPError40x(c *C) {
+ s.testNewHTTPError(c, 402, "unexpected client error.*")
+}
+
+func (s *SuiteCommon) testNewHTTPError(c *C, code int, msg string) {
+ req, _ := http.NewRequest("GET", "foo", nil)
+ res := &http.Response{
+ StatusCode: code,
+ Request: req,
+ }
+
+ err := NewHTTPError(res)
+ c.Assert(err, NotNil)
+ c.Assert(err, ErrorMatches, msg)
+}
diff --git a/plumbing/client/http/git_upload_pack.go b/plumbing/client/http/git_upload_pack.go
new file mode 100644
index 0000000..c1f4a0b
--- /dev/null
+++ b/plumbing/client/http/git_upload_pack.go
@@ -0,0 +1,186 @@
+package http
+
+import (
+ "bufio"
+ "bytes"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+
+ "gopkg.in/src-d/go-git.v4/plumbing"
+ "gopkg.in/src-d/go-git.v4/plumbing/client/common"
+ "gopkg.in/src-d/go-git.v4/plumbing/format/packp/pktline"
+)
+
+// GitUploadPackService git-upoad-pack service over HTTP
+type GitUploadPackService struct {
+ client *http.Client
+ endpoint common.Endpoint
+ auth HTTPAuthMethod
+}
+
+// NewGitUploadPackService connects to a git-upload-pack service over HTTP, the
+// auth is extracted from the URL, or can be provided using the SetAuth method
+func NewGitUploadPackService(endpoint common.Endpoint) common.GitUploadPackService {
+ s := &GitUploadPackService{
+ client: http.DefaultClient,
+ endpoint: endpoint,
+ }
+
+ s.setBasicAuthFromEndpoint()
+ return s
+}
+
+// Connect has not any effect, is here just for meet the interface
+func (s *GitUploadPackService) Connect() error {
+ return nil
+}
+
+func (s *GitUploadPackService) setBasicAuthFromEndpoint() {
+ info := s.endpoint.User
+ if info == nil {
+ return
+ }
+
+ p, ok := info.Password()
+ if !ok {
+ return
+ }
+
+ u := info.Username()
+ s.auth = NewBasicAuth(u, p)
+}
+
+// SetAuth sets the AuthMethod
+func (s *GitUploadPackService) SetAuth(auth common.AuthMethod) error {
+ httpAuth, ok := auth.(HTTPAuthMethod)
+ if !ok {
+ return common.ErrInvalidAuthMethod
+ }
+
+ s.auth = httpAuth
+ return nil
+}
+
+// Info returns the references info and capabilities from the service
+func (s *GitUploadPackService) Info() (*common.GitUploadPackInfo, error) {
+ url := fmt.Sprintf(
+ "%s/info/refs?service=%s",
+ s.endpoint.String(), common.GitUploadPackServiceName,
+ )
+
+ res, err := s.doRequest("GET", url, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ defer res.Body.Close()
+
+ i := common.NewGitUploadPackInfo()
+ return i, i.Decode(res.Body)
+}
+
+// Fetch request and returns a reader to a packfile
+func (s *GitUploadPackService) Fetch(r *common.GitUploadPackRequest) (io.ReadCloser, error) {
+ url := fmt.Sprintf(
+ "%s/%s",
+ s.endpoint.String(), common.GitUploadPackServiceName,
+ )
+
+ res, err := s.doRequest("POST", url, r.Reader())
+ if err != nil {
+ return nil, err
+ }
+
+ reader := newBufferedReadCloser(res.Body)
+ if _, err := reader.Peek(1); err != nil {
+ if err == io.ErrUnexpectedEOF {
+ return nil, common.ErrEmptyGitUploadPack
+ }
+
+ return nil, err
+ }
+
+ if err := discardResponseInfo(reader); err != nil {
+ return nil, err
+ }
+
+ return reader, nil
+}
+
+func discardResponseInfo(r io.Reader) error {
+ s := pktline.NewScanner(r)
+ for s.Scan() {
+ if bytes.Equal(s.Bytes(), []byte{'N', 'A', 'K', '\n'}) {
+ break
+ }
+ }
+
+ return s.Err()
+}
+
+func (s *GitUploadPackService) doRequest(method, url string, content *strings.Reader) (*http.Response, error) {
+ var body io.Reader
+ if content != nil {
+ body = content
+ }
+
+ req, err := http.NewRequest(method, url, body)
+ if err != nil {
+ return nil, plumbing.NewPermanentError(err)
+ }
+
+ s.applyHeadersToRequest(req, content)
+ s.applyAuthToRequest(req)
+
+ res, err := s.client.Do(req)
+ if err != nil {
+ return nil, plumbing.NewUnexpectedError(err)
+ }
+
+ if err := NewHTTPError(res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+}
+
+func (s *GitUploadPackService) applyHeadersToRequest(req *http.Request, content *strings.Reader) {
+ req.Header.Add("User-Agent", "git/1.0")
+ req.Header.Add("Host", "github.com")
+
+ if content == nil {
+ req.Header.Add("Accept", "*/*")
+ } else {
+ req.Header.Add("Accept", "application/x-git-upload-pack-result")
+ req.Header.Add("Content-Type", "application/x-git-upload-pack-request")
+ req.Header.Add("Content-Length", string(content.Len()))
+ }
+}
+
+func (s *GitUploadPackService) applyAuthToRequest(req *http.Request) {
+ if s.auth == nil {
+ return
+ }
+
+ s.auth.setAuth(req)
+}
+
+// Disconnect do nothing
+func (s *GitUploadPackService) Disconnect() (err error) {
+ return nil
+}
+
+type bufferedReadCloser struct {
+ *bufio.Reader
+ closer io.Closer
+}
+
+func newBufferedReadCloser(r io.ReadCloser) *bufferedReadCloser {
+ return &bufferedReadCloser{bufio.NewReader(r), r}
+}
+
+func (r *bufferedReadCloser) Close() error {
+ return r.closer.Close()
+}
diff --git a/plumbing/client/http/git_upload_pack_test.go b/plumbing/client/http/git_upload_pack_test.go
new file mode 100644
index 0000000..a50dbdf
--- /dev/null
+++ b/plumbing/client/http/git_upload_pack_test.go
@@ -0,0 +1,135 @@
+package http
+
+import (
+ "io/ioutil"
+
+ . "gopkg.in/check.v1"
+ "gopkg.in/src-d/go-git.v4/plumbing"
+ "gopkg.in/src-d/go-git.v4/plumbing/client/common"
+)
+
+type RemoteSuite struct {
+ Endpoint common.Endpoint
+}
+
+var _ = Suite(&RemoteSuite{})
+
+func (s *RemoteSuite) SetUpSuite(c *C) {
+ var err error
+ s.Endpoint, err = common.NewEndpoint("https://github.com/git-fixtures/basic")
+ c.Assert(err, IsNil)
+}
+
+func (s *RemoteSuite) TestNewGitUploadPackServiceAuth(c *C) {
+ e, err := common.NewEndpoint("https://foo:bar@github.com/git-fixtures/basic")
+ c.Assert(err, IsNil)
+
+ r := NewGitUploadPackService(e)
+ auth := r.(*GitUploadPackService).auth
+
+ c.Assert(auth.String(), Equals, "http-basic-auth - foo:*******")
+}
+
+func (s *RemoteSuite) TestConnect(c *C) {
+ r := NewGitUploadPackService(s.Endpoint)
+ c.Assert(r.Connect(), IsNil)
+}
+
+func (s *RemoteSuite) TestSetAuth(c *C) {
+ auth := &BasicAuth{}
+ r := NewGitUploadPackService(s.Endpoint)
+ r.SetAuth(auth)
+ c.Assert(auth, Equals, r.(*GitUploadPackService).auth)
+}
+
+type mockAuth struct{}
+
+func (*mockAuth) Name() string { return "" }
+func (*mockAuth) String() string { return "" }
+
+func (s *RemoteSuite) TestSetAuthWrongType(c *C) {
+ r := NewGitUploadPackService(s.Endpoint)
+ c.Assert(r.SetAuth(&mockAuth{}), Equals, common.ErrInvalidAuthMethod)
+}
+
+func (s *RemoteSuite) TestInfoEmpty(c *C) {
+ endpoint, _ := common.NewEndpoint("https://github.com/git-fixture/empty")
+ r := NewGitUploadPackService(endpoint)
+ c.Assert(r.Connect(), IsNil)
+
+ info, err := r.Info()
+ c.Assert(err, Equals, common.ErrAuthorizationRequired)
+ c.Assert(info, IsNil)
+}
+
+func (s *RemoteSuite) TestInfoNotExists(c *C) {
+ endpoint, _ := common.NewEndpoint("https://github.com/git-fixture/not-exists")
+ r := NewGitUploadPackService(endpoint)
+ c.Assert(r.Connect(), IsNil)
+
+ info, err := r.Info()
+ c.Assert(err, Equals, common.ErrAuthorizationRequired)
+ c.Assert(info, IsNil)
+}
+
+func (s *RemoteSuite) TestDefaultBranch(c *C) {
+ r := NewGitUploadPackService(s.Endpoint)
+ c.Assert(r.Connect(), IsNil)
+
+ info, err := r.Info()
+ c.Assert(err, IsNil)
+ c.Assert(info.Capabilities.SymbolicReference("HEAD"), Equals, "refs/heads/master")
+}
+
+func (s *RemoteSuite) TestCapabilities(c *C) {
+ r := NewGitUploadPackService(s.Endpoint)
+ c.Assert(r.Connect(), IsNil)
+
+ info, err := r.Info()
+ c.Assert(err, IsNil)
+ c.Assert(info.Capabilities.Get("agent").Values, HasLen, 1)
+}
+
+func (s *RemoteSuite) TestFetch(c *C) {
+ r := NewGitUploadPackService(s.Endpoint)
+ c.Assert(r.Connect(), IsNil)
+
+ req := &common.GitUploadPackRequest{}
+ req.Want(plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5"))
+
+ reader, err := r.Fetch(req)
+ c.Assert(err, IsNil)
+
+ b, err := ioutil.ReadAll(reader)
+ c.Assert(err, IsNil)
+ c.Assert(b, HasLen, 85374)
+}
+
+func (s *RemoteSuite) TestFetchNoChanges(c *C) {
+ r := NewGitUploadPackService(s.Endpoint)
+ c.Assert(r.Connect(), IsNil)
+
+ req := &common.GitUploadPackRequest{}
+ req.Want(plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5"))
+ req.Have(plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5"))
+
+ reader, err := r.Fetch(req)
+ c.Assert(err, Equals, common.ErrEmptyGitUploadPack)
+ c.Assert(reader, IsNil)
+}
+
+func (s *RemoteSuite) TestFetchMulti(c *C) {
+ r := NewGitUploadPackService(s.Endpoint)
+ c.Assert(r.Connect(), IsNil)
+
+ req := &common.GitUploadPackRequest{}
+ req.Want(plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5"))
+ req.Want(plumbing.NewHash("e8d3ffab552895c19b9fcf7aa264d277cde33881"))
+
+ reader, err := r.Fetch(req)
+ c.Assert(err, IsNil)
+
+ b, err := ioutil.ReadAll(reader)
+ c.Assert(err, IsNil)
+ c.Assert(b, HasLen, 85585)
+}
diff --git a/plumbing/client/ssh/auth_method.go b/plumbing/client/ssh/auth_method.go
new file mode 100644
index 0000000..587f59a
--- /dev/null
+++ b/plumbing/client/ssh/auth_method.go
@@ -0,0 +1,159 @@
+package ssh
+
+import (
+ "fmt"
+ "net"
+ "os"
+
+ "golang.org/x/crypto/ssh"
+ "golang.org/x/crypto/ssh/agent"
+ "gopkg.in/src-d/go-git.v4/plumbing/client/common"
+)
+
+// AuthMethod is the interface all auth methods for the ssh client
+// must implement. The clientConfig method returns the ssh client
+// configuration needed to establish an ssh connection.
+type AuthMethod interface {
+ common.AuthMethod
+ clientConfig() *ssh.ClientConfig
+}
+
+// The names of the AuthMethod implementations. To be returned by the
+// Name() method. Most git servers only allow PublicKeysName and
+// PublicKeysCallbackName.
+const (
+ KeyboardInteractiveName = "ssh-keyboard-interactive"
+ PasswordName = "ssh-password"
+ PasswordCallbackName = "ssh-password-callback"
+ PublicKeysName = "ssh-public-keys"
+ PublicKeysCallbackName = "ssh-public-key-callback"
+)
+
+// KeyboardInteractive implements AuthMethod by using a
+// prompt/response sequence controlled by the server.
+type KeyboardInteractive struct {
+ User string
+ Challenge ssh.KeyboardInteractiveChallenge
+}
+
+func (a *KeyboardInteractive) Name() string {
+ return KeyboardInteractiveName
+}
+
+func (a *KeyboardInteractive) String() string {
+ return fmt.Sprintf("user: %s, name: %s", a.User, a.Name())
+}
+
+func (a *KeyboardInteractive) clientConfig() *ssh.ClientConfig {
+ return &ssh.ClientConfig{
+ User: a.User,
+ Auth: []ssh.AuthMethod{ssh.KeyboardInteractiveChallenge(a.Challenge)},
+ }
+}
+
+// Password implements AuthMethod by using the given password.
+type Password struct {
+ User string
+ Pass string
+}
+
+func (a *Password) Name() string {
+ return PasswordName
+}
+
+func (a *Password) String() string {
+ return fmt.Sprintf("user: %s, name: %s", a.User, a.Name())
+}
+
+func (a *Password) clientConfig() *ssh.ClientConfig {
+ return &ssh.ClientConfig{
+ User: a.User,
+ Auth: []ssh.AuthMethod{ssh.Password(a.Pass)},
+ }
+}
+
+// PasswordCallback implements AuthMethod by using a callback
+// to fetch the password.
+type PasswordCallback struct {
+ User string
+ Callback func() (pass string, err error)
+}
+
+func (a *PasswordCallback) Name() string {
+ return PasswordCallbackName
+}
+
+func (a *PasswordCallback) String() string {
+ return fmt.Sprintf("user: %s, name: %s", a.User, a.Name())
+}
+
+func (a *PasswordCallback) clientConfig() *ssh.ClientConfig {
+ return &ssh.ClientConfig{
+ User: a.User,
+ Auth: []ssh.AuthMethod{ssh.PasswordCallback(a.Callback)},
+ }
+}
+
+// PublicKeys implements AuthMethod by using the given
+// key pairs.
+type PublicKeys struct {
+ User string
+ Signer ssh.Signer
+}
+
+func (a *PublicKeys) Name() string {
+ return PublicKeysName
+}
+
+func (a *PublicKeys) String() string {
+ return fmt.Sprintf("user: %s, name: %s", a.User, a.Name())
+}
+
+func (a *PublicKeys) clientConfig() *ssh.ClientConfig {
+ return &ssh.ClientConfig{
+ User: a.User,
+ Auth: []ssh.AuthMethod{ssh.PublicKeys(a.Signer)},
+ }
+}
+
+// PublicKeysCallback implements AuthMethod by asking a
+// ssh.agent.Agent to act as a signer.
+type PublicKeysCallback struct {
+ User string
+ Callback func() (signers []ssh.Signer, err error)
+}
+
+func (a *PublicKeysCallback) Name() string {
+ return PublicKeysCallbackName
+}
+
+func (a *PublicKeysCallback) String() string {
+ return fmt.Sprintf("user: %s, name: %s", a.User, a.Name())
+}
+
+func (a *PublicKeysCallback) clientConfig() *ssh.ClientConfig {
+ return &ssh.ClientConfig{
+ User: a.User,
+ Auth: []ssh.AuthMethod{ssh.PublicKeysCallback(a.Callback)},
+ }
+}
+
+const DefaultSSHUsername = "git"
+
+// Opens a pipe with the ssh agent and uses the pipe
+// as the implementer of the public key callback function.
+func NewSSHAgentAuth(user string) (*PublicKeysCallback, error) {
+ if user == "" {
+ user = DefaultSSHUsername
+ }
+
+ pipe, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK"))
+ if err != nil {
+ return nil, err
+ }
+
+ return &PublicKeysCallback{
+ User: user,
+ Callback: agent.NewClient(pipe).Signers,
+ }, nil
+}
diff --git a/plumbing/client/ssh/auth_method_test.go b/plumbing/client/ssh/auth_method_test.go
new file mode 100644
index 0000000..a87c950
--- /dev/null
+++ b/plumbing/client/ssh/auth_method_test.go
@@ -0,0 +1,94 @@
+package ssh
+
+import (
+ "fmt"
+ "testing"
+
+ . "gopkg.in/check.v1"
+)
+
+func Test(t *testing.T) { TestingT(t) }
+
+type SuiteCommon struct{}
+
+var _ = Suite(&SuiteCommon{})
+
+func (s *SuiteCommon) TestKeyboardInteractiveName(c *C) {
+ a := &KeyboardInteractive{
+ User: "test",
+ Challenge: nil,
+ }
+ c.Assert(a.Name(), Equals, KeyboardInteractiveName)
+}
+
+func (s *SuiteCommon) TestKeyboardInteractiveString(c *C) {
+ a := &KeyboardInteractive{
+ User: "test",
+ Challenge: nil,
+ }
+ c.Assert(a.String(), Equals, fmt.Sprintf("user: test, name: %s", KeyboardInteractiveName))
+}
+
+func (s *SuiteCommon) TestPasswordName(c *C) {
+ a := &Password{
+ User: "test",
+ Pass: "",
+ }
+ c.Assert(a.Name(), Equals, PasswordName)
+}
+
+func (s *SuiteCommon) TestPasswordString(c *C) {
+ a := &Password{
+ User: "test",
+ Pass: "",
+ }
+ c.Assert(a.String(), Equals, fmt.Sprintf("user: test, name: %s", PasswordName))
+}
+
+func (s *SuiteCommon) TestPasswordCallbackName(c *C) {
+ a := &PasswordCallback{
+ User: "test",
+ Callback: nil,
+ }
+ c.Assert(a.Name(), Equals, PasswordCallbackName)
+}
+
+func (s *SuiteCommon) TestPasswordCallbackString(c *C) {
+ a := &PasswordCallback{
+ User: "test",
+ Callback: nil,
+ }
+ c.Assert(a.String(), Equals, fmt.Sprintf("user: test, name: %s", PasswordCallbackName))
+}
+
+func (s *SuiteCommon) TestPublicKeysName(c *C) {
+ a := &PublicKeys{
+ User: "test",
+ Signer: nil,
+ }
+ c.Assert(a.Name(), Equals, PublicKeysName)
+}
+
+func (s *SuiteCommon) TestPublicKeysString(c *C) {
+ a := &PublicKeys{
+ User: "test",
+ Signer: nil,
+ }
+ c.Assert(a.String(), Equals, fmt.Sprintf("user: test, name: %s", PublicKeysName))
+}
+
+func (s *SuiteCommon) TestPublicKeysCallbackName(c *C) {
+ a := &PublicKeysCallback{
+ User: "test",
+ Callback: nil,
+ }
+ c.Assert(a.Name(), Equals, PublicKeysCallbackName)
+}
+
+func (s *SuiteCommon) TestPublicKeysCallbackString(c *C) {
+ a := &PublicKeysCallback{
+ User: "test",
+ Callback: nil,
+ }
+ c.Assert(a.String(), Equals, fmt.Sprintf("user: test, name: %s", PublicKeysCallbackName))
+}
diff --git a/plumbing/client/ssh/git_upload_pack.go b/plumbing/client/ssh/git_upload_pack.go
new file mode 100644
index 0000000..e2b73fd
--- /dev/null
+++ b/plumbing/client/ssh/git_upload_pack.go
@@ -0,0 +1,315 @@
+// Package ssh implements a ssh client for go-git.
+package ssh
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "strings"
+
+ "gopkg.in/src-d/go-git.v4/plumbing/client/common"
+ "gopkg.in/src-d/go-git.v4/plumbing/format/packp/advrefs"
+ "gopkg.in/src-d/go-git.v4/plumbing/format/packp/pktline"
+ "gopkg.in/src-d/go-git.v4/plumbing/format/packp/ulreq"
+
+ "golang.org/x/crypto/ssh"
+)
+
+// New errors introduced by this package.
+var (
+ ErrInvalidAuthMethod = errors.New("invalid ssh auth method")
+ ErrAuthRequired = errors.New("cannot connect: auth required")
+ ErrNotConnected = errors.New("not connected")
+ ErrAlreadyConnected = errors.New("already connected")
+ ErrUploadPackAnswerFormat = errors.New("git-upload-pack bad answer format")
+ ErrUnsupportedVCS = errors.New("only git is supported")
+ ErrUnsupportedRepo = errors.New("only github.com is supported")
+
+ nak = []byte("NAK")
+ eol = []byte("\n")
+)
+
+// GitUploadPackService holds the service information.
+// The zero value is safe to use.
+type GitUploadPackService struct {
+ connected bool
+ endpoint common.Endpoint
+ client *ssh.Client
+ auth AuthMethod
+}
+
+// NewGitUploadPackService initialises a GitUploadPackService,
+func NewGitUploadPackService(endpoint common.Endpoint) common.GitUploadPackService {
+ return &GitUploadPackService{endpoint: endpoint}
+}
+
+// Connect connects to the SSH server, unless a AuthMethod was set with SetAuth
+// method, by default uses an auth method based on PublicKeysCallback, it
+// connects to a SSH agent, using the address stored in the SSH_AUTH_SOCK
+// environment var
+func (s *GitUploadPackService) Connect() error {
+ if s.connected {
+ return ErrAlreadyConnected
+ }
+
+ if err := s.setAuthFromEndpoint(); err != nil {
+ return err
+ }
+
+ var err error
+ s.client, err = ssh.Dial("tcp", s.getHostWithPort(), s.auth.clientConfig())
+ if err != nil {
+ return err
+ }
+
+ s.connected = true
+ return nil
+}
+
+func (s *GitUploadPackService) getHostWithPort() string {
+ host := s.endpoint.Host
+ if strings.Index(s.endpoint.Host, ":") == -1 {
+ host += ":22"
+ }
+
+ return host
+}
+
+func (s *GitUploadPackService) setAuthFromEndpoint() error {
+ var u string
+ if info := s.endpoint.User; info != nil {
+ u = info.Username()
+ }
+
+ var err error
+ s.auth, err = NewSSHAgentAuth(u)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// SetAuth sets the AuthMethod
+func (s *GitUploadPackService) SetAuth(auth common.AuthMethod) error {
+ var ok bool
+ s.auth, ok = auth.(AuthMethod)
+ if !ok {
+ return ErrInvalidAuthMethod
+ }
+
+ return nil
+}
+
+// Info returns the GitUploadPackInfo of the repository. The client must be
+// connected with the repository (using the ConnectWithAuth() method) before
+// using this method.
+func (s *GitUploadPackService) Info() (i *common.GitUploadPackInfo, err error) {
+ if !s.connected {
+ return nil, ErrNotConnected
+ }
+
+ session, err := s.client.NewSession()
+ if err != nil {
+ return nil, err
+ }
+ defer func() {
+ // the session can be closed by the other endpoint,
+ // therefore we must ignore a close error.
+ _ = session.Close()
+ }()
+
+ out, err := session.Output(s.getCommand())
+ if err != nil {
+ return nil, err
+ }
+
+ i = common.NewGitUploadPackInfo()
+ return i, i.Decode(bytes.NewReader(out))
+}
+
+// Disconnect the SSH client.
+func (s *GitUploadPackService) Disconnect() (err error) {
+ if !s.connected {
+ return ErrNotConnected
+ }
+ s.connected = false
+ return s.client.Close()
+}
+
+// Fetch returns a packfile for a given upload request. It opens a new
+// SSH session on a connected GitUploadPackService, sends the given
+// upload request to the server and returns a reader for the received
+// packfile. Closing the returned reader will close the SSH session.
+func (s *GitUploadPackService) Fetch(req *common.GitUploadPackRequest) (rc io.ReadCloser, err error) {
+ if !s.connected {
+ return nil, ErrNotConnected
+ }
+
+ session, i, o, done, err := openSSHSession(s.client, s.getCommand())
+ if err != nil {
+ return nil, fmt.Errorf("cannot open SSH session: %s", err)
+ }
+
+ if err := talkPackProtocol(i, o, req); err != nil {
+ return nil, err
+ }
+
+ return &fetchSession{
+ Reader: o,
+ session: session,
+ done: done,
+ }, nil
+}
+
+func openSSHSession(c *ssh.Client, cmd string) (
+ *ssh.Session, io.WriteCloser, io.Reader, <-chan error, error) {
+
+ session, err := c.NewSession()
+ if err != nil {
+ return nil, nil, nil, nil, fmt.Errorf("cannot open SSH session: %s", err)
+ }
+
+ i, err := session.StdinPipe()
+ if err != nil {
+ return nil, nil, nil, nil, fmt.Errorf("cannot pipe remote stdin: %s", err)
+ }
+
+ o, err := session.StdoutPipe()
+ if err != nil {
+ return nil, nil, nil, nil, fmt.Errorf("cannot pipe remote stdout: %s", err)
+ }
+
+ done := make(chan error)
+ go func() {
+ done <- session.Run(cmd)
+ }()
+
+ return session, i, o, done, nil
+}
+
+// TODO support multi_ack mode
+// TODO support multi_ack_detailed mode
+// TODO support acks for common objects
+// TODO build a proper state machine for all these processing options
+func talkPackProtocol(w io.WriteCloser, r io.Reader,
+ req *common.GitUploadPackRequest) error {
+
+ if err := skipAdvRef(r); err != nil {
+ return fmt.Errorf("skipping advertised-refs: %s", err)
+ }
+
+ if err := sendUlReq(w, req); err != nil {
+ return fmt.Errorf("sending upload-req message: %s", err)
+ }
+
+ if err := sendHaves(w, req); err != nil {
+ return fmt.Errorf("sending haves message: %s", err)
+ }
+
+ if err := sendDone(w); err != nil {
+ return fmt.Errorf("sending done message: %s", err)
+ }
+
+ if err := w.Close(); err != nil {
+ return fmt.Errorf("closing input: %s", err)
+ }
+
+ if err := readNAK(r); err != nil {
+ return fmt.Errorf("reading NAK: %s", err)
+ }
+
+ return nil
+}
+
+func skipAdvRef(r io.Reader) error {
+ d := advrefs.NewDecoder(r)
+ ar := advrefs.New()
+
+ return d.Decode(ar)
+}
+
+func sendUlReq(w io.Writer, req *common.GitUploadPackRequest) error {
+ ur := ulreq.New()
+ ur.Wants = req.Wants
+ ur.Depth = ulreq.DepthCommits(req.Depth)
+ e := ulreq.NewEncoder(w)
+
+ return e.Encode(ur)
+}
+
+func sendHaves(w io.Writer, req *common.GitUploadPackRequest) error {
+ e := pktline.NewEncoder(w)
+ for _, have := range req.Haves {
+ if err := e.Encodef("have %s\n", have); err != nil {
+ return fmt.Errorf("sending haves for %q: err ", have, err)
+ }
+ }
+
+ if len(req.Haves) != 0 {
+ if err := e.Flush(); err != nil {
+ return fmt.Errorf("sending flush-pkt after haves: %s", err)
+ }
+ }
+
+ return nil
+}
+
+func sendDone(w io.Writer) error {
+ e := pktline.NewEncoder(w)
+
+ return e.Encodef("done\n")
+}
+
+func readNAK(r io.Reader) error {
+ s := pktline.NewScanner(r)
+ if !s.Scan() {
+ return s.Err()
+ }
+
+ b := s.Bytes()
+ b = bytes.TrimSuffix(b, eol)
+ if !bytes.Equal(b, nak) {
+ return fmt.Errorf("expecting NAK, found %q instead", string(b))
+ }
+
+ return nil
+}
+
+type fetchSession struct {
+ io.Reader
+ session *ssh.Session
+ done <-chan error
+}
+
+// Close closes the session and collects the output state of the remote
+// SSH command.
+//
+// If both the remote command and the closing of the session completes
+// susccessfully it returns nil.
+//
+// If the remote command completes unsuccessfully or is interrupted by a
+// signal, it returns the corresponding *ExitError.
+//
+// Otherwise, if clossing the SSH session fails it returns the close
+// error. Closing the session when the other has already close it is
+// not cosidered an error.
+func (f *fetchSession) Close() (err error) {
+ if err := <-f.done; err != nil {
+ return err
+ }
+
+ if err := f.session.Close(); err != nil && err != io.EOF {
+ return err
+ }
+
+ return nil
+}
+
+func (s *GitUploadPackService) getCommand() string {
+ directory := s.endpoint.Path
+ directory = directory[1:]
+
+ return fmt.Sprintf("git-upload-pack '%s'", directory)
+}
diff --git a/plumbing/client/ssh/git_upload_pack_test.go b/plumbing/client/ssh/git_upload_pack_test.go
new file mode 100644
index 0000000..4d5b2b1
--- /dev/null
+++ b/plumbing/client/ssh/git_upload_pack_test.go
@@ -0,0 +1,144 @@
+package ssh
+
+import (
+ "io/ioutil"
+ "os"
+
+ . "gopkg.in/check.v1"
+ "gopkg.in/src-d/go-git.v4/plumbing"
+ "gopkg.in/src-d/go-git.v4/plumbing/client/common"
+)
+
+type RemoteSuite struct {
+ Endpoint common.Endpoint
+}
+
+var _ = Suite(&RemoteSuite{})
+
+func (s *RemoteSuite) SetUpSuite(c *C) {
+ var err error
+ s.Endpoint, err = common.NewEndpoint("git@github.com:git-fixtures/basic.git")
+ c.Assert(err, IsNil)
+
+ if os.Getenv("SSH_AUTH_SOCK") == "" {
+ c.Skip("SSH_AUTH_SOCK is not set")
+ }
+}
+
+// A mock implementation of client.common.AuthMethod
+// to test non ssh auth method detection.
+type mockAuth struct{}
+
+func (*mockAuth) Name() string { return "" }
+func (*mockAuth) String() string { return "" }
+
+func (s *RemoteSuite) TestSetAuthWrongType(c *C) {
+ r := NewGitUploadPackService(s.Endpoint)
+ c.Assert(r.SetAuth(&mockAuth{}), Equals, ErrInvalidAuthMethod)
+}
+
+func (s *RemoteSuite) TestAlreadyConnected(c *C) {
+ r := NewGitUploadPackService(s.Endpoint)
+ c.Assert(r.Connect(), IsNil)
+ defer func() {
+ c.Assert(r.Disconnect(), IsNil)
+ }()
+
+ c.Assert(r.Connect(), Equals, ErrAlreadyConnected)
+}
+
+func (s *RemoteSuite) TestDisconnect(c *C) {
+ r := NewGitUploadPackService(s.Endpoint)
+ c.Assert(r.Connect(), IsNil)
+ c.Assert(r.Disconnect(), IsNil)
+}
+
+func (s *RemoteSuite) TestDisconnectedWhenNonConnected(c *C) {
+ r := NewGitUploadPackService(s.Endpoint)
+ c.Assert(r.Disconnect(), Equals, ErrNotConnected)
+}
+
+func (s *RemoteSuite) TestAlreadyDisconnected(c *C) {
+ r := NewGitUploadPackService(s.Endpoint)
+ c.Assert(r.Connect(), IsNil)
+ c.Assert(r.Disconnect(), IsNil)
+ c.Assert(r.Disconnect(), Equals, ErrNotConnected)
+}
+
+func (s *RemoteSuite) TestServeralConnections(c *C) {
+ r := NewGitUploadPackService(s.Endpoint)
+ c.Assert(r.Connect(), IsNil)
+ c.Assert(r.Disconnect(), IsNil)
+
+ c.Assert(r.Connect(), IsNil)
+ c.Assert(r.Disconnect(), IsNil)
+
+ c.Assert(r.Connect(), IsNil)
+ c.Assert(r.Disconnect(), IsNil)
+}
+
+func (s *RemoteSuite) TestInfoNotConnected(c *C) {
+ r := NewGitUploadPackService(s.Endpoint)
+ _, err := r.Info()
+ c.Assert(err, Equals, ErrNotConnected)
+}
+
+func (s *RemoteSuite) TestDefaultBranch(c *C) {
+ r := NewGitUploadPackService(s.Endpoint)
+ c.Assert(r.Connect(), IsNil)
+ defer func() { c.Assert(r.Disconnect(), IsNil) }()
+
+ info, err := r.Info()
+ c.Assert(err, IsNil)
+ c.Assert(info.Capabilities.SymbolicReference("HEAD"), Equals, "refs/heads/master")
+}
+
+func (s *RemoteSuite) TestCapabilities(c *C) {
+ r := NewGitUploadPackService(s.Endpoint)
+ c.Assert(r.Connect(), IsNil)
+ defer func() { c.Assert(r.Disconnect(), IsNil) }()
+
+ info, err := r.Info()
+ c.Assert(err, IsNil)
+ c.Assert(info.Capabilities.Get("agent").Values, HasLen, 1)
+}
+
+func (s *RemoteSuite) TestFetchNotConnected(c *C) {
+ r := NewGitUploadPackService(s.Endpoint)
+ pr := &common.GitUploadPackRequest{}
+ pr.Want(plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5"))
+ _, err := r.Fetch(pr)
+ c.Assert(err, Equals, ErrNotConnected)
+}
+
+func (s *RemoteSuite) TestFetch(c *C) {
+ r := NewGitUploadPackService(s.Endpoint)
+ c.Assert(r.Connect(), IsNil)
+ defer func() { c.Assert(r.Disconnect(), IsNil) }()
+
+ req := &common.GitUploadPackRequest{}
+ req.Want(plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5"))
+ req.Want(plumbing.NewHash("e8d3ffab552895c19b9fcf7aa264d277cde33881"))
+ reader, err := r.Fetch(req)
+ c.Assert(err, IsNil)
+ defer func() { c.Assert(reader.Close(), IsNil) }()
+
+ b, err := ioutil.ReadAll(reader)
+ c.Assert(err, IsNil)
+ c.Check(len(b), Equals, 85585)
+}
+
+func (s *RemoteSuite) TestFetchError(c *C) {
+ r := NewGitUploadPackService(s.Endpoint)
+ c.Assert(r.Connect(), IsNil)
+ defer func() { c.Assert(r.Disconnect(), IsNil) }()
+
+ req := &common.GitUploadPackRequest{}
+ req.Want(plumbing.NewHash("1111111111111111111111111111111111111111"))
+
+ reader, err := r.Fetch(req)
+ c.Assert(err, IsNil)
+
+ err = reader.Close()
+ c.Assert(err, Not(IsNil))
+}