mirror of
https://github.com/cheat/cheat.git
synced 2026-05-27 19:48:44 +02:00
4799c2cb17
Bumps [github.com/go-git/go-git/v5](https://github.com/go-git/go-git) from 5.16.5 to 5.19.1. - [Release notes](https://github.com/go-git/go-git/releases) - [Changelog](https://github.com/go-git/go-git/blob/main/HISTORY.md) - [Commits](https://github.com/go-git/go-git/compare/v5.16.5...v5.19.1) --- updated-dependencies: - dependency-name: github.com/go-git/go-git/v5 dependency-version: 5.19.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com>
599 lines
17 KiB
Go
599 lines
17 KiB
Go
package object
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/ProtonMail/go-crypto/openpgp"
|
|
|
|
"github.com/go-git/go-git/v5/plumbing"
|
|
"github.com/go-git/go-git/v5/plumbing/storer"
|
|
"github.com/go-git/go-git/v5/utils/ioutil"
|
|
"github.com/go-git/go-git/v5/utils/sync"
|
|
)
|
|
|
|
const (
|
|
beginpgp string = "-----BEGIN PGP SIGNATURE-----"
|
|
endpgp string = "-----END PGP SIGNATURE-----"
|
|
headerpgp string = "gpgsig"
|
|
headerpgp256 string = "gpgsig-sha256"
|
|
headerencoding string = "encoding"
|
|
|
|
// https://github.com/git/git/blob/bcb6cae2966cc407ca1afc77413b3ef11103c175/Documentation/gitformat-signature.txt#L153
|
|
// When a merge commit is created from a signed tag, the tag is embedded in
|
|
// the commit with the "mergetag" header.
|
|
headermergetag string = "mergetag"
|
|
|
|
defaultUtf8CommitMessageEncoding MessageEncoding = "UTF-8"
|
|
)
|
|
|
|
// Hash represents the hash of an object
|
|
type Hash plumbing.Hash
|
|
|
|
// MessageEncoding represents the encoding of a commit
|
|
type MessageEncoding string
|
|
|
|
// 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
|
|
//
|
|
// When a Commit is populated by Decode it retains a reference to the source
|
|
// plumbing.EncodedObject so that EncodeWithoutSignature can reproduce the
|
|
// exact bytes the signature was computed over. Refer to EncodeWithoutSignature
|
|
// for more information.
|
|
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
|
|
// MergeTag is the embedded tag object when a merge commit is created by
|
|
// merging a signed tag.
|
|
MergeTag string
|
|
// 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
|
|
// Encoding is the encoding of the commit.
|
|
Encoding MessageEncoding
|
|
// List of extra headers of the commit
|
|
ExtraHeaders []ExtraHeader
|
|
|
|
s storer.EncodedObjectStorer
|
|
// src holds the encoded object this Commit was decoded from, used by
|
|
// EncodeWithoutSignature to recover the canonical signed bytes.
|
|
src plumbing.EncodedObject
|
|
}
|
|
|
|
// ExtraHeader holds any non-standard header
|
|
type ExtraHeader struct {
|
|
// Header name
|
|
Key string
|
|
// Value of the header
|
|
Value string
|
|
}
|
|
|
|
// Implement fmt.Formatter for ExtraHeader
|
|
func (h ExtraHeader) Format(f fmt.State, verb rune) {
|
|
switch verb {
|
|
case 'v':
|
|
fmt.Fprintf(f, "ExtraHeader{Key: %v, Value: %v}", h.Key, h.Value)
|
|
default:
|
|
fmt.Fprintf(f, "%s", h.Key)
|
|
if len(h.Value) > 0 {
|
|
fmt.Fprint(f, " ")
|
|
// Content may be spread on multiple lines, if so we need to
|
|
// prepend each of them with a space for "continuation".
|
|
value := strings.TrimSuffix(h.Value, "\n")
|
|
lines := strings.Split(value, "\n")
|
|
fmt.Fprint(f, strings.Join(lines, "\n "))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse an extra header and indicate whether it may be continue on the next line
|
|
func parseExtraHeader(line []byte) (ExtraHeader, bool) {
|
|
split := bytes.SplitN(line, []byte{' '}, 2)
|
|
|
|
out := ExtraHeader{
|
|
Key: string(bytes.TrimRight(split[0], "\n")),
|
|
Value: "",
|
|
}
|
|
|
|
if len(split) == 2 {
|
|
out.Value += string(split[1])
|
|
return out, true
|
|
} else {
|
|
return out, false
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
//
|
|
// NOTE: Since version 5.1.0 the renames are correctly handled, the settings
|
|
// used are the recommended options DefaultDiffTreeOptions.
|
|
func (c *Commit) PatchContext(ctx context.Context, to *Commit) (*Patch, error) {
|
|
fromTree, err := c.Tree()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var toTree *Tree
|
|
if to != nil {
|
|
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.
|
|
//
|
|
// NOTE: Since version 5.1.0 the renames are correctly handled, the settings
|
|
// used are the recommended options DefaultDiffTreeOptions.
|
|
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")
|
|
|
|
// ErrMalformedCommit is returned when a commit object cannot be decoded
|
|
// because its standard headers (tree, parent, author, committer) are missing,
|
|
// duplicated, or out of order.
|
|
var ErrMalformedCommit = errors.New("malformed commit")
|
|
|
|
// 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
|
|
}
|
|
|
|
func (c *Commit) reset() {
|
|
storer := c.s
|
|
*c = Commit{
|
|
Encoding: defaultUtf8CommitMessageEncoding,
|
|
s: storer,
|
|
}
|
|
}
|
|
|
|
// 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.reset()
|
|
c.Hash = o.Hash()
|
|
c.src = o
|
|
|
|
reader, err := o.Reader()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer ioutil.CheckClose(reader, &err)
|
|
|
|
r := sync.GetBufioReader(reader)
|
|
defer sync.PutBufioReader(r)
|
|
|
|
s := &commitScanner{r: r, c: c}
|
|
for state := scanTree; state != nil; {
|
|
state, err = state(s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !s.sawTree {
|
|
return fmt.Errorf("%w: missing tree header", ErrMalformedCommit)
|
|
}
|
|
c.Message = s.msgbuf.String()
|
|
return nil
|
|
}
|
|
|
|
// Encode transforms a Commit into a plumbing.EncodedObject.
|
|
func (c *Commit) Encode(o plumbing.EncodedObject) error {
|
|
return c.encode(o, true)
|
|
}
|
|
|
|
// EncodeWithoutSignature exports a Commit into a plumbing.EncodedObject
|
|
// without any signature headers, producing the payload that PGP/GPG
|
|
// signatures are computed over.
|
|
//
|
|
// Behaviour depends on how the Commit was created:
|
|
//
|
|
// - For Commits populated by Decode whose exported fields still match the
|
|
// source object, the payload is streamed from the raw source bytes with
|
|
// gpgsig and gpgsig-sha256 headers (and their continuation lines)
|
|
// stripped verbatim. This preserves the exact bytes the signature was
|
|
// computed over, regardless of any normalization performed by Decode.
|
|
//
|
|
// - For Commits constructed in memory, or for decoded Commits whose
|
|
// exported fields have been mutated, the payload is derived from the
|
|
// current struct fields. Mutation is detected by re-decoding the source
|
|
// object and comparing exported fields; if any differ, the in-memory
|
|
// representation prevails.
|
|
func (c *Commit) EncodeWithoutSignature(o plumbing.EncodedObject) error {
|
|
if c.matchesSource() {
|
|
return stripObjectSignatures(o, c.src, plumbing.CommitObject)
|
|
}
|
|
return c.encode(o, false)
|
|
}
|
|
|
|
// matchesSource reports whether c.src is set and re-decoding it produces a
|
|
// Commit whose payload-affecting exported fields are identical to those of
|
|
// c. It is the auto-detection used by EncodeWithoutSignature to decide
|
|
// between the raw bytes and the struct-encoded payload.
|
|
//
|
|
// PGPSignature is intentionally excluded from the comparison: neither path
|
|
// emits it, so mutating it must not trigger a switch to struct-encode (which
|
|
// would change the byte layout the caller is trying to verify against).
|
|
func (c *Commit) matchesSource() bool {
|
|
if c.src == nil {
|
|
return false
|
|
}
|
|
fresh := &Commit{}
|
|
if err := fresh.Decode(c.src); err != nil {
|
|
return false
|
|
}
|
|
return c.Hash == fresh.Hash &&
|
|
signatureEqual(c.Author, fresh.Author) &&
|
|
signatureEqual(c.Committer, fresh.Committer) &&
|
|
c.MergeTag == fresh.MergeTag &&
|
|
c.Message == fresh.Message &&
|
|
c.TreeHash == fresh.TreeHash &&
|
|
c.Encoding == fresh.Encoding &&
|
|
slices.Equal(c.ParentHashes, fresh.ParentHashes) &&
|
|
slices.Equal(c.ExtraHeaders, fresh.ExtraHeaders)
|
|
}
|
|
|
|
func signatureEqual(a, b Signature) bool {
|
|
return a.Name == b.Name &&
|
|
a.Email == b.Email &&
|
|
a.When.Unix() == b.When.Unix() &&
|
|
a.When.Format("-0700") == b.When.Format("-0700")
|
|
}
|
|
|
|
func isStandardHeader(key string) bool {
|
|
switch key {
|
|
case "tree", "parent", "author", "committer",
|
|
headerencoding, headermergetag, headerpgp, headerpgp256:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (c *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", c.TreeHash.String()); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, parent := range c.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 = c.Author.Encode(w); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err = fmt.Fprint(w, "\ncommitter "); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = c.Committer.Encode(w); err != nil {
|
|
return err
|
|
}
|
|
|
|
if c.MergeTag != "" {
|
|
if _, err = fmt.Fprint(w, "\n"+headermergetag+" "); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Split tag information 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. The newline will be added either as part of
|
|
// the PGP signature or the commit message.
|
|
mergetag := strings.TrimSuffix(c.MergeTag, "\n")
|
|
lines := strings.Split(mergetag, "\n")
|
|
if _, err = fmt.Fprint(w, strings.Join(lines, "\n ")); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if string(c.Encoding) != "" && c.Encoding != defaultUtf8CommitMessageEncoding {
|
|
if _, err = fmt.Fprintf(w, "\n%s %s", headerencoding, c.Encoding); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for _, header := range c.ExtraHeaders {
|
|
if isStandardHeader(header.Key) {
|
|
continue
|
|
}
|
|
if _, err = fmt.Fprintf(w, "\n%s", header); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if c.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(c.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", c.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),
|
|
)
|
|
}
|
|
|
|
// ErrMultipleSignatures is returned by Verify when the commit carries more
|
|
// than one armored signature block. Mirrors upstream's parse_gpg_output
|
|
// rejection of GOODSIG/BADSIG status lines after the first
|
|
// (gpg-interface.c:257-269): multi-signature commits are intentionally
|
|
// unsupported because their provenance cannot be reduced to a single
|
|
// authoritative signer.
|
|
var ErrMultipleSignatures = errors.New("commit has multiple signatures")
|
|
|
|
// 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) {
|
|
if countSignatureBlocks([]byte(c.PGPSignature)) > 1 {
|
|
return nil, ErrMultipleSignatures
|
|
}
|
|
|
|
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.EncodeWithoutSignature(encoded); err != nil {
|
|
return nil, err
|
|
}
|
|
er, err := encoded.Reader()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return openpgp.CheckArmoredDetachedSignature(keyring, er, signature, nil)
|
|
}
|
|
|
|
// Less defines a compare function to determine which commit is 'earlier' by:
|
|
// - First use Committer.When
|
|
// - If Committer.When are equal then use Author.When
|
|
// - If Author.When also equal then compare the string value of the hash
|
|
func (c *Commit) Less(rhs *Commit) bool {
|
|
return c.Committer.When.Before(rhs.Committer.When) ||
|
|
(c.Committer.When.Equal(rhs.Committer.When) &&
|
|
(c.Author.When.Before(rhs.Author.When) ||
|
|
(c.Author.When.Equal(rhs.Author.When) && bytes.Compare(c.Hash[:], rhs.Hash[:]) < 0)))
|
|
}
|
|
|
|
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()
|
|
}
|