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