diff options
-rw-r--r-- | plumbing/protocol/packp/advrefs.go | 41 | ||||
-rw-r--r-- | plumbing/protocol/packp/advrefs_decode.go | 41 | ||||
-rw-r--r-- | plumbing/protocol/packp/advrefs_decode_test.go | 2 | ||||
-rw-r--r-- | plumbing/transport/file/common.go | 6 | ||||
-rw-r--r-- | plumbing/transport/file/fetch_pack_test.go | 30 | ||||
-rw-r--r-- | plumbing/transport/internal/common/common.go | 113 |
6 files changed, 175 insertions, 58 deletions
diff --git a/plumbing/protocol/packp/advrefs.go b/plumbing/protocol/packp/advrefs.go index a0587ab..4e031fb 100644 --- a/plumbing/protocol/packp/advrefs.go +++ b/plumbing/protocol/packp/advrefs.go @@ -13,25 +13,32 @@ import ( // AdvRefs values represent the information transmitted on an // advertised-refs message. Values from this type are not zero-value // safe, use the New function instead. -// -// When using this messages over (smart) HTTP, you have to add a pktline -// before the whole thing with the following payload: -// -// '# service=$servicename" LF -// -// Moreover, some (all) git HTTP smart servers will send a flush-pkt -// just after the first pkt-line. -// -// To accomodate both situations, the Prefix field allow you to store -// any data you want to send before the actual pktlines. It will also -// be filled up with whatever is found on the line. type AdvRefs struct { - Prefix [][]byte // payloads of the prefix - Head *plumbing.Hash + // Prefix stores prefix payloads. + // + // When using this message over (smart) HTTP, you have to add a pktline + // before the whole thing with the following payload: + // + // '# service=$servicename" LF + // + // Moreover, some (all) git HTTP smart servers will send a flush-pkt + // just after the first pkt-line. + // + // To accomodate both situations, the Prefix field allow you to store + // any data you want to send before the actual pktlines. It will also + // be filled up with whatever is found on the line. + Prefix [][]byte + // Head stores the resolved HEAD reference if present. + // This can be present with git-upload-pack, not with git-receive-pack. + Head *plumbing.Hash + // Capabilities are the capabilities. Capabilities *capability.List - References map[string]plumbing.Hash - Peeled map[string]plumbing.Hash - Shallows []plumbing.Hash + // References are the hash references. + References map[string]plumbing.Hash + // Peeled are the peeled hash references. + Peeled map[string]plumbing.Hash + // Shallows are the shallow object ids. + Shallows []plumbing.Hash } // NewAdvRefs returns a pointer to a new AdvRefs value, ready to be used. diff --git a/plumbing/protocol/packp/advrefs_decode.go b/plumbing/protocol/packp/advrefs_decode.go index 5926645..964e3eb 100644 --- a/plumbing/protocol/packp/advrefs_decode.go +++ b/plumbing/protocol/packp/advrefs_decode.go @@ -27,8 +27,13 @@ type advRefsDecoder struct { data *AdvRefs // parsed data is stored here } -// ErrEmptyAdvRefs is returned by Decode when there was no advertised-message at all -var ErrEmptyAdvRefs = errors.New("empty advertised-ref message") +var ( + // ErrEmptyAdvRefs is returned by Decode if it gets an empty advertised + // references message. + ErrEmptyAdvRefs = errors.New("empty advertised-ref message") + // ErrEmptyInput is returned by Decode if the input is empty. + ErrEmptyInput = errors.New("empty input") +) func newAdvRefsDecoder(r io.Reader) *advRefsDecoder { return &advRefsDecoder{ @@ -67,7 +72,7 @@ func (d *advRefsDecoder) nextLine() bool { } if d.nLine == 1 { - d.err = ErrEmptyAdvRefs + d.err = ErrEmptyInput return false } @@ -87,33 +92,31 @@ func decodePrefix(d *advRefsDecoder) decoderStateFn { return nil } - // If the repository is empty, we receive a flush here (SSH). - if isFlush(d.line) { - d.err = ErrEmptyAdvRefs + if !isPrefix(d.line) { + return decodeFirstHash + } + + tmp := make([]byte, len(d.line)) + copy(tmp, d.line) + d.data.Prefix = append(d.data.Prefix, tmp) + if ok := d.nextLine(); !ok { return nil } - if isPrefix(d.line) { - tmp := make([]byte, len(d.line)) - copy(tmp, d.line) - d.data.Prefix = append(d.data.Prefix, tmp) - if ok := d.nextLine(); !ok { - return nil - } + if !isFlush(d.line) { + return decodeFirstHash } - if isFlush(d.line) { - d.data.Prefix = append(d.data.Prefix, pktline.Flush) - if ok := d.nextLine(); !ok { - return nil - } + d.data.Prefix = append(d.data.Prefix, pktline.Flush) + if ok := d.nextLine(); !ok { + return nil } return decodeFirstHash } func isPrefix(payload []byte) bool { - return payload[0] == '#' + return len(payload) > 0 && payload[0] == '#' } func isFlush(payload []byte) bool { diff --git a/plumbing/protocol/packp/advrefs_decode_test.go b/plumbing/protocol/packp/advrefs_decode_test.go index f807f15..2cc2568 100644 --- a/plumbing/protocol/packp/advrefs_decode_test.go +++ b/plumbing/protocol/packp/advrefs_decode_test.go @@ -19,7 +19,7 @@ var _ = Suite(&AdvRefsDecodeSuite{}) func (s *AdvRefsDecodeSuite) TestEmpty(c *C) { var buf bytes.Buffer ar := NewAdvRefs() - c.Assert(ar.Decode(&buf), Equals, ErrEmptyAdvRefs) + c.Assert(ar.Decode(&buf), Equals, ErrEmptyInput) } func (s *AdvRefsDecodeSuite) TestEmptyFlush(c *C) { diff --git a/plumbing/transport/file/common.go b/plumbing/transport/file/common.go index 82cbba2..8697121 100644 --- a/plumbing/transport/file/common.go +++ b/plumbing/transport/file/common.go @@ -29,6 +29,12 @@ func NewClient(uploadPackBin, receivePackBin string) transport.Client { } func (r *runner) Command(cmd string, ep transport.Endpoint) (common.Command, error) { + switch cmd { + case transport.UploadPackServiceName: + cmd = r.UploadPackBin + case transport.ReceivePackServiceName: + cmd = r.ReceivePackBin + } return &command{cmd: exec.Command(cmd, ep.Path)}, nil } diff --git a/plumbing/transport/file/fetch_pack_test.go b/plumbing/transport/file/fetch_pack_test.go index 80f11ee..7a23285 100644 --- a/plumbing/transport/file/fetch_pack_test.go +++ b/plumbing/transport/file/fetch_pack_test.go @@ -2,6 +2,7 @@ package file import ( "fmt" + "os" "os/exec" "gopkg.in/src-d/go-git.v4/fixtures" @@ -46,3 +47,32 @@ func (s *FetchPackSuite) SetUpSuite(c *C) { c.Assert(err, IsNil) s.NonExistentEndpoint = ep } + +// TODO: fix test +func (s *FetchPackSuite) TestCommandNoOutput(c *C) { + c.Skip("failing test") + + if _, err := os.Stat("/bin/true"); os.IsNotExist(err) { + c.Skip("/bin/true not found") + } + + client := NewClient("true", "true") + session, err := client.NewFetchPackSession(s.Endpoint) + c.Assert(err, IsNil) + ar, err := session.AdvertisedReferences() + c.Assert(err, IsNil) + c.Assert(ar, IsNil) +} + +func (s *FetchPackSuite) TestMalformedInputNoErrors(c *C) { + if _, err := os.Stat("/usr/bin/yes"); os.IsNotExist(err) { + c.Skip("/usr/bin/yes not found") + } + + client := NewClient("yes", "yes") + session, err := client.NewFetchPackSession(s.Endpoint) + c.Assert(err, IsNil) + ar, err := session.AdvertisedReferences() + c.Assert(err, NotNil) + c.Assert(ar, IsNil) +} diff --git a/plumbing/transport/internal/common/common.go b/plumbing/transport/internal/common/common.go index 56edab0..8b2f9f3 100644 --- a/plumbing/transport/internal/common/common.go +++ b/plumbing/transport/internal/common/common.go @@ -12,6 +12,7 @@ import ( "fmt" "io" "strings" + "time" "gopkg.in/src-d/go-git.v4/plumbing/format/pktline" "gopkg.in/src-d/go-git.v4/plumbing/protocol/packp" @@ -19,6 +20,15 @@ import ( "gopkg.in/src-d/go-git.v4/utils/ioutil" ) +const ( + readErrorSecondsTimeout = 10 + errLinesBuffer = 1000 +) + +var ( + ErrTimeoutExceeded = errors.New("timeout exceeded") +) + // Commander creates Command instances. This is the main entry point for // transport implementations. type Commander interface { @@ -56,10 +66,9 @@ type Command interface { // 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 closes the command and releases any resources used by it. It + // can be called to forcibly finish the command without calling to Wait + // or to release resources after calling Wait. Close() error } @@ -89,10 +98,12 @@ func (c *client) NewSendPackSession(ep transport.Endpoint) ( type session struct { Stdin io.WriteCloser Stdout io.Reader - Stderr io.Reader Command Command advRefsRun bool + packRun bool + finished bool + errLines chan string } func (c *client) newSession(s string, ep transport.Endpoint) (*session, error) { @@ -120,11 +131,20 @@ func (c *client) newSession(s string, ep transport.Endpoint) (*session, error) { return nil, err } + errLines := make(chan string, errLinesBuffer) + go func() { + s := bufio.NewScanner(stderr) + for s.Scan() { + line := string(s.Bytes()) + errLines <- line + } + }() + return &session{ - Stdin: stdin, - Stdout: stdout, - Stderr: stderr, - Command: cmd, + Stdin: stdin, + Stdout: stdout, + Command: cmd, + errLines: errLines, }, nil } @@ -139,26 +159,31 @@ func (s *session) AdvertisedReferences() (*packp.AdvRefs, error) { return nil, transport.ErrAdvertistedReferencesAlreadyCalled } - defer func() { s.advRefsRun = true }() + s.advRefsRun = true ar := packp.NewAdvRefs() if err := ar.Decode(s.Stdout); err != nil { - if err != packp.ErrEmptyAdvRefs { - return nil, err + // If repository is not found, we get empty stdout and server + // writes an error to stderr. + if err == packp.ErrEmptyInput { + if err := s.checkNotFoundError(); err != nil { + return nil, err + } + + return nil, io.ErrUnexpectedEOF } - _ = s.Stdin.Close() - err = transport.ErrEmptyRemoteRepository + // For empty (but existing) repositories, we get empty + // advertised-references message. But valid. That is, it + // includes at least a flush. + if err == packp.ErrEmptyAdvRefs { + if err := s.finish(); err != nil { + return nil, err + } - scan := bufio.NewScanner(s.Stderr) - if !scan.Scan() { return nil, transport.ErrEmptyRemoteRepository } - if isRepoNotFoundError(string(scan.Bytes())) { - return nil, transport.ErrRepositoryNotFound - } - return nil, err } @@ -183,6 +208,8 @@ func (s *session) FetchPack(req *packp.UploadPackRequest) (io.ReadCloser, error) } } + s.packRun = true + if err := fetchPack(s.Stdin, s.Stdout, req); err != nil { return nil, err } @@ -206,11 +233,55 @@ func (s *session) FetchPack(req *packp.UploadPackRequest) (io.ReadCloser, error) return rc, nil } +func (s *session) finish() error { + if s.finished { + return nil + } + + s.finished = true + + // If we did not run fetch-pack or send-pack, we close the connection + // gracefully by sending a flush packet to the server. If the server + // operates correctly, it will exit with status 0. + if !s.packRun { + _, err := s.Stdin.Write(pktline.FlushPkt) + return err + } + + return nil +} + func (s *session) Close() error { + if err := s.finish(); err != nil { + _ = s.Command.Close() + return nil + } + return s.Command.Close() } -const ( +func (s *session) checkNotFoundError() error { + t := time.NewTicker(time.Second * readErrorSecondsTimeout) + defer t.Stop() + + select { + case <-t.C: + return ErrTimeoutExceeded + case line, ok := <-s.errLines: + if !ok { + return nil + } + + if isRepoNotFoundError(line) { + return transport.ErrRepositoryNotFound + } + + return fmt.Errorf("unknown error: %s", line) + } + return nil +} + +var ( githubRepoNotFoundErr = "ERROR: Repository not found." bitbucketRepoNotFoundErr = "conq: repository does not exist." localRepoNotFoundErr = "does not appear to be a git repository" |