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