From cf6274280555a8d6484bfac7a756425592898799 Mon Sep 17 00:00:00 2001 From: "Santiago M. Mola" Date: Fri, 25 Nov 2016 17:21:39 +0100 Subject: [PATCH 1/4] transport: move common packp protocol out of ssh transport. --- plumbing/transport/client/client_test.go | 2 +- plumbing/transport/internal/common/common.go | 250 +++++++++++++++++++ plumbing/transport/ssh/common.go | 139 ++++------- plumbing/transport/ssh/fetch_pack.go | 208 --------------- plumbing/transport/ssh/fetch_pack_test.go | 1 + plumbing/transport/ssh/send_pack.go | 30 --- 6 files changed, 299 insertions(+), 331 deletions(-) create mode 100644 plumbing/transport/internal/common/common.go delete mode 100644 plumbing/transport/ssh/fetch_pack.go delete mode 100644 plumbing/transport/ssh/send_pack.go diff --git a/plumbing/transport/client/client_test.go b/plumbing/transport/client/client_test.go index 0f4b2e5be..a0c82084c 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/internal/common/common.go b/plumbing/transport/internal/common/common.go new file mode 100644 index 000000000..270427218 --- /dev/null +++ b/plumbing/transport/internal/common/common.go @@ -0,0 +1,250 @@ +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" +) + +type Command interface { + SetAuth(transport.AuthMethod) error + Start() error + StderrPipe() (io.Reader, error) + StdinPipe() (io.WriteCloser, error) + StdoutPipe() (io.Reader, error) + Wait() error + io.Closer +} + +type Commander interface { + Command(cmd string, ep transport.Endpoint) (Command, error) +} + +type client struct { + cmdr Commander +} + +// NewClient creates a new client using a CommandRunner. +func NewClient(runner Commander) transport.Client { + return &client{runner} +} + +func (c *client) NewFetchPackSession(ep transport.Endpoint) ( + transport.FetchPackSession, error) { + + return c.newSession(transport.UploadPackServiceName, ep) +} + +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 +} + +func (s *session) SetAuth(auth transport.AuthMethod) error { + return s.Command.SetAuth(auth) +} + +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() + + 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 *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." +) + +func isRepoNotFoundError(s string) bool { + if strings.HasPrefix(s, githubRepoNotFoundErr) { + return true + } + + if strings.HasPrefix(s, bitbucketRepoNotFoundErr) { + 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 c327c4192..a88a32874 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 a0f52f1c2..000000000 --- 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 a0321b319..927e9a818 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 adf67bbec..000000000 --- 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 -} From f2d70c2d06a14dcb6bf5823f6be43a4787f48463 Mon Sep 17 00:00:00 2001 From: "Santiago M. Mola" Date: Mon, 28 Nov 2016 13:00:08 +0100 Subject: [PATCH 2/4] fixtures: add fixture for empty repository. --- ...-4abe340d8d378baf7c2bfb2854c0fa498642bac3.tgz | Bin 0 -> 6033 bytes fixtures/fixtures.go | 8 +++++++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 fixtures/data/git-4abe340d8d378baf7c2bfb2854c0fa498642bac3.tgz diff --git a/fixtures/data/git-4abe340d8d378baf7c2bfb2854c0fa498642bac3.tgz b/fixtures/data/git-4abe340d8d378baf7c2bfb2854c0fa498642bac3.tgz new file mode 100644 index 0000000000000000000000000000000000000000..19044d2d4bc697c552daaba68a5f4b333f472e34 GIT binary patch literal 6033 zcmb7|5%RmG1>iI z;r=<#aL!?h#RKTqzBB>g97{X-E~%tEYM^6C245>e5d5^ZDEU$?}`GnY?WDe{lH& z4&{ud0Xm8@zloOa4LaNd_YD-6nq1g^`dE?)ystzcsu4BfVT18qjfQ!q^SJLqR#sfm z?d_c_e3dh(Fy zpy^iU0p>5M3mIp8+QLq=V7E^epWQ0gO=%LB1-1SP=G?nIPjkAdn>R)}pOsytSZ4Q| zTP#6%wtMGuQZbJ^J3bs3F)V?S(L6N^$_u!vnyi|#1j!dUBy~J_qNoxb;StuUU6ny6 z`Hd_W8hv(#^qy?gc7yw$L}9Q zW;Liy+jq33`0;-9TYSnTV}3Ot*11u zTC#Pos*tCN38fSzks@Nvq|5#V<Xc%b2FMf(sdvi$@#K=I>Jp4B)E}iNb{5?iK=P`Y>0|ll-%sLc_Qy#4eO+ zlz#mZZi3ARw1I?EMk#KW+xebQJa#$dBuuUpX<>CXUaguUb-TzXVEqWpzvu{o?k4A7^kEB9%pcj`41RUA;!0K->?WskHH9pnkjrdA9Ch^4AU-~SR+H*>jPZs;|s@BRfExqWO6(QFS?sDhtL>Mox zh{1{4NuJ2zW~n`sTIx7Y!oPco>X!+*3W~OA0(+U|t$oKeA2XI-*7<{_D^P(mSsn6z z0xyL`e=~UgY^b(bZHNwM+o2eMlIihkB;&l#9_-Z+7*Z81#_sSgb-a=hB_KKevbdx@ z=9w0rQI_x$FU?M1nD>bITzz3m8Fn{3{P~v4muIVwYk0w^7MDF5b#yX+vfNne(VdIy zD8B#4@VOuMcx49>2X%agPiH))MQ;9uT)Mg=wuDet{uy2-!u20#s^8j)`4boWhfigw z4G|JOYq#L*r+Q1f9lYur3m1gl-fjm|zw&;sud&O_Q@%`@6{gU4VD*U)RFv|UpyxA( z=ytS$cS}S(<@yz0B&1%|>6u)wU01ZWTE|UJX$iQR8@rq{)RNaZ}q=@{DL|73r7{B7Dfc3_g4x-*5 zzQ8{z*%iLoAvwC4Mq`q^6y*`Y7B+RBC67hQFu|V4Y1*q$7CnP7s<>0Z+)tDl{rFAT{ev+aM!-cY(ASaPYp0wbN6a|cy=)kgYbRvxJS z5}5He6l6J6%Ay*t8`PZHb5;Z5So`fYCx3pZw`?;>DcKqNkTU%gN-|Ca{iLU}HXAu! z#y0jL@p(E9yAC=x=iQp>>5;!+OzG+1KoO2vL@2RPBGkQXS4A*R!&?{k4YCOo2NtdS~V=?kUev%jx&(FIJGKV$@O`vE>u2Xy@#5Fc}h1=Ebqm% z6aH-j=bXfQhARqIZto4?2eD zhkRD1#++@(78J+?JnSI{xCHu!ifQDOfPUSpt5Clc7uat}k$@I1bCkpknBNts&zeiB zu>d;|Z23t6zh_@C+;qUjz4|I_&QEZUPBc0&GbML4daNfswV%Y)t+=&IU|`t%0gs*7 z2I3+}F@luBiG)|S*kQMSY(pjtO3Ob$sx=Efg9J8(xMejGtqIYidcvVjxy*FKX!01Poahi@9>Q)< zc?qoD*@Jz!+$eWd(%%Ut)bSZ93vERChsa8 zbZiZ2_HJ{9^#z@kkTe8@`cYt4IU1V@UI8t+u%wZE^^@oZhfuZ<4II#yA4#=zhjw88?W(-bqNF>xmlc4`(Qi=$^qT>k_Z=RfH4y`cU#Mtp zEoIBe{rVC0R(>ro_r3V^W>|)GX6E8wDR3_vj!~XASZJZ2M5Uv__MCwxSi)h3$`@dD zMruPa2{MUGvYly>Tg-T7+P2V~iz=iYH5Op`aB7ZQ;%zy)DZBfM%;uH^ihyVaVAF$L z-3|H#@Q6#Su^txC3oN` zX&dBK$jV!{Ue4|@{1ts6B-sVNaj+0ihOAH-OlhuDc#=RZB%{prvx#ZZj6?W_TG1?p zFG*s%gpxpOMZOHG8-GlX&f3IOb90}<#vgyf=UK04^Vk~lR@1(rdZ)TWI93R&cs7uC zvA;$?|H?*lbc{LhB>i<-d(&zs=>Sz5nayy`n`Zv=TiqtIc%Gfne&+q_#Rj?>B0hwl z%@DgHt&rKC#ttu8gXPp>u3~(GSKUkTnN94LS|*u?2TacaVA%l0 z-2tI}@w0g21)K^q(&k4Cj^8w|g4w{y;UVu$tLCK3@MQja~)Bo=zwNVw)7L-$^s zFF6#ITO}cU%~kW#RqB1U>Zc4B1q{I87R=#my zw~e-fGZ-GPfcazh=ZF>x z3@1nu%5>=sSuGhT?lS(BB4_)PXnBdgb6QPbE;~J3gpS{NACGTcdb-puVH=$7z90s3 zD);c5>;|DTZp~e+zuLdm7P#!G%d6~rd@U>NfO=GR@rs*`qkF=Pt_?YDl}g1_TAZNE z1|}2`&s{6Q1>aLhx~&q;0S8aWaPX2-|0$3NFx<@0j;|L&g-U9}NAn)PK(IJ13O(FgR&2zcOirpD_ zFQIDNFZ9W(#Br|){KVAQiKZ!NJ@7TmguSdl24WoKslqr&1*hVZ2!3DR%0Y5S<|8$Rd3+OIg~&s9B3gc;5BF zyR{-+cm3a6;`_mbty=Vq)+=d=#a|*A{=i}5+rEhWTd^mRn)Kc~`Fu_WA7kK*ap{=o zhgd$zvR-U+0a4y|`X@1}B7EMR2~hYbiQYd}ATOTNg#Va?3zA&7KXLR`juC6^4^nmW zzL%5(>ulWqd?_?IQz^q0}30wMp$K_o|$B z4@Y-pbWoEaMNCUO2AdaF6#BM-3CmEm8<4U>9x-k9mU)41q07XaHtUg9crnuL@=rC_ zhE2hykP3d_8>+X%=!ZG7Z-<}+%r}!Z?d{#Dn6KqvwSPIFX-NErU_nMnf_PnM_1>~~5^z=_)2V+QW zaKRClql>hJ%=$+}JF20sRgB?;BwA!^*3zRcFGe{$UpIS%zb%dL;8Bfd;6Spr`jPmx z!#th4Dl@$yF8FyPW@c;8e#mFH4Hcy)uD9$cE@Z}4TrG?b+@E%z9^bDWP0p=?yj~|` zvh1A$rNscRXW_2CyKszMO}ptVdDv0>6rtdi}FR<`8aXFWQ5emB-UAv zS<2~DeW4t4T-_}%h@Z`|SB;P)?UxXMi14eY-GMiL-llOfr2FyTnV69x6G%MIe-XP3 zHm;_@HNqA(=%fx7for`%*@Lh$&yR;5K^3B$j<_OVE%w1Egy*!htn!e4%i6JnxsCaU(1k_liB{Z)hl+1i5Vux~-@5g2=mXK7tZ69`rPk@hC zT=V9F>Rp=90*68Ro0{{;yUHMTT|gC0?KybSCNQJC-$a@MxR^;XozJ3_oOp}R)=;*e z^z7hnu66W!>X(Cn;ne_>`aWEtEYSV&z2^S#q3pMu?&FAiQc?WsOAL6u_y!{m28fW% zOBa}e7>9<0jM_&3;D$_4qp*9{adefxXLN^*&D*`NlG9`~9P2_NqCeSzHZ4!G=?@_c zWy=*maOTSWAh^#|{`enB^Y^|u*Ye7&v}PaDIQ0whHHmd^1Iu2u2f-X_PKkZ~uB*0p z&ZI4whHa2OP9*+g3a0tg+H!5YqBy)SzT(AYEyktlrlmp4AW9b zl$ico|srNl)qAU!jh8jzFSvoI!K!d*doV zW=pawHg1SD(nL~MEDnQ@)0>#M$|w`u1rLw)37ifYKwFH|cmDSyazFN;XR8e(p7N7& z`=U+`BVWOr7&+B*kD>=CYc{gxgDfP0pqZqNeFrQzskm z9c)P^HMPPUHHKh@LgC)-$~QxC^0z5z zO8kJG&&ui~8FoSzKMN3dWqOtiNA` z3j!8`Fy&$Ww06~y__{gqfMBj(K&RfZ$sw|akC3-_wFQCP}^eH?0!1h|Ps}6mlo`ywW;nFVM*Dq`_|oHTqaWxE zMuqMs&r*xAvTDpdPWWFsN>V#_;X_SD%_p=A9hlMWinVphWY^gd5n?_sKPVo46>sdN z+7LxIm#mD$J(C`fDRuN(?ILg8if4Rm-xA9vY`rF(7%Aay#LQhJqV^DXyB75q5A`cQ z4^S2`pd)5m_1h)t9t)>Wq}k6PqMN9t zscM5AD&4+Baki-4m!R*rQf{>`xZ5m)6R=6k3arssdJOYnTS`_#FCdDlbse@muWL76 z1zZU5Seghk>f2`84;{RMF0*mWHDBn7XOXU0n&1|Qp$acm9*m9{w<6&|oV?e`c=ZQ&<^=av5_K(}!4 Date: Mon, 28 Nov 2016 12:52:24 +0100 Subject: [PATCH 3/4] transport: add file:// transport --- plumbing/transport/client/client.go | 4 +- plumbing/transport/common.go | 3 +- plumbing/transport/file/common.go | 72 ++++++++++++++++++++ plumbing/transport/file/common_test.go | 9 +++ plumbing/transport/file/fetch_pack_test.go | 48 +++++++++++++ plumbing/transport/internal/common/common.go | 6 ++ 6 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 plumbing/transport/file/common.go create mode 100644 plumbing/transport/file/common_test.go create mode 100644 plumbing/transport/file/fetch_pack_test.go diff --git a/plumbing/transport/client/client.go b/plumbing/transport/client/client.go index 5c6da05cf..786e0c9f9 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,7 @@ 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, SSH or local. 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/common.go b/plumbing/transport/common.go index 2f6fcee55..9715a3962 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 000000000..82cbba208 --- /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 000000000..94ca4c9b0 --- /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 000000000..80f11eed5 --- /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 index 270427218..18ca544b7 100644 --- a/plumbing/transport/internal/common/common.go +++ b/plumbing/transport/internal/common/common.go @@ -109,6 +109,7 @@ func (s *session) AdvertisedReferences() (*packp.AdvRefs, error) { } _ = s.Stdin.Close() + err = transport.ErrEmptyRemoteRepository scan := bufio.NewScanner(s.Stderr) if !scan.Scan() { @@ -168,6 +169,7 @@ func (s *session) Close() error { 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 { @@ -179,6 +181,10 @@ func isRepoNotFoundError(s string) bool { return true } + if strings.HasSuffix(s, localRepoNotFoundErr) { + return true + } + return false } From 8812bc5b0e1d55e24962a53def2e45c474f6f529 Mon Sep 17 00:00:00 2001 From: "Santiago M. Mola" Date: Tue, 29 Nov 2016 09:55:28 +0100 Subject: [PATCH 4/4] transport: add docs. --- fixtures/fixtures.go | 3 +- plumbing/transport/client/client.go | 3 +- plumbing/transport/internal/common/common.go | 57 ++++++++++++++++---- 3 files changed, 51 insertions(+), 12 deletions(-) diff --git a/fixtures/fixtures.go b/fixtures/fixtures.go index 757fbe378..4e5e2bbd9 100644 --- a/fixtures/fixtures.go +++ b/fixtures/fixtures.go @@ -120,8 +120,7 @@ var fixtures = Fixtures{{ URL: "https://github.com/git-fixtures/empty.git", DotGitHash: plumbing.NewHash("4abe340d8d378baf7c2bfb2854c0fa498642bac3"), ObjectsCount: 0, -}, -} +}} func All() Fixtures { return fixtures diff --git a/plumbing/transport/client/client.go b/plumbing/transport/client/client.go index 786e0c9f9..770b7dc87 100644 --- a/plumbing/transport/client/client.go +++ b/plumbing/transport/client/client.go @@ -23,7 +23,8 @@ func InstallProtocol(scheme string, c transport.Client) { } // NewClient returns the appropriate client among of the set of known protocols: -// HTTP, SSH or local. 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/internal/common/common.go b/plumbing/transport/internal/common/common.go index 18ca544b7..10e395e57 100644 --- a/plumbing/transport/internal/common/common.go +++ b/plumbing/transport/internal/common/common.go @@ -1,3 +1,8 @@ +// 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 ( @@ -14,35 +19,67 @@ import ( "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 - Start() 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 - io.Closer -} - -type Commander interface { - Command(cmd string, ep transport.Endpoint) (Command, 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 a CommandRunner. +// 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) { @@ -91,10 +128,12 @@ func (c *client) newSession(s string, ep transport.Endpoint) (*session, error) { }, 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 @@ -126,8 +165,8 @@ func (s *session) AdvertisedReferences() (*packp.AdvRefs, error) { return ar, nil } -// FetchPack returns a packfile for a given upload request. -// Closing the returned reader will close the SSH session. +// 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