4389da48e4
--- type: change description: |- Fix a bug in typeobj when a type field's name is the same as one of its inner fields This specifically came up with Comment (though it wasn't caught because an error wasn't being caught, that's fixed here as well). Prior to unmarshaling into the selected inner struct field, typeobj.UnmarshalYAML unmarshals into the outer struct in order to unmarshal all non-type fields. However, if one of the fields intended for the inner struct field has the same name as one of the type fields in the outer struct there would be a conflict at this point. The solution is to modify the type of the outer struct being unmarshaled into at this stage, so that all fields with type tags automatically have a `yaml:"-"` tag, and so are ignored by the yaml unmarshaler at this stage. fingerprint: AL32FBVJ7Bu2dz1ysrCiRFz2/y+QuaEyhKygvWP/fihw credentials: - type: pgp_signature pub_key_id: 95C46FA6A41148AC body: iQIzBAABAgAdFiEEJ6tQKp6olvZKJ0lwlcRvpqQRSKwFAl6rQYoACgkQlcRvpqQRSKzs0RAAkdU5Ty2uigHZSXqSgU4JiDLMmzlr4B4ODautUuLBmdskVaAAuOUJuS+egUU6Xz6lmL4+zQRBNGCvaZTxu0OT4H4wFWNQ9RdurLbuSJDeQY4htn5bP6BqcOy5aiTiYpnrZu6yuzMTco4jVSZ961o6t829gDBu1jAk32i/l3ivQpMSijEwjK9m74jKxF+fIVqT3+isgs0qzaDkskpdlDEgd/cf4Ibeb1+BAEZRShMXHBhF415rldjYs9H1Q2TSVwAaP7Zqn9gIV04yB/C8Waysh/NCMsIvQcACbVoO9vSBQ/1d+jttI+KTqOTA8lQ/ygWrFdYtPBjXRO7CVrah7PPE+EbFbPBbjH6ddP20uVeoTPTcjUwaWpdg5e4vZfuqXEe0IWW8NyMh8UL1tJ1LpLlWZKx6tz7gcUgoq+jOLUmUG5EB8HjQfqZx6WDHuyPTpy3c646SaIjg8B8tKkwUR+w9zntId7N4mWB+c+qMDH72mU54sXJ/i+XexqZaQgQiz2jRcltNc4S+/ohT5UDAYuivJsCDBcZdOYiMIB2cnPsm2DbCdbQPAq4oK1Ni+2wo7Pj9nVENamc+g6evqCnBZsWQUt5bDUwneIFwYcqdIPulX0NV9rtZQxexIkCmsO1vSrzdkeNyfWizFlRLavW5OBmxuLtoFoZUv5Oijwu8QT0eXMU= account: mediocregopher
223 lines
6.5 KiB
Go
223 lines
6.5 KiB
Go
package dehub
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"gopkg.in/src-d/go-git.v4/plumbing"
|
|
"gopkg.in/src-d/go-git.v4/plumbing/object"
|
|
)
|
|
|
|
// Commit wraps a single git commit object, and also contains various fields
|
|
// which are parsed out of it, including the payload. It is used as a
|
|
// convenience type, in place of having to manually retrieve and parse specific
|
|
// information out of commit objects.
|
|
type Commit struct {
|
|
Payload PayloadUnion
|
|
|
|
Hash plumbing.Hash
|
|
Object *object.Commit
|
|
TreeObject *object.Tree
|
|
}
|
|
|
|
// GetCommit retrieves the Commit at the given hash, and all of its sub-data
|
|
// which can be pulled out of it.
|
|
func (proj *Project) GetCommit(h plumbing.Hash) (c Commit, err error) {
|
|
if c.Object, err = proj.GitRepo.CommitObject(h); err != nil {
|
|
return c, fmt.Errorf("getting git commit object: %w", err)
|
|
} else if c.TreeObject, err = proj.GitRepo.TreeObject(c.Object.TreeHash); err != nil {
|
|
return c, fmt.Errorf("getting git tree object %q: %w",
|
|
c.Object.TreeHash, err)
|
|
} else if err = c.Payload.UnmarshalText([]byte(c.Object.Message)); err != nil {
|
|
return c, fmt.Errorf("decoding commit message: %w", err)
|
|
}
|
|
c.Hash = c.Object.Hash
|
|
return
|
|
}
|
|
|
|
// ErrHeadIsZero is used to indicate that HEAD resolves to the zero hash. An
|
|
// example of when this can happen is if the project was just initialized and
|
|
// has no commits, or if an orphan branch is checked out.
|
|
var ErrHeadIsZero = errors.New("HEAD resolves to the zero hash")
|
|
|
|
// GetHeadCommit returns the Commit which is currently referenced by HEAD.
|
|
// This method may return ErrHeadIsZero if HEAD resolves to the zero hash.
|
|
func (proj *Project) GetHeadCommit() (Commit, error) {
|
|
headHash, err := proj.ReferenceToHash(plumbing.HEAD)
|
|
if err != nil {
|
|
return Commit{}, fmt.Errorf("resolving HEAD: %w", err)
|
|
} else if headHash == plumbing.ZeroHash {
|
|
return Commit{}, ErrHeadIsZero
|
|
}
|
|
|
|
c, err := proj.GetCommit(headHash)
|
|
if err != nil {
|
|
return Commit{}, fmt.Errorf("getting commit %q: %w", headHash, err)
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
// GetCommitRange returns an ancestry of Commits, with the first being the
|
|
// commit immediately following the given starting hash, and the last being the
|
|
// given ending hash.
|
|
//
|
|
// If start is plumbing.ZeroHash then the root commit will be the starting hash.
|
|
func (proj *Project) GetCommitRange(start, end plumbing.Hash) ([]Commit, error) {
|
|
curr, err := proj.GetCommit(end)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("retrieving commit %q: %w", end, err)
|
|
}
|
|
|
|
var commits []Commit
|
|
var found bool
|
|
for {
|
|
if found = start != plumbing.ZeroHash && curr.Hash == start; found {
|
|
break
|
|
}
|
|
|
|
commits = append(commits, curr)
|
|
numParents := curr.Object.NumParents()
|
|
if numParents == 0 {
|
|
break
|
|
} else if numParents > 1 {
|
|
return nil, fmt.Errorf("commit %q has more than one parent: %+v",
|
|
curr.Hash, curr.Object.ParentHashes)
|
|
}
|
|
|
|
parentHash := curr.Object.ParentHashes[0]
|
|
parent, err := proj.GetCommit(parentHash)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("retrieving commit %q: %w", parentHash, err)
|
|
}
|
|
curr = parent
|
|
}
|
|
if !found && start != plumbing.ZeroHash {
|
|
return nil, fmt.Errorf("unable to find commit %q as an ancestor of %q",
|
|
start, end)
|
|
}
|
|
|
|
// reverse the commits to be in the expected order
|
|
for l, r := 0, len(commits)-1; l < r; l, r = l+1, r-1 {
|
|
commits[l], commits[r] = commits[r], commits[l]
|
|
}
|
|
return commits, nil
|
|
}
|
|
|
|
var (
|
|
hashStrLen = len(plumbing.ZeroHash.String())
|
|
errNotHex = errors.New("not a valid hex string")
|
|
)
|
|
|
|
func (proj *Project) findCommitByShortHash(hashStr string) (plumbing.Hash, error) {
|
|
paddedHashStr := hashStr
|
|
if len(hashStr)%2 > 0 {
|
|
paddedHashStr += "0"
|
|
}
|
|
|
|
if hashB, err := hex.DecodeString(paddedHashStr); err != nil {
|
|
return plumbing.ZeroHash, errNotHex
|
|
} else if len(hashStr) == hashStrLen {
|
|
var hash plumbing.Hash
|
|
copy(hash[:], hashB)
|
|
return hash, nil
|
|
} else if len(hashStr) < 2 {
|
|
return plumbing.ZeroHash, errors.New("hash string must be 2 characters long or more")
|
|
}
|
|
|
|
for i := 2; i < hashStrLen; i++ {
|
|
hashPrefix, hashTail := hashStr[:i], hashStr[i:]
|
|
path := filepath.Join("objects", hashPrefix)
|
|
fileInfos, err := proj.GitDirFS.ReadDir(path)
|
|
if err != nil {
|
|
return plumbing.ZeroHash, fmt.Errorf("listing files in %q: %w", path, err)
|
|
}
|
|
|
|
var matchedHash plumbing.Hash
|
|
for _, fileInfo := range fileInfos {
|
|
objFileName := fileInfo.Name()
|
|
if !strings.HasPrefix(objFileName, hashTail) {
|
|
continue
|
|
}
|
|
|
|
objHash := plumbing.NewHash(hashPrefix + objFileName)
|
|
obj, err := proj.GitRepo.Storer.EncodedObject(plumbing.AnyObject, objHash)
|
|
if err != nil {
|
|
return plumbing.ZeroHash, fmt.Errorf("reading object %q off disk: %w", objHash, err)
|
|
} else if obj.Type() != plumbing.CommitObject {
|
|
continue
|
|
|
|
} else if matchedHash == plumbing.ZeroHash {
|
|
matchedHash = objHash
|
|
continue
|
|
}
|
|
|
|
return plumbing.ZeroHash, fmt.Errorf("both %q and %q match", matchedHash, objHash)
|
|
}
|
|
|
|
if matchedHash != plumbing.ZeroHash {
|
|
return matchedHash, nil
|
|
}
|
|
}
|
|
|
|
return plumbing.ZeroHash, errors.New("failed to find a commit object with a matching prefix")
|
|
}
|
|
|
|
func (proj *Project) resolveRev(rev plumbing.Revision) (plumbing.Hash, error) {
|
|
if rev == plumbing.Revision(plumbing.ZeroHash.String()) {
|
|
return plumbing.ZeroHash, nil
|
|
}
|
|
|
|
{
|
|
// pretend the revision is a short hash until proven otherwise
|
|
shortHash := string(rev)
|
|
hash, err := proj.findCommitByShortHash(shortHash)
|
|
if errors.Is(err, errNotHex) {
|
|
// ok, continue
|
|
} else if err != nil {
|
|
return plumbing.ZeroHash, fmt.Errorf("resolving as short hash: %w", err)
|
|
} else {
|
|
// guess it _is_ a short hash, knew it!
|
|
return hash, nil
|
|
}
|
|
}
|
|
|
|
h, err := proj.GitRepo.ResolveRevision(rev)
|
|
if err != nil {
|
|
return plumbing.ZeroHash, fmt.Errorf("resolving revision %q: %w", rev, err)
|
|
}
|
|
return *h, nil
|
|
}
|
|
|
|
// GetCommitByRevision resolves the revision and returns the Commit it references.
|
|
func (proj *Project) GetCommitByRevision(rev plumbing.Revision) (Commit, error) {
|
|
hash, err := proj.resolveRev(rev)
|
|
if err != nil {
|
|
return Commit{}, err
|
|
}
|
|
|
|
c, err := proj.GetCommit(hash)
|
|
if err != nil {
|
|
return Commit{}, fmt.Errorf("getting commit %q: %w", hash, err)
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
// GetCommitRangeByRevision is like GetCommitRange, first resolving the given
|
|
// revisions into hashes before continuing with GetCommitRange's behavior.
|
|
func (proj *Project) GetCommitRangeByRevision(startRev, endRev plumbing.Revision) ([]Commit, error) {
|
|
start, err := proj.resolveRev(startRev)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
end, err := proj.resolveRev(endRev)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return proj.GetCommitRange(start, end)
|
|
}
|