aboutsummaryrefslogtreecommitdiffstats
path: root/plumbing
diff options
context:
space:
mode:
authorSantiago M. Mola <santi@mola.io>2016-11-23 15:30:34 +0100
committerMáximo Cuadros <mcuadros@gmail.com>2016-11-23 15:38:12 +0100
commit08e08d771ef03df80248c80d81475fe7c5ea6fe7 (patch)
treed12e9befa22409e8cf50c5bbc4895e69fd8a5f48 /plumbing
parent844169a739fb8bf1f252d416f10d8c7034db9fe2 (diff)
downloadgo-git-08e08d771ef03df80248c80d81475fe7c5ea6fe7.tar.gz
transport: create Client interface (#132)
* plumbing: move plumbing/client package to plumbing/transport. * transport: create Client interface. * A Client can instantiate any client transport service. * InstallProtocol installs a Client for a given protocol, instead of just a UploadPackService. * A Client can open a session for fetch-pack or send-pack for a specific Endpoint. * Adapt ssh and http clients to the new client interface. * updated doc
Diffstat (limited to 'plumbing')
-rw-r--r--plumbing/client/common.go46
-rw-r--r--plumbing/client/common/common_test.go126
-rw-r--r--plumbing/client/common_test.go85
-rw-r--r--plumbing/client/http/common.go76
-rw-r--r--plumbing/client/http/common_test.go51
-rw-r--r--plumbing/client/http/git_upload_pack.go202
-rw-r--r--plumbing/client/http/git_upload_pack_test.go149
-rw-r--r--plumbing/client/ssh/git_upload_pack.go311
-rw-r--r--plumbing/client/ssh/git_upload_pack_test.go144
-rw-r--r--plumbing/transport/client/client.go32
-rw-r--r--plumbing/transport/client/client_test.go73
-rw-r--r--plumbing/transport/client/example_test.go21
-rw-r--r--plumbing/transport/common.go121
-rw-r--r--plumbing/transport/common_test.go31
-rw-r--r--plumbing/transport/fetch_pack.go (renamed from plumbing/client/common/common.go)98
-rw-r--r--plumbing/transport/fetch_pack_test.go96
-rw-r--r--plumbing/transport/http/common.go155
-rw-r--r--plumbing/transport/http/common_test.go89
-rw-r--r--plumbing/transport/http/fetch_pack.go155
-rw-r--r--plumbing/transport/http/fetch_pack_test.go122
-rw-r--r--plumbing/transport/http/send_pack.go29
-rw-r--r--plumbing/transport/ssh/auth_method.go (renamed from plumbing/client/ssh/auth_method.go)2
-rw-r--r--plumbing/transport/ssh/auth_method_test.go (renamed from plumbing/client/ssh/auth_method_test.go)3
-rw-r--r--plumbing/transport/ssh/common.go151
-rw-r--r--plumbing/transport/ssh/common_test.go17
-rw-r--r--plumbing/transport/ssh/fetch_pack.go202
-rw-r--r--plumbing/transport/ssh/fetch_pack_test.go100
-rw-r--r--plumbing/transport/ssh/send_pack.go30
28 files changed, 1440 insertions, 1277 deletions
diff --git a/plumbing/client/common.go b/plumbing/client/common.go
deleted file mode 100644
index 1524753..0000000
--- a/plumbing/client/common.go
+++ /dev/null
@@ -1,46 +0,0 @@
-// Package clients includes the implementation for different 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"
-)
-
-// Protocols are the protocols supported by default.
-var Protocols = map[string]common.GitUploadPackServiceFactory{
- "http": http.NewGitUploadPackService,
- "https": http.NewGitUploadPackService,
- "ssh": ssh.NewGitUploadPackService,
-}
-
-// InstallProtocol adds or modifies an existing protocol.
-func InstallProtocol(scheme string, f common.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_test.go b/plumbing/client/common/common_test.go
deleted file mode 100644
index cf4d871..0000000
--- a/plumbing/client/common/common_test.go
+++ /dev/null
@@ -1,126 +0,0 @@
-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
deleted file mode 100644
index 058c4d3..0000000
--- a/plumbing/client/common_test.go
+++ /dev/null
@@ -1,85 +0,0 @@
-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
deleted file mode 100644
index 2447995..0000000
--- a/plumbing/client/http/common.go
+++ /dev/null
@@ -1,76 +0,0 @@
-// 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"
-)
-
-// AuthMethod is concrete implementation of common.AuthMethod for HTTP services
-type AuthMethod 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 is 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)
-}
-
-// Err is a dedicated error to return errors based on status code
-type Err struct {
- Response *http.Response
-}
-
-// NewErr returns a new Err based on a http response
-func NewErr(r *http.Response) error {
- if r.StatusCode >= http.StatusOK && r.StatusCode < http.StatusMultipleChoices {
- return nil
- }
-
- switch r.StatusCode {
- case http.StatusUnauthorized:
- return common.ErrAuthorizationRequired
- case http.StatusNotFound:
- return common.ErrRepositoryNotFound
- }
-
- return plumbing.NewUnexpectedError(&Err{r})
-}
-
-// StatusCode returns the status code of the response
-func (e *Err) StatusCode() int {
- return e.Response.StatusCode
-}
-
-func (e *Err) 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
deleted file mode 100644
index 7503d84..0000000
--- a/plumbing/client/http/common_test.go
+++ /dev/null
@@ -1,51 +0,0 @@
-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) TestNewErrOK(c *C) {
- res := &http.Response{StatusCode: http.StatusOK}
- err := NewErr(res)
- c.Assert(err, IsNil)
-}
-
-func (s *SuiteCommon) TestNewErrUnauthorized(c *C) {
- s.testNewHTTPError(c, http.StatusUnauthorized, "authorization required")
-}
-
-func (s *SuiteCommon) TestNewErrNotFound(c *C) {
- s.testNewHTTPError(c, http.StatusNotFound, "repository not found")
-}
-
-func (s *SuiteCommon) TestNewHTTPError40x(c *C) {
- s.testNewHTTPError(c, http.StatusPaymentRequired, "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 := NewErr(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
deleted file mode 100644
index 1ecf299..0000000
--- a/plumbing/client/http/git_upload_pack.go
+++ /dev/null
@@ -1,202 +0,0 @@
-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-upload-pack service over HTTP
-type GitUploadPackService struct {
- client *http.Client
- endpoint common.Endpoint
- auth AuthMethod
-}
-
-// 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 {
- return newGitUploadPackService(endpoint, http.DefaultClient)
-}
-
-// NewGitUploadPackServiceFactory creates a http client factory with a customizable client
-// See `InstallProtocol` to install and override default http client.
-// Unless a properly initialized client is given, it will fall back into `http.DefaultClient`.
-func NewGitUploadPackServiceFactory(client *http.Client) common.GitUploadPackServiceFactory {
- return func(endpoint common.Endpoint) common.GitUploadPackService {
- return newGitUploadPackService(endpoint, client)
- }
-}
-
-func newGitUploadPackService(endpoint common.Endpoint, client *http.Client) common.GitUploadPackService {
- if client == nil {
- client = http.DefaultClient
- }
- s := &GitUploadPackService{
- client: client,
- endpoint: endpoint,
- }
- s.setBasicAuthFromEndpoint()
- return s
-}
-
-// Connect has not any effect, is here to satisfy 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.(AuthMethod)
- 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 := NewErr(res); err != nil {
- _ = res.Body.Close()
- 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() 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
deleted file mode 100644
index 8010cea..0000000
--- a/plumbing/client/http/git_upload_pack_test.go
+++ /dev/null
@@ -1,149 +0,0 @@
-package http
-
-import (
- "crypto/tls"
- "io/ioutil"
- "net/http"
-
- . "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) TestNewGitUploadPackServiceFactory(c *C) {
- e, err := common.NewEndpoint("https://foo:bar@github.com/git-fixtures/basic")
- c.Assert(err, IsNil)
-
- roundTripper := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
- client := &http.Client{Transport: roundTripper}
- r := NewGitUploadPackServiceFactory(client)(e).(*GitUploadPackService)
-
- c.Assert(r.auth.String(), Equals, "http-basic-auth - foo:*******")
- c.Assert(r.client.Transport, Equals, roundTripper)
-}
-
-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/git_upload_pack.go b/plumbing/client/ssh/git_upload_pack.go
deleted file mode 100644
index db7fa93..0000000
--- a/plumbing/client/ssh/git_upload_pack.go
+++ /dev/null
@@ -1,311 +0,0 @@
-// 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)
- return err
-}
-
-// 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() (*common.GitUploadPackInfo, 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() 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) (io.ReadCloser, 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: %s", 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
deleted file mode 100644
index 4d5b2b1..0000000
--- a/plumbing/client/ssh/git_upload_pack_test.go
+++ /dev/null
@@ -1,144 +0,0 @@
-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))
-}
diff --git a/plumbing/transport/client/client.go b/plumbing/transport/client/client.go
new file mode 100644
index 0000000..5c6da05
--- /dev/null
+++ b/plumbing/transport/client/client.go
@@ -0,0 +1,32 @@
+package client
+
+import (
+ "fmt"
+
+ "gopkg.in/src-d/go-git.v4/plumbing/transport"
+ "gopkg.in/src-d/go-git.v4/plumbing/transport/http"
+ "gopkg.in/src-d/go-git.v4/plumbing/transport/ssh"
+)
+
+// Protocols are the protocols supported by default.
+var Protocols = map[string]transport.Client{
+ "http": http.DefaultClient,
+ "https": http.DefaultClient,
+ "ssh": ssh.DefaultClient,
+}
+
+// InstallProtocol adds or modifies an existing protocol.
+func InstallProtocol(scheme string, c transport.Client) {
+ Protocols[scheme] = c
+}
+
+// NewClient returns the appropriate client among of the set of known protocols:
+// HTTP, SSH. See `InstallProtocol` to add or modify protocols.
+func NewClient(endpoint transport.Endpoint) (transport.Client, error) {
+ f, ok := Protocols[endpoint.Scheme]
+ if !ok {
+ return nil, fmt.Errorf("unsupported scheme %q", endpoint.Scheme)
+ }
+
+ return f, nil
+}
diff --git a/plumbing/transport/client/client_test.go b/plumbing/transport/client/client_test.go
new file mode 100644
index 0000000..90bad57
--- /dev/null
+++ b/plumbing/transport/client/client_test.go
@@ -0,0 +1,73 @@
+package client
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "gopkg.in/src-d/go-git.v4/plumbing/transport"
+
+ . "gopkg.in/check.v1"
+)
+
+func Test(t *testing.T) { TestingT(t) }
+
+type ClientSuite struct{}
+
+var _ = Suite(&ClientSuite{})
+
+func (s *ClientSuite) TestNewClientHTTP(c *C) {
+ e, err := transport.NewEndpoint("http://github.com/src-d/go-git")
+ c.Assert(err, IsNil)
+
+ output, err := NewClient(e)
+ c.Assert(err, IsNil)
+ c.Assert(typeAsString(output), Equals, "*http.Client")
+
+ e, err = transport.NewEndpoint("https://github.com/src-d/go-git")
+ c.Assert(err, IsNil)
+
+ output, err = NewClient(e)
+ c.Assert(err, IsNil)
+ c.Assert(typeAsString(output), Equals, "*http.Client")
+}
+
+func (s *ClientSuite) TestNewClientSSH(c *C) {
+ e, err := transport.NewEndpoint("ssh://github.com/src-d/go-git")
+ c.Assert(err, IsNil)
+
+ output, err := NewClient(e)
+ c.Assert(err, IsNil)
+ c.Assert(typeAsString(output), Equals, "*ssh.Client")
+}
+
+func (s *ClientSuite) TestNewClientUnknown(c *C) {
+ e, err := transport.NewEndpoint("unknown://github.com/src-d/go-git")
+ c.Assert(err, IsNil)
+
+ _, err = NewClient(e)
+ c.Assert(err, NotNil)
+}
+
+func (s *ClientSuite) TestInstallProtocol(c *C) {
+ InstallProtocol("newscheme", &dummyClient{})
+ c.Assert(Protocols["newscheme"], NotNil)
+}
+
+type dummyClient struct {
+ *http.Client
+}
+
+func (*dummyClient) NewFetchPackSession(transport.Endpoint) (
+ transport.FetchPackSession, error) {
+ return nil, nil
+}
+
+func (*dummyClient) NewSendPackSession(transport.Endpoint) (
+ transport.SendPackSession, error) {
+ return nil, nil
+}
+
+func typeAsString(v interface{}) string {
+ return fmt.Sprintf("%T", v)
+}
diff --git a/plumbing/transport/client/example_test.go b/plumbing/transport/client/example_test.go
new file mode 100644
index 0000000..3e7a4f0
--- /dev/null
+++ b/plumbing/transport/client/example_test.go
@@ -0,0 +1,21 @@
+package client_test
+
+import (
+ "crypto/tls"
+ "net/http"
+
+ "gopkg.in/src-d/go-git.v4/plumbing/transport/client"
+ githttp "gopkg.in/src-d/go-git.v4/plumbing/transport/http"
+)
+
+func ExampleInstallProtocol() {
+ // Create custom net/http client that.
+ httpClient := &http.Client{
+ Transport: &http.Transport{
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+ },
+ }
+
+ // Install it as default client for https URLs.
+ client.InstallProtocol("https", githttp.NewClient(httpClient))
+}
diff --git a/plumbing/transport/common.go b/plumbing/transport/common.go
new file mode 100644
index 0000000..cc30564
--- /dev/null
+++ b/plumbing/transport/common.go
@@ -0,0 +1,121 @@
+// Package transport includes the implementation for different transport
+// protocols.
+//
+// `Client` can be used to fetch and send packfiles to a git server.
+// The `client` package provides higher level functions to instantiate the
+// appropiate `Client` based on the repository URL.
+//
+// Go-git supports HTTP and SSH (see `Protocols`), but you can also install
+// your own protocols (see the `client` package).
+//
+// Each protocol has its own implementation of `Client`, but you should
+// generally not use them directly, use `client.NewClient` instead.
+package transport
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "net/url"
+ "regexp"
+
+ "gopkg.in/src-d/go-git.v4/plumbing"
+)
+
+var (
+ ErrRepositoryNotFound = errors.New("repository not found")
+ ErrAuthorizationRequired = errors.New("authorization required")
+ ErrEmptyUploadPackRequest = errors.New("empty git-upload-pack given")
+ ErrInvalidAuthMethod = errors.New("invalid auth method")
+)
+
+const (
+ UploadPackServiceName = "git-upload-pack"
+)
+
+// Client can initiate git-fetch-pack and git-send-pack processes.
+type Client interface {
+ // NewFetchPackSession starts a git-fetch-pack session for an endpoint.
+ NewFetchPackSession(Endpoint) (FetchPackSession, error)
+ // NewSendPackSession starts a git-send-pack session for an endpoint.
+ NewSendPackSession(Endpoint) (SendPackSession, error)
+}
+
+type Session interface {
+ SetAuth(auth AuthMethod) error
+ io.Closer
+}
+
+type AuthMethod interface {
+ fmt.Stringer
+ Name() string
+}
+
+// FetchPackSession represents a git-fetch-pack session.
+// A git-fetch-pack session has two steps: reference discovery
+// (`AdvertisedReferences` function) and fetching pack (`FetchPack` function).
+// In that order.
+type FetchPackSession interface {
+ Session
+ // AdvertisedReferences retrieves the advertised references for a
+ // repository. It should be called before FetchPack, and it cannot be
+ // called after FetchPack.
+ AdvertisedReferences() (*UploadPackInfo, error)
+ // FetchPack takes a request and returns a reader for the packfile
+ // received from the server.
+ FetchPack(req *UploadPackRequest) (io.ReadCloser, error)
+}
+
+// FetchPackSession represents a git-send-pack session.
+// A git-send-pack session has two steps: reference discovery
+// (`AdvertisedReferences` function) and sending pack (`SendPack` function).
+// In that order.
+type SendPackSession interface {
+ Session
+ // AdvertisedReferences retrieves the advertised references for a
+ // repository. It should be called before FetchPack, and it cannot be
+ // called after FetchPack.
+ AdvertisedReferences() (*UploadPackInfo, error)
+ // UpdateReferences sends an update references request and returns a
+ // writer to be used for packfile writing.
+ //TODO: Complete signature.
+ SendPack() (io.WriteCloser, error)
+}
+
+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 (e *Endpoint) String() string {
+ u := url.URL(*e)
+ return u.String()
+}
+
+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
+}
diff --git a/plumbing/transport/common_test.go b/plumbing/transport/common_test.go
new file mode 100644
index 0000000..9ca4459
--- /dev/null
+++ b/plumbing/transport/common_test.go
@@ -0,0 +1,31 @@
+package transport
+
+import (
+ "testing"
+
+ . "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, "")
+}
diff --git a/plumbing/client/common/common.go b/plumbing/transport/fetch_pack.go
index b2d52e8..3b2a39c 100644
--- a/plumbing/client/common/common.go
+++ b/plumbing/transport/fetch_pack.go
@@ -1,14 +1,10 @@
-// Package common contains interfaces and non-specific protocol entities
-package common
+package transport
import (
"bytes"
- "errors"
"fmt"
"io"
"io/ioutil"
- "net/url"
- "regexp"
"strings"
"gopkg.in/src-d/go-git.v4/plumbing"
@@ -19,82 +15,20 @@ import (
"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
-}
-
-// GitUploadPackServiceFactory is capable of instantiating GitUploadPackService with given endpoint
-type GitUploadPackServiceFactory func(Endpoint) GitUploadPackService
-
-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 {
+//TODO: Replace this by advrefs.AdvRefs.
+type UploadPackInfo struct {
Capabilities *packp.Capabilities
Refs memory.ReferenceStorage
}
-func NewGitUploadPackInfo() *GitUploadPackInfo {
- return &GitUploadPackInfo{
+func NewUploadPackInfo() *UploadPackInfo {
+ return &UploadPackInfo{
Capabilities: packp.NewCapabilities(),
Refs: make(memory.ReferenceStorage, 0),
}
}
-func (i *GitUploadPackInfo) Decode(r io.Reader) error {
+func (i *UploadPackInfo) Decode(r io.Reader) error {
d := advrefs.NewDecoder(r)
ar := advrefs.New()
if err := d.Decode(ar); err != nil {
@@ -113,7 +47,7 @@ func (i *GitUploadPackInfo) Decode(r io.Reader) error {
return nil
}
-func (i *GitUploadPackInfo) addRefs(ar *advrefs.AdvRefs) error {
+func (i *UploadPackInfo) addRefs(ar *advrefs.AdvRefs) error {
for name, hash := range ar.References {
ref := plumbing.NewReferenceFromStrings(name, hash.String())
i.Refs.SetReference(ref)
@@ -122,7 +56,7 @@ func (i *GitUploadPackInfo) addRefs(ar *advrefs.AdvRefs) error {
return i.addSymbolicRefs(ar)
}
-func (i *GitUploadPackInfo) addSymbolicRefs(ar *advrefs.AdvRefs) error {
+func (i *UploadPackInfo) addSymbolicRefs(ar *advrefs.AdvRefs) error {
if !hasSymrefs(ar) {
return nil
}
@@ -146,16 +80,16 @@ func hasSymrefs(ar *advrefs.AdvRefs) bool {
return ar.Capabilities.Supports("symref")
}
-func (i *GitUploadPackInfo) Head() *plumbing.Reference {
+func (i *UploadPackInfo) Head() *plumbing.Reference {
ref, _ := storer.ResolveReference(i.Refs, plumbing.HEAD)
return ref
}
-func (i *GitUploadPackInfo) String() string {
+func (i *UploadPackInfo) String() string {
return string(i.Bytes())
}
-func (i *GitUploadPackInfo) Bytes() []byte {
+func (i *UploadPackInfo) Bytes() []byte {
var buf bytes.Buffer
e := pktline.NewEncoder(&buf)
@@ -180,26 +114,26 @@ func (i *GitUploadPackInfo) Bytes() []byte {
return buf.Bytes()
}
-type GitUploadPackRequest struct {
+type UploadPackRequest struct {
Wants []plumbing.Hash
Haves []plumbing.Hash
Depth int
}
-func (r *GitUploadPackRequest) Want(h ...plumbing.Hash) {
+func (r *UploadPackRequest) Want(h ...plumbing.Hash) {
r.Wants = append(r.Wants, h...)
}
-func (r *GitUploadPackRequest) Have(h ...plumbing.Hash) {
+func (r *UploadPackRequest) Have(h ...plumbing.Hash) {
r.Haves = append(r.Haves, h...)
}
-func (r *GitUploadPackRequest) String() string {
+func (r *UploadPackRequest) String() string {
b, _ := ioutil.ReadAll(r.Reader())
return string(b)
}
-func (r *GitUploadPackRequest) Reader() *strings.Reader {
+func (r *UploadPackRequest) Reader() *strings.Reader {
var buf bytes.Buffer
e := pktline.NewEncoder(&buf)
diff --git a/plumbing/transport/fetch_pack_test.go b/plumbing/transport/fetch_pack_test.go
new file mode 100644
index 0000000..7a7f86f
--- /dev/null
+++ b/plumbing/transport/fetch_pack_test.go
@@ -0,0 +1,96 @@
+package transport
+
+import (
+ "bytes"
+ "encoding/base64"
+
+ "gopkg.in/src-d/go-git.v4/plumbing"
+
+ . "gopkg.in/check.v1"
+)
+
+type UploadPackSuite struct{}
+
+var _ = Suite(&UploadPackSuite{})
+
+const UploadPackInfoFixture = "MDAxZSMgc2VydmljZT1naXQtdXBsb2FkLXBhY2sKMDAwMDAxMGM2ZWNmMGVmMmMyZGZmYjc5NjAzM2U1YTAyMjE5YWY4NmVjNjU4NGU1IEhFQUQAbXVsdGlfYWNrIHRoaW4tcGFjayBzaWRlLWJhbmQgc2lkZS1iYW5kLTY0ayBvZnMtZGVsdGEgc2hhbGxvdyBuby1wcm9ncmVzcyBpbmNsdWRlLXRhZyBtdWx0aV9hY2tfZGV0YWlsZWQgbm8tZG9uZSBzeW1yZWY9SEVBRDpyZWZzL2hlYWRzL21hc3RlciBhZ2VudD1naXQvMjoyLjQuOH5kYnVzc2luay1maXgtZW50ZXJwcmlzZS10b2tlbnMtY29tcGlsYXRpb24tMTE2Ny1nYzcwMDZjZgowMDNmZThkM2ZmYWI1NTI4OTVjMTliOWZjZjdhYTI2NGQyNzdjZGUzMzg4MSByZWZzL2hlYWRzL2JyYW5jaAowMDNmNmVjZjBlZjJjMmRmZmI3OTYwMzNlNWEwMjIxOWFmODZlYzY1ODRlNSByZWZzL2hlYWRzL21hc3RlcgowMDNlYjhlNDcxZjU4YmNiY2E2M2IwN2JkYTIwZTQyODE5MDQwOWMyZGI0NyByZWZzL3B1bGwvMS9oZWFkCjAwMDA="
+
+func (s *UploadPackSuite) TestUploadPackInfo(c *C) {
+ b, _ := base64.StdEncoding.DecodeString(UploadPackInfoFixture)
+
+ i := NewUploadPackInfo()
+ 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 UploadPackInfoNoHEADFixture = "MDAxZSMgc2VydmljZT1naXQtdXBsb2FkLXBhY2sKMDAwMDAwYmNkN2UxZmVlMjYxMjM0YmIzYTQzYzA5NmY1NTg3NDhhNTY5ZDc5ZWZmIHJlZnMvaGVhZHMvdjQAbXVsdGlfYWNrIHRoaW4tcGFjayBzaWRlLWJhbmQgc2lkZS1iYW5kLTY0ayBvZnMtZGVsdGEgc2hhbGxvdyBuby1wcm9ncmVzcyBpbmNsdWRlLXRhZyBtdWx0aV9hY2tfZGV0YWlsZWQgbm8tZG9uZSBhZ2VudD1naXQvMS45LjEKMDAwMA=="
+
+func (s *UploadPackSuite) TestUploadPackInfoNoHEAD(c *C) {
+ b, _ := base64.StdEncoding.DecodeString(UploadPackInfoNoHEADFixture)
+
+ i := NewUploadPackInfo()
+ 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 *UploadPackSuite) TestUploadPackInfoEmpty(c *C) {
+ b := bytes.NewBuffer(nil)
+
+ i := NewUploadPackInfo()
+ err := i.Decode(b)
+ c.Assert(err, ErrorMatches, "permanent.*empty.*")
+}
+
+func (s *UploadPackSuite) TestUploadPackEncode(c *C) {
+ info := NewUploadPackInfo()
+ 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 *UploadPackSuite) TestUploadPackRequest(c *C) {
+ r := &UploadPackRequest{}
+ 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/transport/http/common.go b/plumbing/transport/http/common.go
new file mode 100644
index 0000000..038c469
--- /dev/null
+++ b/plumbing/transport/http/common.go
@@ -0,0 +1,155 @@
+// 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/transport"
+)
+
+type Client struct {
+ c *http.Client
+}
+
+var DefaultClient = NewClient(nil)
+
+// NewClient creates a new client with a custom net/http client.
+// See `InstallProtocol` to install and override default http client.
+// Unless a properly initialized client is given, it will fall back into
+// `http.DefaultClient`.
+func NewClient(c *http.Client) transport.Client {
+ if c == nil {
+ return &Client{http.DefaultClient}
+ }
+
+ return &Client{
+ c: c,
+ }
+}
+
+func (c *Client) NewFetchPackSession(ep transport.Endpoint) (
+ transport.FetchPackSession, error) {
+
+ return newFetchPackSession(c.c, ep), nil
+}
+
+func (c *Client) NewSendPackSession(ep transport.Endpoint) (
+ transport.SendPackSession, error) {
+
+ return newSendPackSession(c.c, ep), nil
+}
+
+type session struct {
+ auth AuthMethod
+ client *http.Client
+ endpoint transport.Endpoint
+}
+
+func (s *session) SetAuth(auth transport.AuthMethod) error {
+ a, ok := auth.(AuthMethod)
+ if !ok {
+ return transport.ErrInvalidAuthMethod
+ }
+
+ s.auth = a
+ return nil
+}
+
+func (*session) Close() error {
+ return nil
+}
+
+func (s *session) applyAuthToRequest(req *http.Request) {
+ if s.auth == nil {
+ return
+ }
+
+ s.auth.setAuth(req)
+}
+
+// AuthMethod is concrete implementation of common.AuthMethod for HTTP services
+type AuthMethod interface {
+ transport.AuthMethod
+ setAuth(r *http.Request)
+}
+
+func basicAuthFromEndpoint(ep transport.Endpoint) *BasicAuth {
+ info := ep.User
+ if info == nil {
+ return nil
+ }
+
+ p, ok := info.Password()
+ if !ok {
+ return nil
+ }
+
+ u := info.Username()
+ return NewBasicAuth(u, p)
+}
+
+// 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) {
+ if a == nil {
+ return
+ }
+
+ r.SetBasicAuth(a.username, a.password)
+}
+
+// Name is 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)
+}
+
+// Err is a dedicated error to return errors based on status code
+type Err struct {
+ Response *http.Response
+}
+
+// NewErr returns a new Err based on a http response
+func NewErr(r *http.Response) error {
+ if r.StatusCode >= http.StatusOK && r.StatusCode < http.StatusMultipleChoices {
+ return nil
+ }
+
+ switch r.StatusCode {
+ case http.StatusUnauthorized:
+ return transport.ErrAuthorizationRequired
+ case http.StatusNotFound:
+ return transport.ErrRepositoryNotFound
+ }
+
+ return plumbing.NewUnexpectedError(&Err{r})
+}
+
+// StatusCode returns the status code of the response
+func (e *Err) StatusCode() int {
+ return e.Response.StatusCode
+}
+
+func (e *Err) Error() string {
+ return fmt.Sprintf("unexpected requesting %q status code: %d",
+ e.Response.Request.URL, e.Response.StatusCode,
+ )
+}
diff --git a/plumbing/transport/http/common_test.go b/plumbing/transport/http/common_test.go
new file mode 100644
index 0000000..1d09fba
--- /dev/null
+++ b/plumbing/transport/http/common_test.go
@@ -0,0 +1,89 @@
+package http
+
+import (
+ "crypto/tls"
+ "net/http"
+ "testing"
+
+ "gopkg.in/src-d/go-git.v4/plumbing/transport"
+
+ . "gopkg.in/check.v1"
+)
+
+func Test(t *testing.T) { TestingT(t) }
+
+type ClientSuite struct {
+ Endpoint transport.Endpoint
+}
+
+var _ = Suite(&ClientSuite{})
+
+func (s *ClientSuite) SetUpSuite(c *C) {
+ var err error
+ s.Endpoint, err = transport.NewEndpoint("https://github.com/git-fixtures/basic")
+ c.Assert(err, IsNil)
+}
+
+func (s *FetchPackSuite) TestNewClient(c *C) {
+ roundTripper := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
+ client := &http.Client{Transport: roundTripper}
+ r := NewClient(client).(*Client)
+
+ c.Assert(r.c, Equals, client)
+}
+
+func (s *ClientSuite) 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 *ClientSuite) TestNewErrOK(c *C) {
+ res := &http.Response{StatusCode: http.StatusOK}
+ err := NewErr(res)
+ c.Assert(err, IsNil)
+}
+
+func (s *ClientSuite) TestNewErrUnauthorized(c *C) {
+ s.testNewHTTPError(c, http.StatusUnauthorized, "authorization required")
+}
+
+func (s *ClientSuite) TestNewErrNotFound(c *C) {
+ s.testNewHTTPError(c, http.StatusNotFound, "repository not found")
+}
+
+func (s *ClientSuite) TestNewHTTPError40x(c *C) {
+ s.testNewHTTPError(c, http.StatusPaymentRequired, "unexpected client error.*")
+}
+
+func (s *ClientSuite) testNewHTTPError(c *C, code int, msg string) {
+ req, _ := http.NewRequest("GET", "foo", nil)
+ res := &http.Response{
+ StatusCode: code,
+ Request: req,
+ }
+
+ err := NewErr(res)
+ c.Assert(err, NotNil)
+ c.Assert(err, ErrorMatches, msg)
+}
+
+func (s *ClientSuite) TestSetAuth(c *C) {
+ auth := &BasicAuth{}
+ r, err := DefaultClient.NewFetchPackSession(s.Endpoint)
+ c.Assert(err, IsNil)
+ r.SetAuth(auth)
+ c.Assert(auth, Equals, r.(*fetchPackSession).auth)
+}
+
+type mockAuth struct{}
+
+func (*mockAuth) Name() string { return "" }
+func (*mockAuth) String() string { return "" }
+
+func (s *ClientSuite) TestSetAuthWrongType(c *C) {
+ r, err := DefaultClient.NewFetchPackSession(s.Endpoint)
+ c.Assert(err, IsNil)
+ c.Assert(r.SetAuth(&mockAuth{}), Equals, transport.ErrInvalidAuthMethod)
+}
diff --git a/plumbing/transport/http/fetch_pack.go b/plumbing/transport/http/fetch_pack.go
new file mode 100644
index 0000000..0c32672
--- /dev/null
+++ b/plumbing/transport/http/fetch_pack.go
@@ -0,0 +1,155 @@
+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/format/packp/pktline"
+ "gopkg.in/src-d/go-git.v4/plumbing/transport"
+)
+
+type fetchPackSession struct {
+ *session
+}
+
+func newFetchPackSession(c *http.Client,
+ ep transport.Endpoint) transport.FetchPackSession {
+
+ return &fetchPackSession{
+ session: &session{
+ auth: basicAuthFromEndpoint(ep),
+ client: c,
+ endpoint: ep,
+ },
+ }
+}
+
+func (s *fetchPackSession) AdvertisedReferences() (*transport.UploadPackInfo,
+ error) {
+
+ url := fmt.Sprintf(
+ "%s/info/refs?service=%s",
+ s.endpoint.String(), transport.UploadPackServiceName,
+ )
+
+ req, err := http.NewRequest(http.MethodGet, url, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ s.applyAuthToRequest(req)
+ s.applyHeadersToRequest(req, nil)
+ res, err := s.client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ defer res.Body.Close()
+ if res.StatusCode == http.StatusUnauthorized {
+ return nil, transport.ErrAuthorizationRequired
+ }
+
+ i := transport.NewUploadPackInfo()
+ return i, i.Decode(res.Body)
+}
+
+func (s *fetchPackSession) FetchPack(r *transport.UploadPackRequest) (io.ReadCloser, error) {
+ url := fmt.Sprintf(
+ "%s/%s",
+ s.endpoint.String(), transport.UploadPackServiceName,
+ )
+
+ 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, transport.ErrEmptyUploadPackRequest
+ }
+
+ return nil, err
+ }
+
+ if err := discardResponseInfo(reader); err != nil {
+ return nil, err
+ }
+
+ return reader, nil
+}
+
+// Close does nothing.
+func (s *fetchPackSession) Close() error {
+ return 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 *fetchPackSession) 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 := NewErr(res); err != nil {
+ _ = res.Body.Close()
+ return nil, err
+ }
+
+ return res, nil
+}
+
+func (s *fetchPackSession) 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()))
+ }
+}
+
+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/transport/http/fetch_pack_test.go b/plumbing/transport/http/fetch_pack_test.go
new file mode 100644
index 0000000..5ec9991
--- /dev/null
+++ b/plumbing/transport/http/fetch_pack_test.go
@@ -0,0 +1,122 @@
+package http
+
+import (
+ "fmt"
+ "io/ioutil"
+
+ "gopkg.in/src-d/go-git.v4/plumbing"
+ "gopkg.in/src-d/go-git.v4/plumbing/transport"
+
+ . "gopkg.in/check.v1"
+)
+
+type FetchPackSuite struct {
+ Endpoint transport.Endpoint
+}
+
+var _ = Suite(&FetchPackSuite{})
+
+func (s *FetchPackSuite) SetUpSuite(c *C) {
+ fmt.Println("SetUpSuite\n")
+ var err error
+ s.Endpoint, err = transport.NewEndpoint("https://github.com/git-fixtures/basic")
+ c.Assert(err, IsNil)
+}
+
+func (s *FetchPackSuite) TestInfoEmpty(c *C) {
+ endpoint, _ := transport.NewEndpoint("https://github.com/git-fixture/empty")
+ r, err := DefaultClient.NewFetchPackSession(endpoint)
+ c.Assert(err, IsNil)
+ info, err := r.AdvertisedReferences()
+ c.Assert(err, Equals, transport.ErrAuthorizationRequired)
+ c.Assert(info, IsNil)
+}
+
+//TODO: Test this error with HTTP BasicAuth too.
+func (s *FetchPackSuite) TestInfoNotExists(c *C) {
+ endpoint, _ := transport.NewEndpoint("https://github.com/git-fixture/not-exists")
+ r, err := DefaultClient.NewFetchPackSession(endpoint)
+ c.Assert(err, IsNil)
+ info, err := r.AdvertisedReferences()
+ c.Assert(err, Equals, transport.ErrAuthorizationRequired)
+ c.Assert(info, IsNil)
+}
+
+func (s *FetchPackSuite) TestDefaultBranch(c *C) {
+ r, err := DefaultClient.NewFetchPackSession(s.Endpoint)
+ c.Assert(err, IsNil)
+ info, err := r.AdvertisedReferences()
+ c.Assert(err, IsNil)
+ c.Assert(info.Capabilities.SymbolicReference("HEAD"), Equals, "refs/heads/master")
+}
+
+func (s *FetchPackSuite) TestCapabilities(c *C) {
+ r, err := DefaultClient.NewFetchPackSession(s.Endpoint)
+ c.Assert(err, IsNil)
+ info, err := r.AdvertisedReferences()
+ c.Assert(err, IsNil)
+ c.Assert(info.Capabilities.Get("agent").Values, HasLen, 1)
+}
+
+func (s *FetchPackSuite) TestFullFetchPack(c *C) {
+ r, err := DefaultClient.NewFetchPackSession(s.Endpoint)
+ c.Assert(err, IsNil)
+
+ info, err := r.AdvertisedReferences()
+ c.Assert(err, IsNil)
+ c.Assert(info, NotNil)
+
+ req := &transport.UploadPackRequest{}
+ req.Want(plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5"))
+
+ reader, err := r.FetchPack(req)
+ c.Assert(err, IsNil)
+
+ b, err := ioutil.ReadAll(reader)
+ c.Assert(err, IsNil)
+ c.Assert(b, HasLen, 85374)
+}
+
+func (s *FetchPackSuite) TestFetchPack(c *C) {
+ r, err := DefaultClient.NewFetchPackSession(s.Endpoint)
+ c.Assert(err, IsNil)
+
+ req := &transport.UploadPackRequest{}
+ req.Want(plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5"))
+
+ reader, err := r.FetchPack(req)
+ c.Assert(err, IsNil)
+
+ b, err := ioutil.ReadAll(reader)
+ c.Assert(err, IsNil)
+ c.Assert(b, HasLen, 85374)
+}
+
+func (s *FetchPackSuite) TestFetchPackNoChanges(c *C) {
+ r, err := DefaultClient.NewFetchPackSession(s.Endpoint)
+ c.Assert(err, IsNil)
+
+ req := &transport.UploadPackRequest{}
+ req.Want(plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5"))
+ req.Have(plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5"))
+
+ reader, err := r.FetchPack(req)
+ c.Assert(err, Equals, transport.ErrEmptyUploadPackRequest)
+ c.Assert(reader, IsNil)
+}
+
+func (s *FetchPackSuite) TestFetchPackMulti(c *C) {
+ r, err := DefaultClient.NewFetchPackSession(s.Endpoint)
+ c.Assert(err, IsNil)
+
+ req := &transport.UploadPackRequest{}
+ req.Want(plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5"))
+ req.Want(plumbing.NewHash("e8d3ffab552895c19b9fcf7aa264d277cde33881"))
+
+ reader, err := r.FetchPack(req)
+ c.Assert(err, IsNil)
+
+ b, err := ioutil.ReadAll(reader)
+ c.Assert(err, IsNil)
+ c.Assert(b, HasLen, 85585)
+}
diff --git a/plumbing/transport/http/send_pack.go b/plumbing/transport/http/send_pack.go
new file mode 100644
index 0000000..39be95c
--- /dev/null
+++ b/plumbing/transport/http/send_pack.go
@@ -0,0 +1,29 @@
+package http
+
+import (
+ "errors"
+ "io"
+ "net/http"
+
+ "gopkg.in/src-d/go-git.v4/plumbing/transport"
+)
+
+var errSendPackNotSupported = errors.New("send-pack not supported yet")
+
+type sendPackSession struct{
+ *session
+}
+
+func newSendPackSession(c *http.Client, ep transport.Endpoint) transport.SendPackSession {
+ return &sendPackSession{&session{}}
+}
+
+func (s *sendPackSession) AdvertisedReferences() (*transport.UploadPackInfo,
+ error) {
+
+ return nil, errSendPackNotSupported
+}
+
+func (s *sendPackSession) SendPack() (io.WriteCloser, error) {
+ return nil, errSendPackNotSupported
+}
diff --git a/plumbing/client/ssh/auth_method.go b/plumbing/transport/ssh/auth_method.go
index 587f59a..9c3d6f3 100644
--- a/plumbing/client/ssh/auth_method.go
+++ b/plumbing/transport/ssh/auth_method.go
@@ -7,14 +7,12 @@ import (
"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
}
diff --git a/plumbing/client/ssh/auth_method_test.go b/plumbing/transport/ssh/auth_method_test.go
index a87c950..f9e7dec 100644
--- a/plumbing/client/ssh/auth_method_test.go
+++ b/plumbing/transport/ssh/auth_method_test.go
@@ -2,13 +2,10 @@ package ssh
import (
"fmt"
- "testing"
. "gopkg.in/check.v1"
)
-func Test(t *testing.T) { TestingT(t) }
-
type SuiteCommon struct{}
var _ = Suite(&SuiteCommon{})
diff --git a/plumbing/transport/ssh/common.go b/plumbing/transport/ssh/common.go
new file mode 100644
index 0000000..6f0f3d4
--- /dev/null
+++ b/plumbing/transport/ssh/common.go
@@ -0,0 +1,151 @@
+package ssh
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "strings"
+
+ "gopkg.in/src-d/go-git.v4/plumbing/transport"
+
+ "golang.org/x/crypto/ssh"
+)
+
+// New errors introduced by this package.
+var (
+ ErrAdvertistedReferencesAlreadyCalled = errors.New("cannot call AdvertisedReference twice")
+ ErrAlreadyConnected = errors.New("ssh session already created")
+ ErrAuthRequired = errors.New("cannot connect: auth required")
+ ErrNotConnected = errors.New("not 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")
+)
+
+type Client struct{}
+
+var DefaultClient = NewClient()
+
+func NewClient() transport.Client {
+ return &Client{}
+}
+
+func (c *Client) NewFetchPackSession(ep transport.Endpoint) (
+ transport.FetchPackSession, error) {
+
+ return newFetchPackSession(ep)
+}
+
+func (c *Client) NewSendPackSession(ep transport.Endpoint) (
+ transport.SendPackSession, error) {
+
+ return newSendPackSession(ep)
+}
+
+type session struct {
+ connected bool
+ endpoint transport.Endpoint
+ client *ssh.Client
+ session *ssh.Session
+ stdin io.WriteCloser
+ stdout io.Reader
+ sessionDone chan error
+ auth AuthMethod
+}
+
+func (s *session) SetAuth(auth transport.AuthMethod) error {
+ a, ok := auth.(AuthMethod)
+ if !ok {
+ return transport.ErrInvalidAuthMethod
+ }
+
+ s.auth = a
+ return nil
+}
+
+// Close closes the SSH session.
+func (s *session) Close() error {
+ if !s.connected {
+ return nil
+ }
+
+ s.connected = false
+ return s.client.Close()
+}
+
+// ensureConnected 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 *session) 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
+ }
+
+ if err := s.openSSHSession(); err != nil {
+ _ = s.client.Close()
+ return err
+ }
+
+ s.connected = true
+ return nil
+}
+
+func (s *session) getHostWithPort() string {
+ host := s.endpoint.Host
+ if strings.Index(s.endpoint.Host, ":") == -1 {
+ host += ":22"
+ }
+
+ return host
+}
+
+func (s *session) setAuthFromEndpoint() error {
+ var u string
+ if info := s.endpoint.User; info != nil {
+ u = info.Username()
+ }
+
+ var err error
+ s.auth, err = NewSSHAgentAuth(u)
+ return err
+}
+
+func (s *session) openSSHSession() error {
+ var err error
+ s.session, err = s.client.NewSession()
+ if err != nil {
+ return fmt.Errorf("cannot open SSH session: %s", err)
+ }
+
+ s.stdin, err = s.session.StdinPipe()
+ if err != nil {
+ return fmt.Errorf("cannot pipe remote stdin: %s", err)
+ }
+
+ s.stdout, err = s.session.StdoutPipe()
+ if err != nil {
+ return fmt.Errorf("cannot pipe remote stdout: %s", err)
+ }
+
+ return nil
+}
+
+func (s *session) runCommand(cmd string) chan error {
+ done := make(chan error)
+ go func() {
+ done <- s.session.Run(cmd)
+ }()
+
+ return done
+}
diff --git a/plumbing/transport/ssh/common_test.go b/plumbing/transport/ssh/common_test.go
new file mode 100644
index 0000000..ac4d03e
--- /dev/null
+++ b/plumbing/transport/ssh/common_test.go
@@ -0,0 +1,17 @@
+package ssh
+
+import (
+ "testing"
+
+ . "gopkg.in/check.v1"
+)
+
+func Test(t *testing.T) { TestingT(t) }
+
+type ClientSuite struct{}
+
+var _ = Suite(&ClientSuite{})
+
+func (s *ClientSuite) TestNewClient(c *C) {
+ c.Assert(DefaultClient, DeepEquals, NewClient())
+}
diff --git a/plumbing/transport/ssh/fetch_pack.go b/plumbing/transport/ssh/fetch_pack.go
new file mode 100644
index 0000000..bda4edf
--- /dev/null
+++ b/plumbing/transport/ssh/fetch_pack.go
@@ -0,0 +1,202 @@
+// Package ssh implements a ssh client for go-git.
+package ssh
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+
+ "gopkg.in/src-d/go-git.v4/plumbing/format/packp/pktline"
+ "gopkg.in/src-d/go-git.v4/plumbing/format/packp/ulreq"
+ "gopkg.in/src-d/go-git.v4/plumbing/transport"
+
+ "golang.org/x/crypto/ssh"
+)
+
+type fetchPackSession struct {
+ *session
+ cmdRun bool
+ advRefsRun bool
+ done chan error
+}
+
+func newFetchPackSession(ep transport.Endpoint) (*fetchPackSession, error) {
+ s := &fetchPackSession{
+ session: &session{
+ endpoint: ep,
+ },
+ }
+ if err := s.connect(); err != nil {
+ return nil, err
+ }
+
+ return s, nil
+}
+
+func (s *fetchPackSession) AdvertisedReferences() (*transport.UploadPackInfo, error) {
+ if s.advRefsRun {
+ return nil, ErrAdvertistedReferencesAlreadyCalled
+ }
+
+ if err := s.ensureRunCommand(); err != nil {
+ return nil, err
+ }
+
+ defer func() { s.advRefsRun = true }()
+
+ i := transport.NewUploadPackInfo()
+ return i, i.Decode(s.stdout)
+}
+
+// FetchPack returns a packfile for a given upload request.
+// Closing the returned reader will close the SSH session.
+func (s *fetchPackSession) FetchPack(req *transport.UploadPackRequest) (
+ io.ReadCloser, error) {
+
+ if !s.advRefsRun {
+ if _, err := s.AdvertisedReferences(); err != nil {
+ return nil, err
+ }
+ }
+
+ if err := fetchPack(s.stdin, s.stdout, req); err != nil {
+ return nil, err
+ }
+
+ return &fetchSession{
+ Reader: s.stdout,
+ session: s.session.session,
+ done: s.done,
+ }, nil
+}
+
+func (s *fetchPackSession) ensureRunCommand() error {
+ if s.cmdRun {
+ return nil
+ }
+
+ s.cmdRun = true
+ s.done = s.runCommand(s.getCommand())
+ 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 *fetchPackSession) getCommand() string {
+ directory := s.endpoint.Path
+ directory = directory[1:]
+
+ return fmt.Sprintf("git-upload-pack '%s'", directory)
+}
+
+var (
+ nak = []byte("NAK")
+ eol = []byte("\n")
+)
+
+// FetchPack implements the git-fetch-pack protocol.
+//
+// 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 fetchPack(w io.WriteCloser, r io.Reader,
+ req *transport.UploadPackRequest) error {
+
+ 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 sendUlReq(w io.Writer, req *transport.UploadPackRequest) 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 *transport.UploadPackRequest) 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: %s", 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
+}
diff --git a/plumbing/transport/ssh/fetch_pack_test.go b/plumbing/transport/ssh/fetch_pack_test.go
new file mode 100644
index 0000000..3d62e57
--- /dev/null
+++ b/plumbing/transport/ssh/fetch_pack_test.go
@@ -0,0 +1,100 @@
+package ssh
+
+import (
+ "io/ioutil"
+ "os"
+
+ "gopkg.in/src-d/go-git.v4/plumbing"
+ "gopkg.in/src-d/go-git.v4/plumbing/transport"
+
+ . "gopkg.in/check.v1"
+)
+
+type FetchPackSuite struct {
+ Endpoint transport.Endpoint
+}
+
+var _ = Suite(&FetchPackSuite{})
+
+func (s *FetchPackSuite) SetUpSuite(c *C) {
+ var err error
+ s.Endpoint, err = transport.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")
+ }
+}
+
+func (s *FetchPackSuite) TestDefaultBranch(c *C) {
+ r, err := DefaultClient.NewFetchPackSession(s.Endpoint)
+ c.Assert(err, IsNil)
+ defer func() { c.Assert(r.Close(), IsNil) }()
+
+ info, err := r.AdvertisedReferences()
+ c.Assert(err, IsNil)
+ c.Assert(info.Capabilities.SymbolicReference("HEAD"), Equals, "refs/heads/master")
+}
+
+func (s *FetchPackSuite) TestCapabilities(c *C) {
+ r, err := DefaultClient.NewFetchPackSession(s.Endpoint)
+ c.Assert(err, IsNil)
+ defer func() { c.Assert(r.Close(), IsNil) }()
+
+ info, err := r.AdvertisedReferences()
+ c.Assert(err, IsNil)
+ c.Assert(info.Capabilities.Get("agent").Values, HasLen, 1)
+}
+
+func (s *FetchPackSuite) TestFullFetchPack(c *C) {
+ r, err := DefaultClient.NewFetchPackSession(s.Endpoint)
+ c.Assert(err, IsNil)
+ defer func() { c.Assert(r.Close(), IsNil) }()
+
+ _, err = r.AdvertisedReferences()
+ c.Assert(err, IsNil)
+
+ req := &transport.UploadPackRequest{}
+ req.Want(plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5"))
+ req.Want(plumbing.NewHash("e8d3ffab552895c19b9fcf7aa264d277cde33881"))
+ reader, err := r.FetchPack(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 *FetchPackSuite) TestFetchPack(c *C) {
+ r, err := DefaultClient.NewFetchPackSession(s.Endpoint)
+ c.Assert(err, IsNil)
+ defer func() { c.Assert(r.Close(), IsNil) }()
+
+ req := &transport.UploadPackRequest{}
+ req.Want(plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5"))
+ req.Want(plumbing.NewHash("e8d3ffab552895c19b9fcf7aa264d277cde33881"))
+ reader, err := r.FetchPack(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 *FetchPackSuite) TestFetchError(c *C) {
+ r, err := DefaultClient.NewFetchPackSession(s.Endpoint)
+ c.Assert(err, IsNil)
+ defer func() { c.Assert(r.Close(), IsNil) }()
+
+ req := &transport.UploadPackRequest{}
+ req.Want(plumbing.NewHash("1111111111111111111111111111111111111111"))
+
+ reader, err := r.FetchPack(req)
+ c.Assert(err, IsNil)
+
+ err = reader.Close()
+ c.Assert(err, Not(IsNil))
+}
diff --git a/plumbing/transport/ssh/send_pack.go b/plumbing/transport/ssh/send_pack.go
new file mode 100644
index 0000000..afe7510
--- /dev/null
+++ b/plumbing/transport/ssh/send_pack.go
@@ -0,0 +1,30 @@
+package ssh
+
+import (
+ "errors"
+ "io"
+
+ "gopkg.in/src-d/go-git.v4/plumbing/transport"
+)
+
+var errSendPackNotSupported = errors.New("send-pack not supported yet")
+
+type sendPackSession struct {
+ *session
+}
+
+func newSendPackSession(ep transport.Endpoint) (transport.SendPackSession,
+ error) {
+
+ return &sendPackSession{&session{}}, nil
+}
+
+func (s *sendPackSession) AdvertisedReferences() (*transport.UploadPackInfo,
+ error) {
+
+ return nil, errSendPackNotSupported
+}
+
+func (s *sendPackSession) SendPack() (io.WriteCloser, error) {
+ return nil, errSendPackNotSupported
+}