1package object
2
3import (
4 "bytes"
5 "context"
6 "errors"
7 "fmt"
8 "io"
9 "strings"
10
11 "github.com/ProtonMail/go-crypto/openpgp"
12
13 "github.com/go-git/go-git/v5/plumbing"
14 "github.com/go-git/go-git/v5/plumbing/storer"
15 "github.com/go-git/go-git/v5/utils/ioutil"
16 "github.com/go-git/go-git/v5/utils/sync"
17)
18
19const (
20 beginpgp string = "-----BEGIN PGP SIGNATURE-----"
21 endpgp string = "-----END PGP SIGNATURE-----"
22 headerpgp string = "gpgsig"
23 headerencoding string = "encoding"
24
25 // https://github.com/git/git/blob/bcb6cae2966cc407ca1afc77413b3ef11103c175/Documentation/gitformat-signature.txt#L153
26 // When a merge commit is created from a signed tag, the tag is embedded in
27 // the commit with the "mergetag" header.
28 headermergetag string = "mergetag"
29
30 defaultUtf8CommitMessageEncoding MessageEncoding = "UTF-8"
31)
32
33// Hash represents the hash of an object
34type Hash plumbing.Hash
35
36// MessageEncoding represents the encoding of a commit
37type MessageEncoding string
38
39// Commit points to a single tree, marking it as what the project looked like
40// at a certain point in time. It contains meta-information about that point
41// in time, such as a timestamp, the author of the changes since the last
42// commit, a pointer to the previous commit(s), etc.
43// http://shafiulazam.com/gitbook/1_the_git_object_model.html
44type Commit struct {
45 // Hash of the commit object.
46 Hash plumbing.Hash
47 // Author is the original author of the commit.
48 Author Signature
49 // Committer is the one performing the commit, might be different from
50 // Author.
51 Committer Signature
52 // MergeTag is the embedded tag object when a merge commit is created by
53 // merging a signed tag.
54 MergeTag string
55 // PGPSignature is the PGP signature of the commit.
56 PGPSignature string
57 // Message is the commit message, contains arbitrary text.
58 Message string
59 // TreeHash is the hash of the root tree of the commit.
60 TreeHash plumbing.Hash
61 // ParentHashes are the hashes of the parent commits of the commit.
62 ParentHashes []plumbing.Hash
63 // Encoding is the encoding of the commit.
64 Encoding MessageEncoding
65
66 ExtraHeaders map[string][]byte
67
68 s storer.EncodedObjectStorer
69}
70
71// GetCommit gets a commit from an object storer and decodes it.
72func GetCommit(s storer.EncodedObjectStorer, h plumbing.Hash) (*Commit, error) {
73 o, err := s.EncodedObject(plumbing.CommitObject, h)
74 if err != nil {
75 return nil, err
76 }
77
78 return DecodeCommit(s, o)
79}
80
81// DecodeCommit decodes an encoded object into a *Commit and associates it to
82// the given object storer.
83func DecodeCommit(s storer.EncodedObjectStorer, o plumbing.EncodedObject) (*Commit, error) {
84 c := &Commit{s: s}
85 if err := c.Decode(o); err != nil {
86 return nil, err
87 }
88
89 return c, nil
90}
91
92// Tree returns the Tree from the commit.
93func (c *Commit) Tree() (*Tree, error) {
94 return GetTree(c.s, c.TreeHash)
95}
96
97// PatchContext returns the Patch between the actual commit and the provided one.
98// Error will be return if context expires. Provided context must be non-nil.
99//
100// NOTE: Since version 5.1.0 the renames are correctly handled, the settings
101// used are the recommended options DefaultDiffTreeOptions.
102func (c *Commit) PatchContext(ctx context.Context, to *Commit) (*Patch, error) {
103 fromTree, err := c.Tree()
104 if err != nil {
105 return nil, err
106 }
107
108 var toTree *Tree
109 if to != nil {
110 toTree, err = to.Tree()
111 if err != nil {
112 return nil, err
113 }
114 }
115
116 return fromTree.PatchContext(ctx, toTree)
117}
118
119// Patch returns the Patch between the actual commit and the provided one.
120//
121// NOTE: Since version 5.1.0 the renames are correctly handled, the settings
122// used are the recommended options DefaultDiffTreeOptions.
123func (c *Commit) Patch(to *Commit) (*Patch, error) {
124 return c.PatchContext(context.Background(), to)
125}
126
127// Parents return a CommitIter to the parent Commits.
128func (c *Commit) Parents() CommitIter {
129 return NewCommitIter(c.s,
130 storer.NewEncodedObjectLookupIter(c.s, plumbing.CommitObject, c.ParentHashes),
131 )
132}
133
134// NumParents returns the number of parents in a commit.
135func (c *Commit) NumParents() int {
136 return len(c.ParentHashes)
137}
138
139var ErrParentNotFound = errors.New("commit parent not found")
140
141// Parent returns the ith parent of a commit.
142func (c *Commit) Parent(i int) (*Commit, error) {
143 if len(c.ParentHashes) == 0 || i > len(c.ParentHashes)-1 {
144 return nil, ErrParentNotFound
145 }
146
147 return GetCommit(c.s, c.ParentHashes[i])
148}
149
150// File returns the file with the specified "path" in the commit and a
151// nil error if the file exists. If the file does not exist, it returns
152// a nil file and the ErrFileNotFound error.
153func (c *Commit) File(path string) (*File, error) {
154 tree, err := c.Tree()
155 if err != nil {
156 return nil, err
157 }
158
159 return tree.File(path)
160}
161
162// Files returns a FileIter allowing to iterate over the Tree
163func (c *Commit) Files() (*FileIter, error) {
164 tree, err := c.Tree()
165 if err != nil {
166 return nil, err
167 }
168
169 return tree.Files(), nil
170}
171
172// ID returns the object ID of the commit. The returned value will always match
173// the current value of Commit.Hash.
174//
175// ID is present to fulfill the Object interface.
176func (c *Commit) ID() plumbing.Hash {
177 return c.Hash
178}
179
180// Type returns the type of object. It always returns plumbing.CommitObject.
181//
182// Type is present to fulfill the Object interface.
183func (c *Commit) Type() plumbing.ObjectType {
184 return plumbing.CommitObject
185}
186
187// Decode transforms a plumbing.EncodedObject into a Commit struct.
188func (c *Commit) Decode(o plumbing.EncodedObject) (err error) {
189 if o.Type() != plumbing.CommitObject {
190 return ErrUnsupportedObject
191 }
192
193 c.Hash = o.Hash()
194 c.Encoding = defaultUtf8CommitMessageEncoding
195 c.ExtraHeaders = make(map[string][]byte)
196
197 reader, err := o.Reader()
198 if err != nil {
199 return err
200 }
201 defer ioutil.CheckClose(reader, &err)
202
203 r := sync.GetBufioReader(reader)
204 defer sync.PutBufioReader(r)
205
206 var message bool
207 var mergetag bool
208 var pgpsig bool
209 var msgbuf bytes.Buffer
210 for {
211 line, err := r.ReadBytes('\n')
212 if err != nil && err != io.EOF {
213 return err
214 }
215
216 if mergetag {
217 if len(line) > 0 && line[0] == ' ' {
218 line = bytes.TrimLeft(line, " ")
219 c.MergeTag += string(line)
220 continue
221 } else {
222 mergetag = false
223 }
224 }
225
226 if pgpsig {
227 if len(line) > 0 && line[0] == ' ' {
228 line = bytes.TrimLeft(line, " ")
229 c.PGPSignature += string(line)
230 continue
231 } else {
232 pgpsig = false
233 }
234 }
235
236 if !message {
237 line = bytes.TrimSpace(line)
238 if len(line) == 0 {
239 message = true
240 continue
241 }
242
243 split := bytes.SplitN(line, []byte{' '}, 2)
244
245 var data []byte
246 if len(split) == 2 {
247 data = split[1]
248 }
249
250 first := string(split[0])
251
252 switch first {
253 case "tree":
254 c.TreeHash = plumbing.NewHash(string(data))
255 case "parent":
256 c.ParentHashes = append(c.ParentHashes, plumbing.NewHash(string(data)))
257 case "author":
258 c.Author.Decode(data)
259 case "committer":
260 c.Committer.Decode(data)
261 case headermergetag:
262 c.MergeTag += string(data) + "\n"
263 mergetag = true
264 case headerencoding:
265 c.Encoding = MessageEncoding(data)
266 case headerpgp:
267 c.PGPSignature += string(data) + "\n"
268 pgpsig = true
269 default:
270 c.ExtraHeaders[first] = data
271 }
272 } else {
273 msgbuf.Write(line)
274 }
275
276 if err == io.EOF {
277 break
278 }
279 }
280 c.Message = msgbuf.String()
281 return nil
282}
283
284// Encode transforms a Commit into a plumbing.EncodedObject.
285func (c *Commit) Encode(o plumbing.EncodedObject) error {
286 return c.encode(o, true)
287}
288
289// EncodeWithoutSignature export a Commit into a plumbing.EncodedObject without the signature (correspond to the payload of the PGP signature).
290func (c *Commit) EncodeWithoutSignature(o plumbing.EncodedObject) error {
291 return c.encode(o, false)
292}
293
294func (c *Commit) encode(o plumbing.EncodedObject, includeSig bool) (err error) {
295 o.SetType(plumbing.CommitObject)
296 w, err := o.Writer()
297 if err != nil {
298 return err
299 }
300
301 defer ioutil.CheckClose(w, &err)
302
303 if _, err = fmt.Fprintf(w, "tree %s\n", c.TreeHash.String()); err != nil {
304 return err
305 }
306
307 for _, parent := range c.ParentHashes {
308 if _, err = fmt.Fprintf(w, "parent %s\n", parent.String()); err != nil {
309 return err
310 }
311 }
312
313 if _, err = fmt.Fprint(w, "author "); err != nil {
314 return err
315 }
316
317 if err = c.Author.Encode(w); err != nil {
318 return err
319 }
320
321 if _, err = fmt.Fprint(w, "\ncommitter "); err != nil {
322 return err
323 }
324
325 if err = c.Committer.Encode(w); err != nil {
326 return err
327 }
328
329 if c.MergeTag != "" {
330 if _, err = fmt.Fprint(w, "\n"+headermergetag+" "); err != nil {
331 return err
332 }
333
334 // Split tag information lines and re-write with a left padding and
335 // newline. Use join for this so it's clear that a newline should not be
336 // added after this section. The newline will be added either as part of
337 // the PGP signature or the commit message.
338 mergetag := strings.TrimSuffix(c.MergeTag, "\n")
339 lines := strings.Split(mergetag, "\n")
340 if _, err = fmt.Fprint(w, strings.Join(lines, "\n ")); err != nil {
341 return err
342 }
343 }
344
345 if string(c.Encoding) != "" && c.Encoding != defaultUtf8CommitMessageEncoding {
346 if _, err = fmt.Fprintf(w, "\n%s %s", headerencoding, c.Encoding); err != nil {
347 return err
348 }
349 }
350
351 if c.PGPSignature != "" && includeSig {
352 if _, err = fmt.Fprint(w, "\n"+headerpgp+" "); err != nil {
353 return err
354 }
355
356 // Split all the signature lines and re-write with a left padding and
357 // newline. Use join for this so it's clear that a newline should not be
358 // added after this section, as it will be added when the message is
359 // printed.
360 signature := strings.TrimSuffix(c.PGPSignature, "\n")
361 lines := strings.Split(signature, "\n")
362 if _, err = fmt.Fprint(w, strings.Join(lines, "\n ")); err != nil {
363 return err
364 }
365 }
366
367 // encode any extra headers
368 for header, val := range c.ExtraHeaders {
369 fmt.Fprint(w, "\n"+header+" "+string(val))
370 }
371
372 if _, err = fmt.Fprintf(w, "\n\n%s", c.Message); err != nil {
373 return err
374 }
375
376 return err
377}
378
379// Stats returns the stats of a commit.
380func (c *Commit) Stats() (FileStats, error) {
381 return c.StatsContext(context.Background())
382}
383
384// StatsContext returns the stats of a commit. Error will be return if context
385// expires. Provided context must be non-nil.
386func (c *Commit) StatsContext(ctx context.Context) (FileStats, error) {
387 fromTree, err := c.Tree()
388 if err != nil {
389 return nil, err
390 }
391
392 toTree := &Tree{}
393 if c.NumParents() != 0 {
394 firstParent, err := c.Parents().Next()
395 if err != nil {
396 return nil, err
397 }
398
399 toTree, err = firstParent.Tree()
400 if err != nil {
401 return nil, err
402 }
403 }
404
405 patch, err := toTree.PatchContext(ctx, fromTree)
406 if err != nil {
407 return nil, err
408 }
409
410 return getFileStatsFromFilePatches(patch.FilePatches()), nil
411}
412
413func (c *Commit) String() string {
414 return fmt.Sprintf(
415 "%s %s\nAuthor: %s\nDate: %s\n\n%s\n",
416 plumbing.CommitObject, c.Hash, c.Author.String(),
417 c.Author.When.Format(DateFormat), indent(c.Message),
418 )
419}
420
421// Verify performs PGP verification of the commit with a provided armored
422// keyring and returns openpgp.Entity associated with verifying key on success.
423func (c *Commit) Verify(armoredKeyRing string) (*openpgp.Entity, error) {
424 keyRingReader := strings.NewReader(armoredKeyRing)
425 keyring, err := openpgp.ReadArmoredKeyRing(keyRingReader)
426 if err != nil {
427 return nil, err
428 }
429
430 // Extract signature.
431 signature := strings.NewReader(c.PGPSignature)
432
433 encoded := &plumbing.MemoryObject{}
434 // Encode commit components, excluding signature and get a reader object.
435 if err := c.EncodeWithoutSignature(encoded); err != nil {
436 return nil, err
437 }
438 er, err := encoded.Reader()
439 if err != nil {
440 return nil, err
441 }
442
443 return openpgp.CheckArmoredDetachedSignature(keyring, er, signature, nil)
444}
445
446// Less defines a compare function to determine which commit is 'earlier' by:
447// - First use Committer.When
448// - If Committer.When are equal then use Author.When
449// - If Author.When also equal then compare the string value of the hash
450func (c *Commit) Less(rhs *Commit) bool {
451 return c.Committer.When.Before(rhs.Committer.When) ||
452 (c.Committer.When.Equal(rhs.Committer.When) &&
453 (c.Author.When.Before(rhs.Author.When) ||
454 (c.Author.When.Equal(rhs.Author.When) && bytes.Compare(c.Hash[:], rhs.Hash[:]) < 0)))
455}
456
457func indent(t string) string {
458 var output []string
459 for _, line := range strings.Split(t, "\n") {
460 if len(line) != 0 {
461 line = " " + line
462 }
463
464 output = append(output, line)
465 }
466
467 return strings.Join(output, "\n")
468}
469
470// CommitIter is a generic closable interface for iterating over commits.
471type CommitIter interface {
472 Next() (*Commit, error)
473 ForEach(func(*Commit) error) error
474 Close()
475}
476
477// storerCommitIter provides an iterator from commits in an EncodedObjectStorer.
478type storerCommitIter struct {
479 storer.EncodedObjectIter
480 s storer.EncodedObjectStorer
481}
482
483// NewCommitIter takes a storer.EncodedObjectStorer and a
484// storer.EncodedObjectIter and returns a CommitIter that iterates over all
485// commits contained in the storer.EncodedObjectIter.
486//
487// Any non-commit object returned by the storer.EncodedObjectIter is skipped.
488func NewCommitIter(s storer.EncodedObjectStorer, iter storer.EncodedObjectIter) CommitIter {
489 return &storerCommitIter{iter, s}
490}
491
492// Next moves the iterator to the next commit and returns a pointer to it. If
493// there are no more commits, it returns io.EOF.
494func (iter *storerCommitIter) Next() (*Commit, error) {
495 obj, err := iter.EncodedObjectIter.Next()
496 if err != nil {
497 return nil, err
498 }
499
500 return DecodeCommit(iter.s, obj)
501}
502
503// ForEach call the cb function for each commit contained on this iter until
504// an error appends or the end of the iter is reached. If ErrStop is sent
505// the iteration is stopped but no error is returned. The iterator is closed.
506func (iter *storerCommitIter) ForEach(cb func(*Commit) error) error {
507 return iter.EncodedObjectIter.ForEach(func(obj plumbing.EncodedObject) error {
508 c, err := DecodeCommit(iter.s, obj)
509 if err != nil {
510 return err
511 }
512
513 return cb(c)
514 })
515}
516
517func (iter *storerCommitIter) Close() {
518 iter.EncodedObjectIter.Close()
519}