diff options
author | Santiago M. Mola <santi@mola.io> | 2016-11-29 15:07:09 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-11-29 15:07:09 +0100 |
commit | 47007c70c5a696472576a522cd0e265a777f97a8 (patch) | |
tree | 2f49e52f42556a3707c24a263a571bcae39cfac6 | |
parent | 2c20b7e507a6514be2efa66143c13a60a87ee4b6 (diff) | |
download | go-git-47007c70c5a696472576a522cd0e265a777f97a8.tar.gz |
transport: add local transport (#145)
* transport: move common packp protocol out of ssh transport.
* fixtures: add fixture for empty repository.
* transport: add file:// transport
-rw-r--r-- | fixtures/fixtures.go | 5 | ||||
-rw-r--r-- | plumbing/transport/client/client.go | 5 | ||||
-rw-r--r-- | plumbing/transport/client/client_test.go | 2 | ||||
-rw-r--r-- | plumbing/transport/common.go | 3 | ||||
-rw-r--r-- | plumbing/transport/file/common.go | 72 | ||||
-rw-r--r-- | plumbing/transport/file/common_test.go | 9 | ||||
-rw-r--r-- | plumbing/transport/file/fetch_pack_test.go | 48 | ||||
-rw-r--r-- | plumbing/transport/internal/common/common.go | 295 | ||||
-rw-r--r-- | plumbing/transport/ssh/common.go | 139 | ||||
-rw-r--r-- | plumbing/transport/ssh/fetch_pack.go | 208 | ||||
-rw-r--r-- | plumbing/transport/ssh/fetch_pack_test.go | 1 | ||||
-rw-r--r-- | plumbing/transport/ssh/send_pack.go | 30 |
12 files changed, 484 insertions, 333 deletions
diff --git a/fixtures/fixtures.go b/fixtures/fixtures.go index 324f2da..4e5e2bb 100644 --- a/fixtures/fixtures.go +++ b/fixtures/fixtures.go @@ -115,6 +115,11 @@ var fixtures = Fixtures{{ Tags: []string{"packfile", "diff-tree"}, URL: "https://github.com/toqueteos/ts3.git", PackfileHash: plumbing.NewHash("21b33a26eb7ffbd35261149fe5d886b9debab7cb"), +}, { + Tags: []string{"empty", ".git"}, + URL: "https://github.com/git-fixtures/empty.git", + DotGitHash: plumbing.NewHash("4abe340d8d378baf7c2bfb2854c0fa498642bac3"), + ObjectsCount: 0, }} func All() Fixtures { diff --git a/plumbing/transport/client/client.go b/plumbing/transport/client/client.go index 5c6da05..770b7dc 100644 --- a/plumbing/transport/client/client.go +++ b/plumbing/transport/client/client.go @@ -4,6 +4,7 @@ import ( "fmt" "gopkg.in/src-d/go-git.v4/plumbing/transport" + "gopkg.in/src-d/go-git.v4/plumbing/transport/file" "gopkg.in/src-d/go-git.v4/plumbing/transport/http" "gopkg.in/src-d/go-git.v4/plumbing/transport/ssh" ) @@ -13,6 +14,7 @@ var Protocols = map[string]transport.Client{ "http": http.DefaultClient, "https": http.DefaultClient, "ssh": ssh.DefaultClient, + "file": file.DefaultClient, } // InstallProtocol adds or modifies an existing protocol. @@ -21,7 +23,8 @@ func InstallProtocol(scheme string, c transport.Client) { } // NewClient returns the appropriate client among of the set of known protocols: -// HTTP, SSH. See `InstallProtocol` to add or modify protocols. +// http://, https://, ssh:// and file://. +// See `InstallProtocol` to add or modify protocols. func NewClient(endpoint transport.Endpoint) (transport.Client, error) { f, ok := Protocols[endpoint.Scheme] if !ok { diff --git a/plumbing/transport/client/client_test.go b/plumbing/transport/client/client_test.go index 0f4b2e5..a0c8208 100644 --- a/plumbing/transport/client/client_test.go +++ b/plumbing/transport/client/client_test.go @@ -38,7 +38,7 @@ func (s *ClientSuite) TestNewClientSSH(c *C) { output, err := NewClient(e) c.Assert(err, IsNil) - c.Assert(typeAsString(output), Equals, "*ssh.client") + c.Assert(output, NotNil) } func (s *ClientSuite) TestNewClientUnknown(c *C) { diff --git a/plumbing/transport/common.go b/plumbing/transport/common.go index 2f6fcee..9715a39 100644 --- a/plumbing/transport/common.go +++ b/plumbing/transport/common.go @@ -34,7 +34,8 @@ var ( ) const ( - UploadPackServiceName = "git-upload-pack" + UploadPackServiceName = "git-upload-pack" + ReceivePackServiceName = "git-receive-pack" ) // Client can initiate git-fetch-pack and git-send-pack processes. diff --git a/plumbing/transport/file/common.go b/plumbing/transport/file/common.go new file mode 100644 index 0000000..82cbba2 --- /dev/null +++ b/plumbing/transport/file/common.go @@ -0,0 +1,72 @@ +package file + +import ( + "io" + "os/exec" + + "gopkg.in/src-d/go-git.v4/plumbing/transport" + "gopkg.in/src-d/go-git.v4/plumbing/transport/internal/common" +) + +// DefaultClient is the default local client. +var DefaultClient = NewClient( + transport.UploadPackServiceName, + transport.ReceivePackServiceName, +) + +type runner struct { + UploadPackBin string + ReceivePackBin string +} + +// NewClient returns a new local client using the given git-upload-pack and +// git-receive-pack binaries. +func NewClient(uploadPackBin, receivePackBin string) transport.Client { + return common.NewClient(&runner{ + UploadPackBin: uploadPackBin, + ReceivePackBin: receivePackBin, + }) +} + +func (r *runner) Command(cmd string, ep transport.Endpoint) (common.Command, error) { + return &command{cmd: exec.Command(cmd, ep.Path)}, nil +} + +type command struct { + cmd *exec.Cmd + closed bool +} + +func (c *command) SetAuth(auth transport.AuthMethod) error { + if auth != nil { + return transport.ErrInvalidAuthMethod + } + + return nil +} + +func (c *command) Start() error { + return c.cmd.Start() +} + +func (c *command) StderrPipe() (io.Reader, error) { + return c.cmd.StderrPipe() +} + +func (c *command) StdinPipe() (io.WriteCloser, error) { + return c.cmd.StdinPipe() +} + +func (c *command) StdoutPipe() (io.Reader, error) { + return c.cmd.StdoutPipe() +} + +// Close waits for the command to exit. +func (c *command) Close() error { + return c.cmd.Process.Kill() +} + +func (c *command) Wait() error { + defer func() { c.closed = true }() + return c.cmd.Wait() +} diff --git a/plumbing/transport/file/common_test.go b/plumbing/transport/file/common_test.go new file mode 100644 index 0000000..94ca4c9 --- /dev/null +++ b/plumbing/transport/file/common_test.go @@ -0,0 +1,9 @@ +package file + +import ( + "testing" + + . "gopkg.in/check.v1" +) + +func Test(t *testing.T) { TestingT(t) } diff --git a/plumbing/transport/file/fetch_pack_test.go b/plumbing/transport/file/fetch_pack_test.go new file mode 100644 index 0000000..80f11ee --- /dev/null +++ b/plumbing/transport/file/fetch_pack_test.go @@ -0,0 +1,48 @@ +package file + +import ( + "fmt" + "os/exec" + + "gopkg.in/src-d/go-git.v4/fixtures" + "gopkg.in/src-d/go-git.v4/plumbing/transport" + "gopkg.in/src-d/go-git.v4/plumbing/transport/test" + + . "gopkg.in/check.v1" +) + +type FetchPackSuite struct { + fixtures.Suite + test.FetchPackSuite +} + +var _ = Suite(&FetchPackSuite{}) + +func (s *FetchPackSuite) SetUpSuite(c *C) { + s.Suite.SetUpSuite(c) + + if err := exec.Command("git", "--version").Run(); err != nil { + c.Skip("git command not found") + } + + s.FetchPackSuite.Client = DefaultClient + + fixture := fixtures.Basic().One() + path := fixture.DotGit().Base() + url := fmt.Sprintf("file://%s", path) + ep, err := transport.NewEndpoint(url) + c.Assert(err, IsNil) + s.Endpoint = ep + + fixture = fixtures.ByTag("empty").One() + path = fixture.DotGit().Base() + url = fmt.Sprintf("file://%s", path) + ep, err = transport.NewEndpoint(url) + c.Assert(err, IsNil) + s.EmptyEndpoint = ep + + url = fmt.Sprintf("file://%s/%s", fixtures.DataFolder, "non-existent") + ep, err = transport.NewEndpoint(url) + c.Assert(err, IsNil) + s.NonExistentEndpoint = ep +} diff --git a/plumbing/transport/internal/common/common.go b/plumbing/transport/internal/common/common.go new file mode 100644 index 0000000..10e395e --- /dev/null +++ b/plumbing/transport/internal/common/common.go @@ -0,0 +1,295 @@ +// Package common implements the git pack protocol with a pluggable transport. +// This is a low-level package to implement new transports. Use a concrete +// implementation instead (e.g. http, file, ssh). +// +// A simple example of usage can be found in the file package. +package common + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "io" + "strings" + + "gopkg.in/src-d/go-git.v4/plumbing/format/pktline" + "gopkg.in/src-d/go-git.v4/plumbing/protocol/packp" + "gopkg.in/src-d/go-git.v4/plumbing/transport" + "gopkg.in/src-d/go-git.v4/utils/ioutil" +) + +// Commander creates Command instances. This is the main entry point for +// transport implementations. +type Commander interface { + // Command creates a new Command for the given git command and + // endpoint. cmd can be git-upload-pack or git-receive-pack. An + // error should be returned if the endpoint is not supported or the + // command cannot be created (e.g. binary does not exist, connection + // cannot be established). + Command(cmd string, ep transport.Endpoint) (Command, error) +} + +// Command is used for a single command execution. +// This interface is modeled after exec.Cmd and ssh.Session in the standard +// library. +type Command interface { + // SetAuth sets the authentication method. + SetAuth(transport.AuthMethod) error + // StderrPipe returns a pipe that will be connected to the command's + // standard error when the command starts. It should not be called after + // Start. + StderrPipe() (io.Reader, error) + // StdinPipe returns a pipe that will be connected to the command's + // standard input when the command starts. It should not be called after + // Start. The pipe should be closed when no more input is expected. + StdinPipe() (io.WriteCloser, error) + // StdoutPipe returns a pipe that will be connected to the command's + // standard output when the command starts. It should not be called after + // Start. + StdoutPipe() (io.Reader, error) + // Start starts the specified command. It does not wait for it to + // complete. + Start() error + // Wait waits for the command to exit. It must have been started by + // Start. The returned error is nil if the command runs, has no + // problems copying stdin, stdout, and stderr, and exits with a zero + // exit status. + Wait() error + // Close closes the command without waiting for it to exit and releases + // any resources. It can be called to forcibly finish the command + // without calling to Wait or to release resources after calling + // Wait. + Close() error +} + +type client struct { + cmdr Commander +} + +// NewClient creates a new client using the given Commander. +func NewClient(runner Commander) transport.Client { + return &client{runner} +} + +// NewFetchPackSession creates a new FetchPackSession. +func (c *client) NewFetchPackSession(ep transport.Endpoint) ( + transport.FetchPackSession, error) { + + return c.newSession(transport.UploadPackServiceName, ep) +} + +// NewSendPackSession creates a new SendPackSession. +func (c *client) NewSendPackSession(ep transport.Endpoint) ( + transport.SendPackSession, error) { + + return nil, errors.New("git send-pack not supported") +} + +type session struct { + Stdin io.WriteCloser + Stdout io.Reader + Stderr io.Reader + Command Command + + advRefsRun bool +} + +func (c *client) newSession(s string, ep transport.Endpoint) (*session, error) { + cmd, err := c.cmdr.Command(s, ep) + if err != nil { + return nil, err + } + + stdin, err := cmd.StdinPipe() + if err != nil { + return nil, err + } + + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, err + } + + stderr, err := cmd.StderrPipe() + if err != nil { + return nil, err + } + + if err := cmd.Start(); err != nil { + return nil, err + } + + return &session{ + Stdin: stdin, + Stdout: stdout, + Stderr: stderr, + Command: cmd, + }, nil +} + +// SetAuth delegates to the command's SetAuth. +func (s *session) SetAuth(auth transport.AuthMethod) error { + return s.Command.SetAuth(auth) +} + +// AdvertisedReferences retrieves the advertised references from the server. +func (s *session) AdvertisedReferences() (*packp.AdvRefs, error) { + if s.advRefsRun { + return nil, transport.ErrAdvertistedReferencesAlreadyCalled + } + + defer func() { s.advRefsRun = true }() + + ar := packp.NewAdvRefs() + if err := ar.Decode(s.Stdout); err != nil { + if err != packp.ErrEmptyAdvRefs { + return nil, err + } + + _ = s.Stdin.Close() + err = transport.ErrEmptyRemoteRepository + + scan := bufio.NewScanner(s.Stderr) + if !scan.Scan() { + return nil, transport.ErrEmptyRemoteRepository + } + + if isRepoNotFoundError(string(scan.Bytes())) { + return nil, transport.ErrRepositoryNotFound + } + + return nil, err + } + + return ar, nil +} + +// FetchPack performs a request to the server to fetch a packfile. A reader is +// returned with the packfile content. The reader must be closed after reading. +func (s *session) FetchPack(req *packp.UploadPackRequest) (io.ReadCloser, error) { + if req.IsEmpty() { + return nil, transport.ErrEmptyUploadPackRequest + } + + 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 + } + + r, err := ioutil.NonEmptyReader(s.Stdout) + if err == ioutil.ErrEmptyReader { + if c, ok := s.Stdout.(io.Closer); ok { + _ = c.Close() + } + + return nil, transport.ErrEmptyUploadPackRequest + } + + if err != nil { + return nil, err + } + + wc := &waitCloser{s.Command} + rc := ioutil.NewReadCloser(r, wc) + + return rc, nil +} + +func (s *session) Close() error { + return s.Command.Close() +} + +const ( + githubRepoNotFoundErr = "ERROR: Repository not found." + bitbucketRepoNotFoundErr = "conq: repository does not exist." + localRepoNotFoundErr = "does not appear to be a git repository" +) + +func isRepoNotFoundError(s string) bool { + if strings.HasPrefix(s, githubRepoNotFoundErr) { + return true + } + + if strings.HasPrefix(s, bitbucketRepoNotFoundErr) { + return true + } + + if strings.HasSuffix(s, localRepoNotFoundErr) { + return true + } + + return false +} + +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 *packp.UploadPackRequest) error { + + if err := req.UploadRequest.Encode(w); err != nil { + return fmt.Errorf("sending upload-req message: %s", err) + } + + if err := req.UploadHaves.Encode(w); 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 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 waitCloser struct { + Command Command +} + +// Close waits until the command exits and returns error, if any. +func (c *waitCloser) Close() error { + return c.Command.Wait() +} diff --git a/plumbing/transport/ssh/common.go b/plumbing/transport/ssh/common.go index c327c41..a88a328 100644 --- a/plumbing/transport/ssh/common.go +++ b/plumbing/transport/ssh/common.go @@ -3,10 +3,10 @@ package ssh import ( "errors" "fmt" - "io" "strings" "gopkg.in/src-d/go-git.v4/plumbing/transport" + "gopkg.in/src-d/go-git.v4/plumbing/transport/internal/common" "golang.org/x/crypto/ssh" ) @@ -15,155 +15,110 @@ var ( errAlreadyConnected = errors.New("ssh session already created") ) -type client struct{} - // DefaultClient is the default SSH client. -var DefaultClient = &client{} - -func (c *client) NewFetchPackSession(ep transport.Endpoint) ( - transport.FetchPackSession, error) { +var DefaultClient = common.NewClient(&runner{}) - return newFetchPackSession(ep) -} +type runner struct{} -func (c *client) NewSendPackSession(ep transport.Endpoint) ( - transport.SendPackSession, error) { +func (r *runner) Command(cmd string, ep transport.Endpoint) (common.Command, error) { + c := &command{command: cmd, endpoint: ep} + if err := c.connect(); err != nil { + return nil, err + } - return newSendPackSession(ep) + return c, nil } -type session struct { - connected bool - endpoint transport.Endpoint - client *ssh.Client - session *ssh.Session - stdin io.WriteCloser - stdout io.Reader - stderr io.Reader - sessionDone chan error - auth AuthMethod +type command struct { + *ssh.Session + connected bool + command string + endpoint transport.Endpoint + client *ssh.Client + auth AuthMethod } -func (s *session) SetAuth(auth transport.AuthMethod) error { +func (c *command) SetAuth(auth transport.AuthMethod) error { a, ok := auth.(AuthMethod) if !ok { return transport.ErrInvalidAuthMethod } - s.auth = a + c.auth = a return nil } -// Close closes the SSH session. -func (s *session) Close() error { - if !s.connected { +func (c *command) Start() error { + return c.Session.Start(endpointToCommand(c.command, c.endpoint)) +} + +// Close closes the SSH session and connection. +func (c *command) Close() error { + if !c.connected { return nil } - s.connected = false + c.connected = false //XXX: If did read the full packfile, then the session might be already // closed. - _ = s.session.Close() + _ = c.Session.Close() - return s.client.Close() + return c.client.Close() } -// ensureConnected connects to the SSH server, unless a AuthMethod was set with +// 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 *session) connect() error { - if s.connected { +func (c *command) connect() error { + if c.connected { return errAlreadyConnected } - if err := s.setAuthFromEndpoint(); err != nil { + if err := c.setAuthFromEndpoint(); err != nil { return err } var err error - s.client, err = ssh.Dial("tcp", s.getHostWithPort(), s.auth.clientConfig()) + c.client, err = ssh.Dial("tcp", c.getHostWithPort(), c.auth.clientConfig()) if err != nil { return err } - if err := s.openSSHSession(); err != nil { - _ = s.client.Close() + c.Session, err = c.client.NewSession() + if err != nil { + _ = c.client.Close() return err } - s.connected = true + c.connected = true return nil } -func (s *session) getHostWithPort() string { - host := s.endpoint.Host - if strings.Index(s.endpoint.Host, ":") == -1 { +func (c *command) getHostWithPort() string { + host := c.endpoint.Host + if strings.Index(c.endpoint.Host, ":") == -1 { host += ":22" } return host } -func (s *session) setAuthFromEndpoint() error { +func (c *command) setAuthFromEndpoint() error { var u string - if info := s.endpoint.User; info != nil { + if info := c.endpoint.User; info != nil { u = info.Username() } var err error - s.auth, err = NewSSHAgentAuth(u) + c.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) - } - - s.stderr, err = s.session.StderrPipe() - if err != nil { - return fmt.Errorf("cannot pipe remote stderr: %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 -} - -const ( - githubRepoNotFoundErr = "ERROR: Repository not found." - bitbucketRepoNotFoundErr = "conq: repository does not exist." -) - -func isRepoNotFoundError(s string) bool { - if strings.HasPrefix(s, githubRepoNotFoundErr) { - return true - } - - if strings.HasPrefix(s, bitbucketRepoNotFoundErr) { - return true - } +func endpointToCommand(cmd string, ep transport.Endpoint) string { + directory := ep.Path + directory = directory[1:] - return false + return fmt.Sprintf("%s '%s'", cmd, directory) } diff --git a/plumbing/transport/ssh/fetch_pack.go b/plumbing/transport/ssh/fetch_pack.go deleted file mode 100644 index a0f52f1..0000000 --- a/plumbing/transport/ssh/fetch_pack.go +++ /dev/null @@ -1,208 +0,0 @@ -// Package ssh implements a ssh client for go-git. -package ssh - -import ( - "bufio" - "bytes" - "fmt" - "io" - - "gopkg.in/src-d/go-git.v4/plumbing/format/pktline" - "gopkg.in/src-d/go-git.v4/plumbing/protocol/packp" - "gopkg.in/src-d/go-git.v4/plumbing/transport" - "gopkg.in/src-d/go-git.v4/utils/ioutil" - - "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() (*packp.AdvRefs, error) { - if s.advRefsRun { - return nil, transport.ErrAdvertistedReferencesAlreadyCalled - } - - defer func() { s.advRefsRun = true }() - - if err := s.ensureRunCommand(); err != nil { - return nil, err - } - - ar := packp.NewAdvRefs() - if err := ar.Decode(s.stdout); err != nil { - if err != packp.ErrEmptyAdvRefs { - return nil, err - } - - _ = s.stdin.Close() - scan := bufio.NewScanner(s.stderr) - if !scan.Scan() { - return nil, transport.ErrEmptyRemoteRepository - } - - if isRepoNotFoundError(string(scan.Bytes())) { - return nil, transport.ErrRepositoryNotFound - } - - return nil, err - } - - return ar, nil -} - -// FetchPack returns a packfile for a given upload request. -// Closing the returned reader will close the SSH session. -func (s *fetchPackSession) FetchPack(req *packp.UploadPackRequest) ( - io.ReadCloser, error) { - - if req.IsEmpty() { - return nil, transport.ErrEmptyUploadPackRequest - } - - 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 - } - - fs := &fetchSession{ - Reader: s.stdout, - session: s.session.session, - done: s.done, - } - - r, err := ioutil.NonEmptyReader(fs) - if err == ioutil.ErrEmptyReader { - _ = fs.Close() - return nil, transport.ErrEmptyUploadPackRequest - } - - return ioutil.NewReadCloser(r, fs), 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 *packp.UploadPackRequest) error { - - if err := req.UploadRequest.Encode(w); err != nil { - return fmt.Errorf("sending upload-req message: %s", err) - } - - if err := req.UploadHaves.Encode(w); 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 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 index a0321b3..927e9a8 100644 --- a/plumbing/transport/ssh/fetch_pack_test.go +++ b/plumbing/transport/ssh/fetch_pack_test.go @@ -33,4 +33,5 @@ func (s *FetchPackSuite) SetUpSuite(c *C) { ep, err = transport.NewEndpoint("git@github.com:git-fixtures/non-existent.git") c.Assert(err, IsNil) s.FetchPackSuite.NonExistentEndpoint = ep + } diff --git a/plumbing/transport/ssh/send_pack.go b/plumbing/transport/ssh/send_pack.go deleted file mode 100644 index adf67bb..0000000 --- a/plumbing/transport/ssh/send_pack.go +++ /dev/null @@ -1,30 +0,0 @@ -package ssh - -import ( - "errors" - "io" - - "gopkg.in/src-d/go-git.v4/plumbing/protocol/packp" - "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() (*packp.AdvRefs, error) { - - return nil, errSendPackNotSupported -} - -func (s *sendPackSession) SendPack() (io.WriteCloser, error) { - return nil, errSendPackNotSupported -} |