normalize how git commits are interacted with, including changing VerifyComit -> VerifyCommits
--- type: change message: |- normalize how git commits are interacted with, including changing VerifyComit -> VerifyCommits This commit attempts to normalize git commit interactions in order to reduce the amount of manual `GitRepo.CommitObject`, `GitRepo.TreeObject`, `Commit.UnmarshalText`, and `Commit.Interface` calls are done, by creating a single structure (`GitCommit`) which holds the output of those calls, and is only created by a single method (`GetGitCommit`), which is then used by a bunch of other methods to expand its functionality, including implementing a range request which can be used by verify and the pre-receive hook (though it's only used by the hook, currently). change_hash: AMae4PL6+jrxhn2KEGHejstcdT37Gw/jjkl/UuovHcgd credentials: - type: pgp_signature pub_key_id: 95C46FA6A41148AC body: iQIzBAABAgAdFiEEJ6tQKp6olvZKJ0lwlcRvpqQRSKwFAl5uhvoACgkQlcRvpqQRSKzJrhAAqi2LEQVTyVktfsOBv/CZmefclLLqWTChVoeIZt2EAGDDGygmrx88hI0SEAviOzPMn0kiZFDeY5k7ICJMhJ9RVDU9WjH7fbOboMJW19rVhx6Ke/M2ERtrT0OFLRmFVJVDM0P8SEheQvR3HE/iiypBICVCtp+meHEq9mOJWZlZnoCqMaulAy/Nnq4N1VD0yPPlr16+yxMqedKHcgKbcH8K61ltNAjXDT+tCWwCq1huA5MVSuTm5EwqIeKPN6JKgwATv8Ku2GhYZWHSGUwecP1J3x2XTDPeChCQVDpC232Pxwk8z/D36F3J/XOfkdl0QYQ077xL1IJfYOnuuHir47CokDf3G0XCQnJ/+X4pZdtP387rc045o/2bhUi2U4eJ5HgS7Hvyi6EApT0Czv7SeJePTvdnRUYse8ZYuIwYXj5GWWxnbKQzLpyjcHdQc2a3B3RN84zXqqAOS6ObFrFPZQIfz2rfQojZN8kvcmUvYhJXSaT65XmqFjyJ4n6grrEnK/N+MfbnpzyF/yvlzxWPqGFQOQj9meosbTAdgZbmdwYqa5r1ee8DmlkzNJJxze96h503a733yciN8Ef4hGZNlRV6YFegkK/cCgKaA4NCEALKb1t0Uri5gnPldXk4HsPF+23GANbE7mjytY8ra3fhXG4VhaFt/WsLg3Bu7djQ0H74y+g= account: mediocregopher
This commit is contained in:
parent
326de2afc6
commit
5ebb6597a8
@ -46,12 +46,12 @@ func cmdCommit(ctx context.Context, cmd *dcmd.Cmd) {
|
||||
return fmt.Errorf("accrediting commit: %w", err)
|
||||
}
|
||||
|
||||
hash, err := repo.Commit(commit, *accountID)
|
||||
gitCommit, err := repo.Commit(commit, *accountID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("committing to git: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("committed to HEAD as %s\n", hash)
|
||||
fmt.Printf("committed to HEAD as %s\n", gitCommit.GitCommit.Hash)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -110,28 +110,12 @@ func cmdCommit(ctx context.Context, cmd *dcmd.Cmd) {
|
||||
return nil, errors.New("credential commit cannot have any files changed")
|
||||
}
|
||||
|
||||
// TODO maybe nice to have a helper to do all of this
|
||||
h, err := repo.GitRepo.ResolveRevision(plumbing.Revision(*rev))
|
||||
gitCommit, err := repo.GetGitRevision(plumbing.Revision(*rev))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolving revision: %w", err)
|
||||
return nil, fmt.Errorf("resolving revision %q: %w", *rev, err)
|
||||
}
|
||||
|
||||
commitObj, err := repo.GitRepo.CommitObject(*h)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting commit object %q: %w", h, err)
|
||||
}
|
||||
|
||||
var commit dehub.Commit
|
||||
if err := commit.UnmarshalText([]byte(commitObj.Message)); err != nil {
|
||||
return nil, fmt.Errorf("unmarshaling commit message: %w", err)
|
||||
}
|
||||
|
||||
commitInt, err := commit.Interface()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("casting %#v to CommitInterface: %w", commit, err)
|
||||
}
|
||||
|
||||
credCommit, err := repo.NewCommitCredential(commitInt.GetHash())
|
||||
credCommit, err := repo.NewCommitCredential(gitCommit.Interface.GetHash())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("constructing credential commit: %w", err)
|
||||
} else if err := accreditAndCommit(credCommit); err != nil {
|
||||
|
@ -29,7 +29,7 @@ func cmdHook(ctx context.Context, cmd *dcmd.Cmd) {
|
||||
for {
|
||||
line, err := br.ReadString('\n')
|
||||
if errors.Is(err, io.EOF) {
|
||||
return nil, nil
|
||||
break
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("error reading next line from stdin: %w", err)
|
||||
}
|
||||
@ -40,64 +40,44 @@ func cmdHook(ctx context.Context, cmd *dcmd.Cmd) {
|
||||
return nil, fmt.Errorf("malformed pre-receive hook stdin line %q", line)
|
||||
}
|
||||
|
||||
startRev := plumbing.Revision(lineParts[0])
|
||||
endRev := plumbing.Revision(lineParts[1])
|
||||
branchName := plumbing.ReferenceName(lineParts[2])
|
||||
|
||||
// the zeroRevision gets sent on the very first push
|
||||
const zeroRevision plumbing.Revision = "0000000000000000000000000000000000000000"
|
||||
if !branchName.IsBranch() {
|
||||
return nil, fmt.Errorf("reference %q is not a branch, can't push to it", branchName)
|
||||
}
|
||||
|
||||
fromRev := plumbing.Revision(lineParts[0])
|
||||
var fromHash *plumbing.Hash
|
||||
if fromRev != zeroRevision {
|
||||
fromHash, err = repo.GitRepo.ResolveRevision(fromRev)
|
||||
var startHash, endHash plumbing.Hash
|
||||
// startRev can be a zero hash if these are the first commits for a
|
||||
// branch being pushed.
|
||||
if startRev != plumbing.Revision(plumbing.ZeroHash.String()) {
|
||||
h, err := repo.GitRepo.ResolveRevision(startRev)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to resolve revision %q: %w", fromRev, err)
|
||||
return nil, fmt.Errorf("resolving revision %q: %w", startRev, err)
|
||||
}
|
||||
startHash = *h
|
||||
}
|
||||
|
||||
toRev := plumbing.Revision(lineParts[1])
|
||||
toHash, err := repo.GitRepo.ResolveRevision(toRev)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to resolve revision %q: %w", toRev, err)
|
||||
}
|
||||
|
||||
toCommit, err := repo.GitRepo.CommitObject(*toHash)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to find commit %q: %w", *toHash, err)
|
||||
}
|
||||
|
||||
var hashesToCheck []plumbing.Hash
|
||||
var found bool
|
||||
for currCommit := toCommit; ; {
|
||||
hashesToCheck = append(hashesToCheck, currCommit.Hash)
|
||||
if currCommit.NumParents() == 0 {
|
||||
break
|
||||
} else if currCommit.NumParents() > 1 {
|
||||
return nil, fmt.Errorf("commit %q has more than one parent: %+v",
|
||||
currCommit.Hash, currCommit.ParentHashes)
|
||||
}
|
||||
|
||||
parentCommit, err := currCommit.Parent(0)
|
||||
{
|
||||
h, err := repo.GitRepo.ResolveRevision(endRev)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get parent of commit %q: %w", currCommit.Hash, err)
|
||||
} else if fromHash != nil && parentCommit.Hash == *fromHash {
|
||||
found = true
|
||||
break
|
||||
return nil, fmt.Errorf("resolving revision %q: %w", endRev, err)
|
||||
}
|
||||
currCommit = parentCommit
|
||||
}
|
||||
if !found && fromHash != nil {
|
||||
return nil, fmt.Errorf("unable to find commit %q as an ancestor of %q", *fromHash, *toHash)
|
||||
endHash = *h
|
||||
}
|
||||
|
||||
for i := len(hashesToCheck) - 1; i >= 0; i-- {
|
||||
hash := hashesToCheck[i]
|
||||
fmt.Printf("Verifying change commit %q\n", hash)
|
||||
if err := repo.VerifyCommit(branchName, hash); err != nil {
|
||||
return nil, fmt.Errorf("could not verify change commit %q: %w", hash, err)
|
||||
}
|
||||
gitCommits, err := repo.GetGitCommitRange(startHash, endHash)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting commits from %q to %q: %w",
|
||||
startHash, endHash, err)
|
||||
|
||||
} else if err := repo.VerifyCommits(branchName, gitCommits); err != nil {
|
||||
return nil, fmt.Errorf("verifying commits from %q to %q: %w",
|
||||
startHash, endHash, err)
|
||||
}
|
||||
fmt.Println("All pushed commits have been verified, well done.")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
fmt.Println("All pushed commits have been verified, well done.")
|
||||
return nil, nil
|
||||
})
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"dehub"
|
||||
"dehub/cmd/dehub/dcmd"
|
||||
"fmt"
|
||||
|
||||
@ -16,10 +17,11 @@ func cmdVerify(ctx context.Context, cmd *dcmd.Cmd) {
|
||||
cmd.Run(func() (context.Context, error) {
|
||||
repo := ctxRepo(ctx)
|
||||
|
||||
h, err := repo.GitRepo.ResolveRevision(plumbing.Revision(*rev))
|
||||
gitCommit, err := repo.GetGitRevision(plumbing.Revision(*rev))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not resolve revision %q: %w", *rev, err)
|
||||
return nil, fmt.Errorf("resolving revision %q: %w", *rev, err)
|
||||
}
|
||||
gitCommitHash := gitCommit.GitCommit.Hash
|
||||
|
||||
var branchName plumbing.ReferenceName
|
||||
if *branch == "" {
|
||||
@ -30,11 +32,12 @@ func cmdVerify(ctx context.Context, cmd *dcmd.Cmd) {
|
||||
branchName = plumbing.NewBranchReferenceName(*branch)
|
||||
}
|
||||
|
||||
if err := repo.VerifyCommit(branchName, *h); err != nil {
|
||||
return nil, fmt.Errorf("could not verify commit at %q (%s): %w", *rev, *h, err)
|
||||
if err := repo.VerifyCommits(branchName, []dehub.GitCommit{gitCommit}); err != nil {
|
||||
return nil, fmt.Errorf("could not verify commit at %q (%s): %w",
|
||||
*rev, gitCommitHash, err)
|
||||
}
|
||||
|
||||
fmt.Printf("commit at %q (%s) is good to go!\n", *rev, *h)
|
||||
fmt.Printf("commit at %q (%s) is good to go!\n", *rev, gitCommitHash)
|
||||
return nil, nil
|
||||
})
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ func tmpFileMsg() (string, error) {
|
||||
return "", fmt.Errorf("could not stat EDITOR %q: %w", editor, err)
|
||||
}
|
||||
|
||||
tmpf, err := ioutil.TempFile("", "")
|
||||
tmpf, err := ioutil.TempFile("", "dehub.*.txt")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not open temp file: %w", err)
|
||||
}
|
||||
|
102
commit.go
102
commit.go
@ -116,7 +116,7 @@ func (r *Repo) AccreditCommit(commit Commit, sigInt sigcred.SignifierInterface)
|
||||
return commit, fmt.Errorf("could not cast commit %+v to interface: %w", commit, err)
|
||||
}
|
||||
|
||||
headFS, err := r.HeadFS()
|
||||
headFS, err := r.headFS()
|
||||
if err != nil {
|
||||
return commit, fmt.Errorf("could not grab snapshot of HEAD fs: %w", err)
|
||||
}
|
||||
@ -132,22 +132,31 @@ func (r *Repo) AccreditCommit(commit Commit, sigInt sigcred.SignifierInterface)
|
||||
// Commit uses the given TextMarshaler to create a git commit object (with the
|
||||
// specified accountID as the author) and commits it to the current HEAD,
|
||||
// returning the hash of the commit.
|
||||
func (r *Repo) Commit(m encoding.TextMarshaler, accountID string) (plumbing.Hash, error) {
|
||||
func (r *Repo) Commit(m encoding.TextMarshaler, accountID string) (GitCommit, error) {
|
||||
msgB, err := m.MarshalText()
|
||||
if err != nil {
|
||||
return plumbing.ZeroHash, fmt.Errorf("error marshaling %T to string: %v", m, err)
|
||||
return GitCommit{}, fmt.Errorf("encoding %T to message string: %v", m, err)
|
||||
}
|
||||
|
||||
w, err := r.GitRepo.Worktree()
|
||||
if err != nil {
|
||||
return plumbing.ZeroHash, fmt.Errorf("could not get git worktree: %w", err)
|
||||
return GitCommit{}, fmt.Errorf("getting git worktree: %w", err)
|
||||
}
|
||||
return w.Commit(string(msgB), &git.CommitOptions{
|
||||
h, err := w.Commit(string(msgB), &git.CommitOptions{
|
||||
Author: &object.Signature{
|
||||
Name: accountID,
|
||||
When: time.Now(),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return GitCommit{}, fmt.Errorf("committing to git worktree: %w", err)
|
||||
}
|
||||
|
||||
gc, err := r.GetGitCommit(h)
|
||||
if err != nil {
|
||||
return GitCommit{}, fmt.Errorf("retrieving fresh commit %q back from git: %w", h, err)
|
||||
}
|
||||
return gc, nil
|
||||
}
|
||||
|
||||
// HasStagedChanges returns true if there are file changes which have been
|
||||
@ -176,7 +185,6 @@ func (r *Repo) HasStagedChanges() (bool, error) {
|
||||
type verificationCtx struct {
|
||||
commit *object.Commit
|
||||
commitTree, parentTree *object.Tree
|
||||
isRootCommit bool
|
||||
}
|
||||
|
||||
// non-gophers gonna hate on this method, but I say it's fine
|
||||
@ -188,9 +196,6 @@ func (r *Repo) verificationCtx(h plumbing.Hash) (vctx verificationCtx, err error
|
||||
return vctx, fmt.Errorf("retrieving commit tree object %q: %w",
|
||||
vctx.commit.TreeHash, err)
|
||||
|
||||
} else if vctx.isRootCommit = vctx.commit.NumParents() == 0; vctx.isRootCommit {
|
||||
vctx.parentTree = new(object.Tree)
|
||||
|
||||
} else if parent, err := vctx.commit.Parent(0); err != nil {
|
||||
return vctx, fmt.Errorf("retrieving commit parent: %w", err)
|
||||
|
||||
@ -258,57 +263,84 @@ func (r *Repo) assertAccessControls(
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyCommit verifies that the commit at the given hash, which is presumably
|
||||
// on the given branch, is gucci.
|
||||
func (r *Repo) VerifyCommit(branch plumbing.ReferenceName, h plumbing.Hash) error {
|
||||
vctx, err := r.verificationCtx(h)
|
||||
if err != nil {
|
||||
return err
|
||||
// VerifyCommits verifies that the given commits, which are presumably on the
|
||||
// given branch, are gucci.
|
||||
func (r *Repo) VerifyCommits(branch plumbing.ReferenceName, gitCommits []GitCommit) error {
|
||||
|
||||
for i, gitCommit := range gitCommits {
|
||||
// It's not a requirement that the given GitCommits are in ancestral
|
||||
// order, but usually they are, so we can help verifyCommit not have to
|
||||
// calculate the parentTree if the previous commit is the parent of this
|
||||
// one.
|
||||
var parentTree *object.Tree
|
||||
if i > 0 && gitCommits[i-1].GitCommit.Hash == gitCommit.GitCommit.ParentHashes[0] {
|
||||
parentTree = gitCommits[i-1].GitTree
|
||||
}
|
||||
|
||||
if err := r.verifyCommit(branch, gitCommit, parentTree); err != nil {
|
||||
return fmt.Errorf("verifying commit %q: %w",
|
||||
gitCommit.GitCommit.Hash, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// if parentTree is nil then it will be inferred.
|
||||
func (r *Repo) verifyCommit(branch plumbing.ReferenceName, gitCommit GitCommit, parentTree *object.Tree) error {
|
||||
isRoot := gitCommit.Root()
|
||||
|
||||
if parentTree == nil {
|
||||
if isRoot {
|
||||
parentTree = new(object.Tree)
|
||||
} else if parentCommit, err := gitCommit.GitCommit.Parent(0); err != nil {
|
||||
return fmt.Errorf("getting parent commit %q: %w",
|
||||
gitCommit.GitCommit.ParentHashes[0], err)
|
||||
} else if parentTree, err = r.GitRepo.TreeObject(parentCommit.TreeHash); err != nil {
|
||||
return fmt.Errorf("getting parent tree object %q: %w",
|
||||
parentCommit.TreeHash, err)
|
||||
}
|
||||
}
|
||||
|
||||
vctx := verificationCtx{
|
||||
commit: gitCommit.GitCommit,
|
||||
commitTree: gitCommit.GitTree,
|
||||
parentTree: parentTree,
|
||||
}
|
||||
|
||||
var sigFS fs.FS
|
||||
if vctx.isRootCommit {
|
||||
if isRoot {
|
||||
sigFS = fs.FromTree(vctx.commitTree)
|
||||
} else {
|
||||
sigFS = fs.FromTree(vctx.parentTree)
|
||||
}
|
||||
|
||||
var commit Commit
|
||||
if err := commit.UnmarshalText([]byte(vctx.commit.Message)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg, err := r.loadConfig(sigFS)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error loading config: %w", err)
|
||||
return fmt.Errorf("loading config of parent %q: %w",
|
||||
gitCommit.GitCommit.ParentHashes[0], err)
|
||||
}
|
||||
|
||||
err = r.assertAccessControls(cfg.AccessControls, commit, vctx, branch)
|
||||
err = r.assertAccessControls(cfg.AccessControls, gitCommit.Commit, vctx, branch)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to satisfy all access controls: %w", err)
|
||||
return fmt.Errorf("enforcing access controls: %w", err)
|
||||
}
|
||||
|
||||
commitInt, err := commit.Interface()
|
||||
changeHash := gitCommit.Interface.GetHash()
|
||||
expectedChangeHash, err := gitCommit.Interface.Hash(vctx.parentTree, vctx.commitTree)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not cast commit %+v to interface: %w", commit, err)
|
||||
}
|
||||
|
||||
changeHash := commitInt.GetHash()
|
||||
expectedChangeHash, err := commitInt.Hash(vctx.parentTree, vctx.commitTree)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error calculating expected change hash: %w", err)
|
||||
return fmt.Errorf("calculating expected change hash: %w", err)
|
||||
} else if !bytes.Equal(changeHash, expectedChangeHash) {
|
||||
return fmt.Errorf("malformed change_hash in commit body, is %s but should be %s",
|
||||
base64.StdEncoding.EncodeToString(expectedChangeHash),
|
||||
base64.StdEncoding.EncodeToString(changeHash))
|
||||
}
|
||||
|
||||
for _, cred := range commit.Credentials {
|
||||
for _, cred := range gitCommit.Commit.Credentials {
|
||||
sig, err := r.signifierForCredential(sigFS, cred)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error finding signifier for credential %+v: %w", cred, err)
|
||||
return fmt.Errorf("finding signifier for credential %+v: %w", cred, err)
|
||||
} else if err := sig.Verify(sigFS, expectedChangeHash, cred); err != nil {
|
||||
return fmt.Errorf("error verifying credential %+v: %w", cred, err)
|
||||
return fmt.Errorf("verifying credential %+v: %w", cred, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,9 +4,9 @@ import (
|
||||
"dehub/fs"
|
||||
"dehub/yamlutil"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/src-d/go-git.v4/plumbing"
|
||||
"gopkg.in/src-d/go-git.v4/plumbing/object"
|
||||
)
|
||||
|
||||
@ -22,11 +22,11 @@ var _ CommitInterface = CommitChange{}
|
||||
// encompassing the currently staged file changes. The Credentials of the
|
||||
// returned Commit will _not_ be filled in.
|
||||
func (r *Repo) NewCommitChange(msg string) (Commit, error) {
|
||||
_, headTree, err := r.head()
|
||||
if errors.Is(err, plumbing.ErrReferenceNotFound) {
|
||||
headTree = &object.Tree{}
|
||||
} else if err != nil {
|
||||
return Commit{}, err
|
||||
headTree := new(object.Tree)
|
||||
if head, err := r.GetGitHead(); err != nil && !errors.Is(err, ErrNoHead) {
|
||||
return Commit{}, fmt.Errorf("getting HEAD commit: %w", err)
|
||||
} else if err == nil {
|
||||
headTree = head.GitTree
|
||||
}
|
||||
|
||||
_, stagedTree, err := fs.FromStagedChangesTree(r.GitRepo)
|
||||
|
@ -76,24 +76,22 @@ func TestChangeCommitVerify(t *testing.T) {
|
||||
h.stage(step.tree)
|
||||
account := h.cfg.Accounts[0]
|
||||
|
||||
commit, hash := h.changeCommit(step.msg, account.ID, h.sig)
|
||||
commitObj, err := h.repo.GitRepo.CommitObject(hash)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to retrieve commit %v: %v", hash, err)
|
||||
} else if step.msgHead == "" {
|
||||
gitCommit := h.changeCommit(step.msg, account.ID, h.sig)
|
||||
if step.msgHead == "" {
|
||||
step.msgHead = strings.TrimSpace(step.msg) + "\n\n"
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(commitObj.Message, step.msgHead) {
|
||||
t.Fatalf("commit message %q does not start with expected head %q", commitObj.Message, step.msgHead)
|
||||
if !strings.HasPrefix(gitCommit.GitCommit.Message, step.msgHead) {
|
||||
t.Fatalf("commit message %q does not start with expected head %q",
|
||||
gitCommit.GitCommit.Message, step.msgHead)
|
||||
}
|
||||
|
||||
var actualCommit Commit
|
||||
if err := actualCommit.UnmarshalText([]byte(commitObj.Message)); err != nil {
|
||||
if err := actualCommit.UnmarshalText([]byte(gitCommit.GitCommit.Message)); err != nil {
|
||||
t.Fatalf("error unmarshaling commit body: %v", err)
|
||||
} else if !reflect.DeepEqual(actualCommit, commit) {
|
||||
} else if !reflect.DeepEqual(actualCommit, gitCommit.Commit) {
|
||||
t.Fatalf("returned change commit:\n%s\ndoes not match actual one:\n%s",
|
||||
spew.Sdump(commit), spew.Sdump(actualCommit))
|
||||
spew.Sdump(gitCommit.Commit), spew.Sdump(actualCommit))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -61,11 +61,11 @@ func TestCredentialCommitVerify(t *testing.T) {
|
||||
},
|
||||
}
|
||||
h.stageCfg()
|
||||
rootCommit, _ := h.changeCommit("initial commit", h.cfg.Accounts[0].ID, h.sig)
|
||||
rootGitCommit := h.changeCommit("initial commit", h.cfg.Accounts[0].ID, h.sig)
|
||||
|
||||
// toot user wants to create a credential commit for the root commit, for
|
||||
// whatever reason.
|
||||
rootChangeHash := rootCommit.Change.ChangeHash
|
||||
rootChangeHash := rootGitCommit.Commit.Change.ChangeHash
|
||||
credCommit, err := h.repo.NewCommitCredential(rootChangeHash)
|
||||
if err != nil {
|
||||
t.Fatalf("creating credential commit for hash %x: %v", rootChangeHash, err)
|
||||
|
@ -3,19 +3,17 @@ package dehub
|
||||
import (
|
||||
"dehub/sigcred"
|
||||
"testing"
|
||||
|
||||
"gopkg.in/src-d/go-git.v4/plumbing"
|
||||
)
|
||||
|
||||
func TestConfigChange(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
|
||||
var hashes []plumbing.Hash
|
||||
var gitCommits []GitCommit
|
||||
|
||||
// commit the initial staged changes, which merely include the config and
|
||||
// public key
|
||||
_, hash := h.changeCommit("commit configuration", h.cfg.Accounts[0].ID, h.sig)
|
||||
hashes = append(hashes, hash)
|
||||
gitCommit := h.changeCommit("commit configuration", h.cfg.Accounts[0].ID, h.sig)
|
||||
gitCommits = append(gitCommits, gitCommit)
|
||||
|
||||
// create a new account and add it to the configuration. That commit should
|
||||
// not be verifiable, though
|
||||
@ -38,17 +36,15 @@ func TestConfigChange(t *testing.T) {
|
||||
|
||||
// now add with the root user, this should work.
|
||||
h.stageCfg()
|
||||
_, hash = h.changeCommit("add toot user", h.cfg.Accounts[0].ID, h.sig)
|
||||
hashes = append(hashes, hash)
|
||||
gitCommit = h.changeCommit("add toot user", h.cfg.Accounts[0].ID, h.sig)
|
||||
gitCommits = append(gitCommits, gitCommit)
|
||||
|
||||
// _now_ the toot user should be able to do things.
|
||||
h.stage(map[string]string{"foo/bar": "what a cool file"})
|
||||
_, hash = h.changeCommit("add a cool file", h.cfg.Accounts[1].ID, newSig)
|
||||
hashes = append(hashes, hash)
|
||||
gitCommit = h.changeCommit("add a cool file", h.cfg.Accounts[1].ID, newSig)
|
||||
gitCommits = append(gitCommits, gitCommit)
|
||||
|
||||
for i, hash := range hashes {
|
||||
if err := h.repo.VerifyCommit(MainRefName, hash); err != nil {
|
||||
t.Fatalf("commit %d (%v) should have been verified but wasn't: %v", i, hash, err)
|
||||
}
|
||||
if err := h.repo.VerifyCommits(MainRefName, gitCommits); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ func (r *Repo) loadConfig(fs fs.FS) (Config, error) {
|
||||
// LoadConfig loads the Config object from the HEAD of the repo, or directly
|
||||
// from the filesystem if there is no HEAD yet.
|
||||
func (r *Repo) LoadConfig() (Config, error) {
|
||||
headFS, err := r.HeadFS()
|
||||
headFS, err := r.headFS()
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("error retrieving repo HEAD: %w", err)
|
||||
}
|
||||
|
158
repo.go
158
repo.go
@ -95,7 +95,7 @@ func InitMemRepo() *Repo {
|
||||
func (r *Repo) init() error {
|
||||
h := plumbing.NewSymbolicReference(plumbing.HEAD, MainRefName)
|
||||
if err := r.GitRepo.Storer.SetReference(h); err != nil {
|
||||
return fmt.Errorf("could not set HEAD to %q: %w", MainRefName, err)
|
||||
return fmt.Errorf("setting HEAD reference to %q: %w", MainRefName, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -103,7 +103,7 @@ func (r *Repo) init() error {
|
||||
func (r *Repo) billyFilesystem() (billy.Filesystem, error) {
|
||||
w, err := r.GitRepo.Worktree()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not open git worktree: %w", err)
|
||||
return nil, fmt.Errorf("opening git worktree: %w", err)
|
||||
}
|
||||
return w.Filesystem, nil
|
||||
}
|
||||
@ -114,7 +114,7 @@ func (r *Repo) CheckedOutBranch() (plumbing.ReferenceName, error) {
|
||||
// newly initialized repo very well.
|
||||
ogRef, err := r.GitRepo.Storer.Reference(plumbing.HEAD)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("couldn't de-reference HEAD (is it a bare repo?): %w", err)
|
||||
return "", fmt.Errorf("de-referencing HEAD (is it a bare repo?): %w", err)
|
||||
}
|
||||
|
||||
ref := ogRef
|
||||
@ -137,39 +137,137 @@ func (r *Repo) CheckedOutBranch() (plumbing.ReferenceName, error) {
|
||||
return "", fmt.Errorf("could not de-reference HEAD to a branch: %w", err)
|
||||
}
|
||||
|
||||
func (r *Repo) head() (*object.Commit, *object.Tree, error) {
|
||||
head, err := r.GitRepo.Head()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("could not get repo HEAD: %w", err)
|
||||
}
|
||||
|
||||
headHash := head.Hash()
|
||||
headCommit, err := r.GitRepo.CommitObject(headHash)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("could not get commit at HEAD (%q): %w", headHash, err)
|
||||
}
|
||||
|
||||
headTree, err := r.GitRepo.TreeObject(headCommit.TreeHash)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("could not get tree object at HEAD (commit:%q tree:%q): %w",
|
||||
headHash, headCommit.TreeHash, err)
|
||||
}
|
||||
|
||||
return headCommit, headTree, nil
|
||||
}
|
||||
|
||||
// HeadFS returns an FS based on the HEAD commit, or if there is no HEAD commit
|
||||
// headFS returns an FS based on the HEAD commit, or if there is no HEAD commit
|
||||
// (it's an empty repo) an FS based on the raw filesystem.
|
||||
func (r *Repo) HeadFS() (fs.FS, error) {
|
||||
_, headTree, err := r.head()
|
||||
if errors.Is(err, plumbing.ErrReferenceNotFound) {
|
||||
func (r *Repo) headFS() (fs.FS, error) {
|
||||
head, err := r.GetGitHead()
|
||||
if errors.Is(err, ErrNoHead) {
|
||||
bfs, err := r.billyFilesystem()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get underlying filesystem: %w", err)
|
||||
return nil, fmt.Errorf("getting underlying filesystem: %w", err)
|
||||
}
|
||||
return fs.FromBillyFilesystem(bfs), nil
|
||||
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("could not get HEAD tree: %w", err)
|
||||
}
|
||||
return fs.FromTree(headTree), nil
|
||||
|
||||
return fs.FromTree(head.GitTree), nil
|
||||
}
|
||||
|
||||
// GitCommit wraps a single git commit object, and also contains various fields
|
||||
// which are parsed out of it. It is used as a convenience type, in place of
|
||||
// having to manually retrieve and parse specific information out of commit
|
||||
// objects.
|
||||
type GitCommit struct {
|
||||
GitCommit *object.Commit
|
||||
|
||||
// Fields based on that Commit, which can't be directly gleaned from it.
|
||||
GitTree *object.Tree
|
||||
Commit Commit
|
||||
Interface CommitInterface
|
||||
}
|
||||
|
||||
// Root returns true if this commit is the root commit in its branch (i.e. it
|
||||
// has no parents)
|
||||
func (gc GitCommit) Root() bool {
|
||||
return gc.GitCommit.NumParents() == 0
|
||||
}
|
||||
|
||||
// GetGitCommit retrieves the commit at the given hash, and all of its sub-data
|
||||
// which can be pulled out of it.
|
||||
func (r *Repo) GetGitCommit(h plumbing.Hash) (gc GitCommit, err error) {
|
||||
if gc.GitCommit, err = r.GitRepo.CommitObject(h); err != nil {
|
||||
return gc, fmt.Errorf("getting git commit object: %w", err)
|
||||
} else if gc.GitTree, err = r.GitRepo.TreeObject(gc.GitCommit.TreeHash); err != nil {
|
||||
return gc, fmt.Errorf("getting git tree object %q: %w",
|
||||
gc.GitCommit.TreeHash, err)
|
||||
} else if gc.Commit.UnmarshalText([]byte(gc.GitCommit.Message)); err != nil {
|
||||
return gc, fmt.Errorf("decoding commit message: %w", err)
|
||||
} else if gc.Interface, err = gc.Commit.Interface(); err != nil {
|
||||
return gc, fmt.Errorf("casting %+v to a CommitInterface: %w", gc.Commit, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetGitRevision resolves the revision and returns the GitCommit it references.
|
||||
func (r *Repo) GetGitRevision(rev plumbing.Revision) (GitCommit, error) {
|
||||
// This returns a pointer for some reason, not sure why.
|
||||
h, err := r.GitRepo.ResolveRevision(rev)
|
||||
if err != nil {
|
||||
return GitCommit{}, fmt.Errorf("resolving revision: %w", err)
|
||||
}
|
||||
|
||||
gc, err := r.GetGitCommit(*h)
|
||||
if err != nil {
|
||||
return GitCommit{}, fmt.Errorf("getting commit %q: %w", *h, err)
|
||||
}
|
||||
return gc, nil
|
||||
}
|
||||
|
||||
// ErrNoHead is returns from GetGitHead if there is no HEAD reference defined in
|
||||
// the repo. This can happen if the repo has no commits
|
||||
var ErrNoHead = errors.New("HEAD reference not found")
|
||||
|
||||
// GetGitHead returns the GitCommit which is currently referenced by HEAD.
|
||||
// This method may return ErrNoHead if the repo has no commits.
|
||||
func (r *Repo) GetGitHead() (GitCommit, error) {
|
||||
head, err := r.GitRepo.Head()
|
||||
if errors.Is(err, plumbing.ErrReferenceNotFound) {
|
||||
return GitCommit{}, ErrNoHead
|
||||
} else if err != nil {
|
||||
return GitCommit{}, fmt.Errorf("resolving HEAD: %w", err)
|
||||
}
|
||||
headHash := head.Hash()
|
||||
|
||||
gc, err := r.GetGitCommit(headHash)
|
||||
if err != nil {
|
||||
return GitCommit{}, fmt.Errorf("getting commit %q: %w", headHash, err)
|
||||
}
|
||||
return gc, nil
|
||||
}
|
||||
|
||||
// GetGitCommitRange returns an ancestry of GitCommits, 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 one.
|
||||
func (r *Repo) GetGitCommitRange(start, end plumbing.Hash) ([]GitCommit, error) {
|
||||
curr, err := r.GetGitCommit(end)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("retrieving commit %q: %w", end, err)
|
||||
}
|
||||
|
||||
var commits []GitCommit
|
||||
var found bool
|
||||
for {
|
||||
commits = append(commits, curr)
|
||||
numParents := curr.GitCommit.NumParents()
|
||||
if numParents == 0 {
|
||||
break
|
||||
} else if numParents > 1 {
|
||||
return nil, fmt.Errorf("commit %q has more than one parent: %+v",
|
||||
curr.GitCommit.Hash, curr.GitCommit.ParentHashes)
|
||||
}
|
||||
|
||||
parentHash := curr.GitCommit.ParentHashes[0]
|
||||
parent, err := r.GetGitCommit(parentHash)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("retrieving commit %q: %w", parentHash, err)
|
||||
} else if start != plumbing.ZeroHash && parentHash == start {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
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
|
||||
}
|
||||
|
51
repo_test.go
51
repo_test.go
@ -122,7 +122,7 @@ func (h *harness) checkout(branch plumbing.ReferenceName) {
|
||||
h.t.Fatal(err)
|
||||
}
|
||||
|
||||
head, _, err := h.repo.head()
|
||||
head, err := h.repo.GetGitHead()
|
||||
if err != nil {
|
||||
h.t.Fatal(err)
|
||||
}
|
||||
@ -130,7 +130,7 @@ func (h *harness) checkout(branch plumbing.ReferenceName) {
|
||||
_, err = h.repo.GitRepo.Branch(branch.Short())
|
||||
if errors.Is(err, git.ErrBranchNotFound) {
|
||||
err = w.Checkout(&git.CheckoutOptions{
|
||||
Hash: head.Hash,
|
||||
Hash: head.GitCommit.Hash,
|
||||
Branch: branch,
|
||||
Create: true,
|
||||
})
|
||||
@ -166,9 +166,7 @@ func (h *harness) tryCommit(
|
||||
shouldSucceed bool,
|
||||
commit Commit,
|
||||
accountID string, accountSig sigcred.SignifierInterface,
|
||||
) (
|
||||
Commit, plumbing.Hash,
|
||||
) {
|
||||
) GitCommit {
|
||||
if accountSig != nil {
|
||||
var err error
|
||||
if commit, err = h.repo.AccreditCommit(commit, accountSig); err != nil {
|
||||
@ -176,7 +174,7 @@ func (h *harness) tryCommit(
|
||||
}
|
||||
}
|
||||
|
||||
hash, err := h.repo.Commit(commit, accountID)
|
||||
gitCommit, err := h.repo.Commit(commit, accountID)
|
||||
if err != nil {
|
||||
h.t.Fatalf("failed to commit ChangeCommit: %v", err)
|
||||
}
|
||||
@ -186,35 +184,28 @@ func (h *harness) tryCommit(
|
||||
h.t.Fatalf("determining checked out branch: %v", err)
|
||||
}
|
||||
|
||||
err = h.repo.VerifyCommit(branch, hash)
|
||||
err = h.repo.VerifyCommits(branch, []GitCommit{gitCommit})
|
||||
if shouldSucceed && err != nil {
|
||||
h.t.Fatalf("verifying commit %q: %v", hash, err)
|
||||
h.t.Fatalf("verifying commit %q: %v", gitCommit.GitCommit.Hash, err)
|
||||
} else if shouldSucceed {
|
||||
return commit, hash
|
||||
return gitCommit
|
||||
} else if !shouldSucceed && err == nil {
|
||||
h.t.Fatalf("verifying commit %q should have failed", hash)
|
||||
h.t.Fatalf("verifying commit %q should have failed", gitCommit.GitCommit.Hash)
|
||||
}
|
||||
|
||||
// commit verifying didn't succeed, reset it back. first get parent commit
|
||||
// to reset to
|
||||
commitObj, err := h.repo.GitRepo.CommitObject(hash)
|
||||
if err != nil {
|
||||
h.t.Fatalf("getting commit object of unverifiable hash %q: %v", hash, err)
|
||||
} else if commitObj.NumParents() == 0 {
|
||||
h.t.Fatalf("unverifiable commit %q has no parents, but it should", hash)
|
||||
if gitCommit.GitCommit.NumParents() == 0 {
|
||||
h.t.Fatalf("unverifiable commit %q has no parents, but it should", gitCommit.GitCommit.NumParents())
|
||||
}
|
||||
|
||||
h.reset(commitObj.ParentHashes[0], git.HardReset)
|
||||
return commit, hash
|
||||
h.reset(gitCommit.GitCommit.ParentHashes[0], git.HardReset)
|
||||
return gitCommit
|
||||
}
|
||||
|
||||
func (h *harness) changeCommit(
|
||||
msg string,
|
||||
accountID string,
|
||||
sig sigcred.SignifierInterface,
|
||||
) (
|
||||
Commit, plumbing.Hash,
|
||||
) {
|
||||
) GitCommit {
|
||||
commit, err := h.repo.NewCommitChange(msg)
|
||||
if err != nil {
|
||||
h.t.Fatalf("creating ChangeCommit: %v", err)
|
||||
@ -304,9 +295,15 @@ func TestThisRepoStillVerifies(t *testing.T) {
|
||||
t.Fatalf("error opening repo: %v", err)
|
||||
}
|
||||
|
||||
headCommit, _, err := repo.head()
|
||||
headGitCommit, err := repo.GetGitHead()
|
||||
if err != nil {
|
||||
t.Fatalf("error getting repo head: %v", err)
|
||||
t.Fatalf("getting repo head: %v", err)
|
||||
}
|
||||
|
||||
allCommits, err := repo.GetGitCommitRange(plumbing.ZeroHash, headGitCommit.GitCommit.Hash)
|
||||
if err != nil {
|
||||
t.Fatalf("getting all commits (up to %q): %v",
|
||||
headGitCommit.GitCommit.Hash, err)
|
||||
}
|
||||
|
||||
checkedOutBranch, err := repo.CheckedOutBranch()
|
||||
@ -314,9 +311,7 @@ func TestThisRepoStillVerifies(t *testing.T) {
|
||||
t.Fatalf("error determining checked out branch: %v", err)
|
||||
}
|
||||
|
||||
for _, hash := range headCommit.ParentHashes {
|
||||
if err := repo.VerifyCommit(checkedOutBranch, hash); err != nil {
|
||||
t.Fatalf("error verifying commit %q of branch %q: %v", hash, checkedOutBranch, err)
|
||||
}
|
||||
if err := repo.VerifyCommits(checkedOutBranch, allCommits); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user