424 lines
10 KiB
Go
424 lines
10 KiB
Go
package object
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
|
|
"golang.org/x/crypto/openpgp"
|
|
|
|
"gopkg.in/src-d/go-git.v4/plumbing"
|
|
"gopkg.in/src-d/go-git.v4/plumbing/storer"
|
|
"gopkg.in/src-d/go-git.v4/utils/ioutil"
|
|
)
|
|
|
|
const (
|
|
beginpgp string = "-----BEGIN PGP SIGNATURE-----"
|
|
endpgp string = "-----END PGP SIGNATURE-----"
|
|
headerpgp string = "gpgsig"
|
|
)
|
|
|
|
// Hash represents the hash of an object
|
|
type Hash plumbing.Hash
|
|
|
|
// Commit points to a single tree, marking it as what the project looked like
|
|
// at a certain point in time. It contains meta-information about that point
|
|
// in time, such as a timestamp, the author of the changes since the last
|
|
// commit, a pointer to the previous commit(s), etc.
|
|
// http://shafiulazam.com/gitbook/1_the_git_object_model.html
|
|
type Commit struct {
|
|
// Hash of the commit object.
|
|
Hash plumbing.Hash
|
|
// Author is the original author of the commit.
|
|
Author Signature
|
|
// Committer is the one performing the commit, might be different from
|
|
// Author.
|
|
Committer Signature
|
|
// PGPSignature is the PGP signature of the commit.
|
|
PGPSignature string
|
|
// Message is the commit message, contains arbitrary text.
|
|
Message string
|
|
// TreeHash is the hash of the root tree of the commit.
|
|
TreeHash plumbing.Hash
|
|
// ParentHashes are the hashes of the parent commits of the commit.
|
|
ParentHashes []plumbing.Hash
|
|
|
|
s storer.EncodedObjectStorer
|
|
}
|
|
|
|
// GetCommit gets a commit from an object storer and decodes it.
|
|
func GetCommit(s storer.EncodedObjectStorer, h plumbing.Hash) (*Commit, error) {
|
|
o, err := s.EncodedObject(plumbing.CommitObject, h)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return DecodeCommit(s, o)
|
|
}
|
|
|
|
// DecodeCommit decodes an encoded object into a *Commit and associates it to
|
|
// the given object storer.
|
|
func DecodeCommit(s storer.EncodedObjectStorer, o plumbing.EncodedObject) (*Commit, error) {
|
|
c := &Commit{s: s}
|
|
if err := c.Decode(o); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
// Tree returns the Tree from the commit.
|
|
func (c *Commit) Tree() (*Tree, error) {
|
|
return GetTree(c.s, c.TreeHash)
|
|
}
|
|
|
|
// PatchContext returns the Patch between the actual commit and the provided one.
|
|
// Error will be return if context expires. Provided context must be non-nil.
|
|
func (c *Commit) PatchContext(ctx context.Context, to *Commit) (*Patch, error) {
|
|
fromTree, err := c.Tree()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
toTree, err := to.Tree()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return fromTree.PatchContext(ctx, toTree)
|
|
}
|
|
|
|
// Patch returns the Patch between the actual commit and the provided one.
|
|
func (c *Commit) Patch(to *Commit) (*Patch, error) {
|
|
return c.PatchContext(context.Background(), to)
|
|
}
|
|
|
|
// Parents return a CommitIter to the parent Commits.
|
|
func (c *Commit) Parents() CommitIter {
|
|
return NewCommitIter(c.s,
|
|
storer.NewEncodedObjectLookupIter(c.s, plumbing.CommitObject, c.ParentHashes),
|
|
)
|
|
}
|
|
|
|
// NumParents returns the number of parents in a commit.
|
|
func (c *Commit) NumParents() int {
|
|
return len(c.ParentHashes)
|
|
}
|
|
|
|
var ErrParentNotFound = errors.New("commit parent not found")
|
|
|
|
// Parent returns the ith parent of a commit.
|
|
func (c *Commit) Parent(i int) (*Commit, error) {
|
|
if len(c.ParentHashes) == 0 || i > len(c.ParentHashes)-1 {
|
|
return nil, ErrParentNotFound
|
|
}
|
|
|
|
return GetCommit(c.s, c.ParentHashes[i])
|
|
}
|
|
|
|
// File returns the file with the specified "path" in the commit and a
|
|
// nil error if the file exists. If the file does not exist, it returns
|
|
// a nil file and the ErrFileNotFound error.
|
|
func (c *Commit) File(path string) (*File, error) {
|
|
tree, err := c.Tree()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return tree.File(path)
|
|
}
|
|
|
|
// Files returns a FileIter allowing to iterate over the Tree
|
|
func (c *Commit) Files() (*FileIter, error) {
|
|
tree, err := c.Tree()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return tree.Files(), nil
|
|
}
|
|
|
|
// ID returns the object ID of the commit. The returned value will always match
|
|
// the current value of Commit.Hash.
|
|
//
|
|
// ID is present to fulfill the Object interface.
|
|
func (c *Commit) ID() plumbing.Hash {
|
|
return c.Hash
|
|
}
|
|
|
|
// Type returns the type of object. It always returns plumbing.CommitObject.
|
|
//
|
|
// Type is present to fulfill the Object interface.
|
|
func (c *Commit) Type() plumbing.ObjectType {
|
|
return plumbing.CommitObject
|
|
}
|
|
|
|
// Decode transforms a plumbing.EncodedObject into a Commit struct.
|
|
func (c *Commit) Decode(o plumbing.EncodedObject) (err error) {
|
|
if o.Type() != plumbing.CommitObject {
|
|
return ErrUnsupportedObject
|
|
}
|
|
|
|
c.Hash = o.Hash()
|
|
|
|
reader, err := o.Reader()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer ioutil.CheckClose(reader, &err)
|
|
|
|
r := bufio.NewReader(reader)
|
|
|
|
var message bool
|
|
var pgpsig bool
|
|
for {
|
|
line, err := r.ReadBytes('\n')
|
|
if err != nil && err != io.EOF {
|
|
return err
|
|
}
|
|
|
|
if pgpsig {
|
|
if len(line) > 0 && line[0] == ' ' {
|
|
line = bytes.TrimLeft(line, " ")
|
|
c.PGPSignature += string(line)
|
|
continue
|
|
} else {
|
|
pgpsig = false
|
|
}
|
|
}
|
|
|
|
if !message {
|
|
line = bytes.TrimSpace(line)
|
|
if len(line) == 0 {
|
|
message = true
|
|
continue
|
|
}
|
|
|
|
split := bytes.SplitN(line, []byte{' '}, 2)
|
|
|
|
var data []byte
|
|
if len(split) == 2 {
|
|
data = split[1]
|
|
}
|
|
|
|
switch string(split[0]) {
|
|
case "tree":
|
|
c.TreeHash = plumbing.NewHash(string(data))
|
|
case "parent":
|
|
c.ParentHashes = append(c.ParentHashes, plumbing.NewHash(string(data)))
|
|
case "author":
|
|
c.Author.Decode(data)
|
|
case "committer":
|
|
c.Committer.Decode(data)
|
|
case headerpgp:
|
|
c.PGPSignature += string(data) + "\n"
|
|
pgpsig = true
|
|
}
|
|
} else {
|
|
c.Message += string(line)
|
|
}
|
|
|
|
if err == io.EOF {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// Encode transforms a Commit into a plumbing.EncodedObject.
|
|
func (b *Commit) Encode(o plumbing.EncodedObject) error {
|
|
return b.encode(o, true)
|
|
}
|
|
|
|
func (b *Commit) encode(o plumbing.EncodedObject, includeSig bool) (err error) {
|
|
o.SetType(plumbing.CommitObject)
|
|
w, err := o.Writer()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer ioutil.CheckClose(w, &err)
|
|
|
|
if _, err = fmt.Fprintf(w, "tree %s\n", b.TreeHash.String()); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, parent := range b.ParentHashes {
|
|
if _, err = fmt.Fprintf(w, "parent %s\n", parent.String()); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if _, err = fmt.Fprint(w, "author "); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = b.Author.Encode(w); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err = fmt.Fprint(w, "\ncommitter "); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = b.Committer.Encode(w); err != nil {
|
|
return err
|
|
}
|
|
|
|
if b.PGPSignature != "" && includeSig {
|
|
if _, err = fmt.Fprint(w, "\n"+headerpgp+" "); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Split all the signature lines and re-write with a left padding and
|
|
// newline. Use join for this so it's clear that a newline should not be
|
|
// added after this section, as it will be added when the message is
|
|
// printed.
|
|
signature := strings.TrimSuffix(b.PGPSignature, "\n")
|
|
lines := strings.Split(signature, "\n")
|
|
if _, err = fmt.Fprint(w, strings.Join(lines, "\n ")); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if _, err = fmt.Fprintf(w, "\n\n%s", b.Message); err != nil {
|
|
return err
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// Stats returns the stats of a commit.
|
|
func (c *Commit) Stats() (FileStats, error) {
|
|
return c.StatsContext(context.Background())
|
|
}
|
|
|
|
// StatsContext returns the stats of a commit. Error will be return if context
|
|
// expires. Provided context must be non-nil.
|
|
func (c *Commit) StatsContext(ctx context.Context) (FileStats, error) {
|
|
fromTree, err := c.Tree()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
toTree := &Tree{}
|
|
if c.NumParents() != 0 {
|
|
firstParent, err := c.Parents().Next()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
toTree, err = firstParent.Tree()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
patch, err := toTree.PatchContext(ctx, fromTree)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return getFileStatsFromFilePatches(patch.FilePatches()), nil
|
|
}
|
|
|
|
func (c *Commit) String() string {
|
|
return fmt.Sprintf(
|
|
"%s %s\nAuthor: %s\nDate: %s\n\n%s\n",
|
|
plumbing.CommitObject, c.Hash, c.Author.String(),
|
|
c.Author.When.Format(DateFormat), indent(c.Message),
|
|
)
|
|
}
|
|
|
|
// Verify performs PGP verification of the commit with a provided armored
|
|
// keyring and returns openpgp.Entity associated with verifying key on success.
|
|
func (c *Commit) Verify(armoredKeyRing string) (*openpgp.Entity, error) {
|
|
keyRingReader := strings.NewReader(armoredKeyRing)
|
|
keyring, err := openpgp.ReadArmoredKeyRing(keyRingReader)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Extract signature.
|
|
signature := strings.NewReader(c.PGPSignature)
|
|
|
|
encoded := &plumbing.MemoryObject{}
|
|
// Encode commit components, excluding signature and get a reader object.
|
|
if err := c.encode(encoded, false); err != nil {
|
|
return nil, err
|
|
}
|
|
er, err := encoded.Reader()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return openpgp.CheckArmoredDetachedSignature(keyring, er, signature)
|
|
}
|
|
|
|
func indent(t string) string {
|
|
var output []string
|
|
for _, line := range strings.Split(t, "\n") {
|
|
if len(line) != 0 {
|
|
line = " " + line
|
|
}
|
|
|
|
output = append(output, line)
|
|
}
|
|
|
|
return strings.Join(output, "\n")
|
|
}
|
|
|
|
// CommitIter is a generic closable interface for iterating over commits.
|
|
type CommitIter interface {
|
|
Next() (*Commit, error)
|
|
ForEach(func(*Commit) error) error
|
|
Close()
|
|
}
|
|
|
|
// storerCommitIter provides an iterator from commits in an EncodedObjectStorer.
|
|
type storerCommitIter struct {
|
|
storer.EncodedObjectIter
|
|
s storer.EncodedObjectStorer
|
|
}
|
|
|
|
// NewCommitIter takes a storer.EncodedObjectStorer and a
|
|
// storer.EncodedObjectIter and returns a CommitIter that iterates over all
|
|
// commits contained in the storer.EncodedObjectIter.
|
|
//
|
|
// Any non-commit object returned by the storer.EncodedObjectIter is skipped.
|
|
func NewCommitIter(s storer.EncodedObjectStorer, iter storer.EncodedObjectIter) CommitIter {
|
|
return &storerCommitIter{iter, s}
|
|
}
|
|
|
|
// Next moves the iterator to the next commit and returns a pointer to it. If
|
|
// there are no more commits, it returns io.EOF.
|
|
func (iter *storerCommitIter) Next() (*Commit, error) {
|
|
obj, err := iter.EncodedObjectIter.Next()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return DecodeCommit(iter.s, obj)
|
|
}
|
|
|
|
// ForEach call the cb function for each commit contained on this iter until
|
|
// an error appends or the end of the iter is reached. If ErrStop is sent
|
|
// the iteration is stopped but no error is returned. The iterator is closed.
|
|
func (iter *storerCommitIter) ForEach(cb func(*Commit) error) error {
|
|
return iter.EncodedObjectIter.ForEach(func(obj plumbing.EncodedObject) error {
|
|
c, err := DecodeCommit(iter.s, obj)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return cb(c)
|
|
})
|
|
}
|
|
|
|
func (iter *storerCommitIter) Close() {
|
|
iter.EncodedObjectIter.Close()
|
|
}
|