package packp
import (
"bytes"
"fmt"
"io"
"strings"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/format/pktline"
)
const (
ok = "ok"
)
// ReportStatus is a report status message, as used in the git-receive-pack
// process whenever the 'report-status' capability is negotiated.
type ReportStatus struct {
UnpackStatus string
CommandStatuses []*CommandStatus
}
// NewReportStatus creates a new ReportStatus message.
func NewReportStatus() *ReportStatus {
return &ReportStatus{}
}
// Error returns the first error if any.
func (s *ReportStatus) Error() error {
if s.UnpackStatus != ok {
return fmt.Errorf("unpack error: %s", s.UnpackStatus)
}
for _, s := range s.CommandStatuses {
if err := s.Error(); err != nil {
return err
}
}
return nil
}
// Encode writes the report status to a writer.
func (s *ReportStatus) Encode(w io.Writer) error {
e := pktline.NewEncoder(w)
if err := e.Encodef("unpack %s\n", s.UnpackStatus); err != nil {
return err
}
for _, cs := range s.CommandStatuses {
if err := cs.encode(w); err != nil {
return err
}
}
return e.Flush()
}
// Decode reads from the given reader and decodes a report-status message. It
// does not read more input than what is needed to fill the report status.
func (s *ReportStatus) Decode(r io.Reader) error {
scan := pktline.NewScanner(r)
if err := s.scanFirstLine(scan); err != nil {
return err
}
if err := s.decodeReportStatus(scan.Bytes()); err != nil {
return err
}
flushed := false
for scan.Scan() {
b := scan.Bytes()
if isFlush(b) {
flushed = true
break
}
if err := s.decodeCommandStatus(b); err != nil {
return err
}
}
if !flushed {
return fmt.Errorf("missing flush")
}
return scan.Err()
}
func (s *ReportStatus) scanFirstLine(scan *pktline.Scanner) error {
if scan.Scan() {
return nil
}
if scan.Err() != nil {
return scan.Err()
}
return io.ErrUnexpectedEOF
}
func (s *ReportStatus) decodeReportStatus(b []byte) error {
if isFlush(b) {
return fmt.Errorf("premature flush")
}
b = bytes.TrimSuffix(b, eol)
line := string(b)
fields := strings.SplitN(line, " ", 2)
if len(fields) != 2 || fields[0] != "unpack" {
return fmt.Errorf("malformed unpack status: %s", line)
}
s.UnpackStatus = fields[1]
return nil
}
func (s *ReportStatus) decodeCommandStatus(b []byte) error {
b = bytes.TrimSuffix(b, eol)
line := string(b)
fields := strings.SplitN(line, " ", 3)
status := ok
if len(fields) == 3 && fields[0] == "ng" {
status = fields[2]
} else if len(fields) != 2 || fields[0] != "ok" {
return fmt.Errorf("malformed command status: %s", line)
}
cs := &CommandStatus{
ReferenceName: plumbing.ReferenceName(fields[1]),
Status: status,
}
s.CommandStatuses = append(s.CommandStatuses, cs)
return nil
}
// CommandStatus is the status of a reference in a report status.
// See ReportStatus struct.
type CommandStatus struct {
ReferenceName plumbing.ReferenceName
Status string
}
// Error returns the error, if any.
func (s *CommandStatus) Error() error {
if s.Status == ok {
return nil
}
return fmt.Errorf("command error on %s: %s",
s.ReferenceName.String(), s.Status)
}
func (s *CommandStatus) encode(w io.Writer) error {
e := pktline.NewEncoder(w)
if s.Error() == nil {
return e.Encodef("ok %s\n", s.ReferenceName.String())
}
return e.Encodef("ng %s %s\n", s.ReferenceName.String(), s.Status)
}