package commit import ( "bytes" "errors" "fmt" "lindenii.org/go/furgit/object/id" "lindenii.org/go/furgit/object/signature" ) // ErrInvalidCommit indicates a malformed commit object. var ErrInvalidCommit = errors.New("object/commit: invalid commit") // Parse decodes a commit object body. func Parse(body []byte, objectFormat id.ObjectFormat) (*Commit, error) { c := new(Commit) i := 0 state := parseStateTree sawHeaderEnd := false for i < len(body) { lineStart := i rel := bytes.IndexByte(body[i:], '\n') if rel < 0 { return nil, fmt.Errorf("%w: unterminated header line at offset %d", ErrInvalidCommit, lineStart) } line := body[i : i+rel] i += rel + 1 if len(line) == 0 { sawHeaderEnd = true break } key, value, found := bytes.Cut(line, []byte{' '}) if !found { return nil, fmt.Errorf("%w: header line at offset %d has no ' ' separator", ErrInvalidCommit, lineStart) } switch string(key) { case "tree": if state != parseStateTree { return nil, fmt.Errorf("%w: unexpected tree header at offset %d", ErrInvalidCommit, lineStart) } id, err := objectFormat.FromString(string(value)) if err != nil { return nil, fmt.Errorf("%w: tree: %w", ErrInvalidCommit, err) } c.Tree = id state = parseStateParentOrAuthor case "parent": if state != parseStateParentOrAuthor { return nil, fmt.Errorf("%w: unexpected parent header at offset %d", ErrInvalidCommit, lineStart) } id, err := objectFormat.FromString(string(value)) if err != nil { return nil, fmt.Errorf("%w: parent: %w", ErrInvalidCommit, err) } c.Parents = append(c.Parents, id) case "author": if state != parseStateParentOrAuthor { return nil, fmt.Errorf("%w: unexpected author header at offset %d", ErrInvalidCommit, lineStart) } idt, err := signature.Parse(value) if err != nil { return nil, fmt.Errorf("%w: author: %w", ErrInvalidCommit, err) } c.Author = *idt state = parseStateCommitter case "committer": if state != parseStateCommitter { return nil, fmt.Errorf("%w: unexpected committer header at offset %d", ErrInvalidCommit, lineStart) } idt, err := signature.Parse(value) if err != nil { return nil, fmt.Errorf("%w: committer: %w", ErrInvalidCommit, err) } c.Committer = *idt state = parseStateExtra case "change-id": if state != parseStateExtra { return nil, fmt.Errorf("%w: unexpected change-id header at offset %d", ErrInvalidCommit, lineStart) } c.ChangeID = string(value) case "gpgsig", "gpgsig-sha256": if state != parseStateExtra { return nil, fmt.Errorf("%w: unexpected %s header at offset %d", ErrInvalidCommit, key, lineStart) } for i < len(body) { nextRel := bytes.IndexByte(body[i:], '\n') if nextRel < 0 { return nil, fmt.Errorf("%w: unterminated signature header at offset %d", ErrInvalidCommit, i) } if body[i] != ' ' { break } i += nextRel + 1 } default: if state != parseStateExtra { return nil, fmt.Errorf("%w: unexpected %s header at offset %d", ErrInvalidCommit, key, lineStart) } c.ExtraHeaders = append(c.ExtraHeaders, ExtraHeader{ Key: string(key), Value: append([]byte(nil), value...), }) } } if !sawHeaderEnd { return nil, fmt.Errorf("%w: missing blank line before message", ErrInvalidCommit) } switch state { case parseStateTree: return nil, fmt.Errorf("%w: missing tree header", ErrInvalidCommit) case parseStateParentOrAuthor: return nil, fmt.Errorf("%w: missing author header", ErrInvalidCommit) case parseStateCommitter: return nil, fmt.Errorf("%w: missing committer header", ErrInvalidCommit) case parseStateExtra: default: panic("unreachable parse state") } c.Message = append([]byte(nil), body[i:]...) return c, nil } type parseState uint8 const ( parseStateTree parseState = iota parseStateParentOrAuthor parseStateCommitter parseStateExtra )