// 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"
"time"
"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"
)
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 {
// 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 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
}
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
Command Command
advRefsRun bool
packRun bool
finished bool
errLines chan string
}
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
}
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,
Command: cmd,
errLines: errLines,
}, 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
}
s.advRefsRun = true
ar := packp.NewAdvRefs()
if err := ar.Decode(s.Stdout); err != nil {
// 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
}
// 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
}
return nil, transport.ErrEmptyRemoteRepository
}
return nil, err
}
transport.FilterUnsupportedCapabilities(ar.Capabilities)
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 err := req.Validate(); err != nil {
return nil, err
}
if !s.advRefsRun {
if _, err := s.AdvertisedReferences(); err != nil {
return nil, err
}
}
s.packRun = true
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) 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()
}
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"
)
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()
}