aboutsummaryrefslogtreecommitdiff
path: root/object/commit/parse.go
blob: 090cf3561b9452829b8a69df34f48bebdb9a7bdb (about) (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
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
)