Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added initial support for git wire protocol v2 #876

Draft
wants to merge 21 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 18 additions & 0 deletions plumbing/format/pktline/encoder.go
Expand Up @@ -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")
Expand All @@ -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
}
blmayer marked this conversation as resolved.
Show resolved Hide resolved

// 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.
Expand Down
33 changes: 32 additions & 1 deletion plumbing/format/pktline/scanner.go
Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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)
Expand All @@ -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
}

Expand All @@ -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
}
Comment on lines +95 to +99
Copy link
Member

@pjbgf pjbgf Oct 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a comment around any diff in behaviour calling this on v0/v2? Assuming the former should never really happen.


// 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 {
Expand All @@ -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
}
}
Expand Down
6 changes: 3 additions & 3 deletions plumbing/format/pktline/scanner_test.go
Expand Up @@ -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",
Expand Down Expand Up @@ -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++
Expand Down
95 changes: 95 additions & 0 deletions 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")
}
Comment on lines +42 to +46
Copy link
Member

@pjbgf pjbgf Oct 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if len(vals) > 0 {
pe.EncodeString(c.String() + "=" + strings.Join(vals, " ") + "\n")
} else {
pe.EncodeString(c.String() + "\n")
}
line := c.String()
if len(vals) > 0 {
line = fmt.Sprintf("%s=%s, line, strings.Join(vals, " "))
}
pe.EncodeString(line + "\n")

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this suggestion works, since each invocation of EncodeString will create the line size prefix (part of the pktline protocol). And on v2 all that payload must be on the same line.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are absolutely right, updated it to handle that as a var of its own.

}

return pe.Flush()
}

func (a *AdvCaps) Decode(r io.Reader) error {
s := pktline.NewScanner(r)

// decode # SP service=<service> LF
s.Scan()
f := string(s.Bytes())
if i := strings.Index(f, "service="); i < 0 {
Copy link
Member

@pjbgf pjbgf Nov 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if i := strings.Index(f, "service="); i < 0 {
i := strings.Index(f, "service=")
if i < 0 {

return fmt.Errorf("missing service indication")
}

a.Service = f[i+8 : len(f)-1]

Check failure on line 62 in plumbing/protocol/packp/advcaps.go

View workflow job for this annotation

GitHub Actions / test (master, ubuntu-latest)

undefined: i

Check failure on line 62 in plumbing/protocol/packp/advcaps.go

View workflow job for this annotation

GitHub Actions / test (master, ubuntu-latest)

undefined: i

Check failure on line 62 in plumbing/protocol/packp/advcaps.go

View workflow job for this annotation

GitHub Actions / test (v2.11.0, ubuntu-latest)

undefined: i

Check failure on line 62 in plumbing/protocol/packp/advcaps.go

View workflow job for this annotation

GitHub Actions / test (v2.11.0, ubuntu-latest)

undefined: i

Check failure on line 62 in plumbing/protocol/packp/advcaps.go

View workflow job for this annotation

GitHub Actions / version-matrix (1.20.x, ubuntu-latest)

undefined: i

Check failure on line 62 in plumbing/protocol/packp/advcaps.go

View workflow job for this annotation

GitHub Actions / version-matrix (1.20.x, ubuntu-latest)

undefined: i

Check failure on line 62 in plumbing/protocol/packp/advcaps.go

View workflow job for this annotation

GitHub Actions / version-matrix (1.20.x, macos-latest)

undefined: i

Check failure on line 62 in plumbing/protocol/packp/advcaps.go

View workflow job for this annotation

GitHub Actions / version-matrix (1.20.x, macos-latest)

undefined: i

Check failure on line 62 in plumbing/protocol/packp/advcaps.go

View workflow job for this annotation

GitHub Actions / version-matrix (1.20.x, windows-latest)

undefined: i

Check failure on line 62 in plumbing/protocol/packp/advcaps.go

View workflow job for this annotation

GitHub Actions / version-matrix (1.20.x, windows-latest)

undefined: i

Check failure on line 62 in plumbing/protocol/packp/advcaps.go

View workflow job for this annotation

GitHub Actions / version-matrix (1.21.x, ubuntu-latest)

undefined: i

Check failure on line 62 in plumbing/protocol/packp/advcaps.go

View workflow job for this annotation

GitHub Actions / version-matrix (1.21.x, ubuntu-latest)

undefined: i

Check failure on line 62 in plumbing/protocol/packp/advcaps.go

View workflow job for this annotation

GitHub Actions / version-matrix (1.21.x, macos-latest)

undefined: i

Check failure on line 62 in plumbing/protocol/packp/advcaps.go

View workflow job for this annotation

GitHub Actions / version-matrix (1.21.x, macos-latest)

undefined: i

Check failure on line 62 in plumbing/protocol/packp/advcaps.go

View workflow job for this annotation

GitHub Actions / version-matrix (1.21.x, windows-latest)

undefined: i

Check failure on line 62 in plumbing/protocol/packp/advcaps.go

View workflow job for this annotation

GitHub Actions / version-matrix (1.21.x, windows-latest)

undefined: i

// 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], " ")...,
)
}
Comment on lines +78 to +85
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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], " ")...,
)
}
var values []string
if sp := strings.Split(string(s.Bytes()), "="); len(sp) != 2 {
values = strings.Split(sp[1], " ")
a.Capabilities.Add(capability.Capability((sp[0])))
}
a.Capabilities.Add(capability.Capability(sp[0]), values...)

}

// read final flush
s.Scan()
if !isFlush(s.Bytes()) {
return fmt.Errorf("missing flush after capability")
}

return nil
}
121 changes: 121 additions & 0 deletions plumbing/protocol/packp/capability/capability.go
Expand Up @@ -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:
// "<command>=<feature 1> <feature 2>"
//
// 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 <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:
// "<command>=<feature 1> <feature 2>"
//
// A fetch request can take the following arguments:
//
// want <oid>
// Indicates to the server an object which the client wants to
// retrieve. Wants can be anything and are not limited to
// advertised objects.
//
// have <oid>
// 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=<option>" capability line
// in the capability-list section of a request.
// The provided options must not contain a NUL or LF character.
ServerOption Capability = "server-option"
// ObjectInfo s the command to retrieve information about one or more
// objects. Its main purpose is to allow a client to make decisions
// based on this information without having to fully fetch objects.
// Object size is the only information that is currently supported.
//
// An object-info request takes the following arguments:
//
// size
// Requests size information to be returned for each listed object
// id.
//
// oid <oid>
// Indicates to the server an object which the client wants to
// obtain information for.
//
// The response of `object-info` is a list of the requested object ids
// and associated requested information, each separated by a single
// space.
//
// output = info flush-pkt
//
// info = PKT-LINE(attrs) LF)
// *PKT-LINE(obj-info LF)
//
// attrs = attr | attrs SP attrs
//
// attr = "size"
//
// obj-info = obj-id SP obj-size
ObjectInfo Capability = "object-info"
// Unborn the server will send information about HEAD even if it is a
// symref pointing to an unborn branch in the form "unborn HEAD
// symref-target:<target>".
Unborn Capability = "unborn"
// WaitForDone indicates to the server that it should never send
// "ready", but should wait for the client to say "done" before
// sending the packfile.
WaitForDone Capability = "wait-for-done"
)

const userAgent = "go-git/5.x"
Expand Down
1 change: 1 addition & 0 deletions plumbing/protocol/packp/common.go
Expand Up @@ -39,6 +39,7 @@ var (
// server-response
ack = []byte("ACK")
nak = []byte("NAK")
ready = []byte("ready")

// updreq
shallowNoSp = []byte("shallow")
Expand Down