1// SPDX-License-Identifier: MIT
2
3package git
4
5import (
6 "bytes"
7 "time"
8)
9
10// Signature is the author or committer line of a commit or the tagger of a
11// tag. Name and Email point into the buffer of the object they come from.
12type Signature struct {
13 Name []byte
14 Email []byte
15 Time int64 // seconds since the Unix epoch
16 Offset int // time zone, in seconds east of UTC
17}
18
19// Date returns the time of the signature, in its own time zone. It allocates.
20func (s Signature) Date() time.Time {
21 t := time.Unix(s.Time, 0)
22 if s.Offset == 0 {
23 return t.UTC()
24 }
25 return t.In(time.FixedZone("", s.Offset))
26}
27
28// Commit is a parsed commit. A Commit is meant to be reused: ReadCommit and
29// ParseCommit overwrite it and reuse its storage, so a hot loop allocates
30// nothing after the first few commits. Every []byte field is valid until the
31// next call that fills this Commit.
32type Commit struct {
33 ID ObjectID // set by ReadCommit
34 Tree ObjectID
35 Parents []ObjectID
36 Author Signature
37 Committer Signature
38 // Encoding is the value of the encoding header, nil when absent. The
39 // other headers (gpgsig, mergetag...) are skipped.
40 Encoding []byte
41 // Message is everything after the blank line that ends the headers,
42 // as raw bytes: it may not be valid UTF-8.
43 Message []byte
44
45 buf []byte
46}
47
48// ReadCommit reads and parses the commit id into c.
49func (r *Repo) ReadCommit(id ObjectID, c *Commit) error {
50 typ, data, err := r.ReadObject(id, c.buf)
51 if err != nil {
52 return err
53 }
54 c.buf = data
55 if typ != TypeCommit {
56 return wrongType(id, typ, TypeCommit)
57 }
58 if err := ParseCommit(data, c); err != nil {
59 return wrapObj(id, err)
60 }
61 c.ID = id
62 return nil
63}
64
65// ParseCommit parses the content of a commit object into c. The fields of c
66// point into data, which must stay unchanged while they are used.
67//
68// It reads headers the way Git does: the first line must be the tree, the
69// parent lines are the ones that immediately follow it, and any tree or
70// parent line further down is an ordinary, ignored header. It returns
71// ErrCorrupt if the first line is not a valid tree or a parent is malformed.
72func ParseCommit(data []byte, c *Commit) error {
73 c.ID = ObjectID{}
74 c.Parents = c.Parents[:0]
75 c.Author, c.Committer = Signature{}, Signature{}
76 c.Encoding, c.Message = nil, nil
77 const (
78 stTree = iota
79 stParents
80 stRest
81 )
82 state := stTree
83 for len(data) > 0 {
84 if data[0] == '\n' && state != stTree {
85 c.Message = data[1:]
86 break
87 }
88 var line []byte
89 if i := bytes.IndexByte(data, '\n'); i >= 0 {
90 line, data = data[:i], data[i+1:]
91 } else {
92 line, data = data, nil
93 }
94 key, val, _ := bytes.Cut(line, []byte{' '})
95 switch state {
96 case stTree:
97 id, ok := parseIDBytes(val)
98 if string(key) != "tree" || !ok {
99 return corrupt("commit: the first header is not a valid tree")
100 }
101 c.Tree = id
102 state = stParents
103 continue
104 case stParents:
105 if string(key) == "parent" {
106 id, ok := parseIDBytes(val)
107 if !ok {
108 return corrupt("commit: bad parent header")
109 }
110 c.Parents = append(c.Parents, id)
111 continue
112 }
113 state = stRest
114 }
115 if line[0] == ' ' {
116 continue // continuation of a multi-line header such as gpgsig
117 }
118 switch string(key) {
119 case "author":
120 c.Author = parseSignature(val)
121 case "committer":
122 c.Committer = parseSignature(val)
123 case "encoding":
124 c.Encoding = val
125 }
126 }
127 if state == stTree {
128 return corrupt("commit: no tree")
129 }
130 return nil
131}
132
133// parseSignature parses "Name <email> <seconds> <+hhmm>". It is as lenient
134// as Git: what cannot be parsed is left zero.
135func parseSignature(b []byte) Signature {
136 var s Signature
137 lt := bytes.IndexByte(b, '<')
138 gt := bytes.LastIndexByte(b, '>')
139 if lt < 0 || gt < lt {
140 s.Name = bytes.TrimSpace(b)
141 return s
142 }
143 s.Name = bytes.TrimRight(b[:lt], " \t\r\n")
144 s.Email = b[lt+1 : gt]
145 rest := bytes.TrimLeft(b[gt+1:], " ")
146 secs, rest, _ := bytes.Cut(rest, []byte{' '})
147 if t, ok := parseInt(secs); ok {
148 s.Time = t
149 if tz := bytes.TrimSpace(rest); len(tz) == 5 && (tz[0] == '+' || tz[0] == '-') {
150 hh, ok1 := parseInt(tz[1:3])
151 mm, ok2 := parseInt(tz[3:5])
152 if ok1 && ok2 {
153 off := int(hh*3600 + mm*60)
154 if tz[0] == '-' {
155 off = -off
156 }
157 s.Offset = off
158 }
159 }
160 }
161 return s
162}
163
164// parseInt parses a decimal integer of up to 18 digits.
165func parseInt(b []byte) (int64, bool) {
166 if len(b) == 0 || len(b) > 18 {
167 return 0, false
168 }
169 var v int64
170 for _, c := range b {
171 if c < '0' || c > '9' {
172 return 0, false
173 }
174 v = v*10 + int64(c-'0')
175 }
176 return v, true
177}
178
179// Tag is a parsed annotated tag. Name, Tagger and Message point into the
180// data that was parsed.
181type Tag struct {
182 Object ObjectID
183 Type Type // the type of Object
184 Name []byte
185 Tagger Signature
186 Message []byte
187}
188
189// ReadTag reads and parses the annotated tag id into t. The byte fields of t
190// are copies, owned by the caller.
191func (r *Repo) ReadTag(id ObjectID, t *Tag) error {
192 b, err := r.readTyped(id, TypeTag)
193 if err != nil {
194 return err
195 }
196 defer putBuf(b)
197 if err := ParseTag(b.b, t); err != nil {
198 return wrapObj(id, err)
199 }
200 // The pooled buffer goes back: detach the fields.
201 t.Name = bytes.Clone(t.Name)
202 t.Message = bytes.Clone(t.Message)
203 t.Tagger.Name = bytes.Clone(t.Tagger.Name)
204 t.Tagger.Email = bytes.Clone(t.Tagger.Email)
205 return nil
206}
207
208// ParseTag parses the content of a tag object into t, whose byte fields point
209// into data. It returns ErrCorrupt if the object or its type is missing.
210func ParseTag(data []byte, t *Tag) error {
211 *t = Tag{}
212 haveObj, haveType := false, false
213 for len(data) > 0 {
214 if data[0] == '\n' {
215 t.Message = data[1:]
216 break
217 }
218 var line []byte
219 if i := bytes.IndexByte(data, '\n'); i >= 0 {
220 line, data = data[:i], data[i+1:]
221 } else {
222 line, data = data, nil
223 }
224 key, val, _ := bytes.Cut(line, []byte{' '})
225 switch string(key) {
226 case "object":
227 id, ok := parseIDBytes(val)
228 if !ok {
229 return corrupt("tag: bad object header")
230 }
231 t.Object, haveObj = id, true
232 case "type":
233 typ, ok := parseTypeName(val)
234 if !ok {
235 return corrupt("tag: bad type header")
236 }
237 t.Type, haveType = typ, true
238 case "tag":
239 t.Name = val
240 case "tagger":
241 t.Tagger = parseSignature(val)
242 }
243 }
244 if !haveObj || !haveType {
245 return corrupt("tag: missing object or type")
246 }
247 return nil
248}