From ac095bb12c4d29722b60ba9f20590fa7cfa6bc7d Mon Sep 17 00:00:00 2001 From: Máximo Cuadros Date: Tue, 8 Nov 2016 23:46:38 +0100 Subject: new plumbing package (#118) * plumbing: now core was renamed to core, and formats and clients moved inside --- plumbing/format/packp/advrefs/advrefs.go | 58 +++ plumbing/format/packp/advrefs/advrefs_test.go | 315 +++++++++++ plumbing/format/packp/advrefs/decoder.go | 288 ++++++++++ plumbing/format/packp/advrefs/decoder_test.go | 500 ++++++++++++++++++ plumbing/format/packp/advrefs/encoder.go | 155 ++++++ plumbing/format/packp/advrefs/encoder_test.go | 249 +++++++++ plumbing/format/packp/capabilities.go | 136 +++++ plumbing/format/packp/capabilities_test.go | 46 ++ plumbing/format/packp/doc.go | 724 ++++++++++++++++++++++++++ plumbing/format/packp/pktline/encoder.go | 123 +++++ plumbing/format/packp/pktline/encoder_test.go | 249 +++++++++ plumbing/format/packp/pktline/scanner.go | 133 +++++ plumbing/format/packp/pktline/scanner_test.go | 225 ++++++++ plumbing/format/packp/ulreq/decoder.go | 287 ++++++++++ plumbing/format/packp/ulreq/decoder_test.go | 541 +++++++++++++++++++ plumbing/format/packp/ulreq/encoder.go | 140 +++++ plumbing/format/packp/ulreq/encoder_test.go | 268 ++++++++++ plumbing/format/packp/ulreq/ulreq.go | 56 ++ plumbing/format/packp/ulreq/ulreq_test.go | 91 ++++ 19 files changed, 4584 insertions(+) create mode 100644 plumbing/format/packp/advrefs/advrefs.go create mode 100644 plumbing/format/packp/advrefs/advrefs_test.go create mode 100644 plumbing/format/packp/advrefs/decoder.go create mode 100644 plumbing/format/packp/advrefs/decoder_test.go create mode 100644 plumbing/format/packp/advrefs/encoder.go create mode 100644 plumbing/format/packp/advrefs/encoder_test.go create mode 100644 plumbing/format/packp/capabilities.go create mode 100644 plumbing/format/packp/capabilities_test.go create mode 100644 plumbing/format/packp/doc.go create mode 100644 plumbing/format/packp/pktline/encoder.go create mode 100644 plumbing/format/packp/pktline/encoder_test.go create mode 100644 plumbing/format/packp/pktline/scanner.go create mode 100644 plumbing/format/packp/pktline/scanner_test.go create mode 100644 plumbing/format/packp/ulreq/decoder.go create mode 100644 plumbing/format/packp/ulreq/decoder_test.go create mode 100644 plumbing/format/packp/ulreq/encoder.go create mode 100644 plumbing/format/packp/ulreq/encoder_test.go create mode 100644 plumbing/format/packp/ulreq/ulreq.go create mode 100644 plumbing/format/packp/ulreq/ulreq_test.go (limited to 'plumbing/format/packp') diff --git a/plumbing/format/packp/advrefs/advrefs.go b/plumbing/format/packp/advrefs/advrefs.go new file mode 100644 index 0000000..4d7c897 --- /dev/null +++ b/plumbing/format/packp/advrefs/advrefs.go @@ -0,0 +1,58 @@ +// Package advrefs implements encoding and decoding advertised-refs +// messages from a git-upload-pack command. +package advrefs + +import ( + "gopkg.in/src-d/go-git.v4/plumbing" + "gopkg.in/src-d/go-git.v4/plumbing/format/packp" +) + +const ( + hashSize = 40 + head = "HEAD" + noHead = "capabilities^{}" +) + +var ( + sp = []byte(" ") + null = []byte("\x00") + eol = []byte("\n") + peeled = []byte("^{}") + shallow = []byte("shallow ") + noHeadMark = []byte(" capabilities^{}\x00") +) + +// 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 + Capabilities *packp.Capabilities + References map[string]plumbing.Hash + Peeled map[string]plumbing.Hash + Shallows []plumbing.Hash +} + +// New returns a pointer to a new AdvRefs value, ready to be used. +func New() *AdvRefs { + return &AdvRefs{ + Prefix: [][]byte{}, + Capabilities: packp.NewCapabilities(), + References: make(map[string]plumbing.Hash), + Peeled: make(map[string]plumbing.Hash), + Shallows: []plumbing.Hash{}, + } +} diff --git a/plumbing/format/packp/advrefs/advrefs_test.go b/plumbing/format/packp/advrefs/advrefs_test.go new file mode 100644 index 0000000..2639b6e --- /dev/null +++ b/plumbing/format/packp/advrefs/advrefs_test.go @@ -0,0 +1,315 @@ +package advrefs_test + +import ( + "bytes" + "fmt" + "io" + "strings" + "testing" + + "gopkg.in/src-d/go-git.v4/plumbing" + "gopkg.in/src-d/go-git.v4/plumbing/format/packp/advrefs" + "gopkg.in/src-d/go-git.v4/plumbing/format/packp/pktline" + + . "gopkg.in/check.v1" +) + +func Test(t *testing.T) { TestingT(t) } + +type SuiteDecodeEncode struct{} + +var _ = Suite(&SuiteDecodeEncode{}) + +func (s *SuiteDecodeEncode) test(c *C, in []string, exp []string) { + var err error + var input io.Reader + { + var buf bytes.Buffer + p := pktline.NewEncoder(&buf) + err = p.EncodeString(in...) + c.Assert(err, IsNil) + input = &buf + } + + var expected []byte + { + var buf bytes.Buffer + p := pktline.NewEncoder(&buf) + err = p.EncodeString(exp...) + c.Assert(err, IsNil) + + expected = buf.Bytes() + } + + var obtained []byte + { + ar := advrefs.New() + d := advrefs.NewDecoder(input) + err = d.Decode(ar) + c.Assert(err, IsNil) + + var buf bytes.Buffer + e := advrefs.NewEncoder(&buf) + err := e.Encode(ar) + c.Assert(err, IsNil) + + obtained = buf.Bytes() + } + + c.Assert(obtained, DeepEquals, expected, + Commentf("input = %v\nobtained = %q\nexpected = %q\n", + in, string(obtained), string(expected))) +} + +func (s *SuiteDecodeEncode) TestNoHead(c *C) { + input := []string{ + "0000000000000000000000000000000000000000 capabilities^{}\x00", + pktline.FlushString, + } + + expected := []string{ + "0000000000000000000000000000000000000000 capabilities^{}\x00\n", + pktline.FlushString, + } + + s.test(c, input, expected) +} + +func (s *SuiteDecodeEncode) TestNoHeadSmart(c *C) { + input := []string{ + "# service=git-upload-pack\n", + "0000000000000000000000000000000000000000 capabilities^{}\x00", + pktline.FlushString, + } + + expected := []string{ + "# service=git-upload-pack\n", + "0000000000000000000000000000000000000000 capabilities^{}\x00\n", + pktline.FlushString, + } + + s.test(c, input, expected) +} + +func (s *SuiteDecodeEncode) TestNoHeadSmartBug(c *C) { + input := []string{ + "# service=git-upload-pack\n", + pktline.FlushString, + "0000000000000000000000000000000000000000 capabilities^{}\x00\n", + pktline.FlushString, + } + + expected := []string{ + "# service=git-upload-pack\n", + pktline.FlushString, + "0000000000000000000000000000000000000000 capabilities^{}\x00\n", + pktline.FlushString, + } + + s.test(c, input, expected) +} + +func (s *SuiteDecodeEncode) TestRefs(c *C) { + input := []string{ + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00symref=HEAD:/refs/heads/master ofs-delta multi_ack", + "a6930aaee06755d1bdcfd943fbf614e4d92bb0c7 refs/heads/master", + "5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11-tree\n", + "7777777777777777777777777777777777777777 refs/tags/v2.6.12-tree", + pktline.FlushString, + } + + expected := []string{ + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00multi_ack ofs-delta symref=HEAD:/refs/heads/master\n", + "a6930aaee06755d1bdcfd943fbf614e4d92bb0c7 refs/heads/master\n", + "5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11-tree\n", + "7777777777777777777777777777777777777777 refs/tags/v2.6.12-tree\n", + pktline.FlushString, + } + + s.test(c, input, expected) +} + +func (s *SuiteDecodeEncode) TestPeeled(c *C) { + input := []string{ + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00symref=HEAD:/refs/heads/master ofs-delta multi_ack", + "7777777777777777777777777777777777777777 refs/tags/v2.6.12-tree\n", + "8888888888888888888888888888888888888888 refs/tags/v2.6.12-tree^{}", + "a6930aaee06755d1bdcfd943fbf614e4d92bb0c7 refs/heads/master\n", + "5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11-tree", + "c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11-tree^{}\n", + pktline.FlushString, + } + + expected := []string{ + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00multi_ack ofs-delta symref=HEAD:/refs/heads/master\n", + "a6930aaee06755d1bdcfd943fbf614e4d92bb0c7 refs/heads/master\n", + "5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11-tree\n", + "c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11-tree^{}\n", + "7777777777777777777777777777777777777777 refs/tags/v2.6.12-tree\n", + "8888888888888888888888888888888888888888 refs/tags/v2.6.12-tree^{}\n", + pktline.FlushString, + } + + s.test(c, input, expected) +} + +func (s *SuiteDecodeEncode) TestAll(c *C) { + input := []string{ + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00symref=HEAD:/refs/heads/master ofs-delta multi_ack\n", + "a6930aaee06755d1bdcfd943fbf614e4d92bb0c7 refs/heads/master\n", + "5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11-tree", + "c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11-tree^{}\n", + "7777777777777777777777777777777777777777 refs/tags/v2.6.12-tree\n", + "8888888888888888888888888888888888888888 refs/tags/v2.6.12-tree^{}", + "shallow 1111111111111111111111111111111111111111", + "shallow 2222222222222222222222222222222222222222\n", + pktline.FlushString, + } + + expected := []string{ + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00multi_ack ofs-delta symref=HEAD:/refs/heads/master\n", + "a6930aaee06755d1bdcfd943fbf614e4d92bb0c7 refs/heads/master\n", + "5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11-tree\n", + "c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11-tree^{}\n", + "7777777777777777777777777777777777777777 refs/tags/v2.6.12-tree\n", + "8888888888888888888888888888888888888888 refs/tags/v2.6.12-tree^{}\n", + "shallow 1111111111111111111111111111111111111111\n", + "shallow 2222222222222222222222222222222222222222\n", + pktline.FlushString, + } + + s.test(c, input, expected) +} + +func (s *SuiteDecodeEncode) TestAllSmart(c *C) { + input := []string{ + "# service=git-upload-pack\n", + pktline.FlushString, + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00symref=HEAD:/refs/heads/master ofs-delta multi_ack\n", + "a6930aaee06755d1bdcfd943fbf614e4d92bb0c7 refs/heads/master\n", + "5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11-tree\n", + "c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11-tree^{}\n", + "7777777777777777777777777777777777777777 refs/tags/v2.6.12-tree\n", + "8888888888888888888888888888888888888888 refs/tags/v2.6.12-tree^{}\n", + "shallow 1111111111111111111111111111111111111111\n", + "shallow 2222222222222222222222222222222222222222\n", + pktline.FlushString, + } + + expected := []string{ + "# service=git-upload-pack\n", + pktline.FlushString, + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00multi_ack ofs-delta symref=HEAD:/refs/heads/master\n", + "a6930aaee06755d1bdcfd943fbf614e4d92bb0c7 refs/heads/master\n", + "5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11-tree\n", + "c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11-tree^{}\n", + "7777777777777777777777777777777777777777 refs/tags/v2.6.12-tree\n", + "8888888888888888888888888888888888888888 refs/tags/v2.6.12-tree^{}\n", + "shallow 1111111111111111111111111111111111111111\n", + "shallow 2222222222222222222222222222222222222222\n", + pktline.FlushString, + } + + s.test(c, input, expected) +} + +func (s *SuiteDecodeEncode) TestAllSmartBug(c *C) { + input := []string{ + "# service=git-upload-pack\n", + pktline.FlushString, + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00symref=HEAD:/refs/heads/master ofs-delta multi_ack\n", + "a6930aaee06755d1bdcfd943fbf614e4d92bb0c7 refs/heads/master\n", + "5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11-tree\n", + "c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11-tree^{}\n", + "7777777777777777777777777777777777777777 refs/tags/v2.6.12-tree\n", + "8888888888888888888888888888888888888888 refs/tags/v2.6.12-tree^{}\n", + "shallow 1111111111111111111111111111111111111111\n", + "shallow 2222222222222222222222222222222222222222\n", + pktline.FlushString, + } + + expected := []string{ + "# service=git-upload-pack\n", + pktline.FlushString, + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00multi_ack ofs-delta symref=HEAD:/refs/heads/master\n", + "a6930aaee06755d1bdcfd943fbf614e4d92bb0c7 refs/heads/master\n", + "5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11-tree\n", + "c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11-tree^{}\n", + "7777777777777777777777777777777777777777 refs/tags/v2.6.12-tree\n", + "8888888888888888888888888888888888888888 refs/tags/v2.6.12-tree^{}\n", + "shallow 1111111111111111111111111111111111111111\n", + "shallow 2222222222222222222222222222222222222222\n", + pktline.FlushString, + } + + s.test(c, input, expected) +} + +func ExampleDecoder_Decode() { + // Here is a raw advertised-ref message. + raw := "" + + "0065a6930aaee06755d1bdcfd943fbf614e4d92bb0c7 HEAD\x00multi_ack ofs-delta symref=HEAD:/refs/heads/master\n" + + "003fa6930aaee06755d1bdcfd943fbf614e4d92bb0c7 refs/heads/master\n" + + "00441111111111111111111111111111111111111111 refs/tags/v2.6.11-tree\n" + + "00475555555555555555555555555555555555555555 refs/tags/v2.6.11-tree^{}\n" + + "0035shallow 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c\n" + + "0000" + + // Use the raw message as our input. + input := strings.NewReader(raw) + + // Create a advref.Decoder reading from our input. + d := advrefs.NewDecoder(input) + + // Decode the input into a newly allocated AdvRefs value. + ar := advrefs.New() + _ = d.Decode(ar) // error check ignored for brevity + + // Do something interesting with the AdvRefs, e.g. print its contents. + fmt.Println("head =", ar.Head) + fmt.Println("capabilities =", ar.Capabilities.String()) + fmt.Println("...") + fmt.Println("shallows =", ar.Shallows) + // Output: head = a6930aaee06755d1bdcfd943fbf614e4d92bb0c7 + // capabilities = multi_ack ofs-delta symref=HEAD:/refs/heads/master + // ... + // shallows = [5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c] +} + +func ExampleEncoder_Encode() { + // Create an AdvRefs with the contents you want... + ar := advrefs.New() + + // ...add a hash for the HEAD... + head := plumbing.NewHash("1111111111111111111111111111111111111111") + ar.Head = &head + + // ...add some server capabilities... + ar.Capabilities.Add("symref", "HEAD:/refs/heads/master") + ar.Capabilities.Add("ofs-delta") + ar.Capabilities.Add("multi_ack") + + // ...add a couple of references... + ar.References["refs/heads/master"] = plumbing.NewHash("2222222222222222222222222222222222222222") + ar.References["refs/tags/v1"] = plumbing.NewHash("3333333333333333333333333333333333333333") + + // ...including a peeled ref... + ar.Peeled["refs/tags/v1"] = plumbing.NewHash("4444444444444444444444444444444444444444") + + // ...and finally add a shallow + ar.Shallows = append(ar.Shallows, plumbing.NewHash("5555555555555555555555555555555555555555")) + + // Encode the advrefs.Contents to a bytes.Buffer. + // You can encode into stdout too, but you will not be able + // see the '\x00' after "HEAD". + var buf bytes.Buffer + e := advrefs.NewEncoder(&buf) + _ = e.Encode(ar) // error checks ignored for brevity + + // Print the contents of the buffer as a quoted string. + // Printing is as a non-quoted string will be prettier but you + // will miss the '\x00' after "HEAD". + fmt.Printf("%q", buf.String()) + // Output: + // "00651111111111111111111111111111111111111111 HEAD\x00multi_ack ofs-delta symref=HEAD:/refs/heads/master\n003f2222222222222222222222222222222222222222 refs/heads/master\n003a3333333333333333333333333333333333333333 refs/tags/v1\n003d4444444444444444444444444444444444444444 refs/tags/v1^{}\n0035shallow 5555555555555555555555555555555555555555\n0000" +} diff --git a/plumbing/format/packp/advrefs/decoder.go b/plumbing/format/packp/advrefs/decoder.go new file mode 100644 index 0000000..b654882 --- /dev/null +++ b/plumbing/format/packp/advrefs/decoder.go @@ -0,0 +1,288 @@ +package advrefs + +import ( + "bytes" + "encoding/hex" + "errors" + "fmt" + "io" + + "gopkg.in/src-d/go-git.v4/plumbing" + "gopkg.in/src-d/go-git.v4/plumbing/format/packp/pktline" +) + +// 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 + hash plumbing.Hash // last hash read + err error // sticky error, use the parser.error() method to fill this out + data *AdvRefs // parsed data is stored here +} + +// ErrEmpty is returned by Decode when there was no advertised-message at all +var ErrEmpty = errors.New("empty advertised-ref message") + +// 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 advertised-refs message form its input and +// stores it in the value pointed to by v. +func (d *Decoder) Decode(v *AdvRefs) error { + d.data = v + + for state := decodePrefix; 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 + } + + if d.nLine == 1 { + d.err = ErrEmpty + return false + } + + d.error("EOF") + return false + } + + d.line = d.s.Bytes() + d.line = bytes.TrimSuffix(d.line, eol) + + return true +} + +// The HTTP smart prefix is often followed by a flush-pkt. +func decodePrefix(d *Decoder) decoderStateFn { + 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) { + 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] == '#' +} + +func isFlush(payload []byte) bool { + return len(payload) == 0 +} + +// If the first hash is zero, then a no-refs is comming. Otherwise, a +// list-of-refs is comming, and the hash will be followed by the first +// advertised ref. +func decodeFirstHash(p *Decoder) decoderStateFn { + if len(p.line) < hashSize { + p.error("cannot read hash, pkt-line too short") + return nil + } + + if _, err := hex.Decode(p.hash[:], p.line[:hashSize]); err != nil { + p.error("invalid hash text: %s", err) + return nil + } + + p.line = p.line[hashSize:] + + if p.hash.IsZero() { + return decodeSkipNoRefs + } + + return decodeFirstRef +} + +// Skips SP "capabilities^{}" NUL +func decodeSkipNoRefs(p *Decoder) decoderStateFn { + if len(p.line) < len(noHeadMark) { + p.error("too short zero-id ref") + return nil + } + + if !bytes.HasPrefix(p.line, noHeadMark) { + p.error("malformed zero-id ref") + return nil + } + + p.line = p.line[len(noHeadMark):] + + return decodeCaps +} + +// decode the refname, expectes SP refname NULL +func decodeFirstRef(l *Decoder) decoderStateFn { + if len(l.line) < 3 { + l.error("line too short after hash") + return nil + } + + if !bytes.HasPrefix(l.line, sp) { + l.error("no space after hash") + return nil + } + l.line = l.line[1:] + + chunks := bytes.SplitN(l.line, null, 2) + if len(chunks) < 2 { + l.error("NULL not found") + return nil + } + ref := chunks[0] + l.line = chunks[1] + + if bytes.Equal(ref, []byte(head)) { + l.data.Head = &l.hash + } else { + l.data.References[string(ref)] = l.hash + } + + return decodeCaps +} + +func decodeCaps(p *Decoder) decoderStateFn { + if len(p.line) == 0 { + return decodeOtherRefs + } + + for _, c := range bytes.Split(p.line, sp) { + name, values := readCapability(c) + p.data.Capabilities.Add(name, values...) + } + + return decodeOtherRefs +} + +// 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 +} + +// The refs are either tips (obj-id SP refname) or a peeled (obj-id SP refname^{}). +// If there are no refs, then there might be a shallow or flush-ptk. +func decodeOtherRefs(p *Decoder) decoderStateFn { + if ok := p.nextLine(); !ok { + return nil + } + + if bytes.HasPrefix(p.line, shallow) { + return decodeShallow + } + + if len(p.line) == 0 { + return nil + } + + saveTo := p.data.References + if bytes.HasSuffix(p.line, peeled) { + p.line = bytes.TrimSuffix(p.line, peeled) + saveTo = p.data.Peeled + } + + ref, hash, err := readRef(p.line) + if err != nil { + p.error("%s", err) + return nil + } + saveTo[ref] = hash + + return decodeOtherRefs +} + +// Reads a ref-name +func readRef(data []byte) (string, plumbing.Hash, error) { + chunks := bytes.Split(data, sp) + switch { + case len(chunks) == 1: + return "", plumbing.ZeroHash, fmt.Errorf("malformed ref data: no space was found") + case len(chunks) > 2: + return "", plumbing.ZeroHash, fmt.Errorf("malformed ref data: more than one space found") + default: + return string(chunks[1]), plumbing.NewHash(string(chunks[0])), nil + } +} + +// Keeps reading shallows until a flush-pkt is found +func decodeShallow(p *Decoder) decoderStateFn { + if !bytes.HasPrefix(p.line, shallow) { + p.error("malformed shallow prefix, found %q... instead", p.line[:len(shallow)]) + return nil + } + p.line = bytes.TrimPrefix(p.line, shallow) + + if len(p.line) != hashSize { + p.error(fmt.Sprintf( + "malformed shallow hash: wrong length, expected 40 bytes, read %d bytes", + len(p.line))) + return nil + } + + text := p.line[:hashSize] + var h plumbing.Hash + if _, err := hex.Decode(h[:], text); err != nil { + p.error("invalid hash text: %s", err) + return nil + } + + p.data.Shallows = append(p.data.Shallows, h) + + if ok := p.nextLine(); !ok { + return nil + } + + if len(p.line) == 0 { + return nil // succesfull parse of the advertised-refs message + } + + return decodeShallow +} diff --git a/plumbing/format/packp/advrefs/decoder_test.go b/plumbing/format/packp/advrefs/decoder_test.go new file mode 100644 index 0000000..03867d3 --- /dev/null +++ b/plumbing/format/packp/advrefs/decoder_test.go @@ -0,0 +1,500 @@ +package advrefs_test + +import ( + "bytes" + "io" + "strings" + + "gopkg.in/src-d/go-git.v4/plumbing" + "gopkg.in/src-d/go-git.v4/plumbing/format/packp" + "gopkg.in/src-d/go-git.v4/plumbing/format/packp/advrefs" + "gopkg.in/src-d/go-git.v4/plumbing/format/packp/pktline" + + . "gopkg.in/check.v1" +) + +type SuiteDecoder struct{} + +var _ = Suite(&SuiteDecoder{}) + +func (s *SuiteDecoder) TestEmpty(c *C) { + ar := advrefs.New() + var buf bytes.Buffer + d := advrefs.NewDecoder(&buf) + + err := d.Decode(ar) + c.Assert(err, Equals, advrefs.ErrEmpty) +} + +func (s *SuiteDecoder) TestShortForHash(c *C) { + payloads := []string{ + "6ecf0ef2c2dffb796", + pktline.FlushString, + } + r := toPktLines(c, payloads) + testDecoderErrorMatches(c, r, ".*too short") +} + +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) { + ar := advrefs.New() + d := advrefs.NewDecoder(input) + + err := d.Decode(ar) + c.Assert(err, ErrorMatches, pattern) +} + +func (s *SuiteDecoder) TestInvalidFirstHash(c *C) { + payloads := []string{ + "6ecf0ef2c2dffb796alberto2219af86ec6584e5 HEAD\x00multi_ack thin-pack\n", + pktline.FlushString, + } + r := toPktLines(c, payloads) + testDecoderErrorMatches(c, r, ".*invalid hash.*") +} + +func (s *SuiteDecoder) TestZeroId(c *C) { + payloads := []string{ + "0000000000000000000000000000000000000000 capabilities^{}\x00multi_ack thin-pack\n", + pktline.FlushString, + } + ar := testDecodeOK(c, payloads) + c.Assert(ar.Head, IsNil) +} + +func testDecodeOK(c *C, payloads []string) *advrefs.AdvRefs { + var buf bytes.Buffer + e := pktline.NewEncoder(&buf) + err := e.EncodeString(payloads...) + c.Assert(err, IsNil) + + ar := advrefs.New() + d := advrefs.NewDecoder(&buf) + + err = d.Decode(ar) + c.Assert(err, IsNil) + + return ar +} + +func (s *SuiteDecoder) TestMalformedZeroId(c *C) { + payloads := []string{ + "0000000000000000000000000000000000000000 wrong\x00multi_ack thin-pack\n", + pktline.FlushString, + } + r := toPktLines(c, payloads) + testDecoderErrorMatches(c, r, ".*malformed zero-id.*") +} + +func (s *SuiteDecoder) TestShortZeroId(c *C) { + payloads := []string{ + "0000000000000000000000000000000000000000 capabi", + pktline.FlushString, + } + r := toPktLines(c, payloads) + testDecoderErrorMatches(c, r, ".*too short zero-id.*") +} + +func (s *SuiteDecoder) TestHead(c *C) { + payloads := []string{ + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00", + pktline.FlushString, + } + ar := testDecodeOK(c, payloads) + c.Assert(*ar.Head, Equals, + plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5")) +} + +func (s *SuiteDecoder) TestFirstIsNotHead(c *C) { + payloads := []string{ + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 refs/heads/master\x00", + pktline.FlushString, + } + ar := testDecodeOK(c, payloads) + c.Assert(ar.Head, IsNil) + c.Assert(ar.References["refs/heads/master"], Equals, + plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5")) +} + +func (s *SuiteDecoder) TestShortRef(c *C) { + payloads := []string{ + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 H", + pktline.FlushString, + } + r := toPktLines(c, payloads) + testDecoderErrorMatches(c, r, ".*too short.*") +} + +func (s *SuiteDecoder) TestNoNULL(c *C) { + payloads := []string{ + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEADofs-delta multi_ack", + pktline.FlushString, + } + r := toPktLines(c, payloads) + testDecoderErrorMatches(c, r, ".*NULL not found.*") +} + +func (s *SuiteDecoder) TestNoSpaceAfterHash(c *C) { + payloads := []string{ + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5-HEAD\x00", + pktline.FlushString, + } + r := toPktLines(c, payloads) + testDecoderErrorMatches(c, r, ".*no space after hash.*") +} + +func (s *SuiteDecoder) TestNoCaps(c *C) { + payloads := []string{ + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00", + pktline.FlushString, + } + ar := testDecodeOK(c, payloads) + c.Assert(ar.Capabilities.IsEmpty(), Equals, true) +} + +func (s *SuiteDecoder) TestCaps(c *C) { + for _, test := range [...]struct { + input []string + capabilities []packp.Capability + }{ + { + input: []string{ + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00", + pktline.FlushString, + }, + capabilities: []packp.Capability{}, + }, + { + input: []string{ + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00\n", + pktline.FlushString, + }, + capabilities: []packp.Capability{}, + }, + { + input: []string{ + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00ofs-delta", + pktline.FlushString, + }, + capabilities: []packp.Capability{ + { + Name: "ofs-delta", + Values: []string(nil), + }, + }, + }, + { + input: []string{ + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00ofs-delta multi_ack", + pktline.FlushString, + }, + capabilities: []packp.Capability{ + {Name: "ofs-delta", Values: []string(nil)}, + {Name: "multi_ack", Values: []string(nil)}, + }, + }, + { + input: []string{ + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00ofs-delta multi_ack\n", + pktline.FlushString, + }, + capabilities: []packp.Capability{ + {Name: "ofs-delta", Values: []string(nil)}, + {Name: "multi_ack", Values: []string(nil)}, + }, + }, + { + input: []string{ + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00symref=HEAD:refs/heads/master agent=foo=bar\n", + pktline.FlushString, + }, + capabilities: []packp.Capability{ + {Name: "symref", Values: []string{"HEAD:refs/heads/master"}}, + {Name: "agent", Values: []string{"foo=bar"}}, + }, + }, + { + input: []string{ + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00symref=HEAD:refs/heads/master agent=foo=bar agent=new-agent\n", + pktline.FlushString, + }, + capabilities: []packp.Capability{ + {Name: "symref", Values: []string{"HEAD:refs/heads/master"}}, + {Name: "agent", Values: []string{"foo=bar", "new-agent"}}, + }, + }, + } { + ar := testDecodeOK(c, test.input) + for _, fixCap := range test.capabilities { + c.Assert(ar.Capabilities.Supports(fixCap.Name), Equals, true, + Commentf("input = %q, capability = %q", test.input, fixCap.Name)) + c.Assert(ar.Capabilities.Get(fixCap.Name).Values, DeepEquals, fixCap.Values, + Commentf("input = %q, capability = %q", test.input, fixCap.Name)) + } + } +} + +func (s *SuiteDecoder) TestWithPrefix(c *C) { + payloads := []string{ + "# this is a prefix\n", + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00foo\n", + pktline.FlushString, + } + ar := testDecodeOK(c, payloads) + c.Assert(len(ar.Prefix), Equals, 1) + c.Assert(ar.Prefix[0], DeepEquals, []byte("# this is a prefix")) +} + +func (s *SuiteDecoder) TestWithPrefixAndFlush(c *C) { + payloads := []string{ + "# this is a prefix\n", + pktline.FlushString, + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00foo\n", + pktline.FlushString, + } + ar := testDecodeOK(c, payloads) + c.Assert(len(ar.Prefix), Equals, 2) + c.Assert(ar.Prefix[0], DeepEquals, []byte("# this is a prefix")) + c.Assert(ar.Prefix[1], DeepEquals, []byte(pktline.FlushString)) +} + +func (s *SuiteDecoder) TestOtherRefs(c *C) { + for _, test := range [...]struct { + input []string + references map[string]plumbing.Hash + peeled map[string]plumbing.Hash + }{ + { + input: []string{ + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00ofs-delta symref=HEAD:/refs/heads/master\n", + pktline.FlushString, + }, + references: make(map[string]plumbing.Hash), + peeled: make(map[string]plumbing.Hash), + }, { + input: []string{ + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00ofs-delta symref=HEAD:/refs/heads/master\n", + "1111111111111111111111111111111111111111 ref/foo", + pktline.FlushString, + }, + references: map[string]plumbing.Hash{ + "ref/foo": plumbing.NewHash("1111111111111111111111111111111111111111"), + }, + peeled: make(map[string]plumbing.Hash), + }, { + input: []string{ + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00ofs-delta symref=HEAD:/refs/heads/master\n", + "1111111111111111111111111111111111111111 ref/foo\n", + pktline.FlushString, + }, + references: map[string]plumbing.Hash{ + "ref/foo": plumbing.NewHash("1111111111111111111111111111111111111111"), + }, + peeled: make(map[string]plumbing.Hash), + }, { + input: []string{ + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00ofs-delta symref=HEAD:/refs/heads/master\n", + "1111111111111111111111111111111111111111 ref/foo\n", + "2222222222222222222222222222222222222222 ref/bar", + pktline.FlushString, + }, + references: map[string]plumbing.Hash{ + "ref/foo": plumbing.NewHash("1111111111111111111111111111111111111111"), + "ref/bar": plumbing.NewHash("2222222222222222222222222222222222222222"), + }, + peeled: make(map[string]plumbing.Hash), + }, { + input: []string{ + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00ofs-delta symref=HEAD:/refs/heads/master\n", + "1111111111111111111111111111111111111111 ref/foo^{}\n", + pktline.FlushString, + }, + references: make(map[string]plumbing.Hash), + peeled: map[string]plumbing.Hash{ + "ref/foo": plumbing.NewHash("1111111111111111111111111111111111111111"), + }, + }, { + input: []string{ + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00ofs-delta symref=HEAD:/refs/heads/master\n", + "1111111111111111111111111111111111111111 ref/foo\n", + "2222222222222222222222222222222222222222 ref/bar^{}", + pktline.FlushString, + }, + references: map[string]plumbing.Hash{ + "ref/foo": plumbing.NewHash("1111111111111111111111111111111111111111"), + }, + peeled: map[string]plumbing.Hash{ + "ref/bar": plumbing.NewHash("2222222222222222222222222222222222222222"), + }, + }, { + input: []string{ + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00ofs-delta symref=HEAD:/refs/heads/master\n", + "a6930aaee06755d1bdcfd943fbf614e4d92bb0c7 refs/heads/master\n", + "51b8b4fb32271d39fbdd760397406177b2b0fd36 refs/pull/10/head\n", + "02b5a6031ba7a8cbfde5d65ff9e13ecdbc4a92ca refs/pull/100/head\n", + "c284c212704c43659bf5913656b8b28e32da1621 refs/pull/100/merge\n", + "3d6537dce68c8b7874333a1720958bd8db3ae8ca refs/pull/101/merge\n", + "5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11\n", + "c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}\n", + "5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11-tree\n", + "c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11-tree^{}\n", + pktline.FlushString, + }, + references: map[string]plumbing.Hash{ + "refs/heads/master": plumbing.NewHash("a6930aaee06755d1bdcfd943fbf614e4d92bb0c7"), + "refs/pull/10/head": plumbing.NewHash("51b8b4fb32271d39fbdd760397406177b2b0fd36"), + "refs/pull/100/head": plumbing.NewHash("02b5a6031ba7a8cbfde5d65ff9e13ecdbc4a92ca"), + "refs/pull/100/merge": plumbing.NewHash("c284c212704c43659bf5913656b8b28e32da1621"), + "refs/pull/101/merge": plumbing.NewHash("3d6537dce68c8b7874333a1720958bd8db3ae8ca"), + "refs/tags/v2.6.11": plumbing.NewHash("5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c"), + "refs/tags/v2.6.11-tree": plumbing.NewHash("5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c"), + }, + peeled: map[string]plumbing.Hash{ + "refs/tags/v2.6.11": plumbing.NewHash("c39ae07f393806ccf406ef966e9a15afc43cc36a"), + "refs/tags/v2.6.11-tree": plumbing.NewHash("c39ae07f393806ccf406ef966e9a15afc43cc36a"), + }, + }, + } { + ar := testDecodeOK(c, test.input) + comment := Commentf("input = %v\n", test.input) + c.Assert(ar.References, DeepEquals, test.references, comment) + c.Assert(ar.Peeled, DeepEquals, test.peeled, comment) + } +} + +func (s *SuiteDecoder) TestMalformedOtherRefsNoSpace(c *C) { + payloads := []string{ + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00multi_ack thin-pack\n", + "5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8crefs/tags/v2.6.11\n", + pktline.FlushString, + } + r := toPktLines(c, payloads) + testDecoderErrorMatches(c, r, ".*malformed ref data.*") +} + +func (s *SuiteDecoder) TestMalformedOtherRefsMultipleSpaces(c *C) { + payloads := []string{ + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00multi_ack thin-pack\n", + "5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags v2.6.11\n", + pktline.FlushString, + } + r := toPktLines(c, payloads) + testDecoderErrorMatches(c, r, ".*malformed ref data.*") +} + +func (s *SuiteDecoder) TestShallow(c *C) { + for _, test := range [...]struct { + input []string + shallows []plumbing.Hash + }{ + { + input: []string{ + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00ofs-delta symref=HEAD:/refs/heads/master\n", + "a6930aaee06755d1bdcfd943fbf614e4d92bb0c7 refs/heads/master\n", + "5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11-tree\n", + "c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11-tree^{}\n", + pktline.FlushString, + }, + shallows: []plumbing.Hash{}, + }, { + input: []string{ + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00ofs-delta symref=HEAD:/refs/heads/master\n", + "a6930aaee06755d1bdcfd943fbf614e4d92bb0c7 refs/heads/master\n", + "5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11-tree\n", + "c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11-tree^{}\n", + "shallow 1111111111111111111111111111111111111111\n", + pktline.FlushString, + }, + shallows: []plumbing.Hash{plumbing.NewHash("1111111111111111111111111111111111111111")}, + }, { + input: []string{ + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00ofs-delta symref=HEAD:/refs/heads/master\n", + "a6930aaee06755d1bdcfd943fbf614e4d92bb0c7 refs/heads/master\n", + "5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11-tree\n", + "c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11-tree^{}\n", + "shallow 1111111111111111111111111111111111111111\n", + "shallow 2222222222222222222222222222222222222222\n", + pktline.FlushString, + }, + shallows: []plumbing.Hash{ + plumbing.NewHash("1111111111111111111111111111111111111111"), + plumbing.NewHash("2222222222222222222222222222222222222222"), + }, + }, + } { + ar := testDecodeOK(c, test.input) + comment := Commentf("input = %v\n", test.input) + c.Assert(ar.Shallows, DeepEquals, test.shallows, comment) + } +} + +func (s *SuiteDecoder) TestInvalidShallowHash(c *C) { + payloads := []string{ + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00ofs-delta symref=HEAD:/refs/heads/master\n", + "a6930aaee06755d1bdcfd943fbf614e4d92bb0c7 refs/heads/master\n", + "5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11-tree\n", + "c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11-tree^{}\n", + "shallow 11111111alcortes111111111111111111111111\n", + "shallow 2222222222222222222222222222222222222222\n", + pktline.FlushString, + } + r := toPktLines(c, payloads) + testDecoderErrorMatches(c, r, ".*invalid hash text.*") +} + +func (s *SuiteDecoder) TestGarbageAfterShallow(c *C) { + payloads := []string{ + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00ofs-delta symref=HEAD:/refs/heads/master\n", + "a6930aaee06755d1bdcfd943fbf614e4d92bb0c7 refs/heads/master\n", + "5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11-tree\n", + "c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11-tree^{}\n", + "shallow 1111111111111111111111111111111111111111\n", + "shallow 2222222222222222222222222222222222222222\n", + "b5be40b90dbaa6bd337f3b77de361bfc0723468b refs/tags/v4.4", + pktline.FlushString, + } + r := toPktLines(c, payloads) + testDecoderErrorMatches(c, r, ".*malformed shallow prefix.*") +} + +func (s *SuiteDecoder) TestMalformedShallowHash(c *C) { + payloads := []string{ + "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00ofs-delta symref=HEAD:/refs/heads/master\n", + "a6930aaee06755d1bdcfd943fbf614e4d92bb0c7 refs/heads/master\n", + "5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11-tree\n", + "c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11-tree^{}\n", + "shallow 1111111111111111111111111111111111111111\n", + "shallow 2222222222222222222222222222222222222222 malformed\n", + pktline.FlushString, + } + r := toPktLines(c, payloads) + testDecoderErrorMatches(c, r, ".*malformed shallow hash.*") +} + +func (s *SuiteDecoder) TestEOFRefs(c *C) { + input := strings.NewReader("" + + "005b6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00ofs-delta symref=HEAD:/refs/heads/master\n" + + "003fa6930aaee06755d1bdcfd943fbf614e4d92bb0c7 refs/heads/master\n" + + "00355dc01c595e6c6ec9ccda4f6ffbf614e4d92bb0c7 refs/foo\n", + ) + testDecoderErrorMatches(c, input, ".*invalid pkt-len.*") +} + +func (s *SuiteDecoder) TestEOFShallows(c *C) { + input := strings.NewReader("" + + "005b6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00ofs-delta symref=HEAD:/refs/heads/master\n" + + "003fa6930aaee06755d1bdcfd943fbf614e4d92bb0c7 refs/heads/master\n" + + "00445dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11-tree\n" + + "0047c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11-tree^{}\n" + + "0035shallow 1111111111111111111111111111111111111111\n" + + "0034shallow 222222222222222222222222") + testDecoderErrorMatches(c, input, ".*unexpected EOF.*") +} diff --git a/plumbing/format/packp/advrefs/encoder.go b/plumbing/format/packp/advrefs/encoder.go new file mode 100644 index 0000000..8c52f14 --- /dev/null +++ b/plumbing/format/packp/advrefs/encoder.go @@ -0,0 +1,155 @@ +package advrefs + +import ( + "bytes" + "io" + "sort" + + "gopkg.in/src-d/go-git.v4/plumbing" + "gopkg.in/src-d/go-git.v4/plumbing/format/packp" + "gopkg.in/src-d/go-git.v4/plumbing/format/packp/pktline" +) + +// An Encoder writes AdvRefs values to an output stream. +type Encoder struct { + data *AdvRefs // data to encode + pe *pktline.Encoder // where to write the encoded data + 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 AdvRefs encoding of v to the stream. +// +// All the payloads will end with a newline character. Capabilities, +// references and shallows are writen in alphabetical order, except for +// peeled references that always follow their corresponding references. +func (e *Encoder) Encode(v *AdvRefs) error { + e.data = v + + for state := encodePrefix; state != nil; { + state = state(e) + } + + return e.err +} + +type encoderStateFn func(*Encoder) encoderStateFn + +func encodePrefix(e *Encoder) encoderStateFn { + for _, p := range e.data.Prefix { + if bytes.Equal(p, pktline.Flush) { + if e.err = e.pe.Flush(); e.err != nil { + return nil + } + continue + } + if e.err = e.pe.Encodef("%s\n", string(p)); e.err != nil { + return nil + } + } + + return encodeFirstLine +} + +// Adds the first pkt-line payload: head hash, head ref and capabilities. +// Also handle the special case when no HEAD ref is found. +func encodeFirstLine(e *Encoder) encoderStateFn { + head := formatHead(e.data.Head) + separator := formatSeparator(e.data.Head) + capabilities := formatCaps(e.data.Capabilities) + + if e.err = e.pe.Encodef("%s %s\x00%s\n", head, separator, capabilities); e.err != nil { + return nil + } + + return encodeRefs +} + +func formatHead(h *plumbing.Hash) string { + if h == nil { + return plumbing.ZeroHash.String() + } + + return h.String() +} + +func formatSeparator(h *plumbing.Hash) string { + if h == nil { + return noHead + } + + return head +} + +func formatCaps(c *packp.Capabilities) string { + if c == nil { + return "" + } + + c.Sort() + + return c.String() +} + +// Adds the (sorted) refs: hash SP refname EOL +// and their peeled refs if any. +func encodeRefs(e *Encoder) encoderStateFn { + refs := sortRefs(e.data.References) + for _, r := range refs { + hash, _ := e.data.References[r] + if e.err = e.pe.Encodef("%s %s\n", hash.String(), r); e.err != nil { + return nil + } + + if hash, ok := e.data.Peeled[r]; ok { + if e.err = e.pe.Encodef("%s %s^{}\n", hash.String(), r); e.err != nil { + return nil + } + } + } + + return encodeShallow +} + +func sortRefs(m map[string]plumbing.Hash) []string { + ret := make([]string, 0, len(m)) + for k := range m { + ret = append(ret, k) + } + sort.Strings(ret) + + return ret +} + +// Adds the (sorted) shallows: "shallow" SP hash EOL +func encodeShallow(e *Encoder) encoderStateFn { + sorted := sortShallows(e.data.Shallows) + for _, hash := range sorted { + if e.err = e.pe.Encodef("shallow %s\n", hash); e.err != nil { + return nil + } + } + + return encodeFlush +} + +func sortShallows(c []plumbing.Hash) []string { + ret := []string{} + for _, h := range c { + ret = append(ret, h.String()) + } + sort.Strings(ret) + + return ret +} + +func encodeFlush(e *Encoder) encoderStateFn { + e.err = e.pe.Flush() + return nil +} diff --git a/plumbing/format/packp/advrefs/encoder_test.go b/plumbing/format/packp/advrefs/encoder_test.go new file mode 100644 index 0000000..b4b085c --- /dev/null +++ b/plumbing/format/packp/advrefs/encoder_test.go @@ -0,0 +1,249 @@ +package advrefs_test + +import ( + "bytes" + "strings" + + "gopkg.in/src-d/go-git.v4/plumbing" + "gopkg.in/src-d/go-git.v4/plumbing/format/packp" + "gopkg.in/src-d/go-git.v4/plumbing/format/packp/advrefs" + "gopkg.in/src-d/go-git.v4/plumbing/format/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 ...[]byte) []byte { + var buf bytes.Buffer + e := pktline.NewEncoder(&buf) + err := e.Encode(payloads...) + c.Assert(err, IsNil, Commentf("building pktlines for %v\n", payloads)) + + return buf.Bytes() +} + +func testEncode(c *C, input *advrefs.AdvRefs, expected []byte) { + var buf bytes.Buffer + e := advrefs.NewEncoder(&buf) + err := e.Encode(input) + c.Assert(err, IsNil) + obtained := buf.Bytes() + + comment := Commentf("\nobtained = %s\nexpected = %s\n", string(obtained), string(expected)) + + c.Assert(obtained, DeepEquals, expected, comment) +} + +func (s *SuiteEncoder) TestZeroValue(c *C) { + ar := &advrefs.AdvRefs{} + + expected := pktlines(c, + []byte("0000000000000000000000000000000000000000 capabilities^{}\x00\n"), + pktline.Flush, + ) + + testEncode(c, ar, expected) +} + +func (s *SuiteEncoder) TestHead(c *C) { + hash := plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5") + ar := &advrefs.AdvRefs{ + Head: &hash, + } + + expected := pktlines(c, + []byte("6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00\n"), + pktline.Flush, + ) + + testEncode(c, ar, expected) +} + +func (s *SuiteEncoder) TestCapsNoHead(c *C) { + capabilities := packp.NewCapabilities() + capabilities.Add("symref", "HEAD:/refs/heads/master") + capabilities.Add("ofs-delta") + capabilities.Add("multi_ack") + ar := &advrefs.AdvRefs{ + Capabilities: capabilities, + } + + expected := pktlines(c, + []byte("0000000000000000000000000000000000000000 capabilities^{}\x00multi_ack ofs-delta symref=HEAD:/refs/heads/master\n"), + pktline.Flush, + ) + + testEncode(c, ar, expected) +} + +func (s *SuiteEncoder) TestCapsWithHead(c *C) { + hash := plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5") + capabilities := packp.NewCapabilities() + capabilities.Add("symref", "HEAD:/refs/heads/master") + capabilities.Add("ofs-delta") + capabilities.Add("multi_ack") + ar := &advrefs.AdvRefs{ + Head: &hash, + Capabilities: capabilities, + } + + expected := pktlines(c, + []byte("6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00multi_ack ofs-delta symref=HEAD:/refs/heads/master\n"), + pktline.Flush, + ) + + testEncode(c, ar, expected) +} + +func (s *SuiteEncoder) TestRefs(c *C) { + references := map[string]plumbing.Hash{ + "refs/heads/master": plumbing.NewHash("a6930aaee06755d1bdcfd943fbf614e4d92bb0c7"), + "refs/tags/v2.6.12-tree": plumbing.NewHash("1111111111111111111111111111111111111111"), + "refs/tags/v2.7.13-tree": plumbing.NewHash("3333333333333333333333333333333333333333"), + "refs/tags/v2.6.13-tree": plumbing.NewHash("2222222222222222222222222222222222222222"), + "refs/tags/v2.6.11-tree": plumbing.NewHash("5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c"), + } + ar := &advrefs.AdvRefs{ + References: references, + } + + expected := pktlines(c, + []byte("0000000000000000000000000000000000000000 capabilities^{}\x00\n"), + []byte("a6930aaee06755d1bdcfd943fbf614e4d92bb0c7 refs/heads/master\n"), + []byte("5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11-tree\n"), + []byte("1111111111111111111111111111111111111111 refs/tags/v2.6.12-tree\n"), + []byte("2222222222222222222222222222222222222222 refs/tags/v2.6.13-tree\n"), + []byte("3333333333333333333333333333333333333333 refs/tags/v2.7.13-tree\n"), + pktline.Flush, + ) + + testEncode(c, ar, expected) +} + +func (s *SuiteEncoder) TestPeeled(c *C) { + references := map[string]plumbing.Hash{ + "refs/heads/master": plumbing.NewHash("a6930aaee06755d1bdcfd943fbf614e4d92bb0c7"), + "refs/tags/v2.6.12-tree": plumbing.NewHash("1111111111111111111111111111111111111111"), + "refs/tags/v2.7.13-tree": plumbing.NewHash("3333333333333333333333333333333333333333"), + "refs/tags/v2.6.13-tree": plumbing.NewHash("2222222222222222222222222222222222222222"), + "refs/tags/v2.6.11-tree": plumbing.NewHash("5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c"), + } + peeled := map[string]plumbing.Hash{ + "refs/tags/v2.7.13-tree": plumbing.NewHash("4444444444444444444444444444444444444444"), + "refs/tags/v2.6.12-tree": plumbing.NewHash("5555555555555555555555555555555555555555"), + } + ar := &advrefs.AdvRefs{ + References: references, + Peeled: peeled, + } + + expected := pktlines(c, + []byte("0000000000000000000000000000000000000000 capabilities^{}\x00\n"), + []byte("a6930aaee06755d1bdcfd943fbf614e4d92bb0c7 refs/heads/master\n"), + []byte("5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11-tree\n"), + []byte("1111111111111111111111111111111111111111 refs/tags/v2.6.12-tree\n"), + []byte("5555555555555555555555555555555555555555 refs/tags/v2.6.12-tree^{}\n"), + []byte("2222222222222222222222222222222222222222 refs/tags/v2.6.13-tree\n"), + []byte("3333333333333333333333333333333333333333 refs/tags/v2.7.13-tree\n"), + []byte("4444444444444444444444444444444444444444 refs/tags/v2.7.13-tree^{}\n"), + pktline.Flush, + ) + + testEncode(c, ar, expected) +} + +func (s *SuiteEncoder) TestShallow(c *C) { + shallows := []plumbing.Hash{ + plumbing.NewHash("1111111111111111111111111111111111111111"), + plumbing.NewHash("4444444444444444444444444444444444444444"), + plumbing.NewHash("3333333333333333333333333333333333333333"), + plumbing.NewHash("2222222222222222222222222222222222222222"), + } + ar := &advrefs.AdvRefs{ + Shallows: shallows, + } + + expected := pktlines(c, + []byte("0000000000000000000000000000000000000000 capabilities^{}\x00\n"), + []byte("shallow 1111111111111111111111111111111111111111\n"), + []byte("shallow 2222222222222222222222222222222222222222\n"), + []byte("shallow 3333333333333333333333333333333333333333\n"), + []byte("shallow 4444444444444444444444444444444444444444\n"), + pktline.Flush, + ) + + testEncode(c, ar, expected) +} + +func (s *SuiteEncoder) TestAll(c *C) { + hash := plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5") + + capabilities := packp.NewCapabilities() + capabilities.Add("symref", "HEAD:/refs/heads/master") + capabilities.Add("ofs-delta") + capabilities.Add("multi_ack") + + references := map[string]plumbing.Hash{ + "refs/heads/master": plumbing.NewHash("a6930aaee06755d1bdcfd943fbf614e4d92bb0c7"), + "refs/tags/v2.6.12-tree": plumbing.NewHash("1111111111111111111111111111111111111111"), + "refs/tags/v2.7.13-tree": plumbing.NewHash("3333333333333333333333333333333333333333"), + "refs/tags/v2.6.13-tree": plumbing.NewHash("2222222222222222222222222222222222222222"), + "refs/tags/v2.6.11-tree": plumbing.NewHash("5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c"), + } + + peeled := map[string]plumbing.Hash{ + "refs/tags/v2.7.13-tree": plumbing.NewHash("4444444444444444444444444444444444444444"), + "refs/tags/v2.6.12-tree": plumbing.NewHash("5555555555555555555555555555555555555555"), + } + + shallows := []plumbing.Hash{ + plumbing.NewHash("1111111111111111111111111111111111111111"), + plumbing.NewHash("4444444444444444444444444444444444444444"), + plumbing.NewHash("3333333333333333333333333333333333333333"), + plumbing.NewHash("2222222222222222222222222222222222222222"), + } + + ar := &advrefs.AdvRefs{ + Head: &hash, + Capabilities: capabilities, + References: references, + Peeled: peeled, + Shallows: shallows, + } + + expected := pktlines(c, + []byte("6ecf0ef2c2dffb796033e5a02219af86ec6584e5 HEAD\x00multi_ack ofs-delta symref=HEAD:/refs/heads/master\n"), + []byte("a6930aaee06755d1bdcfd943fbf614e4d92bb0c7 refs/heads/master\n"), + []byte("5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11-tree\n"), + []byte("1111111111111111111111111111111111111111 refs/tags/v2.6.12-tree\n"), + []byte("5555555555555555555555555555555555555555 refs/tags/v2.6.12-tree^{}\n"), + []byte("2222222222222222222222222222222222222222 refs/tags/v2.6.13-tree\n"), + []byte("3333333333333333333333333333333333333333 refs/tags/v2.7.13-tree\n"), + []byte("4444444444444444444444444444444444444444 refs/tags/v2.7.13-tree^{}\n"), + []byte("shallow 1111111111111111111111111111111111111111\n"), + []byte("shallow 2222222222222222222222222222222222222222\n"), + []byte("shallow 3333333333333333333333333333333333333333\n"), + []byte("shallow 4444444444444444444444444444444444444444\n"), + pktline.Flush, + ) + + testEncode(c, ar, expected) +} + +func (s *SuiteEncoder) TestErrorTooLong(c *C) { + references := map[string]plumbing.Hash{ + strings.Repeat("a", pktline.MaxPayloadSize): plumbing.NewHash("a6930aaee06755d1bdcfd943fbf614e4d92bb0c7"), + } + ar := &advrefs.AdvRefs{ + References: references, + } + + var buf bytes.Buffer + e := advrefs.NewEncoder(&buf) + err := e.Encode(ar) + c.Assert(err, ErrorMatches, ".*payload is too long.*") +} diff --git a/plumbing/format/packp/capabilities.go b/plumbing/format/packp/capabilities.go new file mode 100644 index 0000000..d77c2fa --- /dev/null +++ b/plumbing/format/packp/capabilities.go @@ -0,0 +1,136 @@ +package packp + +import ( + "fmt" + "sort" + "strings" +) + +// Capabilities contains all the server capabilities +// https://github.com/git/git/blob/master/Documentation/technical/protocol-capabilities.txt +type Capabilities struct { + m map[string]*Capability + o []string +} + +// Capability represents a server capability +type Capability struct { + Name string + Values []string +} + +// NewCapabilities returns a new Capabilities struct +func NewCapabilities() *Capabilities { + return &Capabilities{ + m: make(map[string]*Capability), + } +} + +func (c *Capabilities) IsEmpty() bool { + return len(c.o) == 0 +} + +// Decode decodes a string +func (c *Capabilities) Decode(raw string) { + params := strings.Split(raw, " ") + for _, p := range params { + s := strings.SplitN(p, "=", 2) + + var value string + if len(s) == 2 { + value = s[1] + } + + c.Add(s[0], value) + } +} + +// Get returns the values for a capability +func (c *Capabilities) Get(capability string) *Capability { + return c.m[capability] +} + +// Set sets a capability removing the values +func (c *Capabilities) Set(capability string, values ...string) { + if _, ok := c.m[capability]; ok { + delete(c.m, capability) + } + + c.Add(capability, values...) +} + +// Add adds a capability, values are optional +func (c *Capabilities) Add(capability string, values ...string) { + if !c.Supports(capability) { + c.m[capability] = &Capability{Name: capability} + c.o = append(c.o, capability) + } + + if len(values) == 0 { + return + } + + c.m[capability].Values = append(c.m[capability].Values, values...) +} + +// Supports returns true if capability is present +func (c *Capabilities) Supports(capability string) bool { + _, ok := c.m[capability] + return ok +} + +// SymbolicReference returns the reference for a given symbolic reference +func (c *Capabilities) SymbolicReference(sym string) string { + if !c.Supports("symref") { + return "" + } + + for _, symref := range c.Get("symref").Values { + parts := strings.Split(symref, ":") + if len(parts) != 2 { + continue + } + + if parts[0] == sym { + return parts[1] + } + } + + return "" +} + +// Sorts capabilities in increasing order of their name +func (c *Capabilities) Sort() { + sort.Strings(c.o) +} + +func (c *Capabilities) String() string { + if len(c.o) == 0 { + return "" + } + + var o string + for _, key := range c.o { + cap := c.m[key] + + added := false + for _, value := range cap.Values { + if value == "" { + continue + } + + added = true + o += fmt.Sprintf("%s=%s ", key, value) + } + + if len(cap.Values) == 0 || !added { + o += key + " " + } + } + + if len(o) == 0 { + return o + } + + return o[:len(o)-1] +} diff --git a/plumbing/format/packp/capabilities_test.go b/plumbing/format/packp/capabilities_test.go new file mode 100644 index 0000000..e42a0c7 --- /dev/null +++ b/plumbing/format/packp/capabilities_test.go @@ -0,0 +1,46 @@ +package packp + +import ( + "testing" + + . "gopkg.in/check.v1" +) + +func Test(t *testing.T) { TestingT(t) } + +type SuiteCapabilities struct{} + +var _ = Suite(&SuiteCapabilities{}) + +func (s *SuiteCapabilities) TestDecode(c *C) { + cap := NewCapabilities() + cap.Decode("symref=foo symref=qux thin-pack") + + c.Assert(cap.m, HasLen, 2) + c.Assert(cap.Get("symref").Values, DeepEquals, []string{"foo", "qux"}) + c.Assert(cap.Get("thin-pack").Values, DeepEquals, []string{""}) +} + +func (s *SuiteCapabilities) TestSet(c *C) { + cap := NewCapabilities() + cap.Add("symref", "foo", "qux") + cap.Set("symref", "bar") + + c.Assert(cap.m, HasLen, 1) + c.Assert(cap.Get("symref").Values, DeepEquals, []string{"bar"}) +} + +func (s *SuiteCapabilities) TestSetEmpty(c *C) { + cap := NewCapabilities() + cap.Set("foo", "bar") + + c.Assert(cap.Get("foo").Values, HasLen, 1) +} + +func (s *SuiteCapabilities) TestAdd(c *C) { + cap := NewCapabilities() + cap.Add("symref", "foo", "qux") + cap.Add("thin-pack") + + c.Assert(cap.String(), Equals, "symref=foo symref=qux thin-pack") +} diff --git a/plumbing/format/packp/doc.go b/plumbing/format/packp/doc.go new file mode 100644 index 0000000..4950d1d --- /dev/null +++ b/plumbing/format/packp/doc.go @@ -0,0 +1,724 @@ +package packp + +/* + +A nice way to trace the real data transmitted and received by git, use: + +GIT_TRACE_PACKET=true git ls-remote http://github.com/src-d/go-git +GIT_TRACE_PACKET=true git clone http://github.com/src-d/go-git + +Here follows a copy of the current protocol specification at the time of +this writing. + +(Please notice that most http git servers will add a flush-pkt after the +first pkt-line when using HTTP smart.) + + +Documentation Common to Pack and Http Protocols +=============================================== + +ABNF Notation +------------- + +ABNF notation as described by RFC 5234 is used within the protocol documents, +except the following replacement core rules are used: +---- + HEXDIG = DIGIT / "a" / "b" / "c" / "d" / "e" / "f" +---- + +We also define the following common rules: +---- + NUL = %x00 + zero-id = 40*"0" + obj-id = 40*(HEXDIGIT) + + refname = "HEAD" + refname /= "refs/" +---- + +A refname is a hierarchical octet string beginning with "refs/" and +not violating the 'git-check-ref-format' command's validation rules. +More specifically, they: + +. They can include slash `/` for hierarchical (directory) + grouping, but no slash-separated component can begin with a + dot `.`. + +. They must contain at least one `/`. This enforces the presence of a + category like `heads/`, `tags/` etc. but the actual names are not + restricted. + +. They cannot have two consecutive dots `..` anywhere. + +. They cannot have ASCII control characters (i.e. bytes whose + values are lower than \040, or \177 `DEL`), space, tilde `~`, + caret `^`, colon `:`, question-mark `?`, asterisk `*`, + or open bracket `[` anywhere. + +. They cannot end with a slash `/` or a dot `.`. + +. They cannot end with the sequence `.lock`. + +. They cannot contain a sequence `@{`. + +. They cannot contain a `\\`. + + +pkt-line Format +--------------- + +Much (but not all) of the payload is described around pkt-lines. + +A pkt-line is a variable length binary string. The first four bytes +of the line, the pkt-len, indicates the total length of the line, +in hexadecimal. The pkt-len includes the 4 bytes used to contain +the length's hexadecimal representation. + +A pkt-line MAY contain binary data, so implementors MUST ensure +pkt-line parsing/formatting routines are 8-bit clean. + +A non-binary line SHOULD BE terminated by an LF, which if present +MUST be included in the total length. Receivers MUST treat pkt-lines +with non-binary data the same whether or not they contain the trailing +LF (stripping the LF if present, and not complaining when it is +missing). + +The maximum length of a pkt-line's data component is 65516 bytes. +Implementations MUST NOT send pkt-line whose length exceeds 65520 +(65516 bytes of payload + 4 bytes of length data). + +Implementations SHOULD NOT send an empty pkt-line ("0004"). + +A pkt-line with a length field of 0 ("0000"), called a flush-pkt, +is a special case and MUST be handled differently than an empty +pkt-line ("0004"). + +---- + pkt-line = data-pkt / flush-pkt + + data-pkt = pkt-len pkt-payload + pkt-len = 4*(HEXDIG) + pkt-payload = (pkt-len - 4)*(OCTET) + + flush-pkt = "0000" +---- + +Examples (as C-style strings): + +---- + pkt-line actual value + --------------------------------- + "0006a\n" "a\n" + "0005a" "a" + "000bfoobar\n" "foobar\n" + "0004" "" +---- + +Packfile transfer protocols +=========================== + +Git supports transferring data in packfiles over the ssh://, git://, http:// and +file:// transports. There exist two sets of protocols, one for pushing +data from a client to a server and another for fetching data from a +server to a client. The three transports (ssh, git, file) use the same +protocol to transfer data. http is documented in http-protocol.txt. + +The processes invoked in the canonical Git implementation are 'upload-pack' +on the server side and 'fetch-pack' on the client side for fetching data; +then 'receive-pack' on the server and 'send-pack' on the client for pushing +data. The protocol functions to have a server tell a client what is +currently on the server, then for the two to negotiate the smallest amount +of data to send in order to fully update one or the other. + +pkt-line Format +--------------- + +The descriptions below build on the pkt-line format described in +protocol-common.txt. When the grammar indicate `PKT-LINE(...)`, unless +otherwise noted the usual pkt-line LF rules apply: the sender SHOULD +include a LF, but the receiver MUST NOT complain if it is not present. + +Transports +---------- +There are three transports over which the packfile protocol is +initiated. The Git transport is a simple, unauthenticated server that +takes the command (almost always 'upload-pack', though Git +servers can be configured to be globally writable, in which 'receive- +pack' initiation is also allowed) with which the client wishes to +communicate and executes it and connects it to the requesting +process. + +In the SSH transport, the client just runs the 'upload-pack' +or 'receive-pack' process on the server over the SSH protocol and then +communicates with that invoked process over the SSH connection. + +The file:// transport runs the 'upload-pack' or 'receive-pack' +process locally and communicates with it over a pipe. + +Git Transport +------------- + +The Git transport starts off by sending the command and repository +on the wire using the pkt-line format, followed by a NUL byte and a +hostname parameter, terminated by a NUL byte. + + 0032git-upload-pack /project.git\0host=myserver.com\0 + +-- + git-proto-request = request-command SP pathname NUL [ host-parameter NUL ] + request-command = "git-upload-pack" / "git-receive-pack" / + "git-upload-archive" ; case sensitive + pathname = *( %x01-ff ) ; exclude NUL + host-parameter = "host=" hostname [ ":" port ] +-- + +Only host-parameter is allowed in the git-proto-request. Clients +MUST NOT attempt to send additional parameters. It is used for the +git-daemon name based virtual hosting. See --interpolated-path +option to git daemon, with the %H/%CH format characters. + +Basically what the Git client is doing to connect to an 'upload-pack' +process on the server side over the Git protocol is this: + + $ echo -e -n \ + "0039git-upload-pack /schacon/gitbook.git\0host=example.com\0" | + nc -v example.com 9418 + +If the server refuses the request for some reasons, it could abort +gracefully with an error message. + +---- + error-line = PKT-LINE("ERR" SP explanation-text) +---- + + +SSH Transport +------------- + +Initiating the upload-pack or receive-pack processes over SSH is +executing the binary on the server via SSH remote execution. +It is basically equivalent to running this: + + $ ssh git.example.com "git-upload-pack '/project.git'" + +For a server to support Git pushing and pulling for a given user over +SSH, that user needs to be able to execute one or both of those +commands via the SSH shell that they are provided on login. On some +systems, that shell access is limited to only being able to run those +two commands, or even just one of them. + +In an ssh:// format URI, it's absolute in the URI, so the '/' after +the host name (or port number) is sent as an argument, which is then +read by the remote git-upload-pack exactly as is, so it's effectively +an absolute path in the remote filesystem. + + git clone ssh://user@example.com/project.git + | + v + ssh user@example.com "git-upload-pack '/project.git'" + +In a "user@host:path" format URI, its relative to the user's home +directory, because the Git client will run: + + git clone user@example.com:project.git + | + v + ssh user@example.com "git-upload-pack 'project.git'" + +The exception is if a '~' is used, in which case +we execute it without the leading '/'. + + ssh://user@example.com/~alice/project.git, + | + v + ssh user@example.com "git-upload-pack '~alice/project.git'" + +A few things to remember here: + +- The "command name" is spelled with dash (e.g. git-upload-pack), but + this can be overridden by the client; + +- The repository path is always quoted with single quotes. + +Fetching Data From a Server +--------------------------- + +When one Git repository wants to get data that a second repository +has, the first can 'fetch' from the second. This operation determines +what data the server has that the client does not then streams that +data down to the client in packfile format. + + +Reference Discovery +------------------- + +When the client initially connects the server will immediately respond +with a listing of each reference it has (all branches and tags) along +with the object name that each reference currently points to. + + $ echo -e -n "0039git-upload-pack /schacon/gitbook.git\0host=example.com\0" | + nc -v example.com 9418 + 00887217a7c7e582c46cec22a130adf4b9d7d950fba0 HEAD\0multi_ack thin-pack + side-band side-band-64k ofs-delta shallow no-progress include-tag + 00441d3fcd5ced445d1abc402225c0b8a1299641f497 refs/heads/integration + 003f7217a7c7e582c46cec22a130adf4b9d7d950fba0 refs/heads/master + 003cb88d2441cac0977faf98efc80305012112238d9d refs/tags/v0.9 + 003c525128480b96c89e6418b1e40909bf6c5b2d580f refs/tags/v1.0 + 003fe92df48743b7bc7d26bcaabfddde0a1e20cae47c refs/tags/v1.0^{} + 0000 + +The returned response is a pkt-line stream describing each ref and +its current value. The stream MUST be sorted by name according to +the C locale ordering. + +If HEAD is a valid ref, HEAD MUST appear as the first advertised +ref. If HEAD is not a valid ref, HEAD MUST NOT appear in the +advertisement list at all, but other refs may still appear. + +The stream MUST include capability declarations behind a NUL on the +first ref. The peeled value of a ref (that is "ref^{}") MUST be +immediately after the ref itself, if presented. A conforming server +MUST peel the ref if it's an annotated tag. + +---- + advertised-refs = (no-refs / list-of-refs) + *shallow + flush-pkt + + no-refs = PKT-LINE(zero-id SP "capabilities^{}" + NUL capability-list) + + list-of-refs = first-ref *other-ref + first-ref = PKT-LINE(obj-id SP refname + NUL capability-list) + + other-ref = PKT-LINE(other-tip / other-peeled) + other-tip = obj-id SP refname + other-peeled = obj-id SP refname "^{}" + + shallow = PKT-LINE("shallow" SP obj-id) + + capability-list = capability *(SP capability) + capability = 1*(LC_ALPHA / DIGIT / "-" / "_") + LC_ALPHA = %x61-7A +---- + +Server and client MUST use lowercase for obj-id, both MUST treat obj-id +as case-insensitive. + +See protocol-capabilities.txt for a list of allowed server capabilities +and descriptions. + +Packfile Negotiation +-------------------- +After reference and capabilities discovery, the client can decide to +terminate the connection by sending a flush-pkt, telling the server it can +now gracefully terminate, and disconnect, when it does not need any pack +data. This can happen with the ls-remote command, and also can happen when +the client already is up-to-date. + +Otherwise, it enters the negotiation phase, where the client and +server determine what the minimal packfile necessary for transport is, +by telling the server what objects it wants, its shallow objects +(if any), and the maximum commit depth it wants (if any). The client +will also send a list of the capabilities it wants to be in effect, +out of what the server said it could do with the first 'want' line. + +---- + upload-request = want-list + *shallow-line + *1depth-request + flush-pkt + + want-list = first-want + *additional-want + + shallow-line = PKT-LINE("shallow" SP obj-id) + + depth-request = PKT-LINE("deepen" SP depth) / + PKT-LINE("deepen-since" SP timestamp) / + PKT-LINE("deepen-not" SP ref) + + first-want = PKT-LINE("want" SP obj-id SP capability-list) + additional-want = PKT-LINE("want" SP obj-id) + + depth = 1*DIGIT +---- + +Clients MUST send all the obj-ids it wants from the reference +discovery phase as 'want' lines. Clients MUST send at least one +'want' command in the request body. Clients MUST NOT mention an +obj-id in a 'want' command which did not appear in the response +obtained through ref discovery. + +The client MUST write all obj-ids which it only has shallow copies +of (meaning that it does not have the parents of a commit) as +'shallow' lines so that the server is aware of the limitations of +the client's history. + +The client now sends the maximum commit history depth it wants for +this transaction, which is the number of commits it wants from the +tip of the history, if any, as a 'deepen' line. A depth of 0 is the +same as not making a depth request. The client does not want to receive +any commits beyond this depth, nor does it want objects needed only to +complete those commits. Commits whose parents are not received as a +result are defined as shallow and marked as such in the server. This +information is sent back to the client in the next step. + +Once all the 'want's and 'shallow's (and optional 'deepen') are +transferred, clients MUST send a flush-pkt, to tell the server side +that it is done sending the list. + +Otherwise, if the client sent a positive depth request, the server +will determine which commits will and will not be shallow and +send this information to the client. If the client did not request +a positive depth, this step is skipped. + +---- + shallow-update = *shallow-line + *unshallow-line + flush-pkt + + shallow-line = PKT-LINE("shallow" SP obj-id) + + unshallow-line = PKT-LINE("unshallow" SP obj-id) +---- + +If the client has requested a positive depth, the server will compute +the set of commits which are no deeper than the desired depth. The set +of commits start at the client's wants. + +The server writes 'shallow' lines for each +commit whose parents will not be sent as a result. The server writes +an 'unshallow' line for each commit which the client has indicated is +shallow, but is no longer shallow at the currently requested depth +(that is, its parents will now be sent). The server MUST NOT mark +as unshallow anything which the client has not indicated was shallow. + +Now the client will send a list of the obj-ids it has using 'have' +lines, so the server can make a packfile that only contains the objects +that the client needs. In multi_ack mode, the canonical implementation +will send up to 32 of these at a time, then will send a flush-pkt. The +canonical implementation will skip ahead and send the next 32 immediately, +so that there is always a block of 32 "in-flight on the wire" at a time. + +---- + upload-haves = have-list + compute-end + + have-list = *have-line + have-line = PKT-LINE("have" SP obj-id) + compute-end = flush-pkt / PKT-LINE("done") +---- + +If the server reads 'have' lines, it then will respond by ACKing any +of the obj-ids the client said it had that the server also has. The +server will ACK obj-ids differently depending on which ack mode is +chosen by the client. + +In multi_ack mode: + + * the server will respond with 'ACK obj-id continue' for any common + commits. + + * once the server has found an acceptable common base commit and is + ready to make a packfile, it will blindly ACK all 'have' obj-ids + back to the client. + + * the server will then send a 'NAK' and then wait for another response + from the client - either a 'done' or another list of 'have' lines. + +In multi_ack_detailed mode: + + * the server will differentiate the ACKs where it is signaling + that it is ready to send data with 'ACK obj-id ready' lines, and + signals the identified common commits with 'ACK obj-id common' lines. + +Without either multi_ack or multi_ack_detailed: + + * upload-pack sends "ACK obj-id" on the first common object it finds. + After that it says nothing until the client gives it a "done". + + * upload-pack sends "NAK" on a flush-pkt if no common object + has been found yet. If one has been found, and thus an ACK + was already sent, it's silent on the flush-pkt. + +After the client has gotten enough ACK responses that it can determine +that the server has enough information to send an efficient packfile +(in the canonical implementation, this is determined when it has received +enough ACKs that it can color everything left in the --date-order queue +as common with the server, or the --date-order queue is empty), or the +client determines that it wants to give up (in the canonical implementation, +this is determined when the client sends 256 'have' lines without getting +any of them ACKed by the server - meaning there is nothing in common and +the server should just send all of its objects), then the client will send +a 'done' command. The 'done' command signals to the server that the client +is ready to receive its packfile data. + +However, the 256 limit *only* turns on in the canonical client +implementation if we have received at least one "ACK %s continue" +during a prior round. This helps to ensure that at least one common +ancestor is found before we give up entirely. + +Once the 'done' line is read from the client, the server will either +send a final 'ACK obj-id' or it will send a 'NAK'. 'obj-id' is the object +name of the last commit determined to be common. The server only sends +ACK after 'done' if there is at least one common base and multi_ack or +multi_ack_detailed is enabled. The server always sends NAK after 'done' +if there is no common base found. + +Then the server will start sending its packfile data. + +---- + server-response = *ack_multi ack / nak + ack_multi = PKT-LINE("ACK" SP obj-id ack_status) + ack_status = "continue" / "common" / "ready" + ack = PKT-LINE("ACK" SP obj-id) + nak = PKT-LINE("NAK") +---- + +A simple clone may look like this (with no 'have' lines): + +---- + C: 0054want 74730d410fcb6603ace96f1dc55ea6196122532d multi_ack \ + side-band-64k ofs-delta\n + C: 0032want 7d1665144a3a975c05f1f43902ddaf084e784dbe\n + C: 0032want 5a3f6be755bbb7deae50065988cbfa1ffa9ab68a\n + C: 0032want 7e47fe2bd8d01d481f44d7af0531bd93d3b21c01\n + C: 0032want 74730d410fcb6603ace96f1dc55ea6196122532d\n + C: 0000 + C: 0009done\n + + S: 0008NAK\n + S: [PACKFILE] +---- + +An incremental update (fetch) response might look like this: + +---- + C: 0054want 74730d410fcb6603ace96f1dc55ea6196122532d multi_ack \ + side-band-64k ofs-delta\n + C: 0032want 7d1665144a3a975c05f1f43902ddaf084e784dbe\n + C: 0032want 5a3f6be755bbb7deae50065988cbfa1ffa9ab68a\n + C: 0000 + C: 0032have 7e47fe2bd8d01d481f44d7af0531bd93d3b21c01\n + C: [30 more have lines] + C: 0032have 74730d410fcb6603ace96f1dc55ea6196122532d\n + C: 0000 + + S: 003aACK 7e47fe2bd8d01d481f44d7af0531bd93d3b21c01 continue\n + S: 003aACK 74730d410fcb6603ace96f1dc55ea6196122532d continue\n + S: 0008NAK\n + + C: 0009done\n + + S: 0031ACK 74730d410fcb6603ace96f1dc55ea6196122532d\n + S: [PACKFILE] +---- + + +Packfile Data +------------- + +Now that the client and server have finished negotiation about what +the minimal amount of data that needs to be sent to the client is, the server +will construct and send the required data in packfile format. + +See pack-format.txt for what the packfile itself actually looks like. + +If 'side-band' or 'side-band-64k' capabilities have been specified by +the client, the server will send the packfile data multiplexed. + +Each packet starting with the packet-line length of the amount of data +that follows, followed by a single byte specifying the sideband the +following data is coming in on. + +In 'side-band' mode, it will send up to 999 data bytes plus 1 control +code, for a total of up to 1000 bytes in a pkt-line. In 'side-band-64k' +mode it will send up to 65519 data bytes plus 1 control code, for a +total of up to 65520 bytes in a pkt-line. + +The sideband byte will be a '1', '2' or a '3'. Sideband '1' will contain +packfile data, sideband '2' will be used for progress information that the +client will generally print to stderr and sideband '3' is used for error +information. + +If no 'side-band' capability was specified, the server will stream the +entire packfile without multiplexing. + + +Pushing Data To a Server +------------------------ + +Pushing data to a server will invoke the 'receive-pack' process on the +server, which will allow the client to tell it which references it should +update and then send all the data the server will need for those new +references to be complete. Once all the data is received and validated, +the server will then update its references to what the client specified. + +Authentication +-------------- + +The protocol itself contains no authentication mechanisms. That is to be +handled by the transport, such as SSH, before the 'receive-pack' process is +invoked. If 'receive-pack' is configured over the Git transport, those +repositories will be writable by anyone who can access that port (9418) as +that transport is unauthenticated. + +Reference Discovery +------------------- + +The reference discovery phase is done nearly the same way as it is in the +fetching protocol. Each reference obj-id and name on the server is sent +in packet-line format to the client, followed by a flush-pkt. The only +real difference is that the capability listing is different - the only +possible values are 'report-status', 'delete-refs', 'ofs-delta' and +'push-options'. + +Reference Update Request and Packfile Transfer +---------------------------------------------- + +Once the client knows what references the server is at, it can send a +list of reference update requests. For each reference on the server +that it wants to update, it sends a line listing the obj-id currently on +the server, the obj-id the client would like to update it to and the name +of the reference. + +This list is followed by a flush-pkt. Then the push options are transmitted +one per packet followed by another flush-pkt. After that the packfile that +should contain all the objects that the server will need to complete the new +references will be sent. + +---- + update-request = *shallow ( command-list | push-cert ) [packfile] + + shallow = PKT-LINE("shallow" SP obj-id) + + command-list = PKT-LINE(command NUL capability-list) + *PKT-LINE(command) + flush-pkt + + command = create / delete / update + create = zero-id SP new-id SP name + delete = old-id SP zero-id SP name + update = old-id SP new-id SP name + + old-id = obj-id + new-id = obj-id + + push-cert = PKT-LINE("push-cert" NUL capability-list LF) + PKT-LINE("certificate version 0.1" LF) + PKT-LINE("pusher" SP ident LF) + PKT-LINE("pushee" SP url LF) + PKT-LINE("nonce" SP nonce LF) + PKT-LINE(LF) + *PKT-LINE(command LF) + *PKT-LINE(gpg-signature-lines LF) + PKT-LINE("push-cert-end" LF) + + packfile = "PACK" 28*(OCTET) +---- + +If the receiving end does not support delete-refs, the sending end MUST +NOT ask for delete command. + +If the receiving end does not support push-cert, the sending end +MUST NOT send a push-cert command. When a push-cert command is +sent, command-list MUST NOT be sent; the commands recorded in the +push certificate is used instead. + +The packfile MUST NOT be sent if the only command used is 'delete'. + +A packfile MUST be sent if either create or update command is used, +even if the server already has all the necessary objects. In this +case the client MUST send an empty packfile. The only time this +is likely to happen is if the client is creating +a new branch or a tag that points to an existing obj-id. + +The server will receive the packfile, unpack it, then validate each +reference that is being updated that it hasn't changed while the request +was being processed (the obj-id is still the same as the old-id), and +it will run any update hooks to make sure that the update is acceptable. +If all of that is fine, the server will then update the references. + +Push Certificate +---------------- + +A push certificate begins with a set of header lines. After the +header and an empty line, the protocol commands follow, one per +line. Note that the trailing LF in push-cert PKT-LINEs is _not_ +optional; it must be present. + +Currently, the following header fields are defined: + +`pusher` ident:: + Identify the GPG key in "Human Readable Name " + format. + +`pushee` url:: + The repository URL (anonymized, if the URL contains + authentication material) the user who ran `git push` + intended to push into. + +`nonce` nonce:: + The 'nonce' string the receiving repository asked the + pushing user to include in the certificate, to prevent + replay attacks. + +The GPG signature lines are a detached signature for the contents +recorded in the push certificate before the signature block begins. +The detached signature is used to certify that the commands were +given by the pusher, who must be the signer. + +Report Status +------------- + +After receiving the pack data from the sender, the receiver sends a +report if 'report-status' capability is in effect. +It is a short listing of what happened in that update. It will first +list the status of the packfile unpacking as either 'unpack ok' or +'unpack [error]'. Then it will list the status for each of the references +that it tried to update. Each line is either 'ok [refname]' if the +update was successful, or 'ng [refname] [error]' if the update was not. + +---- + report-status = unpack-status + 1*(command-status) + flush-pkt + + unpack-status = PKT-LINE("unpack" SP unpack-result) + unpack-result = "ok" / error-msg + + command-status = command-ok / command-fail + command-ok = PKT-LINE("ok" SP refname) + command-fail = PKT-LINE("ng" SP refname SP error-msg) + + error-msg = 1*(OCTECT) ; where not "ok" +---- + +Updates can be unsuccessful for a number of reasons. The reference can have +changed since the reference discovery phase was originally sent, meaning +someone pushed in the meantime. The reference being pushed could be a +non-fast-forward reference and the update hooks or configuration could be +set to not allow that, etc. Also, some references can be updated while others +can be rejected. + +An example client/server communication might look like this: + +---- + S: 007c74730d410fcb6603ace96f1dc55ea6196122532d refs/heads/local\0report-status delete-refs ofs-delta\n + S: 003e7d1665144a3a975c05f1f43902ddaf084e784dbe refs/heads/debug\n + S: 003f74730d410fcb6603ace96f1dc55ea6196122532d refs/heads/master\n + S: 003f74730d410fcb6603ace96f1dc55ea6196122532d refs/heads/team\n + S: 0000 + + C: 003e7d1665144a3a975c05f1f43902ddaf084e784dbe 74730d410fcb6603ace96f1dc55ea6196122532d refs/heads/debug\n + C: 003e74730d410fcb6603ace96f1dc55ea6196122532d 5a3f6be755bbb7deae50065988cbfa1ffa9ab68a refs/heads/master\n + C: 0000 + C: [PACKDATA] + + S: 000eunpack ok\n + S: 0018ok refs/heads/debug\n + S: 002ang refs/heads/master non-fast-forward\n +---- +*/ diff --git a/plumbing/format/packp/pktline/encoder.go b/plumbing/format/packp/pktline/encoder.go new file mode 100644 index 0000000..0a88a9b --- /dev/null +++ b/plumbing/format/packp/pktline/encoder.go @@ -0,0 +1,123 @@ +// Package pktline implements reading payloads form pkt-lines and encoding pkt-lines from payloads. +package pktline + +import ( + "bytes" + "errors" + "fmt" + "io" +) + +// An Encoder writes pkt-lines to an output stream. +type Encoder struct { + w io.Writer +} + +const ( + // MaxPayloadSize is the maximum payload size of a pkt-line in bytes. + MaxPayloadSize = 65516 +) + +var ( + // FlushPkt are the contents of a flush-pkt pkt-line. + FlushPkt = []byte{'0', '0', '0', '0'} + // Flush is the payload to use with the Encode method to encode a flush-pkt. + Flush = []byte{} + // FlushString is the payload to use with the EncodeString method to encode a flush-pkt. + FlushString = "" + // ErrPayloadTooLong is returned by the Encode methods when any of the + // provided payloads is bigger than MaxPayloadSize. + ErrPayloadTooLong = errors.New("payload is too long") +) + +// NewEncoder returns a new encoder that writes to w. +func NewEncoder(w io.Writer) *Encoder { + return &Encoder{ + w: w, + } +} + +// Flush encodes a flush-pkt to the output stream. +func (e *Encoder) Flush() error { + _, err := e.w.Write(FlushPkt) + return err +} + +// Encode encodes a pkt-line with the payload specified and write it to +// the output stream. If several payloads are specified, each of them +// will get streamed in their own pkt-lines. +func (e *Encoder) Encode(payloads ...[]byte) error { + for _, p := range payloads { + if err := e.encodeLine(p); err != nil { + return err + } + } + + return nil +} + +func (e *Encoder) encodeLine(p []byte) error { + if len(p) > MaxPayloadSize { + return ErrPayloadTooLong + } + + if bytes.Equal(p, Flush) { + if err := e.Flush(); err != nil { + return err + } + return nil + } + + n := len(p) + 4 + if _, err := e.w.Write(asciiHex16(n)); err != nil { + return err + } + if _, err := e.w.Write(p); err != nil { + return err + } + + return nil +} + +// Returns the hexadecimal ascii representation of the 16 less +// significant bits of n. The length of the returned slice will always +// be 4. Example: if n is 1234 (0x4d2), the return value will be +// []byte{'0', '4', 'd', '2'}. +func asciiHex16(n int) []byte { + var ret [4]byte + ret[0] = byteToASCIIHex(byte(n & 0xf000 >> 12)) + ret[1] = byteToASCIIHex(byte(n & 0x0f00 >> 8)) + ret[2] = byteToASCIIHex(byte(n & 0x00f0 >> 4)) + ret[3] = byteToASCIIHex(byte(n & 0x000f)) + + return ret[:] +} + +// turns a byte into its hexadecimal ascii representation. Example: +// from 11 (0xb) to 'b'. +func byteToASCIIHex(n byte) byte { + if n < 10 { + return '0' + n + } + + return 'a' - 10 + n +} + +// EncodeString works similarly as Encode but payloads are specified as strings. +func (e *Encoder) EncodeString(payloads ...string) error { + for _, p := range payloads { + if err := e.Encode([]byte(p)); err != nil { + return err + } + } + + return nil +} + +// Encodef encodes a single pkt-line with the payload formatted as +// the format specifier and the rest of the arguments suggest. +func (e *Encoder) Encodef(format string, a ...interface{}) error { + return e.EncodeString( + fmt.Sprintf(format, a...), + ) +} diff --git a/plumbing/format/packp/pktline/encoder_test.go b/plumbing/format/packp/pktline/encoder_test.go new file mode 100644 index 0000000..cd97593 --- /dev/null +++ b/plumbing/format/packp/pktline/encoder_test.go @@ -0,0 +1,249 @@ +package pktline_test + +import ( + "bytes" + "os" + "strings" + "testing" + + "gopkg.in/src-d/go-git.v4/plumbing/format/packp/pktline" + + . "gopkg.in/check.v1" +) + +func Test(t *testing.T) { TestingT(t) } + +type SuiteEncoder struct{} + +var _ = Suite(&SuiteEncoder{}) + +func (s *SuiteEncoder) TestFlush(c *C) { + var buf bytes.Buffer + e := pktline.NewEncoder(&buf) + + err := e.Flush() + c.Assert(err, IsNil) + + obtained := buf.Bytes() + c.Assert(obtained, DeepEquals, pktline.FlushPkt) +} + +func (s *SuiteEncoder) TestEncode(c *C) { + for i, test := range [...]struct { + input [][]byte + expected []byte + }{ + { + input: [][]byte{ + []byte("hello\n"), + }, + expected: []byte("000ahello\n"), + }, { + input: [][]byte{ + []byte("hello\n"), + pktline.Flush, + }, + expected: []byte("000ahello\n0000"), + }, { + input: [][]byte{ + []byte("hello\n"), + []byte("world!\n"), + []byte("foo"), + }, + expected: []byte("000ahello\n000bworld!\n0007foo"), + }, { + input: [][]byte{ + []byte("hello\n"), + pktline.Flush, + []byte("world!\n"), + []byte("foo"), + pktline.Flush, + }, + expected: []byte("000ahello\n0000000bworld!\n0007foo0000"), + }, { + input: [][]byte{ + []byte(strings.Repeat("a", pktline.MaxPayloadSize)), + }, + expected: []byte( + "fff0" + strings.Repeat("a", pktline.MaxPayloadSize)), + }, { + input: [][]byte{ + []byte(strings.Repeat("a", pktline.MaxPayloadSize)), + []byte(strings.Repeat("b", pktline.MaxPayloadSize)), + }, + expected: []byte( + "fff0" + strings.Repeat("a", pktline.MaxPayloadSize) + + "fff0" + strings.Repeat("b", pktline.MaxPayloadSize)), + }, + } { + comment := Commentf("input %d = %v\n", i, test.input) + + var buf bytes.Buffer + e := pktline.NewEncoder(&buf) + + err := e.Encode(test.input...) + c.Assert(err, IsNil, comment) + + c.Assert(buf.Bytes(), DeepEquals, test.expected, comment) + } +} + +func (s *SuiteEncoder) TestEncodeErrPayloadTooLong(c *C) { + for i, input := range [...][][]byte{ + { + []byte(strings.Repeat("a", pktline.MaxPayloadSize+1)), + }, + { + []byte("hello world!"), + []byte(strings.Repeat("a", pktline.MaxPayloadSize+1)), + }, + { + []byte("hello world!"), + []byte(strings.Repeat("a", pktline.MaxPayloadSize+1)), + []byte("foo"), + }, + } { + comment := Commentf("input %d = %v\n", i, input) + + var buf bytes.Buffer + e := pktline.NewEncoder(&buf) + + err := e.Encode(input...) + c.Assert(err, Equals, pktline.ErrPayloadTooLong, comment) + } +} + +func (s *SuiteEncoder) TestEncodeStrings(c *C) { + for i, test := range [...]struct { + input []string + expected []byte + }{ + { + input: []string{ + "hello\n", + }, + expected: []byte("000ahello\n"), + }, { + input: []string{ + "hello\n", + pktline.FlushString, + }, + expected: []byte("000ahello\n0000"), + }, { + input: []string{ + "hello\n", + "world!\n", + "foo", + }, + expected: []byte("000ahello\n000bworld!\n0007foo"), + }, { + input: []string{ + "hello\n", + pktline.FlushString, + "world!\n", + "foo", + pktline.FlushString, + }, + expected: []byte("000ahello\n0000000bworld!\n0007foo0000"), + }, { + input: []string{ + strings.Repeat("a", pktline.MaxPayloadSize), + }, + expected: []byte( + "fff0" + strings.Repeat("a", pktline.MaxPayloadSize)), + }, { + input: []string{ + strings.Repeat("a", pktline.MaxPayloadSize), + strings.Repeat("b", pktline.MaxPayloadSize), + }, + expected: []byte( + "fff0" + strings.Repeat("a", pktline.MaxPayloadSize) + + "fff0" + strings.Repeat("b", pktline.MaxPayloadSize)), + }, + } { + comment := Commentf("input %d = %v\n", i, test.input) + + var buf bytes.Buffer + e := pktline.NewEncoder(&buf) + + err := e.EncodeString(test.input...) + c.Assert(err, IsNil, comment) + c.Assert(buf.Bytes(), DeepEquals, test.expected, comment) + } +} + +func (s *SuiteEncoder) TestEncodeStringErrPayloadTooLong(c *C) { + for i, input := range [...][]string{ + { + strings.Repeat("a", pktline.MaxPayloadSize+1), + }, + { + "hello world!", + strings.Repeat("a", pktline.MaxPayloadSize+1), + }, + { + "hello world!", + strings.Repeat("a", pktline.MaxPayloadSize+1), + "foo", + }, + } { + comment := Commentf("input %d = %v\n", i, input) + + var buf bytes.Buffer + e := pktline.NewEncoder(&buf) + + err := e.EncodeString(input...) + c.Assert(err, Equals, pktline.ErrPayloadTooLong, comment) + } +} + +func (s *SuiteEncoder) TestEncodef(c *C) { + format := " %s %d\n" + str := "foo" + d := 42 + + var buf bytes.Buffer + e := pktline.NewEncoder(&buf) + + err := e.Encodef(format, str, d) + c.Assert(err, IsNil) + + expected := []byte("000c foo 42\n") + c.Assert(buf.Bytes(), DeepEquals, expected) +} + +func ExampleEncoder() { + // Create an encoder that writes pktlines to stdout. + e := pktline.NewEncoder(os.Stdout) + + // Encode some data as a new pkt-line. + _ = e.Encode([]byte("data\n")) // error checks removed for brevity + + // Encode a flush-pkt. + _ = e.Flush() + + // Encode a couple of byte slices and a flush in one go. Each of + // them will end up as payloads of their own pktlines. + _ = e.Encode( + []byte("hello\n"), + []byte("world!\n"), + pktline.Flush, + ) + + // You can also encode strings: + _ = e.EncodeString( + "foo\n", + "bar\n", + pktline.FlushString, + ) + + // You can also format and encode a payload: + _ = e.Encodef(" %s %d\n", "foo", 42) + // Output: + // 0009data + // 0000000ahello + // 000bworld! + // 00000008foo + // 0008bar + // 0000000c foo 42 +} diff --git a/plumbing/format/packp/pktline/scanner.go b/plumbing/format/packp/pktline/scanner.go new file mode 100644 index 0000000..3ce2adf --- /dev/null +++ b/plumbing/format/packp/pktline/scanner.go @@ -0,0 +1,133 @@ +package pktline + +import ( + "errors" + "io" +) + +const ( + lenSize = 4 +) + +// ErrInvalidPktLen is returned by Err() when an invalid pkt-len is found. +var ErrInvalidPktLen = errors.New("invalid pkt-len found") + +// Scanner provides a convenient interface for reading the payloads of a +// series of pkt-lines. It takes an io.Reader providing the source, +// which then can be tokenized through repeated calls to the Scan +// method. +// +// After each Scan call, the Bytes method will return the payload of the +// corresponding pkt-line on a shared buffer, which will be 65516 bytes +// or smaller. Flush pkt-lines are represented by empty byte slices. +// +// Scanning stops at EOF or the first I/O error. +type Scanner struct { + r io.Reader // The reader provided by the client + err error // Sticky error + payload []byte // Last pkt-payload + len [lenSize]byte // Last pkt-len +} + +// NewScanner returns a new Scanner to read from r. +func NewScanner(r io.Reader) *Scanner { + return &Scanner{ + r: r, + } +} + +// Err returns the first error encountered by the Scanner. +func (s *Scanner) Err() error { + return s.err +} + +// Scan advances the Scanner to the next pkt-line, whose payload will +// then be available through the Bytes method. Scanning stops at EOF +// or the first I/O error. After Scan returns false, the Err method +// will return any error that occurred during scanning, except that if +// it was io.EOF, Err will return nil. +func (s *Scanner) Scan() bool { + var l int + l, s.err = s.readPayloadLen() + if s.err == io.EOF { + s.err = nil + return false + } + if s.err != nil { + return false + } + + if cap(s.payload) < l { + s.payload = make([]byte, 0, l) + } + + if _, s.err = io.ReadFull(s.r, s.payload[:l]); s.err != nil { + return false + } + s.payload = s.payload[:l] + + return true +} + +// Bytes returns the most recent payload generated by a call to Scan. +// The underlying array may point to data that will be overwritten by a +// subsequent call to Scan. It does no allocation. +func (s *Scanner) Bytes() []byte { + return s.payload +} + +// Method readPayloadLen returns the payload length by reading the +// pkt-len and substracting the pkt-len size. +func (s *Scanner) readPayloadLen() (int, error) { + if _, err := io.ReadFull(s.r, s.len[:]); err != nil { + if err == io.EOF { + return 0, err + } + return 0, ErrInvalidPktLen + } + + n, err := hexDecode(s.len) + if err != nil { + return 0, err + } + + switch { + case n == 0: + return 0, nil + case n <= lenSize: + return 0, ErrInvalidPktLen + case n > MaxPayloadSize+lenSize: + return 0, ErrInvalidPktLen + default: + return n - lenSize, nil + } +} + +// Turns the hexadecimal representation of a number in a byte slice into +// a number. This function substitute strconv.ParseUint(string(buf), 16, +// 16) and/or hex.Decode, to avoid generating new strings, thus helping the +// GC. +func hexDecode(buf [lenSize]byte) (int, error) { + var ret int + for i := 0; i < lenSize; i++ { + n, err := asciiHexToByte(buf[i]) + if err != nil { + return 0, ErrInvalidPktLen + } + ret = 16*ret + int(n) + } + return ret, nil +} + +// turns the hexadecimal ascii representation of a byte into its +// numerical value. Example: from 'b' to 11 (0xb). +func asciiHexToByte(b byte) (byte, error) { + switch { + case b >= '0' && b <= '9': + return b - '0', nil + case b >= 'a' && b <= 'f': + return b - 'a' + 10, nil + default: + return 0, ErrInvalidPktLen + } +} diff --git a/plumbing/format/packp/pktline/scanner_test.go b/plumbing/format/packp/pktline/scanner_test.go new file mode 100644 index 0000000..c5395cf --- /dev/null +++ b/plumbing/format/packp/pktline/scanner_test.go @@ -0,0 +1,225 @@ +package pktline_test + +import ( + "bytes" + "fmt" + "io" + "strings" + + "gopkg.in/src-d/go-git.v4/plumbing/format/packp/pktline" + + . "gopkg.in/check.v1" +) + +type SuiteScanner struct{} + +var _ = Suite(&SuiteScanner{}) + +func (s *SuiteScanner) TestInvalid(c *C) { + for _, test := range [...]string{ + "0001", "0002", "0003", "0004", + "0001asdfsadf", "0004foo", + "fff1", "fff2", + "gorka", + "0", "003", + " 5a", "5 a", "5 \n", + "-001", "-000", + } { + r := strings.NewReader(test) + sc := pktline.NewScanner(r) + _ = sc.Scan() + c.Assert(sc.Err(), ErrorMatches, pktline.ErrInvalidPktLen.Error(), + Commentf("data = %q", test)) + } +} + +func (s *SuiteScanner) TestEmptyReader(c *C) { + r := strings.NewReader("") + sc := pktline.NewScanner(r) + hasPayload := sc.Scan() + c.Assert(hasPayload, Equals, false) + c.Assert(sc.Err(), Equals, nil) +} + +func (s *SuiteScanner) TestFlush(c *C) { + var buf bytes.Buffer + e := pktline.NewEncoder(&buf) + err := e.Flush() + c.Assert(err, IsNil) + + sc := pktline.NewScanner(&buf) + c.Assert(sc.Scan(), Equals, true) + + payload := sc.Bytes() + c.Assert(len(payload), Equals, 0) +} + +func (s *SuiteScanner) TestPktLineTooShort(c *C) { + r := strings.NewReader("010cfoobar") + + sc := pktline.NewScanner(r) + + c.Assert(sc.Scan(), Equals, false) + c.Assert(sc.Err(), ErrorMatches, "unexpected EOF") +} + +func (s *SuiteScanner) TestScanAndPayload(c *C) { + for _, test := range [...]string{ + "a", + "a\n", + strings.Repeat("a", 100), + strings.Repeat("a", 100) + "\n", + strings.Repeat("\x00", 100), + strings.Repeat("\x00", 100) + "\n", + strings.Repeat("a", pktline.MaxPayloadSize), + strings.Repeat("a", pktline.MaxPayloadSize-1) + "\n", + } { + var buf bytes.Buffer + e := pktline.NewEncoder(&buf) + err := e.EncodeString(test) + c.Assert(err, IsNil, + Commentf("input len=%x, contents=%.10q\n", len(test), test)) + + sc := pktline.NewScanner(&buf) + c.Assert(sc.Scan(), Equals, true, + Commentf("test = %.20q...", test)) + + obtained := sc.Bytes() + c.Assert(obtained, DeepEquals, []byte(test), + Commentf("in = %.20q out = %.20q", test, string(obtained))) + } +} + +func (s *SuiteScanner) TestSkip(c *C) { + for _, test := range [...]struct { + input []string + n int + expected []byte + }{ + { + input: []string{ + "first", + "second", + "third"}, + n: 1, + expected: []byte("second"), + }, + { + input: []string{ + "first", + "second", + "third"}, + n: 2, + expected: []byte("third"), + }, + } { + var buf bytes.Buffer + e := pktline.NewEncoder(&buf) + err := e.EncodeString(test.input...) + c.Assert(err, IsNil) + + sc := pktline.NewScanner(&buf) + for i := 0; i < test.n; i++ { + c.Assert(sc.Scan(), Equals, true, + Commentf("scan error = %s", sc.Err())) + } + c.Assert(sc.Scan(), Equals, true, + Commentf("scan error = %s", sc.Err())) + + obtained := sc.Bytes() + c.Assert(obtained, DeepEquals, test.expected, + Commentf("\nin = %.20q\nout = %.20q\nexp = %.20q", + test.input, obtained, test.expected)) + } +} + +func (s *SuiteScanner) TestEOF(c *C) { + var buf bytes.Buffer + e := pktline.NewEncoder(&buf) + err := e.EncodeString("first", "second") + c.Assert(err, IsNil) + + sc := pktline.NewScanner(&buf) + for sc.Scan() { + } + c.Assert(sc.Err(), IsNil) +} + +// A section are several non flush-pkt lines followed by a flush-pkt, which +// how the git protocol sends long messages. +func (s *SuiteScanner) TestReadSomeSections(c *C) { + nSections := 2 + nLines := 4 + data := sectionsExample(c, nSections, nLines) + sc := pktline.NewScanner(data) + + sectionCounter := 0 + lineCounter := 0 + for sc.Scan() { + if len(sc.Bytes()) == 0 { + sectionCounter++ + } + lineCounter++ + } + c.Assert(sc.Err(), IsNil) + c.Assert(sectionCounter, Equals, nSections) + c.Assert(lineCounter, Equals, (1+nLines)*nSections) +} + +// returns nSection sections, each of them with nLines pkt-lines (not +// counting the flush-pkt: +// +// 0009 0.0\n +// 0009 0.1\n +// ... +// 0000 +// and so on +func sectionsExample(c *C, nSections, nLines int) io.Reader { + var buf bytes.Buffer + e := pktline.NewEncoder(&buf) + + for section := 0; section < nSections; section++ { + ss := []string{} + for line := 0; line < nLines; line++ { + line := fmt.Sprintf(" %d.%d\n", section, line) + ss = append(ss, line) + } + err := e.EncodeString(ss...) + c.Assert(err, IsNil) + err = e.Flush() + c.Assert(err, IsNil) + } + + return &buf +} + +func ExampleScanner() { + // A reader is needed as input. + input := strings.NewReader("000ahello\n" + + "000bworld!\n" + + "0000", + ) + + // Create the scanner... + s := pktline.NewScanner(input) + + // and scan every pkt-line found in the input. + for s.Scan() { + payload := s.Bytes() + if len(payload) == 0 { // zero sized payloads correspond to flush-pkts. + fmt.Println("FLUSH-PKT DETECTED\n") + } else { // otherwise, you will be able to access the full payload. + fmt.Printf("PAYLOAD = %q\n", string(payload)) + } + } + + // this will catch any error when reading from the input, if any. + if s.Err() != nil { + fmt.Println(s.Err()) + } + + // Output: + // PAYLOAD = "hello\n" + // PAYLOAD = "world!\n" + // FLUSH-PKT DETECTED +} diff --git a/plumbing/format/packp/ulreq/decoder.go b/plumbing/format/packp/ulreq/decoder.go new file mode 100644 index 0000000..9083e04 --- /dev/null +++ b/plumbing/format/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/plumbing" + "gopkg.in/src-d/go-git.v4/plumbing/format/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() (plumbing.Hash, bool) { + if len(d.line) < hashSize { + d.err = fmt.Errorf("malformed hash: %v", d.line) + return plumbing.ZeroHash, false + } + + var hash plumbing.Hash + if _, err := hex.Decode(hash[:], d.line[:hashSize]); err != nil { + d.error("invalid hash text: %s", err) + return plumbing.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/plumbing/format/packp/ulreq/decoder_test.go b/plumbing/format/packp/ulreq/decoder_test.go new file mode 100644 index 0000000..01e4f90 --- /dev/null +++ b/plumbing/format/packp/ulreq/decoder_test.go @@ -0,0 +1,541 @@ +package ulreq + +import ( + "bytes" + "io" + "sort" + "time" + + "gopkg.in/src-d/go-git.v4/plumbing" + "gopkg.in/src-d/go-git.v4/plumbing/format/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, []plumbing.Hash{ + plumbing.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, []plumbing.Hash{ + plumbing.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 := []plumbing.Hash{ + plumbing.NewHash("1111111111111111111111111111111111111111"), + plumbing.NewHash("2222222222222222222222222222222222222222"), + plumbing.NewHash("3333333333333333333333333333333333333333"), + plumbing.NewHash("4444444444444444444444444444444444444444"), + } + + sort.Sort(byHash(ur.Wants)) + sort.Sort(byHash(expected)) + c.Assert(ur.Wants, DeepEquals, expected) +} + +type byHash []plumbing.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 := []plumbing.Hash{ + plumbing.NewHash("1111111111111111111111111111111111111111"), + plumbing.NewHash("2222222222222222222222222222222222222222"), + plumbing.NewHash("3333333333333333333333333333333333333333"), + plumbing.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 := []plumbing.Hash{ + plumbing.NewHash("3333333333333333333333333333333333333333"), + } + + expectedShallows := []plumbing.Hash{ + plumbing.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 := []plumbing.Hash{ + plumbing.NewHash("1111111111111111111111111111111111111111"), + plumbing.NewHash("2222222222222222222222222222222222222222"), + plumbing.NewHash("3333333333333333333333333333333333333333"), + plumbing.NewHash("4444444444444444444444444444444444444444"), + } + sort.Sort(byHash(expectedWants)) + + expectedShallows := []plumbing.Hash{ + plumbing.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 := []plumbing.Hash{ + plumbing.NewHash("3333333333333333333333333333333333333333"), + } + + expectedShallows := []plumbing.Hash{ + plumbing.NewHash("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + plumbing.NewHash("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), + plumbing.NewHash("cccccccccccccccccccccccccccccccccccccccc"), + plumbing.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 := []plumbing.Hash{ + plumbing.NewHash("1111111111111111111111111111111111111111"), + plumbing.NewHash("2222222222222222222222222222222222222222"), + plumbing.NewHash("3333333333333333333333333333333333333333"), + plumbing.NewHash("4444444444444444444444444444444444444444"), + } + sort.Sort(byHash(expectedWants)) + + expectedShallows := []plumbing.Hash{ + plumbing.NewHash("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + plumbing.NewHash("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), + plumbing.NewHash("cccccccccccccccccccccccccccccccccccccccc"), + plumbing.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 := []plumbing.Hash{ + plumbing.NewHash("1111111111111111111111111111111111111111"), + plumbing.NewHash("2222222222222222222222222222222222222222"), + plumbing.NewHash("3333333333333333333333333333333333333333"), + plumbing.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 := []plumbing.Hash{ + plumbing.NewHash("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + plumbing.NewHash("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), + plumbing.NewHash("cccccccccccccccccccccccccccccccccccccccc"), + plumbing.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/plumbing/format/packp/ulreq/encoder.go b/plumbing/format/packp/ulreq/encoder.go new file mode 100644 index 0000000..1264e0e --- /dev/null +++ b/plumbing/format/packp/ulreq/encoder.go @@ -0,0 +1,140 @@ +package ulreq + +import ( + "fmt" + "io" + "sort" + "time" + + "gopkg.in/src-d/go-git.v4/plumbing" + "gopkg.in/src-d/go-git.v4/plumbing/format/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 []plumbing.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/plumbing/format/packp/ulreq/encoder_test.go b/plumbing/format/packp/ulreq/encoder_test.go new file mode 100644 index 0000000..44c6d26 --- /dev/null +++ b/plumbing/format/packp/ulreq/encoder_test.go @@ -0,0 +1,268 @@ +package ulreq + +import ( + "bytes" + "time" + + "gopkg.in/src-d/go-git.v4/plumbing" + "gopkg.in/src-d/go-git.v4/plumbing/format/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, plumbing.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, plumbing.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, plumbing.NewHash("4444444444444444444444444444444444444444")) + ur.Wants = append(ur.Wants, plumbing.NewHash("1111111111111111111111111111111111111111")) + ur.Wants = append(ur.Wants, plumbing.NewHash("3333333333333333333333333333333333333333")) + ur.Wants = append(ur.Wants, plumbing.NewHash("2222222222222222222222222222222222222222")) + ur.Wants = append(ur.Wants, plumbing.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, plumbing.NewHash("4444444444444444444444444444444444444444")) + ur.Wants = append(ur.Wants, plumbing.NewHash("1111111111111111111111111111111111111111")) + ur.Wants = append(ur.Wants, plumbing.NewHash("3333333333333333333333333333333333333333")) + ur.Wants = append(ur.Wants, plumbing.NewHash("2222222222222222222222222222222222222222")) + ur.Wants = append(ur.Wants, plumbing.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, plumbing.NewHash("1111111111111111111111111111111111111111")) + ur.Capabilities.Add("multi_ack") + ur.Shallows = append(ur.Shallows, plumbing.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, plumbing.NewHash("1111111111111111111111111111111111111111")) + ur.Capabilities.Add("multi_ack") + ur.Shallows = append(ur.Shallows, plumbing.NewHash("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")) + ur.Shallows = append(ur.Shallows, plumbing.NewHash("dddddddddddddddddddddddddddddddddddddddd")) + ur.Shallows = append(ur.Shallows, plumbing.NewHash("cccccccccccccccccccccccccccccccccccccccc")) + ur.Shallows = append(ur.Shallows, plumbing.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, plumbing.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, plumbing.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, plumbing.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, plumbing.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, plumbing.NewHash("4444444444444444444444444444444444444444")) + ur.Wants = append(ur.Wants, plumbing.NewHash("1111111111111111111111111111111111111111")) + ur.Wants = append(ur.Wants, plumbing.NewHash("3333333333333333333333333333333333333333")) + ur.Wants = append(ur.Wants, plumbing.NewHash("2222222222222222222222222222222222222222")) + ur.Wants = append(ur.Wants, plumbing.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, plumbing.NewHash("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")) + ur.Shallows = append(ur.Shallows, plumbing.NewHash("dddddddddddddddddddddddddddddddddddddddd")) + ur.Shallows = append(ur.Shallows, plumbing.NewHash("cccccccccccccccccccccccccccccccccccccccc")) + ur.Shallows = append(ur.Shallows, plumbing.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/plumbing/format/packp/ulreq/ulreq.go b/plumbing/format/packp/ulreq/ulreq.go new file mode 100644 index 0000000..d2cc7c0 --- /dev/null +++ b/plumbing/format/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/plumbing" + "gopkg.in/src-d/go-git.v4/plumbing/format/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 []plumbing.Hash + Shallows []plumbing.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: []plumbing.Hash{}, + Shallows: []plumbing.Hash{}, + Depth: DepthCommits(0), + } +} diff --git a/plumbing/format/packp/ulreq/ulreq_test.go b/plumbing/format/packp/ulreq/ulreq_test.go new file mode 100644 index 0000000..06963ff --- /dev/null +++ b/plumbing/format/packp/ulreq/ulreq_test.go @@ -0,0 +1,91 @@ +package ulreq + +import ( + "fmt" + "os" + "strings" + "testing" + "time" + + "gopkg.in/src-d/go-git.v4/plumbing" + "gopkg.in/src-d/go-git.v4/plumbing/format/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, plumbing.NewHash("3333333333333333333333333333333333333333")) + ur.Wants = append(ur.Wants, plumbing.NewHash("1111111111111111111111111111111111111111")) + ur.Wants = append(ur.Wants, plumbing.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, plumbing.NewHash("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")) + ur.Shallows = append(ur.Shallows, plumbing.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