diff --git a/plumbing/format/pktline/encoder.go b/plumbing/format/pktline/encoder.go index 6d409795b..6a91888da 100644 --- a/plumbing/format/pktline/encoder.go +++ b/plumbing/format/pktline/encoder.go @@ -29,6 +29,12 @@ var ( Flush = []byte{} // FlushString is the payload to use with the EncodeString method to encode a flush-pkt. FlushString = "" + // DelimPkt is the delimiter packet used in v2 + DelimPkt = []byte{'0', '0', '0', '1'} + // EndPkt is the end packet used in v2 + EndPkt = []byte{'0', '0', '0', '2'} + // Delim is the payload of a delimpkt + Delim = []byte{'0'} // ErrPayloadTooLong is returned by the Encode methods when any of the // provided payloads is bigger than MaxPayloadSize. ErrPayloadTooLong = errors.New("payload is too long") @@ -47,6 +53,18 @@ func (e *Encoder) Flush() error { return err } +// Delim encodes a delim-pkt to the output stream. +func (e *Encoder) Delim() error { + _, err := e.w.Write(DelimPkt) + return err +} + +// End encodes an end-pkt to the output stream. +func (e *Encoder) End() error { + _, err := e.w.Write(EndPkt) + 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. diff --git a/plumbing/format/pktline/scanner.go b/plumbing/format/pktline/scanner.go index 99aab46e8..b08a2a484 100644 --- a/plumbing/format/pktline/scanner.go +++ b/plumbing/format/pktline/scanner.go @@ -5,8 +5,15 @@ import ( "io" ) +type PktType int + const ( lenSize = 4 + + FlushType = PktType(iota) + DelimType + EndType + DataType ) // ErrInvalidPktLen is returned by Err() when an invalid pkt-len is found. @@ -27,6 +34,7 @@ type Scanner struct { err error // Sticky error payload []byte // Last pkt-payload len [lenSize]byte // Last pkt-len + pktType PktType } // NewScanner returns a new Scanner to read from r. @@ -56,6 +64,9 @@ func (s *Scanner) Scan() bool { if s.err != nil { return false } + if s.pktType != DataType { + return true + } if cap(s.payload) < l { s.payload = make([]byte, 0, l) @@ -66,6 +77,11 @@ func (s *Scanner) Scan() bool { } s.payload = s.payload[:l] + if len(s.payload) != l { + s.err = ErrInvalidPktLen + return false + } + return true } @@ -76,8 +92,15 @@ func (s *Scanner) Bytes() []byte { return s.payload } +// PktType returns the type of packet, use this for special cases like +// flush, delim and end. +func (s *Scanner) PktType() PktType { + return s.pktType +} + // Method readPayloadLen returns the payload length by reading the -// pkt-len and subtracting the pkt-len size. +// pkt-len and subtracting the pkt-len size. For special purpose tokens +// like 0001 (delim) and 0002 (end) it returns -3 and -2. func (s *Scanner) readPayloadLen() (int, error) { if _, err := io.ReadFull(s.r, s.len[:]); err != nil { if err == io.ErrUnexpectedEOF { @@ -94,12 +117,20 @@ func (s *Scanner) readPayloadLen() (int, error) { switch { case n == 0: + s.pktType = FlushType + return 0, nil + case n == 1: + s.pktType = DelimType + return 0, nil + case n == 2: + s.pktType = EndType return 0, nil case n <= lenSize: return 0, ErrInvalidPktLen case n > OversizePayloadMax+lenSize: return 0, ErrInvalidPktLen default: + s.pktType = DataType return n - lenSize, nil } } diff --git a/plumbing/format/pktline/scanner_test.go b/plumbing/format/pktline/scanner_test.go index 60b622407..16e4121a7 100644 --- a/plumbing/format/pktline/scanner_test.go +++ b/plumbing/format/pktline/scanner_test.go @@ -18,8 +18,8 @@ var _ = Suite(&SuiteScanner{}) func (s *SuiteScanner) TestInvalid(c *C) { for _, test := range [...]string{ - "0001", "0002", "0003", "0004", - "0001asdfsadf", "0004foo", + "0003", "0004", + "0004foo", "fff5", "ffff", "gorka", "0", "003", @@ -181,7 +181,7 @@ func (s *SuiteScanner) TestReadSomeSections(c *C) { sectionCounter := 0 lineCounter := 0 for sc.Scan() { - if len(sc.Bytes()) == 0 { + if sc.PktType() == pktline.FlushType { sectionCounter++ } lineCounter++ diff --git a/plumbing/protocol/packp/advcaps.go b/plumbing/protocol/packp/advcaps.go new file mode 100644 index 000000000..935b3f941 --- /dev/null +++ b/plumbing/protocol/packp/advcaps.go @@ -0,0 +1,95 @@ +package packp + +import ( + "fmt" + "io" + "strings" + + "github.com/go-git/go-git/v5/plumbing/format/pktline" + "github.com/go-git/go-git/v5/plumbing/protocol/packp/capability" +) + +// AdvCaps values represent the information transmitted on an the first v2 +// message. Values from this type are not zero-value +// safe, use the New function instead. +type AdvCaps struct { + // Service represents the requested service. + Service string + // Capabilities are the capabilities. + Capabilities *capability.List +} + +// NewAdvCaps creates a new AdvCaps object, ready to be used. +func NewAdvCaps() *AdvCaps { + return &AdvCaps{ + Capabilities: capability.NewList(), + } +} + +// IsEmpty returns true if doesn't contain any capability. +func (a *AdvCaps) IsEmpty() bool { + return a.Capabilities.IsEmpty() +} + +func (a *AdvCaps) Encode(w io.Writer) error { + pe := pktline.NewEncoder(w) + pe.EncodeString("# service=" + a.Service + "\n") + pe.Flush() + pe.EncodeString("version 2\n") + + for _, c := range a.Capabilities.All() { + vals := a.Capabilities.Get(c) + if len(vals) > 0 { + pe.EncodeString(c.String() + "=" + strings.Join(vals, " ") + "\n") + } else { + pe.EncodeString(c.String() + "\n") + } + } + + return pe.Flush() +} + +func (a *AdvCaps) Decode(r io.Reader) error { + s := pktline.NewScanner(r) + + // decode # SP service= LF + s.Scan() + f := string(s.Bytes()) + if i := strings.Index(f, "service="); i < 0 { + return fmt.Errorf("missing service indication") + } + + a.Service = f[i+8 : len(f)-1] + + // scan flush + s.Scan() + if !isFlush(s.Bytes()) { + return fmt.Errorf("missing flush after service indication") + } + + // now version LF + s.Scan() + if string(s.Bytes()) != "version 2\n" { + return fmt.Errorf("missing version after flush") + } + + // now read capabilities + for s.Scan(); !isFlush(s.Bytes()); { + if sp := strings.Split(string(s.Bytes()), "="); len(sp) == 2 { + a.Capabilities.Add(capability.Capability((sp[0]))) + } else { + a.Capabilities.Add( + capability.Capability(sp[0]), + strings.Split(sp[1], " ")..., + ) + } + } + + // read final flush + s.Scan() + if !isFlush(s.Bytes()) { + return fmt.Errorf("missing flush after capability") + } + + return nil +} diff --git a/plumbing/protocol/packp/capability/capability.go b/plumbing/protocol/packp/capability/capability.go index b52e8a49d..77da2d9c6 100644 --- a/plumbing/protocol/packp/capability/capability.go +++ b/plumbing/protocol/packp/capability/capability.go @@ -241,6 +241,127 @@ const ( // Filter if present, fetch-pack may send "filter" commands to request a // partial clone or partial fetch and request that the server omit various objects from the packfile Filter Capability = "filter" + // LsRefs is the command used to request a reference advertisement in v2. + // Unlike the current reference advertisement, ls-refs takes in arguments + // which can be used to limit the refs sent from the server. + // + // Additional features not supported in the base command will be + // advertised as the value of the command in the capability + // advertisement in the form of a space separated list of features: + // "= " + // + // ls-refs takes in the following arguments: + // + // symrefs + // In addition to the object pointed by it, show the underlying ref + // pointed by it when showing a symbolic ref. + // peel + // Show peeled tags. + // ref-prefix + // When specified, only references having a prefix matching one of + // the provided prefixes are displayed. Multiple instances may be + // given, in which case references matching any prefix will be + // shown. Note that this is purely for optimization; a server MAY + // show refs not matching the prefix if it chooses, and clients + // should filter the result themselves. + LsRefs Capability = "ls-refs" + // Fetch is the command used to fetch a packfile in v2. It can be + // looked at as a modified version of the v1 fetch where the + // ref-advertisement is stripped out (since the ls-refs command fills + // that role) and the message format is tweaked to eliminate + // redundancies and permit easy addition of future extensions. + // + // Additional features not supported in the base command will be + // advertised as the value of the command in the capability + // advertisement in the form of a space separated list of features: + // "= " + // + // A fetch request can take the following arguments: + // + // want + // Indicates to the server an object which the client wants to + // retrieve. Wants can be anything and are not limited to + // advertised objects. + // + // have + // Indicates to the server an object which the client has locally. + // This allows the server to make a packfile which only contains + // the objects that the client needs. Multiple 'have' lines can be + // supplied. + // + // done + // Indicates to the server that negotiation should terminate (or + // not even begin if performing a clone) and that the server should + // use the information supplied in the request to construct the + // packfile. + // + // thin-pack + // Request that a thin pack be sent, which is a pack with deltas + // which reference base objects not contained within the pack (but + // are known to exist at the receiving end). This can reduce the + // network traffic significantly, but it requires the receiving end + // to know how to "thicken" these packs by adding the missing bases + // to the pack. + // + // no-progress + // Request that progress information that would normally be sent on + // side-band channel 2, during the packfile transfer, should not be + // sent. However, the side-band channel 3 is still used for error + // responses. + // + // include-tag + // Request that annotated tags should be sent if the objects they + // point to are being sent. + // + // ofs-delta + // Indicate that the client understands PACKv2 with delta referring + // to its base by position in pack rather than by an oid. That is, + // they can read OBJ_OFS_DELTA (aka type 6) in a packfile. + Fetch Capability = "fetch" + // ServerOption if advertised, indicates that any number of server + // specific options can be included in a request. This is done by + // sending each option as a "server-option=