package packp
import (
"bytes"
"fmt"
"io"
"sort"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/format/pktline"
"github.com/go-git/go-git/v5/plumbing/protocol/packp/capability"
)
// Encode writes the AdvRefs encoding to a writer.
//
// All the payloads will end with a newline character. Capabilities,
// references and shallows are written in alphabetical order, except for
// peeled references that always follow their corresponding references.
func (a *AdvRefs) Encode(w io.Writer) error {
e := newAdvRefsEncoder(w)
return e.Encode(a)
}
type advRefsEncoder struct {
data *AdvRefs // data to encode
pe *pktline.Encoder // where to write the encoded data
firstRefName string // reference name to encode in the first pkt-line (HEAD if present)
firstRefHash plumbing.Hash // hash referenced to encode in the first pkt-line (HEAD if present)
sortedRefs []string // hash references to encode ordered by increasing order
err error // sticky error
}
func newAdvRefsEncoder(w io.Writer) *advRefsEncoder {
return &advRefsEncoder{
pe: pktline.NewEncoder(w),
}
}
func (e *advRefsEncoder) Encode(v *AdvRefs) error {
e.data = v
e.sortRefs()
e.setFirstRef()
for state := encodePrefix; state != nil; {
state = state(e)
}
return e.err
}
func (e *advRefsEncoder) sortRefs() {
if len(e.data.References) > 0 {
refs := make([]string, 0, len(e.data.References))
for refName := range e.data.References {
refs = append(refs, refName)
}
sort.Strings(refs)
e.sortedRefs = refs
}
}
func (e *advRefsEncoder) setFirstRef() {
if e.data.Head != nil {
e.firstRefName = head
e.firstRefHash = *e.data.Head
return
}
if len(e.sortedRefs) > 0 {
refName := e.sortedRefs[0]
e.firstRefName = refName
e.firstRefHash = e.data.References[refName]
}
}
type encoderStateFn func(*advRefsEncoder) encoderStateFn
func encodePrefix(e *advRefsEncoder) encoderStateFn {
for _, p := range e.data.Prefix {
if bytes.Equal(p, pktline.Flush) {
if e.err = e.pe.Flush(); e.err != nil {
return nil
}
continue
}
if e.err = e.pe.Encodef("%s\n", string(p)); e.err != nil {
return nil
}
}
return encodeFirstLine
}
// Adds the first pkt-line payload: head hash, head ref and capabilities.
// If HEAD ref is not found, the first reference ordered in increasing order will be used.
// If there aren't HEAD neither refs, the first line will be "PKT-LINE(zero-id SP "capabilities^{}" NUL capability-list)".
// See: https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt
// See: https://github.com/git/git/blob/master/Documentation/technical/protocol-common.txt
func encodeFirstLine(e *advRefsEncoder) encoderStateFn {
const formatFirstLine = "%s %s\x00%s\n"
var firstLine string
capabilities := formatCaps(e.data.Capabilities)
if e.firstRefName == "" {
firstLine = fmt.Sprintf(formatFirstLine, plumbing.ZeroHash.String(), "capabilities^{}", capabilities)
} else {
firstLine = fmt.Sprintf(formatFirstLine, e.firstRefHash.String(), e.firstRefName, capabilities)
}
if e.err = e.pe.EncodeString(firstLine); e.err != nil {
return nil
}
return encodeRefs
}
func formatCaps(c *capability.List) string {
if c == nil {
return ""
}
return c.String()
}
// Adds the (sorted) refs: hash SP refname EOL
// and their peeled refs if any.
func encodeRefs(e *advRefsEncoder) encoderStateFn {
for _, r := range e.sortedRefs {
if r == e.firstRefName {
continue
}
hash := e.data.References[r]
if e.err = e.pe.Encodef("%s %s\n", hash.String(), r); e.err != nil {
return nil
}
if hash, ok := e.data.Peeled[r]; ok {
if e.err = e.pe.Encodef("%s %s^{}\n", hash.String(), r); e.err != nil {
return nil
}
}
}
return encodeShallow
}
// Adds the (sorted) shallows: "shallow" SP hash EOL
func encodeShallow(e *advRefsEncoder) encoderStateFn {
sorted := sortShallows(e.data.Shallows)
for _, hash := range sorted {
if e.err = e.pe.Encodef("shallow %s\n", hash); e.err != nil {
return nil
}
}
return encodeFlush
}
func sortShallows(c []plumbing.Hash) []string {
ret := []string{}
for _, h := range c {
ret = append(ret, h.String())
}
sort.Strings(ret)
return ret
}
func encodeFlush(e *advRefsEncoder) encoderStateFn {
e.err = e.pe.Flush()
return nil
}