From ac095bb12c4d29722b60ba9f20590fa7cfa6bc7d Mon Sep 17 00:00:00 2001 From: Máximo Cuadros Date: Tue, 8 Nov 2016 23:46:38 +0100 Subject: new plumbing package (#118) * plumbing: now core was renamed to core, and formats and clients moved inside --- plumbing/client/common.go | 48 ++++ plumbing/client/common/common.go | 219 +++++++++++++++++++ plumbing/client/common/common_test.go | 126 +++++++++++ plumbing/client/common_test.go | 85 ++++++++ plumbing/client/http/common.go | 77 +++++++ plumbing/client/http/common_test.go | 52 +++++ plumbing/client/http/git_upload_pack.go | 186 ++++++++++++++++ plumbing/client/http/git_upload_pack_test.go | 135 ++++++++++++ plumbing/client/ssh/auth_method.go | 159 ++++++++++++++ plumbing/client/ssh/auth_method_test.go | 94 ++++++++ plumbing/client/ssh/git_upload_pack.go | 315 +++++++++++++++++++++++++++ plumbing/client/ssh/git_upload_pack_test.go | 144 ++++++++++++ 12 files changed, 1640 insertions(+) create mode 100644 plumbing/client/common.go create mode 100644 plumbing/client/common/common.go create mode 100644 plumbing/client/common/common_test.go create mode 100644 plumbing/client/common_test.go create mode 100644 plumbing/client/http/common.go create mode 100644 plumbing/client/http/common_test.go create mode 100644 plumbing/client/http/git_upload_pack.go create mode 100644 plumbing/client/http/git_upload_pack_test.go create mode 100644 plumbing/client/ssh/auth_method.go create mode 100644 plumbing/client/ssh/auth_method_test.go create mode 100644 plumbing/client/ssh/git_upload_pack.go create mode 100644 plumbing/client/ssh/git_upload_pack_test.go (limited to 'plumbing/client') 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[^@]+@)?(?P[^:]+):/?(?P.+)$") +) + +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 = "" + } + + 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)) +} -- cgit