aboutsummaryrefslogtreecommitdiffstats
path: root/plumbing
diff options
context:
space:
mode:
authorSantiago M. Mola <santi@mola.io>2016-12-05 11:59:49 +0100
committerMáximo Cuadros <mcuadros@gmail.com>2016-12-05 11:59:49 +0100
commit0042bb031676a20ffc789f94e332a6da70e2756d (patch)
tree22a142575c6f12894b4faec73807b57afc287529 /plumbing
parent19f59e782b92d32cc430619c77053c764a3180f9 (diff)
downloadgo-git-0042bb031676a20ffc789f94e332a6da70e2756d.tar.gz
protocol/packp: add reference update request encoder. (#147)
* add ReferenceUpdateRequest struct. * add ReferenceUpdateRequest decoder. * add ReferenceUpdateRequest encoder.
Diffstat (limited to 'plumbing')
-rw-r--r--plumbing/protocol/packp/advrefs_decode.go1
-rw-r--r--plumbing/protocol/packp/common.go4
-rw-r--r--plumbing/protocol/packp/updreq.go84
-rw-r--r--plumbing/protocol/packp/updreq_decode.go231
-rw-r--r--plumbing/protocol/packp/updreq_decode_test.go253
-rw-r--r--plumbing/protocol/packp/updreq_encode.go67
-rw-r--r--plumbing/protocol/packp/updreq_encode_test.go119
7 files changed, 759 insertions, 0 deletions
diff --git a/plumbing/protocol/packp/advrefs_decode.go b/plumbing/protocol/packp/advrefs_decode.go
index c8f8394..5926645 100644
--- a/plumbing/protocol/packp/advrefs_decode.go
+++ b/plumbing/protocol/packp/advrefs_decode.go
@@ -199,6 +199,7 @@ func decodeFirstRef(l *advRefsDecoder) decoderStateFn {
func decodeCaps(p *advRefsDecoder) decoderStateFn {
if err := p.data.Capabilities.Decode(p.line); err != nil {
p.error("invalid capabilities: %s", err)
+ return nil
}
return decodeOtherRefs
diff --git a/plumbing/protocol/packp/common.go b/plumbing/protocol/packp/common.go
index 3d7786b..c8db931 100644
--- a/plumbing/protocol/packp/common.go
+++ b/plumbing/protocol/packp/common.go
@@ -15,6 +15,7 @@ var (
// common
sp = []byte(" ")
eol = []byte("\n")
+ eq = []byte{'='}
// advrefs
null = []byte("\x00")
@@ -28,4 +29,7 @@ var (
deepenCommits = []byte("deepen ")
deepenSince = []byte("deepen-since ")
deepenReference = []byte("deepen-not ")
+
+ // updreq
+ shallowNoSp = []byte("shallow")
)
diff --git a/plumbing/protocol/packp/updreq.go b/plumbing/protocol/packp/updreq.go
new file mode 100644
index 0000000..90d6e09
--- /dev/null
+++ b/plumbing/protocol/packp/updreq.go
@@ -0,0 +1,84 @@
+package packp
+
+import (
+ "errors"
+
+ "gopkg.in/src-d/go-git.v4/plumbing"
+ "gopkg.in/src-d/go-git.v4/plumbing/protocol/packp/capability"
+)
+
+var (
+ ErrEmptyCommands = errors.New("commands cannot be empty")
+ ErrMalformedCommand = errors.New("malformed command")
+)
+
+// ReferenceUpdateRequest values represent reference upload requests.
+// Values from this type are not zero-value safe, use the New function instead.
+//
+// TODO: Add support for push-cert
+type ReferenceUpdateRequest struct {
+ Capabilities *capability.List
+ Commands []*Command
+ Shallow *plumbing.Hash
+}
+
+// New returns a pointer to a new ReferenceUpdateRequest value.
+func NewReferenceUpdateRequest() *ReferenceUpdateRequest {
+ return &ReferenceUpdateRequest{
+ Capabilities: capability.NewList(),
+ Commands: nil,
+ }
+}
+
+func (r *ReferenceUpdateRequest) validate() error {
+ if len(r.Commands) == 0 {
+ return ErrEmptyCommands
+ }
+
+ for _, c := range r.Commands {
+ if err := c.validate(); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+type Action string
+
+const (
+ Create Action = "create"
+ Update = "update"
+ Delete = "delete"
+ Invalid = "invalid"
+)
+
+type Command struct {
+ Name string
+ Old plumbing.Hash
+ New plumbing.Hash
+}
+
+func (c *Command) Action() Action {
+ if c.Old == plumbing.ZeroHash && c.New == plumbing.ZeroHash {
+ return Invalid
+ }
+
+ if c.Old == plumbing.ZeroHash {
+ return Create
+ }
+
+ if c.New == plumbing.ZeroHash {
+ return Delete
+ }
+
+ return Update
+}
+
+func (c *Command) validate() error {
+ if c.Action() == Invalid {
+ return ErrMalformedCommand
+ }
+
+ return nil
+}
diff --git a/plumbing/protocol/packp/updreq_decode.go b/plumbing/protocol/packp/updreq_decode.go
new file mode 100644
index 0000000..0740871
--- /dev/null
+++ b/plumbing/protocol/packp/updreq_decode.go
@@ -0,0 +1,231 @@
+package packp
+
+import (
+ "bytes"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "io"
+
+ "gopkg.in/src-d/go-git.v4/plumbing"
+ "gopkg.in/src-d/go-git.v4/plumbing/format/pktline"
+)
+
+var (
+ shallowLineLength = len(shallow) + hashSize
+ minCommandLength = hashSize*2 + 2 + 1
+ minCommandAndCapsLenth = minCommandLength + 1
+)
+
+var (
+ ErrEmpty = errors.New("empty update-request message")
+ errNoCommands = errors.New("unexpected EOF before any command")
+ errMissingCapabilitiesDelimiter = errors.New("capabilities delimiter not found")
+)
+
+func errMalformedRequest(reason string) error {
+ return fmt.Errorf("malformed request: %s", reason)
+}
+
+func errInvalidHashSize(got int) error {
+ return fmt.Errorf("invalid hash size: expected %d, got %d",
+ hashSize, got)
+}
+
+func errInvalidHash(err error) error {
+ return fmt.Errorf("invalid hash: %s", err.Error())
+}
+
+func errInvalidShallowLineLength(got int) error {
+ return errMalformedRequest(fmt.Sprintf(
+ "invalid shallow line length: expected %d, got %d",
+ shallowLineLength, got))
+}
+
+func errInvalidCommandCapabilitiesLineLength(got int) error {
+ return errMalformedRequest(fmt.Sprintf(
+ "invalid command and capabilities line length: expected at least %d, got %d",
+ minCommandAndCapsLenth, got))
+}
+
+func errInvalidCommandLineLength(got int) error {
+ return errMalformedRequest(fmt.Sprintf(
+ "invalid command line length: expected at least %d, got %d",
+ minCommandLength, got))
+}
+
+func errInvalidShallowObjId(err error) error {
+ return errMalformedRequest(
+ fmt.Sprintf("invalid shallow object id: %s", err.Error()))
+}
+
+func errInvalidOldObjId(err error) error {
+ return errMalformedRequest(
+ fmt.Sprintf("invalid old object id: %s", err.Error()))
+}
+
+func errInvalidNewObjId(err error) error {
+ return errMalformedRequest(
+ fmt.Sprintf("invalid new object id: %s", err.Error()))
+}
+
+func errMalformedCommand(err error) error {
+ return errMalformedRequest(fmt.Sprintf(
+ "malformed command: %s", err.Error()))
+}
+
+// Decode reads the next update-request message form the reader and wr
+func (req *ReferenceUpdateRequest) Decode(r io.Reader) error {
+ d := &updReqDecoder{s: pktline.NewScanner(r)}
+ return d.Decode(req)
+}
+
+type updReqDecoder struct {
+ s *pktline.Scanner
+ r *ReferenceUpdateRequest
+}
+
+func (d *updReqDecoder) Decode(r *ReferenceUpdateRequest) error {
+ d.r = r
+ funcs := []func() error{
+ d.scanLine,
+ d.decodeShallow,
+ d.decodeCommandAndCapabilities,
+ d.decodeCommands,
+ r.validate,
+ }
+
+ for _, f := range funcs {
+ if err := f(); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (d *updReqDecoder) scanLine() error {
+ if ok := d.s.Scan(); !ok {
+ return d.scanErrorOr(ErrEmpty)
+ }
+
+ return nil
+}
+
+func (d *updReqDecoder) decodeShallow() error {
+ b := d.s.Bytes()
+
+ if !bytes.HasPrefix(b, shallowNoSp) {
+ return nil
+ }
+
+ if len(b) != shallowLineLength {
+ return errInvalidShallowLineLength(len(b))
+ }
+
+ h, err := parseHash(string(b[len(shallow):]))
+ if err != nil {
+ return errInvalidShallowObjId(err)
+ }
+
+ if ok := d.s.Scan(); !ok {
+ return d.scanErrorOr(errNoCommands)
+ }
+
+ d.r.Shallow = &h
+
+ return nil
+}
+
+func (d *updReqDecoder) decodeCommands() error {
+ for {
+ b := d.s.Bytes()
+ if bytes.Equal(b, pktline.Flush) {
+ return nil
+ }
+
+ c, err := parseCommand(b)
+ if err != nil {
+ return err
+ }
+
+ d.r.Commands = append(d.r.Commands, c)
+
+ if ok := d.s.Scan(); !ok {
+ return d.s.Err()
+ }
+ }
+}
+
+func (d *updReqDecoder) decodeCommandAndCapabilities() error {
+ b := d.s.Bytes()
+ i := bytes.IndexByte(b, 0)
+ if i == -1 {
+ return errMissingCapabilitiesDelimiter
+ }
+
+ if len(b) < minCommandAndCapsLenth {
+ return errInvalidCommandCapabilitiesLineLength(len(b))
+ }
+
+ cmd, err := parseCommand(b[:i])
+ if err != nil {
+ return err
+ }
+
+ d.r.Commands = append(d.r.Commands, cmd)
+
+ if err := d.r.Capabilities.Decode(b[i+1:]); err != nil {
+ return err
+ }
+
+ if err := d.scanLine(); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func parseCommand(b []byte) (*Command, error) {
+ if len(b) < minCommandLength {
+ return nil, errInvalidCommandLineLength(len(b))
+ }
+
+ var os, ns, n string
+ if _, err := fmt.Sscanf(string(b), "%s %s %s", &os, &ns, &n); err != nil {
+ return nil, errMalformedCommand(err)
+ }
+
+ oh, err := parseHash(os)
+ if err != nil {
+ return nil, errInvalidOldObjId(err)
+ }
+
+ nh, err := parseHash(ns)
+ if err != nil {
+ return nil, errInvalidNewObjId(err)
+ }
+
+ return &Command{Old: oh, New: nh, Name: n}, nil
+}
+
+func parseHash(s string) (plumbing.Hash, error) {
+ if len(s) != hashSize {
+ return plumbing.ZeroHash, errInvalidHashSize(len(s))
+ }
+
+ if _, err := hex.DecodeString(s); err != nil {
+ return plumbing.ZeroHash, errInvalidHash(err)
+ }
+
+ h := plumbing.NewHash(s)
+ return h, nil
+}
+
+func (d *updReqDecoder) scanErrorOr(origErr error) error {
+ if err := d.s.Err(); err != nil {
+ return err
+ }
+
+ return origErr
+}
diff --git a/plumbing/protocol/packp/updreq_decode_test.go b/plumbing/protocol/packp/updreq_decode_test.go
new file mode 100644
index 0000000..66d9180
--- /dev/null
+++ b/plumbing/protocol/packp/updreq_decode_test.go
@@ -0,0 +1,253 @@
+package packp
+
+import (
+ "bytes"
+ "io"
+
+ "gopkg.in/src-d/go-git.v4/plumbing"
+ "gopkg.in/src-d/go-git.v4/plumbing/format/pktline"
+
+ . "gopkg.in/check.v1"
+)
+
+type UpdReqDecodeSuite struct{}
+
+var _ = Suite(&UpdReqDecodeSuite{})
+
+func (s *UpdReqDecodeSuite) TestEmpty(c *C) {
+ r := NewReferenceUpdateRequest()
+ var buf bytes.Buffer
+ c.Assert(r.Decode(&buf), Equals, ErrEmpty)
+ c.Assert(r, DeepEquals, NewReferenceUpdateRequest())
+}
+
+func (s *UpdReqDecodeSuite) TestInvalidPktlines(c *C) {
+ r := NewReferenceUpdateRequest()
+ input := bytes.NewReader([]byte("xxxxxxxxxx"))
+ c.Assert(r.Decode(input), ErrorMatches, "invalid pkt-len found")
+}
+
+func (s *UpdReqDecodeSuite) TestInvalidShadow(c *C) {
+ payloads := []string{
+ "shallow",
+ "1ecf0ef2c2dffb796033e5a02219af86ec6584e5 2ecf0ef2c2dffb796033e5a02219af86ec6584e5 myref\x00",
+ pktline.FlushString,
+ }
+ s.testDecoderErrorMatches(c, toPktLines(c, payloads), "^malformed request: invalid shallow line length: expected 48, got 7$")
+
+ payloads = []string{
+ "shallow ",
+ "1ecf0ef2c2dffb796033e5a02219af86ec6584e5 2ecf0ef2c2dffb796033e5a02219af86ec6584e5 myref\x00",
+ pktline.FlushString,
+ }
+ s.testDecoderErrorMatches(c, toPktLines(c, payloads), "^malformed request: invalid shallow line length: expected 48, got 8$")
+
+ payloads = []string{
+ "shallow 1ecf0ef2c2dffb796033e5a02219af86ec65",
+ "1ecf0ef2c2dffb796033e5a02219af86ec6584e5 2ecf0ef2c2dffb796033e5a02219af86ec6584e5 myref\x00",
+ pktline.FlushString,
+ }
+ s.testDecoderErrorMatches(c, toPktLines(c, payloads), "^malformed request: invalid shallow line length: expected 48, got 44$")
+
+ payloads = []string{
+ "shallow 1ecf0ef2c2dffb796033e5a02219af86ec6584e54",
+ "1ecf0ef2c2dffb796033e5a02219af86ec6584e5 2ecf0ef2c2dffb796033e5a02219af86ec6584e5 myref\x00",
+ pktline.FlushString,
+ }
+ s.testDecoderErrorMatches(c, toPktLines(c, payloads), "^malformed request: invalid shallow line length: expected 48, got 49$")
+
+ payloads = []string{
+ "shallow 1ecf0ef2c2dffb796033e5a02219af86ec6584eu",
+ "1ecf0ef2c2dffb796033e5a02219af86ec6584e5 2ecf0ef2c2dffb796033e5a02219af86ec6584e5 myref\x00",
+ pktline.FlushString,
+ }
+ s.testDecoderErrorMatches(c, toPktLines(c, payloads), "^malformed request: invalid shallow object id: invalid hash: .*")
+}
+
+func (s *UpdReqDecodeSuite) TestMalformedCommand(c *C) {
+ payloads := []string{
+ "1ecf0ef2c2dffb796033e5a02219af86ec6584e5x2ecf0ef2c2dffb796033e5a02219af86ec6584e5xmyref\x00",
+ pktline.FlushString,
+ }
+ s.testDecoderErrorMatches(c, toPktLines(c, payloads), "^malformed request: malformed command: EOF$")
+
+ payloads = []string{
+ "1ecf0ef2c2dffb796033e5a02219af86ec6584e5 2ecf0ef2c2dffb796033e5a02219af86ec6584e5 myref\x00",
+ "1ecf0ef2c2dffb796033e5a02219af86ec6584e5x2ecf0ef2c2dffb796033e5a02219af86ec6584e5xmyref",
+ pktline.FlushString,
+ }
+ s.testDecoderErrorMatches(c, toPktLines(c, payloads), "^malformed request: malformed command: EOF$")
+}
+
+func (s *UpdReqDecodeSuite) TestInvalidCommandInvalidHash(c *C) {
+ payloads := []string{
+ "1ecf0ef2c2dffb796033e5a02219af86ec6584e 2ecf0ef2c2dffb796033e5a02219af86ec6584e5 myref\x00",
+ pktline.FlushString,
+ }
+ s.testDecoderErrorMatches(c, toPktLines(c, payloads), "^malformed request: invalid old object id: invalid hash size: expected 40, got 39$")
+
+ payloads = []string{
+ "1ecf0ef2c2dffb796033e5a02219af86ec6584e5 2ecf0ef2c2dffb796033e5a02219af86ec6584e myref\x00",
+ pktline.FlushString,
+ }
+ s.testDecoderErrorMatches(c, toPktLines(c, payloads), "^malformed request: invalid new object id: invalid hash size: expected 40, got 39$")
+
+ payloads = []string{
+ "1ecf0ef2c2dffb796033e5a02219af86e 2ecf0ef2c2dffb796033e5a02219af86ec6 m\x00",
+ pktline.FlushString,
+ }
+ s.testDecoderErrorMatches(c, toPktLines(c, payloads), "^malformed request: invalid command and capabilities line length: expected at least 84, got 72$")
+
+ payloads = []string{
+ "1ecf0ef2c2dffb796033e5a02219af86ec6584eu 2ecf0ef2c2dffb796033e5a02219af86ec6584e5 myref\x00",
+ pktline.FlushString,
+ }
+ s.testDecoderErrorMatches(c, toPktLines(c, payloads), "^malformed request: invalid old object id: invalid hash: .*$")
+
+ payloads = []string{
+ "1ecf0ef2c2dffb796033e5a02219af86ec6584e5 2ecf0ef2c2dffb796033e5a02219af86ec6584eu myref\x00",
+ pktline.FlushString,
+ }
+ s.testDecoderErrorMatches(c, toPktLines(c, payloads), "^malformed request: invalid new object id: invalid hash: .*$")
+}
+
+func (s *UpdReqDecodeSuite) TestInvalidCommandMissingNullDelimiter(c *C) {
+ payloads := []string{
+ "1ecf0ef2c2dffb796033e5a02219af86ec6584e5 2ecf0ef2c2dffb796033e5a02219af86ec6584e5 myref",
+ pktline.FlushString,
+ }
+ s.testDecoderErrorMatches(c, toPktLines(c, payloads), "capabilities delimiter not found")
+}
+
+func (s *UpdReqDecodeSuite) TestInvalidCommandMissingName(c *C) {
+ payloads := []string{
+ "1ecf0ef2c2dffb796033e5a02219af86ec6584e5 2ecf0ef2c2dffb796033e5a02219af86ec6584e5\x00",
+ pktline.FlushString,
+ }
+ s.testDecoderErrorMatches(c, toPktLines(c, payloads), "^malformed request: invalid command and capabilities line length: expected at least 84, got 82$")
+
+ payloads = []string{
+ "1ecf0ef2c2dffb796033e5a02219af86ec6584e5 2ecf0ef2c2dffb796033e5a02219af86ec6584e5 \x00",
+ pktline.FlushString,
+ }
+ s.testDecoderErrorMatches(c, toPktLines(c, payloads), "^malformed request: invalid command and capabilities line length: expected at least 84, got 83$")
+
+ payloads = []string{
+ "1ecf0ef2c2dffb796033e5a02219af86ec6584e5 2ecf0ef2c2dffb796033e5a02219af86ec6584e5 myref\x00",
+ "1ecf0ef2c2dffb796033e5a02219af86ec6584e5 2ecf0ef2c2dffb796033e5a02219af86ec6584e5",
+ pktline.FlushString,
+ }
+ s.testDecoderErrorMatches(c, toPktLines(c, payloads), "^malformed request: invalid command line length: expected at least 83, got 81$")
+
+ payloads = []string{
+ "1ecf0ef2c2dffb796033e5a02219af86ec6584e5 2ecf0ef2c2dffb796033e5a02219af86ec6584e5 myref\x00",
+ "1ecf0ef2c2dffb796033e5a02219af86ec6584e5 2ecf0ef2c2dffb796033e5a02219af86ec6584e5 ",
+ pktline.FlushString,
+ }
+ s.testDecoderErrorMatches(c, toPktLines(c, payloads), "^malformed request: invalid command line length: expected at least 83, got 82$")
+}
+
+func (s *UpdReqDecodeSuite) TestOneUpdateCommand(c *C) {
+ hash1 := plumbing.NewHash("1ecf0ef2c2dffb796033e5a02219af86ec6584e5")
+ hash2 := plumbing.NewHash("2ecf0ef2c2dffb796033e5a02219af86ec6584e5")
+ name := "myref"
+
+ expected := NewReferenceUpdateRequest()
+ expected.Commands = []*Command{
+ {Name: name, Old: hash1, New: hash2},
+ }
+
+ payloads := []string{
+ "1ecf0ef2c2dffb796033e5a02219af86ec6584e5 2ecf0ef2c2dffb796033e5a02219af86ec6584e5 myref\x00",
+ pktline.FlushString,
+ }
+
+ c.Assert(s.testDecodeOK(c, payloads), DeepEquals, expected)
+}
+
+func (s *UpdReqDecodeSuite) TestMultipleCommands(c *C) {
+ hash1 := plumbing.NewHash("1ecf0ef2c2dffb796033e5a02219af86ec6584e5")
+ hash2 := plumbing.NewHash("2ecf0ef2c2dffb796033e5a02219af86ec6584e5")
+
+ expected := NewReferenceUpdateRequest()
+ expected.Commands = []*Command{
+ {Name: "myref1", Old: hash1, New: hash2},
+ {Name: "myref2", Old: plumbing.ZeroHash, New: hash2},
+ {Name: "myref3", Old: hash1, New: plumbing.ZeroHash},
+ }
+
+ payloads := []string{
+ "1ecf0ef2c2dffb796033e5a02219af86ec6584e5 2ecf0ef2c2dffb796033e5a02219af86ec6584e5 myref1\x00",
+ "0000000000000000000000000000000000000000 2ecf0ef2c2dffb796033e5a02219af86ec6584e5 myref2",
+ "1ecf0ef2c2dffb796033e5a02219af86ec6584e5 0000000000000000000000000000000000000000 myref3",
+ pktline.FlushString,
+ }
+
+ c.Assert(s.testDecodeOK(c, payloads).Commands, DeepEquals, expected.Commands)
+ c.Assert(s.testDecodeOK(c, payloads).Shallow, DeepEquals, expected.Shallow)
+ c.Assert(s.testDecodeOK(c, payloads).Capabilities, DeepEquals, expected.Capabilities)
+ c.Assert(s.testDecodeOK(c, payloads), DeepEquals, expected)
+}
+
+func (s *UpdReqDecodeSuite) TestMultipleCommandsAndCapabilities(c *C) {
+ hash1 := plumbing.NewHash("1ecf0ef2c2dffb796033e5a02219af86ec6584e5")
+ hash2 := plumbing.NewHash("2ecf0ef2c2dffb796033e5a02219af86ec6584e5")
+
+ expected := NewReferenceUpdateRequest()
+ expected.Commands = []*Command{
+ {Name: "myref1", Old: hash1, New: hash2},
+ {Name: "myref2", Old: plumbing.ZeroHash, New: hash2},
+ {Name: "myref3", Old: hash1, New: plumbing.ZeroHash},
+ }
+ expected.Capabilities.Add("shallow")
+
+ payloads := []string{
+ "1ecf0ef2c2dffb796033e5a02219af86ec6584e5 2ecf0ef2c2dffb796033e5a02219af86ec6584e5 myref1\x00shallow",
+ "0000000000000000000000000000000000000000 2ecf0ef2c2dffb796033e5a02219af86ec6584e5 myref2",
+ "1ecf0ef2c2dffb796033e5a02219af86ec6584e5 0000000000000000000000000000000000000000 myref3",
+ pktline.FlushString,
+ }
+
+ c.Assert(s.testDecodeOK(c, payloads), DeepEquals, expected)
+}
+
+func (s *UpdReqDecodeSuite) TestMultipleCommandsAndCapabilitiesShallow(c *C) {
+ hash1 := plumbing.NewHash("1ecf0ef2c2dffb796033e5a02219af86ec6584e5")
+ hash2 := plumbing.NewHash("2ecf0ef2c2dffb796033e5a02219af86ec6584e5")
+
+ expected := NewReferenceUpdateRequest()
+ expected.Commands = []*Command{
+ {Name: "myref1", Old: hash1, New: hash2},
+ {Name: "myref2", Old: plumbing.ZeroHash, New: hash2},
+ {Name: "myref3", Old: hash1, New: plumbing.ZeroHash},
+ }
+ expected.Capabilities.Add("shallow")
+ expected.Shallow = &hash1
+
+ payloads := []string{
+ "shallow 1ecf0ef2c2dffb796033e5a02219af86ec6584e5",
+ "1ecf0ef2c2dffb796033e5a02219af86ec6584e5 2ecf0ef2c2dffb796033e5a02219af86ec6584e5 myref1\x00shallow",
+ "0000000000000000000000000000000000000000 2ecf0ef2c2dffb796033e5a02219af86ec6584e5 myref2",
+ "1ecf0ef2c2dffb796033e5a02219af86ec6584e5 0000000000000000000000000000000000000000 myref3",
+ pktline.FlushString,
+ }
+
+ c.Assert(s.testDecodeOK(c, payloads), DeepEquals, expected)
+}
+
+func (s *UpdReqDecodeSuite) testDecoderErrorMatches(c *C, input io.Reader, pattern string) {
+ r := NewReferenceUpdateRequest()
+ c.Assert(r.Decode(input), ErrorMatches, pattern)
+}
+
+func (s *UpdReqDecodeSuite) testDecodeOK(c *C, payloads []string) *ReferenceUpdateRequest {
+ var buf bytes.Buffer
+ e := pktline.NewEncoder(&buf)
+ err := e.EncodeString(payloads...)
+ c.Assert(err, IsNil)
+
+ r := NewReferenceUpdateRequest()
+ c.Assert(r.Decode(&buf), IsNil)
+
+ return r
+}
diff --git a/plumbing/protocol/packp/updreq_encode.go b/plumbing/protocol/packp/updreq_encode.go
new file mode 100644
index 0000000..b2b7944
--- /dev/null
+++ b/plumbing/protocol/packp/updreq_encode.go
@@ -0,0 +1,67 @@
+package packp
+
+import (
+ "fmt"
+ "io"
+
+ "gopkg.in/src-d/go-git.v4/plumbing"
+ "gopkg.in/src-d/go-git.v4/plumbing/format/pktline"
+ "gopkg.in/src-d/go-git.v4/plumbing/protocol/packp/capability"
+)
+
+var (
+ zeroHashString = plumbing.ZeroHash.String()
+)
+
+// Encode writes the ReferenceUpdateRequest encoding to the stream.
+func (r *ReferenceUpdateRequest) Encode(w io.Writer) error {
+ if err := r.validate(); err != nil {
+ return err
+ }
+
+ e := pktline.NewEncoder(w)
+
+ if err := r.encodeShallow(e, r.Shallow); err != nil {
+ return err
+ }
+
+ if err := r.encodeCommands(e, r.Commands, r.Capabilities); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (r *ReferenceUpdateRequest) encodeShallow(e *pktline.Encoder,
+ h *plumbing.Hash) error {
+
+ if h == nil {
+ return nil
+ }
+
+ objId := []byte(h.String())
+ return e.Encodef("%s%s", shallow, objId)
+}
+
+func (r *ReferenceUpdateRequest) encodeCommands(e *pktline.Encoder,
+ cmds []*Command, cap *capability.List) error {
+
+ if err := e.Encodef("%s\x00%s",
+ formatCommand(cmds[0]), cap.String()); err != nil {
+ return err
+ }
+
+ for _, cmd := range cmds[1:] {
+ if err := e.Encodef(formatCommand(cmd)); err != nil {
+ return err
+ }
+ }
+
+ return e.Flush()
+}
+
+func formatCommand(cmd *Command) string {
+ o := cmd.Old.String()
+ n := cmd.New.String()
+ return fmt.Sprintf("%s %s %s", o, n, cmd.Name)
+}
diff --git a/plumbing/protocol/packp/updreq_encode_test.go b/plumbing/protocol/packp/updreq_encode_test.go
new file mode 100644
index 0000000..47958fd
--- /dev/null
+++ b/plumbing/protocol/packp/updreq_encode_test.go
@@ -0,0 +1,119 @@
+package packp
+
+import (
+ "bytes"
+
+ "gopkg.in/src-d/go-git.v4/plumbing"
+ "gopkg.in/src-d/go-git.v4/plumbing/format/pktline"
+
+ . "gopkg.in/check.v1"
+)
+
+type UpdReqEncodeSuite struct{}
+
+var _ = Suite(&UpdReqEncodeSuite{})
+
+func (s *UpdReqEncodeSuite) testEncode(c *C, input *ReferenceUpdateRequest,
+ expected []byte) {
+
+ var buf bytes.Buffer
+ c.Assert(input.Encode(&buf), IsNil)
+ obtained := buf.Bytes()
+
+ comment := Commentf("\nobtained = %s\nexpected = %s\n", string(obtained), string(expected))
+ c.Assert(obtained, DeepEquals, expected, comment)
+}
+
+func (s *UpdReqEncodeSuite) TestZeroValue(c *C) {
+ r := &ReferenceUpdateRequest{}
+ var buf bytes.Buffer
+ c.Assert(r.Encode(&buf), Equals, ErrEmptyCommands)
+
+ r = NewReferenceUpdateRequest()
+ c.Assert(r.Encode(&buf), Equals, ErrEmptyCommands)
+}
+
+func (s *UpdReqEncodeSuite) TestOneUpdateCommand(c *C) {
+ hash1 := plumbing.NewHash("1ecf0ef2c2dffb796033e5a02219af86ec6584e5")
+ hash2 := plumbing.NewHash("2ecf0ef2c2dffb796033e5a02219af86ec6584e5")
+ name := "myref"
+
+ r := NewReferenceUpdateRequest()
+ r.Commands = []*Command{
+ {Name: name, Old: hash1, New: hash2},
+ }
+
+ expected := pktlines(c,
+ "1ecf0ef2c2dffb796033e5a02219af86ec6584e5 2ecf0ef2c2dffb796033e5a02219af86ec6584e5 myref\x00",
+ pktline.FlushString,
+ )
+
+ s.testEncode(c, r, expected)
+}
+
+func (s *UpdReqEncodeSuite) TestMultipleCommands(c *C) {
+ hash1 := plumbing.NewHash("1ecf0ef2c2dffb796033e5a02219af86ec6584e5")
+ hash2 := plumbing.NewHash("2ecf0ef2c2dffb796033e5a02219af86ec6584e5")
+
+ r := NewReferenceUpdateRequest()
+ r.Commands = []*Command{
+ {Name: "myref1", Old: hash1, New: hash2},
+ {Name: "myref2", Old: plumbing.ZeroHash, New: hash2},
+ {Name: "myref3", Old: hash1, New: plumbing.ZeroHash},
+ }
+
+ expected := pktlines(c,
+ "1ecf0ef2c2dffb796033e5a02219af86ec6584e5 2ecf0ef2c2dffb796033e5a02219af86ec6584e5 myref1\x00",
+ "0000000000000000000000000000000000000000 2ecf0ef2c2dffb796033e5a02219af86ec6584e5 myref2",
+ "1ecf0ef2c2dffb796033e5a02219af86ec6584e5 0000000000000000000000000000000000000000 myref3",
+ pktline.FlushString,
+ )
+
+ s.testEncode(c, r, expected)
+}
+
+func (s *UpdReqEncodeSuite) TestMultipleCommandsAndCapabilities(c *C) {
+ hash1 := plumbing.NewHash("1ecf0ef2c2dffb796033e5a02219af86ec6584e5")
+ hash2 := plumbing.NewHash("2ecf0ef2c2dffb796033e5a02219af86ec6584e5")
+
+ r := NewReferenceUpdateRequest()
+ r.Commands = []*Command{
+ {Name: "myref1", Old: hash1, New: hash2},
+ {Name: "myref2", Old: plumbing.ZeroHash, New: hash2},
+ {Name: "myref3", Old: hash1, New: plumbing.ZeroHash},
+ }
+ r.Capabilities.Add("shallow")
+
+ expected := pktlines(c,
+ "1ecf0ef2c2dffb796033e5a02219af86ec6584e5 2ecf0ef2c2dffb796033e5a02219af86ec6584e5 myref1\x00shallow",
+ "0000000000000000000000000000000000000000 2ecf0ef2c2dffb796033e5a02219af86ec6584e5 myref2",
+ "1ecf0ef2c2dffb796033e5a02219af86ec6584e5 0000000000000000000000000000000000000000 myref3",
+ pktline.FlushString,
+ )
+
+ s.testEncode(c, r, expected)
+}
+
+func (s *UpdReqEncodeSuite) TestMultipleCommandsAndCapabilitiesShallow(c *C) {
+ hash1 := plumbing.NewHash("1ecf0ef2c2dffb796033e5a02219af86ec6584e5")
+ hash2 := plumbing.NewHash("2ecf0ef2c2dffb796033e5a02219af86ec6584e5")
+
+ r := NewReferenceUpdateRequest()
+ r.Commands = []*Command{
+ {Name: "myref1", Old: hash1, New: hash2},
+ {Name: "myref2", Old: plumbing.ZeroHash, New: hash2},
+ {Name: "myref3", Old: hash1, New: plumbing.ZeroHash},
+ }
+ r.Capabilities.Add("shallow")
+ r.Shallow = &hash1
+
+ expected := pktlines(c,
+ "shallow 1ecf0ef2c2dffb796033e5a02219af86ec6584e5",
+ "1ecf0ef2c2dffb796033e5a02219af86ec6584e5 2ecf0ef2c2dffb796033e5a02219af86ec6584e5 myref1\x00shallow",
+ "0000000000000000000000000000000000000000 2ecf0ef2c2dffb796033e5a02219af86ec6584e5 myref2",
+ "1ecf0ef2c2dffb796033e5a02219af86ec6584e5 0000000000000000000000000000000000000000 myref3",
+ pktline.FlushString,
+ )
+
+ s.testEncode(c, r, expected)
+}