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