From 1918bfe458729488e4a656cbbef7a2430e7ec2f5 Mon Sep 17 00:00:00 2001 From: Alberto Cortés Date: Wed, 2 Nov 2016 10:14:15 +0100 Subject: ulreq: adds encoder and decoder for upload-request messages (#106) * ulreq: adds encoder and decoder for upload-request messages * ulreq: stop using _test suffix for testing package names (@mcuadros comment) --- clients/ssh/git_upload_pack.go | 139 ++++++--- clients/ssh/git_upload_pack_test.go | 5 +- formats/packp/ulreq/decoder.go | 287 +++++++++++++++++++ formats/packp/ulreq/decoder_test.go | 541 ++++++++++++++++++++++++++++++++++++ formats/packp/ulreq/encoder.go | 140 ++++++++++ formats/packp/ulreq/encoder_test.go | 268 ++++++++++++++++++ formats/packp/ulreq/ulreq.go | 56 ++++ formats/packp/ulreq/ulreq_test.go | 91 ++++++ 8 files changed, 1493 insertions(+), 34 deletions(-) create mode 100644 formats/packp/ulreq/decoder.go create mode 100644 formats/packp/ulreq/decoder_test.go create mode 100644 formats/packp/ulreq/encoder.go create mode 100644 formats/packp/ulreq/encoder_test.go create mode 100644 formats/packp/ulreq/ulreq.go create mode 100644 formats/packp/ulreq/ulreq_test.go diff --git a/clients/ssh/git_upload_pack.go b/clients/ssh/git_upload_pack.go index e3b59a5..e172f1f 100644 --- a/clients/ssh/git_upload_pack.go +++ b/clients/ssh/git_upload_pack.go @@ -10,6 +10,8 @@ import ( "gopkg.in/src-d/go-git.v4/clients/common" "gopkg.in/src-d/go-git.v4/formats/packp/advrefs" + "gopkg.in/src-d/go-git.v4/formats/packp/pktline" + "gopkg.in/src-d/go-git.v4/formats/packp/ulreq" "golang.org/x/crypto/ssh" ) @@ -24,7 +26,8 @@ var ( ErrUnsupportedVCS = errors.New("only git is supported") ErrUnsupportedRepo = errors.New("only github.com is supported") - nak = []byte("0008NAK\n") + nak = []byte("NAK") + eol = []byte("\n") ) // GitUploadPackService holds the service information. @@ -139,75 +142,145 @@ func (s *GitUploadPackService) Disconnect() (err error) { // 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(r *common.GitUploadPackRequest) (rc io.ReadCloser, err error) { +func (s *GitUploadPackService) Fetch(req *common.GitUploadPackRequest) (rc io.ReadCloser, err error) { if !s.connected { return nil, ErrNotConnected } - session, err := s.client.NewSession() + session, i, o, done, err := openSSHSession(s.client, s.getCommand()) if err != nil { return nil, fmt.Errorf("cannot open SSH session: %s", err) } - si, err := session.StdinPipe() + 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, fmt.Errorf("cannot pipe remote stdin: %s", err) + return nil, nil, nil, nil, fmt.Errorf("cannot open SSH session: %s", err) } - so, err := session.StdoutPipe() + i, err := session.StdinPipe() if err != nil { - return nil, fmt.Errorf("cannot pipe remote stdout: %s", err) + 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(s.getCommand()) + done <- session.Run(cmd) }() - if err := skipAdvRef(so); err != nil { - return nil, fmt.Errorf("skipping advertised-refs: %s", err) + 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) } - // send the upload request - _, err = io.Copy(si, r.Reader()) - if err != nil { - return nil, fmt.Errorf("sending upload-req message: %s", err) + if err := sendUlReq(w, req); err != nil { + return fmt.Errorf("sending upload-req message: %s", err) } - if err := si.Close(); err != nil { - return nil, fmt.Errorf("closing input: %s", err) + if err := sendHaves(w, req); err != nil { + return fmt.Errorf("sending haves message: %s", err) } - // 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 - buf := make([]byte, len(nak)) - if _, err := io.ReadFull(so, buf); err != nil { - return nil, fmt.Errorf("looking for NAK: %s", err) + if err := sendDone(w); err != nil { + return fmt.Errorf("sending done message: %s", err) } - if !bytes.Equal(buf, nak) { - return nil, fmt.Errorf("NAK answer not found") + + if err := w.Close(); err != nil { + return fmt.Errorf("closing input: %s", err) } - return &fetchSession{ - Reader: so, - session: session, - done: done, - }, nil + if err := readNAK(r); err != nil { + return fmt.Errorf("reading NAK: %s", err) + } + + return nil } -func skipAdvRef(so io.Reader) error { - d := advrefs.NewDecoder(so) +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 + done <-chan error } // Close closes the session and collects the output state of the remote diff --git a/clients/ssh/git_upload_pack_test.go b/clients/ssh/git_upload_pack_test.go index ff27cc2..d7160dc 100644 --- a/clients/ssh/git_upload_pack_test.go +++ b/clients/ssh/git_upload_pack_test.go @@ -136,6 +136,9 @@ func (s *RemoteSuite) TestFetchError(c *C) { req := &common.GitUploadPackRequest{} req.Want(core.NewHash("1111111111111111111111111111111111111111")) - _, err := r.Fetch(req) + reader, err := r.Fetch(req) + c.Assert(err, IsNil) + + err = reader.Close() c.Assert(err, Not(IsNil)) } diff --git a/formats/packp/ulreq/decoder.go b/formats/packp/ulreq/decoder.go new file mode 100644 index 0000000..63ccd4d --- /dev/null +++ b/formats/packp/ulreq/decoder.go @@ -0,0 +1,287 @@ +package ulreq + +import ( + "bytes" + "encoding/hex" + "fmt" + "io" + "strconv" + "time" + + "gopkg.in/src-d/go-git.v4/core" + "gopkg.in/src-d/go-git.v4/formats/packp/pktline" +) + +const ( + hashSize = 40 +) + +var ( + eol = []byte("\n") + sp = []byte(" ") + want = []byte("want ") + shallow = []byte("shallow ") + deepen = []byte("deepen") + deepenCommits = []byte("deepen ") + deepenSince = []byte("deepen-since ") + deepenReference = []byte("deepen-not ") +) + +// A Decoder reads and decodes AdvRef values from an input stream. +type Decoder struct { + s *pktline.Scanner // a pkt-line scanner from the input stream + line []byte // current pkt-line contents, use parser.nextLine() to make it advance + nLine int // current pkt-line number for debugging, begins at 1 + err error // sticky error, use the parser.error() method to fill this out + data *UlReq // parsed data is stored here +} + +// NewDecoder returns a new decoder that reads from r. +// +// Will not read more data from r than necessary. +func NewDecoder(r io.Reader) *Decoder { + return &Decoder{ + s: pktline.NewScanner(r), + } +} + +// Decode reads the next upload-request form its input and +// stores it in the value pointed to by v. +func (d *Decoder) Decode(v *UlReq) error { + d.data = v + + for state := decodeFirstWant; state != nil; { + state = state(d) + } + + return d.err +} + +type decoderStateFn func(*Decoder) decoderStateFn + +// fills out the parser stiky error +func (d *Decoder) error(format string, a ...interface{}) { + d.err = fmt.Errorf("pkt-line %d: %s", d.nLine, + fmt.Sprintf(format, a...)) +} + +// Reads a new pkt-line from the scanner, makes its payload available as +// p.line and increments p.nLine. A successful invocation returns true, +// otherwise, false is returned and the sticky error is filled out +// accordingly. Trims eols at the end of the payloads. +func (d *Decoder) nextLine() bool { + d.nLine++ + + if !d.s.Scan() { + if d.err = d.s.Err(); d.err != nil { + return false + } + + d.error("EOF") + return false + } + + d.line = d.s.Bytes() + d.line = bytes.TrimSuffix(d.line, eol) + + return true +} + +// Expected format: want [ capabilities] +func decodeFirstWant(d *Decoder) decoderStateFn { + if ok := d.nextLine(); !ok { + return nil + } + + if !bytes.HasPrefix(d.line, want) { + d.error("missing 'want ' prefix") + return nil + } + d.line = bytes.TrimPrefix(d.line, want) + + hash, ok := d.readHash() + if !ok { + return nil + } + d.data.Wants = append(d.data.Wants, hash) + + return decodeCaps +} + +func (d *Decoder) readHash() (core.Hash, bool) { + if len(d.line) < hashSize { + d.err = fmt.Errorf("malformed hash: %v", d.line) + return core.ZeroHash, false + } + + var hash core.Hash + if _, err := hex.Decode(hash[:], d.line[:hashSize]); err != nil { + d.error("invalid hash text: %s", err) + return core.ZeroHash, false + } + d.line = d.line[hashSize:] + + return hash, true +} + +// Expected format: sp cap1 sp cap2 sp cap3... +func decodeCaps(d *Decoder) decoderStateFn { + if len(d.line) == 0 { + return decodeOtherWants + } + + d.line = bytes.TrimPrefix(d.line, sp) + + for _, c := range bytes.Split(d.line, sp) { + name, values := readCapability(c) + d.data.Capabilities.Add(name, values...) + } + + return decodeOtherWants +} + +// Capabilities are a single string or a name=value. +// Even though we are only going to read at moust 1 value, we return +// a slice of values, as Capability.Add receives that. +func readCapability(data []byte) (name string, values []string) { + pair := bytes.SplitN(data, []byte{'='}, 2) + if len(pair) == 2 { + values = append(values, string(pair[1])) + } + + return string(pair[0]), values +} + +// Expected format: want +func decodeOtherWants(d *Decoder) decoderStateFn { + if ok := d.nextLine(); !ok { + return nil + } + + if bytes.HasPrefix(d.line, shallow) { + return decodeShallow + } + + if bytes.HasPrefix(d.line, deepen) { + return decodeDeepen + } + + if len(d.line) == 0 { + return nil + } + + if !bytes.HasPrefix(d.line, want) { + d.error("unexpected payload while expecting a want: %q", d.line) + return nil + } + d.line = bytes.TrimPrefix(d.line, want) + + hash, ok := d.readHash() + if !ok { + return nil + } + d.data.Wants = append(d.data.Wants, hash) + + return decodeOtherWants +} + +// Expected format: shallow +func decodeShallow(d *Decoder) decoderStateFn { + if bytes.HasPrefix(d.line, deepen) { + return decodeDeepen + } + + if len(d.line) == 0 { + return nil + } + + if !bytes.HasPrefix(d.line, shallow) { + d.error("unexpected payload while expecting a shallow: %q", d.line) + return nil + } + d.line = bytes.TrimPrefix(d.line, shallow) + + hash, ok := d.readHash() + if !ok { + return nil + } + d.data.Shallows = append(d.data.Shallows, hash) + + if ok := d.nextLine(); !ok { + return nil + } + + return decodeShallow +} + +// Expected format: deepen / deepen-since
    / deepen-not +func decodeDeepen(d *Decoder) decoderStateFn { + if bytes.HasPrefix(d.line, deepenCommits) { + return decodeDeepenCommits + } + + if bytes.HasPrefix(d.line, deepenSince) { + return decodeDeepenSince + } + + if bytes.HasPrefix(d.line, deepenReference) { + return decodeDeepenReference + } + + if len(d.line) == 0 { + return nil + } + + d.error("unexpected deepen specification: %q", d.line) + return nil +} + +func decodeDeepenCommits(d *Decoder) decoderStateFn { + d.line = bytes.TrimPrefix(d.line, deepenCommits) + + var n int + if n, d.err = strconv.Atoi(string(d.line)); d.err != nil { + return nil + } + if n < 0 { + d.err = fmt.Errorf("negative depth") + return nil + } + d.data.Depth = DepthCommits(n) + + return decodeFlush +} + +func decodeDeepenSince(d *Decoder) decoderStateFn { + d.line = bytes.TrimPrefix(d.line, deepenSince) + + var secs int64 + secs, d.err = strconv.ParseInt(string(d.line), 10, 64) + if d.err != nil { + return nil + } + t := time.Unix(secs, 0).UTC() + d.data.Depth = DepthSince(t) + + return decodeFlush +} + +func decodeDeepenReference(d *Decoder) decoderStateFn { + d.line = bytes.TrimPrefix(d.line, deepenReference) + + d.data.Depth = DepthReference(string(d.line)) + + return decodeFlush +} + +func decodeFlush(d *Decoder) decoderStateFn { + if ok := d.nextLine(); !ok { + return nil + } + + if len(d.line) != 0 { + d.err = fmt.Errorf("unexpected payload while expecting a flush-pkt: %q", d.line) + } + + return nil +} diff --git a/formats/packp/ulreq/decoder_test.go b/formats/packp/ulreq/decoder_test.go new file mode 100644 index 0000000..b313395 --- /dev/null +++ b/formats/packp/ulreq/decoder_test.go @@ -0,0 +1,541 @@ +package ulreq + +import ( + "bytes" + "io" + "sort" + "time" + + "gopkg.in/src-d/go-git.v4/core" + "gopkg.in/src-d/go-git.v4/formats/packp/pktline" + + . "gopkg.in/check.v1" +) + +type SuiteDecoder struct{} + +var _ = Suite(&SuiteDecoder{}) + +func (s *SuiteDecoder) TestEmpty(c *C) { + ur := New() + var buf bytes.Buffer + d := NewDecoder(&buf) + + err := d.Decode(ur) + c.Assert(err, ErrorMatches, "pkt-line 1: EOF") +} + +func (s *SuiteDecoder) TestNoWant(c *C) { + payloads := []string{ + "foobar", + pktline.FlushString, + } + r := toPktLines(c, payloads) + testDecoderErrorMatches(c, r, ".*missing 'want '.*") +} + +func toPktLines(c *C, payloads []string) io.Reader { + var buf bytes.Buffer + e := pktline.NewEncoder(&buf) + err := e.EncodeString(payloads...) + c.Assert(err, IsNil) + + return &buf +} + +func testDecoderErrorMatches(c *C, input io.Reader, pattern string) { + ur := New() + d := NewDecoder(input) + + err := d.Decode(ur) + c.Assert(err, ErrorMatches, pattern) +} + +func (s *SuiteDecoder) TestInvalidFirstHash(c *C) { + payloads := []string{ + "want 6ecf0ef2c2dffb796alberto2219af86ec6584e5\n", + pktline.FlushString, + } + r := toPktLines(c, payloads) + testDecoderErrorMatches(c, r, ".*invalid hash.*") +} + +func (s *SuiteDecoder) TestWantOK(c *C) { + payloads := []string{ + "want 1111111111111111111111111111111111111111", + pktline.FlushString, + } + ur := testDecodeOK(c, payloads) + + c.Assert(ur.Wants, DeepEquals, []core.Hash{ + core.NewHash("1111111111111111111111111111111111111111"), + }) +} + +func testDecodeOK(c *C, payloads []string) *UlReq { + var buf bytes.Buffer + e := pktline.NewEncoder(&buf) + err := e.EncodeString(payloads...) + c.Assert(err, IsNil) + + ur := New() + d := NewDecoder(&buf) + + err = d.Decode(ur) + c.Assert(err, IsNil) + + return ur +} + +func (s *SuiteDecoder) TestWantWithCapabilities(c *C) { + payloads := []string{ + "want 1111111111111111111111111111111111111111 ofs-delta multi_ack", + pktline.FlushString, + } + ur := testDecodeOK(c, payloads) + c.Assert(ur.Wants, DeepEquals, []core.Hash{ + core.NewHash("1111111111111111111111111111111111111111")}) + + c.Assert(ur.Capabilities.Supports("ofs-delta"), Equals, true) + c.Assert(ur.Capabilities.Supports("multi_ack"), Equals, true) +} + +func (s *SuiteDecoder) TestManyWantsNoCapabilities(c *C) { + payloads := []string{ + "want 3333333333333333333333333333333333333333", + "want 4444444444444444444444444444444444444444", + "want 1111111111111111111111111111111111111111", + "want 2222222222222222222222222222222222222222", + pktline.FlushString, + } + ur := testDecodeOK(c, payloads) + + expected := []core.Hash{ + core.NewHash("1111111111111111111111111111111111111111"), + core.NewHash("2222222222222222222222222222222222222222"), + core.NewHash("3333333333333333333333333333333333333333"), + core.NewHash("4444444444444444444444444444444444444444"), + } + + sort.Sort(byHash(ur.Wants)) + sort.Sort(byHash(expected)) + c.Assert(ur.Wants, DeepEquals, expected) +} + +type byHash []core.Hash + +func (a byHash) Len() int { return len(a) } +func (a byHash) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a byHash) Less(i, j int) bool { + ii := [20]byte(a[i]) + jj := [20]byte(a[j]) + return bytes.Compare(ii[:], jj[:]) < 0 +} + +func (s *SuiteDecoder) TestManyWantsBadWant(c *C) { + payloads := []string{ + "want 3333333333333333333333333333333333333333", + "want 4444444444444444444444444444444444444444", + "foo", + "want 2222222222222222222222222222222222222222", + pktline.FlushString, + } + r := toPktLines(c, payloads) + testDecoderErrorMatches(c, r, ".*unexpected payload.*") +} + +func (s *SuiteDecoder) TestManyWantsInvalidHash(c *C) { + payloads := []string{ + "want 3333333333333333333333333333333333333333", + "want 4444444444444444444444444444444444444444", + "want 1234567890abcdef", + "want 2222222222222222222222222222222222222222", + pktline.FlushString, + } + r := toPktLines(c, payloads) + testDecoderErrorMatches(c, r, ".*malformed hash.*") +} + +func (s *SuiteDecoder) TestManyWantsWithCapabilities(c *C) { + payloads := []string{ + "want 3333333333333333333333333333333333333333 ofs-delta multi_ack", + "want 4444444444444444444444444444444444444444", + "want 1111111111111111111111111111111111111111", + "want 2222222222222222222222222222222222222222", + pktline.FlushString, + } + ur := testDecodeOK(c, payloads) + + expected := []core.Hash{ + core.NewHash("1111111111111111111111111111111111111111"), + core.NewHash("2222222222222222222222222222222222222222"), + core.NewHash("3333333333333333333333333333333333333333"), + core.NewHash("4444444444444444444444444444444444444444"), + } + + sort.Sort(byHash(ur.Wants)) + sort.Sort(byHash(expected)) + c.Assert(ur.Wants, DeepEquals, expected) + + c.Assert(ur.Capabilities.Supports("ofs-delta"), Equals, true) + c.Assert(ur.Capabilities.Supports("multi_ack"), Equals, true) +} + +func (s *SuiteDecoder) TestSingleShallowSingleWant(c *C) { + payloads := []string{ + "want 3333333333333333333333333333333333333333 ofs-delta multi_ack", + "shallow aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + pktline.FlushString, + } + ur := testDecodeOK(c, payloads) + + expectedWants := []core.Hash{ + core.NewHash("3333333333333333333333333333333333333333"), + } + + expectedShallows := []core.Hash{ + core.NewHash("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + } + + c.Assert(ur.Wants, DeepEquals, expectedWants) + c.Assert(ur.Capabilities.Supports("ofs-delta"), Equals, true) + c.Assert(ur.Capabilities.Supports("multi_ack"), Equals, true) + + c.Assert(ur.Shallows, DeepEquals, expectedShallows) +} + +func (s *SuiteDecoder) TestSingleShallowManyWants(c *C) { + payloads := []string{ + "want 3333333333333333333333333333333333333333 ofs-delta multi_ack", + "want 4444444444444444444444444444444444444444", + "want 1111111111111111111111111111111111111111", + "want 2222222222222222222222222222222222222222", + "shallow aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + pktline.FlushString, + } + ur := testDecodeOK(c, payloads) + + expectedWants := []core.Hash{ + core.NewHash("1111111111111111111111111111111111111111"), + core.NewHash("2222222222222222222222222222222222222222"), + core.NewHash("3333333333333333333333333333333333333333"), + core.NewHash("4444444444444444444444444444444444444444"), + } + sort.Sort(byHash(expectedWants)) + + expectedShallows := []core.Hash{ + core.NewHash("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + } + + sort.Sort(byHash(ur.Wants)) + c.Assert(ur.Wants, DeepEquals, expectedWants) + c.Assert(ur.Capabilities.Supports("ofs-delta"), Equals, true) + c.Assert(ur.Capabilities.Supports("multi_ack"), Equals, true) + + c.Assert(ur.Shallows, DeepEquals, expectedShallows) +} + +func (s *SuiteDecoder) TestManyShallowSingleWant(c *C) { + payloads := []string{ + "want 3333333333333333333333333333333333333333 ofs-delta multi_ack", + "shallow aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "shallow bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "shallow cccccccccccccccccccccccccccccccccccccccc", + "shallow dddddddddddddddddddddddddddddddddddddddd", + pktline.FlushString, + } + ur := testDecodeOK(c, payloads) + + expectedWants := []core.Hash{ + core.NewHash("3333333333333333333333333333333333333333"), + } + + expectedShallows := []core.Hash{ + core.NewHash("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + core.NewHash("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), + core.NewHash("cccccccccccccccccccccccccccccccccccccccc"), + core.NewHash("dddddddddddddddddddddddddddddddddddddddd"), + } + sort.Sort(byHash(expectedShallows)) + + c.Assert(ur.Wants, DeepEquals, expectedWants) + c.Assert(ur.Capabilities.Supports("ofs-delta"), Equals, true) + c.Assert(ur.Capabilities.Supports("multi_ack"), Equals, true) + + sort.Sort(byHash(ur.Shallows)) + c.Assert(ur.Shallows, DeepEquals, expectedShallows) +} + +func (s *SuiteDecoder) TestManyShallowManyWants(c *C) { + payloads := []string{ + "want 3333333333333333333333333333333333333333 ofs-delta multi_ack", + "want 4444444444444444444444444444444444444444", + "want 1111111111111111111111111111111111111111", + "want 2222222222222222222222222222222222222222", + "shallow aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "shallow bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "shallow cccccccccccccccccccccccccccccccccccccccc", + "shallow dddddddddddddddddddddddddddddddddddddddd", + pktline.FlushString, + } + ur := testDecodeOK(c, payloads) + + expectedWants := []core.Hash{ + core.NewHash("1111111111111111111111111111111111111111"), + core.NewHash("2222222222222222222222222222222222222222"), + core.NewHash("3333333333333333333333333333333333333333"), + core.NewHash("4444444444444444444444444444444444444444"), + } + sort.Sort(byHash(expectedWants)) + + expectedShallows := []core.Hash{ + core.NewHash("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + core.NewHash("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), + core.NewHash("cccccccccccccccccccccccccccccccccccccccc"), + core.NewHash("dddddddddddddddddddddddddddddddddddddddd"), + } + sort.Sort(byHash(expectedShallows)) + + sort.Sort(byHash(ur.Wants)) + c.Assert(ur.Wants, DeepEquals, expectedWants) + c.Assert(ur.Capabilities.Supports("ofs-delta"), Equals, true) + c.Assert(ur.Capabilities.Supports("multi_ack"), Equals, true) + + sort.Sort(byHash(ur.Shallows)) + c.Assert(ur.Shallows, DeepEquals, expectedShallows) +} + +func (s *SuiteDecoder) TestMalformedShallow(c *C) { + payloads := []string{ + "want 3333333333333333333333333333333333333333 ofs-delta multi_ack", + "shalow aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + pktline.FlushString, + } + r := toPktLines(c, payloads) + testDecoderErrorMatches(c, r, ".*unexpected payload.*") +} + +func (s *SuiteDecoder) TestMalformedShallowHash(c *C) { + payloads := []string{ + "want 3333333333333333333333333333333333333333 ofs-delta multi_ack", + "shallow aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + pktline.FlushString, + } + r := toPktLines(c, payloads) + testDecoderErrorMatches(c, r, ".*malformed hash.*") +} + +func (s *SuiteDecoder) TestMalformedShallowManyShallows(c *C) { + payloads := []string{ + "want 3333333333333333333333333333333333333333 ofs-delta multi_ack", + "shallow aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "shalow bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "shallow cccccccccccccccccccccccccccccccccccccccc", + pktline.FlushString, + } + r := toPktLines(c, payloads) + testDecoderErrorMatches(c, r, ".*unexpected payload.*") +} + +func (s *SuiteDecoder) TestMalformedDeepenSpec(c *C) { + payloads := []string{ + "want 3333333333333333333333333333333333333333 ofs-delta multi_ack", + "deepen-foo 34", + pktline.FlushString, + } + r := toPktLines(c, payloads) + testDecoderErrorMatches(c, r, ".*unexpected deepen.*") +} + +func (s *SuiteDecoder) TestMalformedDeepenSingleWant(c *C) { + payloads := []string{ + "want 3333333333333333333333333333333333333333 ofs-delta multi_ack", + "depth 32", + pktline.FlushString, + } + r := toPktLines(c, payloads) + testDecoderErrorMatches(c, r, ".*unexpected payload.*") +} + +func (s *SuiteDecoder) TestMalformedDeepenMultiWant(c *C) { + payloads := []string{ + "want 3333333333333333333333333333333333333333 ofs-delta multi_ack", + "want 2222222222222222222222222222222222222222", + "depth 32", + pktline.FlushString, + } + r := toPktLines(c, payloads) + testDecoderErrorMatches(c, r, ".*unexpected payload.*") +} + +func (s *SuiteDecoder) TestMalformedDeepenWithSingleShallow(c *C) { + payloads := []string{ + "want 3333333333333333333333333333333333333333 ofs-delta multi_ack", + "shallow 2222222222222222222222222222222222222222", + "depth 32", + pktline.FlushString, + } + r := toPktLines(c, payloads) + testDecoderErrorMatches(c, r, ".*unexpected payload.*") +} + +func (s *SuiteDecoder) TestMalformedDeepenWithMultiShallow(c *C) { + payloads := []string{ + "want 3333333333333333333333333333333333333333 ofs-delta multi_ack", + "shallow 2222222222222222222222222222222222222222", + "shallow 5555555555555555555555555555555555555555", + "depth 32", + pktline.FlushString, + } + r := toPktLines(c, payloads) + testDecoderErrorMatches(c, r, ".*unexpected payload.*") +} + +func (s *SuiteDecoder) TestDeepenCommits(c *C) { + payloads := []string{ + "want 3333333333333333333333333333333333333333 ofs-delta multi_ack", + "deepen 1234", + pktline.FlushString, + } + ur := testDecodeOK(c, payloads) + + c.Assert(ur.Depth, FitsTypeOf, DepthCommits(0)) + commits, ok := ur.Depth.(DepthCommits) + c.Assert(ok, Equals, true) + c.Assert(int(commits), Equals, 1234) +} + +func (s *SuiteDecoder) TestDeepenCommitsInfiniteInplicit(c *C) { + payloads := []string{ + "want 3333333333333333333333333333333333333333 ofs-delta multi_ack", + "deepen 0", + pktline.FlushString, + } + ur := testDecodeOK(c, payloads) + + c.Assert(ur.Depth, FitsTypeOf, DepthCommits(0)) + commits, ok := ur.Depth.(DepthCommits) + c.Assert(ok, Equals, true) + c.Assert(int(commits), Equals, 0) +} + +func (s *SuiteDecoder) TestDeepenCommitsInfiniteExplicit(c *C) { + payloads := []string{ + "want 3333333333333333333333333333333333333333 ofs-delta multi_ack", + pktline.FlushString, + } + ur := testDecodeOK(c, payloads) + + c.Assert(ur.Depth, FitsTypeOf, DepthCommits(0)) + commits, ok := ur.Depth.(DepthCommits) + c.Assert(ok, Equals, true) + c.Assert(int(commits), Equals, 0) +} + +func (s *SuiteDecoder) TestMalformedDeepenCommits(c *C) { + payloads := []string{ + "want 3333333333333333333333333333333333333333 ofs-delta multi_ack", + "deepen -32", + pktline.FlushString, + } + r := toPktLines(c, payloads) + testDecoderErrorMatches(c, r, ".*negative depth.*") +} + +func (s *SuiteDecoder) TestDeepenCommitsEmpty(c *C) { + payloads := []string{ + "want 3333333333333333333333333333333333333333 ofs-delta multi_ack", + "deepen ", + pktline.FlushString, + } + r := toPktLines(c, payloads) + testDecoderErrorMatches(c, r, ".*invalid syntax.*") +} + +func (s *SuiteDecoder) TestDeepenSince(c *C) { + payloads := []string{ + "want 3333333333333333333333333333333333333333 ofs-delta multi_ack", + "deepen-since 1420167845", // 2015-01-02T03:04:05+00:00 + pktline.FlushString, + } + ur := testDecodeOK(c, payloads) + + expected := time.Date(2015, time.January, 2, 3, 4, 5, 0, time.UTC) + + c.Assert(ur.Depth, FitsTypeOf, DepthSince(time.Now())) + since, ok := ur.Depth.(DepthSince) + c.Assert(ok, Equals, true) + c.Assert(time.Time(since).Equal(expected), Equals, true, + Commentf("obtained=%s\nexpected=%s", time.Time(since), expected)) +} + +func (s *SuiteDecoder) TestDeepenReference(c *C) { + payloads := []string{ + "want 3333333333333333333333333333333333333333 ofs-delta multi_ack", + "deepen-not refs/heads/master", + pktline.FlushString, + } + ur := testDecodeOK(c, payloads) + + expected := "refs/heads/master" + + c.Assert(ur.Depth, FitsTypeOf, DepthReference("")) + reference, ok := ur.Depth.(DepthReference) + c.Assert(ok, Equals, true) + c.Assert(string(reference), Equals, expected) +} + +func (s *SuiteDecoder) TestAll(c *C) { + payloads := []string{ + "want 3333333333333333333333333333333333333333 ofs-delta multi_ack", + "want 4444444444444444444444444444444444444444", + "want 1111111111111111111111111111111111111111", + "want 2222222222222222222222222222222222222222", + "shallow aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "shallow bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "shallow cccccccccccccccccccccccccccccccccccccccc", + "shallow dddddddddddddddddddddddddddddddddddddddd", + "deepen 1234", + pktline.FlushString, + } + ur := testDecodeOK(c, payloads) + + expectedWants := []core.Hash{ + core.NewHash("1111111111111111111111111111111111111111"), + core.NewHash("2222222222222222222222222222222222222222"), + core.NewHash("3333333333333333333333333333333333333333"), + core.NewHash("4444444444444444444444444444444444444444"), + } + sort.Sort(byHash(expectedWants)) + sort.Sort(byHash(ur.Wants)) + c.Assert(ur.Wants, DeepEquals, expectedWants) + + c.Assert(ur.Capabilities.Supports("ofs-delta"), Equals, true) + c.Assert(ur.Capabilities.Supports("multi_ack"), Equals, true) + + expectedShallows := []core.Hash{ + core.NewHash("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + core.NewHash("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), + core.NewHash("cccccccccccccccccccccccccccccccccccccccc"), + core.NewHash("dddddddddddddddddddddddddddddddddddddddd"), + } + sort.Sort(byHash(expectedShallows)) + sort.Sort(byHash(ur.Shallows)) + c.Assert(ur.Shallows, DeepEquals, expectedShallows) + + c.Assert(ur.Depth, FitsTypeOf, DepthCommits(0)) + commits, ok := ur.Depth.(DepthCommits) + c.Assert(ok, Equals, true) + c.Assert(int(commits), Equals, 1234) +} + +func (s *SuiteDecoder) TestExtraData(c *C) { + payloads := []string{ + "want 3333333333333333333333333333333333333333 ofs-delta multi_ack", + "deepen 32", + "foo", + pktline.FlushString, + } + r := toPktLines(c, payloads) + testDecoderErrorMatches(c, r, ".*unexpected payload.*") +} diff --git a/formats/packp/ulreq/encoder.go b/formats/packp/ulreq/encoder.go new file mode 100644 index 0000000..1e40b63 --- /dev/null +++ b/formats/packp/ulreq/encoder.go @@ -0,0 +1,140 @@ +package ulreq + +import ( + "fmt" + "io" + "sort" + "time" + + "gopkg.in/src-d/go-git.v4/core" + "gopkg.in/src-d/go-git.v4/formats/packp/pktline" +) + +// An Encoder writes UlReq values to an output stream. +type Encoder struct { + pe *pktline.Encoder // where to write the encoded data + data *UlReq // the data to encode + sortedWants []string + err error // sticky error +} + +// NewEncoder returns a new encoder that writes to w. +func NewEncoder(w io.Writer) *Encoder { + return &Encoder{ + pe: pktline.NewEncoder(w), + } +} + +// Encode writes the UlReq encoding of v to the stream. +// +// All the payloads will end with a newline character. Wants and +// shallows are sorted alphabetically. A depth of 0 means no depth +// request is sent. +func (e *Encoder) Encode(v *UlReq) error { + if len(v.Wants) == 0 { + return fmt.Errorf("empty wants provided") + } + + e.data = v + e.sortedWants = sortHashes(v.Wants) + + for state := encodeFirstWant; state != nil; { + state = state(e) + } + + return e.err +} + +type encoderStateFn func(*Encoder) encoderStateFn + +func sortHashes(list []core.Hash) []string { + sorted := make([]string, len(list)) + for i, hash := range list { + sorted[i] = hash.String() + } + sort.Strings(sorted) + + return sorted +} + +func encodeFirstWant(e *Encoder) encoderStateFn { + var err error + if e.data.Capabilities.IsEmpty() { + err = e.pe.Encodef("want %s\n", e.sortedWants[0]) + } else { + e.data.Capabilities.Sort() + err = e.pe.Encodef( + "want %s %s\n", + e.sortedWants[0], + e.data.Capabilities.String(), + ) + } + if err != nil { + e.err = fmt.Errorf("encoding first want line: %s", err) + return nil + } + + return encodeAditionalWants +} + +func encodeAditionalWants(e *Encoder) encoderStateFn { + for _, w := range e.sortedWants[1:] { + if err := e.pe.Encodef("want %s\n", w); err != nil { + e.err = fmt.Errorf("encoding want %q: %s", w, err) + return nil + } + } + + return encodeShallows +} + +func encodeShallows(e *Encoder) encoderStateFn { + sorted := sortHashes(e.data.Shallows) + for _, s := range sorted { + if err := e.pe.Encodef("shallow %s\n", s); err != nil { + e.err = fmt.Errorf("encoding shallow %q: %s", s, err) + return nil + } + } + + return encodeDepth +} + +func encodeDepth(e *Encoder) encoderStateFn { + switch depth := e.data.Depth.(type) { + case DepthCommits: + if depth != 0 { + commits := int(depth) + if err := e.pe.Encodef("deepen %d\n", commits); err != nil { + e.err = fmt.Errorf("encoding depth %d: %s", depth, err) + return nil + } + } + case DepthSince: + when := time.Time(depth).UTC() + if err := e.pe.Encodef("deepen-since %d\n", when.Unix()); err != nil { + e.err = fmt.Errorf("encoding depth %s: %s", when, err) + return nil + } + case DepthReference: + reference := string(depth) + if err := e.pe.Encodef("deepen-not %s\n", reference); err != nil { + e.err = fmt.Errorf("encoding depth %s: %s", reference, err) + return nil + } + default: + e.err = fmt.Errorf("unsupported depth type") + return nil + } + + return encodeFlush +} + +func encodeFlush(e *Encoder) encoderStateFn { + if err := e.pe.Flush(); err != nil { + e.err = fmt.Errorf("encoding flush-pkt: %s", err) + return nil + } + + return nil +} diff --git a/formats/packp/ulreq/encoder_test.go b/formats/packp/ulreq/encoder_test.go new file mode 100644 index 0000000..56a8c2a --- /dev/null +++ b/formats/packp/ulreq/encoder_test.go @@ -0,0 +1,268 @@ +package ulreq + +import ( + "bytes" + "time" + + "gopkg.in/src-d/go-git.v4/core" + "gopkg.in/src-d/go-git.v4/formats/packp/pktline" + + . "gopkg.in/check.v1" +) + +type SuiteEncoder struct{} + +var _ = Suite(&SuiteEncoder{}) + +// returns a byte slice with the pkt-lines for the given payloads. +func pktlines(c *C, payloads ...string) []byte { + var buf bytes.Buffer + e := pktline.NewEncoder(&buf) + + err := e.EncodeString(payloads...) + c.Assert(err, IsNil, Commentf("building pktlines for %v\n", payloads)) + + return buf.Bytes() +} + +func testEncode(c *C, ur *UlReq, expectedPayloads []string) { + var buf bytes.Buffer + e := NewEncoder(&buf) + + err := e.Encode(ur) + c.Assert(err, IsNil) + obtained := buf.Bytes() + + expected := pktlines(c, expectedPayloads...) + + comment := Commentf("\nobtained = %s\nexpected = %s\n", string(obtained), string(expected)) + + c.Assert(obtained, DeepEquals, expected, comment) +} + +func testEncodeError(c *C, ur *UlReq, expectedErrorRegEx string) { + var buf bytes.Buffer + e := NewEncoder(&buf) + + err := e.Encode(ur) + c.Assert(err, ErrorMatches, expectedErrorRegEx) +} + +func (s *SuiteEncoder) TestZeroValue(c *C) { + ur := New() + expectedErrorRegEx := ".*empty wants.*" + + testEncodeError(c, ur, expectedErrorRegEx) +} + +func (s *SuiteEncoder) TestOneWant(c *C) { + ur := New() + ur.Wants = append(ur.Wants, core.NewHash("1111111111111111111111111111111111111111")) + + expected := []string{ + "want 1111111111111111111111111111111111111111\n", + pktline.FlushString, + } + + testEncode(c, ur, expected) +} + +func (s *SuiteEncoder) TestOneWantWithCapabilities(c *C) { + ur := New() + ur.Wants = append(ur.Wants, core.NewHash("1111111111111111111111111111111111111111")) + ur.Capabilities.Add("sysref", "HEAD:/refs/heads/master") + ur.Capabilities.Add("multi_ack") + ur.Capabilities.Add("thin-pack") + ur.Capabilities.Add("side-band") + ur.Capabilities.Add("ofs-delta") + + expected := []string{ + "want 1111111111111111111111111111111111111111 multi_ack ofs-delta side-band sysref=HEAD:/refs/heads/master thin-pack\n", + pktline.FlushString, + } + + testEncode(c, ur, expected) +} + +func (s *SuiteEncoder) TestWants(c *C) { + ur := New() + ur.Wants = append(ur.Wants, core.NewHash("4444444444444444444444444444444444444444")) + ur.Wants = append(ur.Wants, core.NewHash("1111111111111111111111111111111111111111")) + ur.Wants = append(ur.Wants, core.NewHash("3333333333333333333333333333333333333333")) + ur.Wants = append(ur.Wants, core.NewHash("2222222222222222222222222222222222222222")) + ur.Wants = append(ur.Wants, core.NewHash("5555555555555555555555555555555555555555")) + + expected := []string{ + "want 1111111111111111111111111111111111111111\n", + "want 2222222222222222222222222222222222222222\n", + "want 3333333333333333333333333333333333333333\n", + "want 4444444444444444444444444444444444444444\n", + "want 5555555555555555555555555555555555555555\n", + pktline.FlushString, + } + + testEncode(c, ur, expected) +} + +func (s *SuiteEncoder) TestWantsWithCapabilities(c *C) { + ur := New() + ur.Wants = append(ur.Wants, core.NewHash("4444444444444444444444444444444444444444")) + ur.Wants = append(ur.Wants, core.NewHash("1111111111111111111111111111111111111111")) + ur.Wants = append(ur.Wants, core.NewHash("3333333333333333333333333333333333333333")) + ur.Wants = append(ur.Wants, core.NewHash("2222222222222222222222222222222222222222")) + ur.Wants = append(ur.Wants, core.NewHash("5555555555555555555555555555555555555555")) + + ur.Capabilities.Add("sysref", "HEAD:/refs/heads/master") + ur.Capabilities.Add("multi_ack") + ur.Capabilities.Add("thin-pack") + ur.Capabilities.Add("side-band") + ur.Capabilities.Add("ofs-delta") + + expected := []string{ + "want 1111111111111111111111111111111111111111 multi_ack ofs-delta side-band sysref=HEAD:/refs/heads/master thin-pack\n", + "want 2222222222222222222222222222222222222222\n", + "want 3333333333333333333333333333333333333333\n", + "want 4444444444444444444444444444444444444444\n", + "want 5555555555555555555555555555555555555555\n", + pktline.FlushString, + } + + testEncode(c, ur, expected) +} + +func (s *SuiteEncoder) TestShallow(c *C) { + ur := New() + ur.Wants = append(ur.Wants, core.NewHash("1111111111111111111111111111111111111111")) + ur.Capabilities.Add("multi_ack") + ur.Shallows = append(ur.Shallows, core.NewHash("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")) + + expected := []string{ + "want 1111111111111111111111111111111111111111 multi_ack\n", + "shallow aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n", + pktline.FlushString, + } + + testEncode(c, ur, expected) +} + +func (s *SuiteEncoder) TestManyShallows(c *C) { + ur := New() + ur.Wants = append(ur.Wants, core.NewHash("1111111111111111111111111111111111111111")) + ur.Capabilities.Add("multi_ack") + ur.Shallows = append(ur.Shallows, core.NewHash("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")) + ur.Shallows = append(ur.Shallows, core.NewHash("dddddddddddddddddddddddddddddddddddddddd")) + ur.Shallows = append(ur.Shallows, core.NewHash("cccccccccccccccccccccccccccccccccccccccc")) + ur.Shallows = append(ur.Shallows, core.NewHash("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")) + + expected := []string{ + "want 1111111111111111111111111111111111111111 multi_ack\n", + "shallow aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n", + "shallow bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\n", + "shallow cccccccccccccccccccccccccccccccccccccccc\n", + "shallow dddddddddddddddddddddddddddddddddddddddd\n", + pktline.FlushString, + } + + testEncode(c, ur, expected) +} + +func (s *SuiteEncoder) TestDepthCommits(c *C) { + ur := New() + ur.Wants = append(ur.Wants, core.NewHash("1111111111111111111111111111111111111111")) + ur.Depth = DepthCommits(1234) + + expected := []string{ + "want 1111111111111111111111111111111111111111\n", + "deepen 1234\n", + pktline.FlushString, + } + + testEncode(c, ur, expected) +} + +func (s *SuiteEncoder) TestDepthSinceUTC(c *C) { + ur := New() + ur.Wants = append(ur.Wants, core.NewHash("1111111111111111111111111111111111111111")) + since := time.Date(2015, time.January, 2, 3, 4, 5, 0, time.UTC) + ur.Depth = DepthSince(since) + + expected := []string{ + "want 1111111111111111111111111111111111111111\n", + "deepen-since 1420167845\n", + pktline.FlushString, + } + + testEncode(c, ur, expected) +} + +func (s *SuiteEncoder) TestDepthSinceNonUTC(c *C) { + ur := New() + ur.Wants = append(ur.Wants, core.NewHash("1111111111111111111111111111111111111111")) + berlin, err := time.LoadLocation("Europe/Berlin") + c.Assert(err, IsNil) + since := time.Date(2015, time.January, 2, 3, 4, 5, 0, berlin) + // since value is 2015-01-02 03:04:05 +0100 UTC (Europe/Berlin) or + // 2015-01-02 02:04:05 +0000 UTC, which is 1420164245 Unix seconds. + ur.Depth = DepthSince(since) + + expected := []string{ + "want 1111111111111111111111111111111111111111\n", + "deepen-since 1420164245\n", + pktline.FlushString, + } + + testEncode(c, ur, expected) +} + +func (s *SuiteEncoder) TestDepthReference(c *C) { + ur := New() + ur.Wants = append(ur.Wants, core.NewHash("1111111111111111111111111111111111111111")) + ur.Depth = DepthReference("refs/heads/feature-foo") + + expected := []string{ + "want 1111111111111111111111111111111111111111\n", + "deepen-not refs/heads/feature-foo\n", + pktline.FlushString, + } + + testEncode(c, ur, expected) +} + +func (s *SuiteEncoder) TestAll(c *C) { + ur := New() + ur.Wants = append(ur.Wants, core.NewHash("4444444444444444444444444444444444444444")) + ur.Wants = append(ur.Wants, core.NewHash("1111111111111111111111111111111111111111")) + ur.Wants = append(ur.Wants, core.NewHash("3333333333333333333333333333333333333333")) + ur.Wants = append(ur.Wants, core.NewHash("2222222222222222222222222222222222222222")) + ur.Wants = append(ur.Wants, core.NewHash("5555555555555555555555555555555555555555")) + + ur.Capabilities.Add("sysref", "HEAD:/refs/heads/master") + ur.Capabilities.Add("multi_ack") + ur.Capabilities.Add("thin-pack") + ur.Capabilities.Add("side-band") + ur.Capabilities.Add("ofs-delta") + + ur.Shallows = append(ur.Shallows, core.NewHash("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")) + ur.Shallows = append(ur.Shallows, core.NewHash("dddddddddddddddddddddddddddddddddddddddd")) + ur.Shallows = append(ur.Shallows, core.NewHash("cccccccccccccccccccccccccccccccccccccccc")) + ur.Shallows = append(ur.Shallows, core.NewHash("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")) + + since := time.Date(2015, time.January, 2, 3, 4, 5, 0, time.UTC) + ur.Depth = DepthSince(since) + + expected := []string{ + "want 1111111111111111111111111111111111111111 multi_ack ofs-delta side-band sysref=HEAD:/refs/heads/master thin-pack\n", + "want 2222222222222222222222222222222222222222\n", + "want 3333333333333333333333333333333333333333\n", + "want 4444444444444444444444444444444444444444\n", + "want 5555555555555555555555555555555555555555\n", + "shallow aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n", + "shallow bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\n", + "shallow cccccccccccccccccccccccccccccccccccccccc\n", + "shallow dddddddddddddddddddddddddddddddddddddddd\n", + "deepen-since 1420167845\n", + pktline.FlushString, + } + + testEncode(c, ur, expected) +} diff --git a/formats/packp/ulreq/ulreq.go b/formats/packp/ulreq/ulreq.go new file mode 100644 index 0000000..e47450a --- /dev/null +++ b/formats/packp/ulreq/ulreq.go @@ -0,0 +1,56 @@ +// Package ulreq implements encoding and decoding upload-request +// messages from a git-upload-pack command. +package ulreq + +import ( + "time" + + "gopkg.in/src-d/go-git.v4/core" + "gopkg.in/src-d/go-git.v4/formats/packp" +) + +// UlReq values represent the information transmitted on a +// upload-request message. Values from this type are not zero-value +// safe, use the New function instead. +type UlReq struct { + Capabilities *packp.Capabilities + Wants []core.Hash + Shallows []core.Hash + Depth Depth +} + +// Depth values stores the desired depth of the requested packfile: see +// DepthCommit, DepthSince and DepthReference. +type Depth interface { + isDepth() +} + +// DepthCommits values stores the maximum number of requested commits in +// the packfile. Zero means infinite. A negative value will have +// undefined consecuences. +type DepthCommits int + +func (d DepthCommits) isDepth() {} + +// DepthSince values requests only commits newer than the specified time. +type DepthSince time.Time + +func (d DepthSince) isDepth() {} + +// DepthReference requests only commits not to found in the specified reference. +type DepthReference string + +func (d DepthReference) isDepth() {} + +// New returns a pointer to a new UlReq value, ready to be used. It has +// no capabilities, wants or shallows and an infinite depth. Please +// note that to encode an upload-request it has to have at least one +// wanted hash. +func New() *UlReq { + return &UlReq{ + Capabilities: packp.NewCapabilities(), + Wants: []core.Hash{}, + Shallows: []core.Hash{}, + Depth: DepthCommits(0), + } +} diff --git a/formats/packp/ulreq/ulreq_test.go b/formats/packp/ulreq/ulreq_test.go new file mode 100644 index 0000000..2c5e85a --- /dev/null +++ b/formats/packp/ulreq/ulreq_test.go @@ -0,0 +1,91 @@ +package ulreq + +import ( + "fmt" + "os" + "strings" + "testing" + "time" + + "gopkg.in/src-d/go-git.v4/core" + "gopkg.in/src-d/go-git.v4/formats/packp/pktline" + + . "gopkg.in/check.v1" +) + +func Test(t *testing.T) { TestingT(t) } + +func ExampleEncoder_Encode() { + // Create an empty UlReq with the contents you want... + ur := New() + + // Add a couple of wants + ur.Wants = append(ur.Wants, core.NewHash("3333333333333333333333333333333333333333")) + ur.Wants = append(ur.Wants, core.NewHash("1111111111111111111111111111111111111111")) + ur.Wants = append(ur.Wants, core.NewHash("2222222222222222222222222222222222222222")) + + // And some capabilities you will like the server to use + ur.Capabilities.Add("sysref", "HEAD:/refs/heads/master") + ur.Capabilities.Add("ofs-delta") + + // Add a couple of shallows + ur.Shallows = append(ur.Shallows, core.NewHash("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")) + ur.Shallows = append(ur.Shallows, core.NewHash("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")) + + // And retrict the answer of the server to commits newer than "2015-01-02 03:04:05 UTC" + since := time.Date(2015, time.January, 2, 3, 4, 5, 0, time.UTC) + ur.Depth = DepthSince(since) + + // Create a new Encode for the stdout... + e := NewEncoder(os.Stdout) + // ...and encode the upload-request to it. + _ = e.Encode(ur) // ignoring errors for brevity + // Output: + // 005bwant 1111111111111111111111111111111111111111 ofs-delta sysref=HEAD:/refs/heads/master + // 0032want 2222222222222222222222222222222222222222 + // 0032want 3333333333333333333333333333333333333333 + // 0035shallow aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + // 0035shallow bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + // 001cdeepen-since 1420167845 + // 0000 +} + +func ExampleDecoder_Decode() { + // Here is a raw advertised-ref message. + raw := "" + + "005bwant 1111111111111111111111111111111111111111 ofs-delta sysref=HEAD:/refs/heads/master\n" + + "0032want 2222222222222222222222222222222222222222\n" + + "0032want 3333333333333333333333333333333333333333\n" + + "0035shallow aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + + "0035shallow bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\n" + + "001cdeepen-since 1420167845\n" + // 2015-01-02 03:04:05 +0000 UTC + pktline.FlushString + + // Use the raw message as our input. + input := strings.NewReader(raw) + + // Create the Decoder reading from our input. + d := NewDecoder(input) + + // Decode the input into a newly allocated UlReq value. + ur := New() + _ = d.Decode(ur) // error check ignored for brevity + + // Do something interesting with the UlReq, e.g. print its contents. + fmt.Println("capabilities =", ur.Capabilities.String()) + fmt.Println("wants =", ur.Wants) + fmt.Println("shallows =", ur.Shallows) + switch depth := ur.Depth.(type) { + case DepthCommits: + fmt.Println("depth =", int(depth)) + case DepthSince: + fmt.Println("depth =", time.Time(depth)) + case DepthReference: + fmt.Println("depth =", string(depth)) + } + // Output: + // capabilities = ofs-delta sysref=HEAD:/refs/heads/master + // wants = [1111111111111111111111111111111111111111 2222222222222222222222222222222222222222 3333333333333333333333333333333333333333] + // shallows = [aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb] + // depth = 2015-01-02 03:04:05 +0000 UTC +} -- cgit