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) }