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:
mediocregopher 2020-03-15 13:50:24 -06:00
parent 326de2afc6
commit 5ebb6597a8
12 changed files with 286 additions and 200 deletions

View File

@ -46,12 +46,12 @@ func cmdCommit(ctx context.Context, cmd *dcmd.Cmd) {
return fmt.Errorf("accrediting commit: %w", err) return fmt.Errorf("accrediting commit: %w", err)
} }
hash, err := repo.Commit(commit, *accountID) gitCommit, err := repo.Commit(commit, *accountID)
if err != nil { if err != nil {
return fmt.Errorf("committing to git: %w", err) 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 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") return nil, errors.New("credential commit cannot have any files changed")
} }
// TODO maybe nice to have a helper to do all of this gitCommit, err := repo.GetGitRevision(plumbing.Revision(*rev))
h, err := repo.GitRepo.ResolveRevision(plumbing.Revision(*rev))
if err != nil { 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) credCommit, err := repo.NewCommitCredential(gitCommit.Interface.GetHash())
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())
if err != nil { if err != nil {
return nil, fmt.Errorf("constructing credential commit: %w", err) return nil, fmt.Errorf("constructing credential commit: %w", err)
} else if err := accreditAndCommit(credCommit); err != nil { } else if err := accreditAndCommit(credCommit); err != nil {

View File

@ -29,7 +29,7 @@ func cmdHook(ctx context.Context, cmd *dcmd.Cmd) {
for { for {
line, err := br.ReadString('\n') line, err := br.ReadString('\n')
if errors.Is(err, io.EOF) { if errors.Is(err, io.EOF) {
return nil, nil break
} else if err != nil { } else if err != nil {
return nil, fmt.Errorf("error reading next line from stdin: %w", err) 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) 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]) branchName := plumbing.ReferenceName(lineParts[2])
// the zeroRevision gets sent on the very first push if !branchName.IsBranch() {
const zeroRevision plumbing.Revision = "0000000000000000000000000000000000000000" return nil, fmt.Errorf("reference %q is not a branch, can't push to it", branchName)
}
fromRev := plumbing.Revision(lineParts[0]) var startHash, endHash plumbing.Hash
var fromHash *plumbing.Hash // startRev can be a zero hash if these are the first commits for a
if fromRev != zeroRevision { // branch being pushed.
fromHash, err = repo.GitRepo.ResolveRevision(fromRev) if startRev != plumbing.Revision(plumbing.ZeroHash.String()) {
h, err := repo.GitRepo.ResolveRevision(startRev)
if err != nil { 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]) h, err := repo.GitRepo.ResolveRevision(endRev)
toHash, err := repo.GitRepo.ResolveRevision(toRev)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to resolve revision %q: %w", toRev, err) return nil, fmt.Errorf("resolving revision %q: %w", endRev, err)
}
endHash = *h
} }
toCommit, err := repo.GitRepo.CommitObject(*toHash) gitCommits, err := repo.GetGitCommitRange(startHash, endHash)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to find commit %q: %w", *toHash, err) 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)
}
} }
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)
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
}
currCommit = parentCommit
}
if !found && fromHash != nil {
return nil, fmt.Errorf("unable to find commit %q as an ancestor of %q", *fromHash, *toHash)
}
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)
}
}
fmt.Println("All pushed commits have been verified, well done.") fmt.Println("All pushed commits have been verified, well done.")
return nil, nil return nil, nil
}
}) })
} }

View File

@ -2,6 +2,7 @@ package main
import ( import (
"context" "context"
"dehub"
"dehub/cmd/dehub/dcmd" "dehub/cmd/dehub/dcmd"
"fmt" "fmt"
@ -16,10 +17,11 @@ func cmdVerify(ctx context.Context, cmd *dcmd.Cmd) {
cmd.Run(func() (context.Context, error) { cmd.Run(func() (context.Context, error) {
repo := ctxRepo(ctx) repo := ctxRepo(ctx)
h, err := repo.GitRepo.ResolveRevision(plumbing.Revision(*rev)) gitCommit, err := repo.GetGitRevision(plumbing.Revision(*rev))
if err != nil { 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 var branchName plumbing.ReferenceName
if *branch == "" { if *branch == "" {
@ -30,11 +32,12 @@ func cmdVerify(ctx context.Context, cmd *dcmd.Cmd) {
branchName = plumbing.NewBranchReferenceName(*branch) branchName = plumbing.NewBranchReferenceName(*branch)
} }
if err := repo.VerifyCommit(branchName, *h); err != nil { if err := repo.VerifyCommits(branchName, []dehub.GitCommit{gitCommit}); err != nil {
return nil, fmt.Errorf("could not verify commit at %q (%s): %w", *rev, *h, err) 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 return nil, nil
}) })
} }

View File

@ -20,7 +20,7 @@ func tmpFileMsg() (string, error) {
return "", fmt.Errorf("could not stat EDITOR %q: %w", editor, err) return "", fmt.Errorf("could not stat EDITOR %q: %w", editor, err)
} }
tmpf, err := ioutil.TempFile("", "") tmpf, err := ioutil.TempFile("", "dehub.*.txt")
if err != nil { if err != nil {
return "", fmt.Errorf("could not open temp file: %w", err) return "", fmt.Errorf("could not open temp file: %w", err)
} }

102
commit.go
View File

@ -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) 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 { if err != nil {
return commit, fmt.Errorf("could not grab snapshot of HEAD fs: %w", err) 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 // 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, // specified accountID as the author) and commits it to the current HEAD,
// returning the hash of the commit. // 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() msgB, err := m.MarshalText()
if err != nil { 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() w, err := r.GitRepo.Worktree()
if err != nil { 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{ Author: &object.Signature{
Name: accountID, Name: accountID,
When: time.Now(), 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 // HasStagedChanges returns true if there are file changes which have been
@ -176,7 +185,6 @@ func (r *Repo) HasStagedChanges() (bool, error) {
type verificationCtx struct { type verificationCtx struct {
commit *object.Commit commit *object.Commit
commitTree, parentTree *object.Tree commitTree, parentTree *object.Tree
isRootCommit bool
} }
// non-gophers gonna hate on this method, but I say it's fine // 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", return vctx, fmt.Errorf("retrieving commit tree object %q: %w",
vctx.commit.TreeHash, err) 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 { } else if parent, err := vctx.commit.Parent(0); err != nil {
return vctx, fmt.Errorf("retrieving commit parent: %w", err) return vctx, fmt.Errorf("retrieving commit parent: %w", err)
@ -258,57 +263,84 @@ func (r *Repo) assertAccessControls(
return nil return nil
} }
// VerifyCommit verifies that the commit at the given hash, which is presumably // VerifyCommits verifies that the given commits, which are presumably on the
// on the given branch, is gucci. // given branch, are gucci.
func (r *Repo) VerifyCommit(branch plumbing.ReferenceName, h plumbing.Hash) error { func (r *Repo) VerifyCommits(branch plumbing.ReferenceName, gitCommits []GitCommit) error {
vctx, err := r.verificationCtx(h)
if err != nil { for i, gitCommit := range gitCommits {
return err // 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 var sigFS fs.FS
if vctx.isRootCommit { if isRoot {
sigFS = fs.FromTree(vctx.commitTree) sigFS = fs.FromTree(vctx.commitTree)
} else { } else {
sigFS = fs.FromTree(vctx.parentTree) 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) cfg, err := r.loadConfig(sigFS)
if err != nil { 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 { 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 { if err != nil {
return fmt.Errorf("could not cast commit %+v to interface: %w", commit, err) return fmt.Errorf("calculating expected change hash: %w", 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)
} else if !bytes.Equal(changeHash, expectedChangeHash) { } else if !bytes.Equal(changeHash, expectedChangeHash) {
return fmt.Errorf("malformed change_hash in commit body, is %s but should be %s", return fmt.Errorf("malformed change_hash in commit body, is %s but should be %s",
base64.StdEncoding.EncodeToString(expectedChangeHash), base64.StdEncoding.EncodeToString(expectedChangeHash),
base64.StdEncoding.EncodeToString(changeHash)) base64.StdEncoding.EncodeToString(changeHash))
} }
for _, cred := range commit.Credentials { for _, cred := range gitCommit.Commit.Credentials {
sig, err := r.signifierForCredential(sigFS, cred) sig, err := r.signifierForCredential(sigFS, cred)
if err != nil { 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 { } 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)
} }
} }

View File

@ -4,9 +4,9 @@ import (
"dehub/fs" "dehub/fs"
"dehub/yamlutil" "dehub/yamlutil"
"errors" "errors"
"fmt"
"strings" "strings"
"gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/plumbing/object" "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 // encompassing the currently staged file changes. The Credentials of the
// returned Commit will _not_ be filled in. // returned Commit will _not_ be filled in.
func (r *Repo) NewCommitChange(msg string) (Commit, error) { func (r *Repo) NewCommitChange(msg string) (Commit, error) {
_, headTree, err := r.head() headTree := new(object.Tree)
if errors.Is(err, plumbing.ErrReferenceNotFound) { if head, err := r.GetGitHead(); err != nil && !errors.Is(err, ErrNoHead) {
headTree = &object.Tree{} return Commit{}, fmt.Errorf("getting HEAD commit: %w", err)
} else if err != nil { } else if err == nil {
return Commit{}, err headTree = head.GitTree
} }
_, stagedTree, err := fs.FromStagedChangesTree(r.GitRepo) _, stagedTree, err := fs.FromStagedChangesTree(r.GitRepo)

View File

@ -76,24 +76,22 @@ func TestChangeCommitVerify(t *testing.T) {
h.stage(step.tree) h.stage(step.tree)
account := h.cfg.Accounts[0] account := h.cfg.Accounts[0]
commit, hash := h.changeCommit(step.msg, account.ID, h.sig) gitCommit := h.changeCommit(step.msg, account.ID, h.sig)
commitObj, err := h.repo.GitRepo.CommitObject(hash) if step.msgHead == "" {
if err != nil {
t.Fatalf("failed to retrieve commit %v: %v", hash, err)
} else if step.msgHead == "" {
step.msgHead = strings.TrimSpace(step.msg) + "\n\n" step.msgHead = strings.TrimSpace(step.msg) + "\n\n"
} }
if !strings.HasPrefix(commitObj.Message, step.msgHead) { if !strings.HasPrefix(gitCommit.GitCommit.Message, step.msgHead) {
t.Fatalf("commit message %q does not start with expected head %q", commitObj.Message, step.msgHead) t.Fatalf("commit message %q does not start with expected head %q",
gitCommit.GitCommit.Message, step.msgHead)
} }
var actualCommit Commit 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) 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", 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))
} }
} }
}) })

View File

@ -61,11 +61,11 @@ func TestCredentialCommitVerify(t *testing.T) {
}, },
} }
h.stageCfg() 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 // toot user wants to create a credential commit for the root commit, for
// whatever reason. // whatever reason.
rootChangeHash := rootCommit.Change.ChangeHash rootChangeHash := rootGitCommit.Commit.Change.ChangeHash
credCommit, err := h.repo.NewCommitCredential(rootChangeHash) credCommit, err := h.repo.NewCommitCredential(rootChangeHash)
if err != nil { if err != nil {
t.Fatalf("creating credential commit for hash %x: %v", rootChangeHash, err) t.Fatalf("creating credential commit for hash %x: %v", rootChangeHash, err)

View File

@ -3,19 +3,17 @@ package dehub
import ( import (
"dehub/sigcred" "dehub/sigcred"
"testing" "testing"
"gopkg.in/src-d/go-git.v4/plumbing"
) )
func TestConfigChange(t *testing.T) { func TestConfigChange(t *testing.T) {
h := newHarness(t) h := newHarness(t)
var hashes []plumbing.Hash var gitCommits []GitCommit
// commit the initial staged changes, which merely include the config and // commit the initial staged changes, which merely include the config and
// public key // public key
_, hash := h.changeCommit("commit configuration", h.cfg.Accounts[0].ID, h.sig) gitCommit := h.changeCommit("commit configuration", h.cfg.Accounts[0].ID, h.sig)
hashes = append(hashes, hash) gitCommits = append(gitCommits, gitCommit)
// create a new account and add it to the configuration. That commit should // create a new account and add it to the configuration. That commit should
// not be verifiable, though // not be verifiable, though
@ -38,17 +36,15 @@ func TestConfigChange(t *testing.T) {
// now add with the root user, this should work. // now add with the root user, this should work.
h.stageCfg() h.stageCfg()
_, hash = h.changeCommit("add toot user", h.cfg.Accounts[0].ID, h.sig) gitCommit = h.changeCommit("add toot user", h.cfg.Accounts[0].ID, h.sig)
hashes = append(hashes, hash) gitCommits = append(gitCommits, gitCommit)
// _now_ the toot user should be able to do things. // _now_ the toot user should be able to do things.
h.stage(map[string]string{"foo/bar": "what a cool file"}) h.stage(map[string]string{"foo/bar": "what a cool file"})
_, hash = h.changeCommit("add a cool file", h.cfg.Accounts[1].ID, newSig) gitCommit = h.changeCommit("add a cool file", h.cfg.Accounts[1].ID, newSig)
hashes = append(hashes, hash) gitCommits = append(gitCommits, gitCommit)
for i, hash := range hashes { if err := h.repo.VerifyCommits(MainRefName, gitCommits); err != nil {
if err := h.repo.VerifyCommit(MainRefName, hash); err != nil { t.Fatal(err)
t.Fatalf("commit %d (%v) should have been verified but wasn't: %v", i, hash, err)
}
} }
} }

View File

@ -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 // LoadConfig loads the Config object from the HEAD of the repo, or directly
// from the filesystem if there is no HEAD yet. // from the filesystem if there is no HEAD yet.
func (r *Repo) LoadConfig() (Config, error) { func (r *Repo) LoadConfig() (Config, error) {
headFS, err := r.HeadFS() headFS, err := r.headFS()
if err != nil { if err != nil {
return Config{}, fmt.Errorf("error retrieving repo HEAD: %w", err) return Config{}, fmt.Errorf("error retrieving repo HEAD: %w", err)
} }

158
repo.go
View File

@ -95,7 +95,7 @@ func InitMemRepo() *Repo {
func (r *Repo) init() error { func (r *Repo) init() error {
h := plumbing.NewSymbolicReference(plumbing.HEAD, MainRefName) h := plumbing.NewSymbolicReference(plumbing.HEAD, MainRefName)
if err := r.GitRepo.Storer.SetReference(h); err != nil { 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 return nil
} }
@ -103,7 +103,7 @@ func (r *Repo) init() error {
func (r *Repo) billyFilesystem() (billy.Filesystem, error) { func (r *Repo) billyFilesystem() (billy.Filesystem, error) {
w, err := r.GitRepo.Worktree() w, err := r.GitRepo.Worktree()
if err != nil { 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 return w.Filesystem, nil
} }
@ -114,7 +114,7 @@ func (r *Repo) CheckedOutBranch() (plumbing.ReferenceName, error) {
// newly initialized repo very well. // newly initialized repo very well.
ogRef, err := r.GitRepo.Storer.Reference(plumbing.HEAD) ogRef, err := r.GitRepo.Storer.Reference(plumbing.HEAD)
if err != nil { 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 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) return "", fmt.Errorf("could not de-reference HEAD to a branch: %w", err)
} }
func (r *Repo) head() (*object.Commit, *object.Tree, error) { // headFS returns an FS based on the HEAD commit, or if there is no HEAD commit
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
// (it's an empty repo) an FS based on the raw filesystem. // (it's an empty repo) an FS based on the raw filesystem.
func (r *Repo) HeadFS() (fs.FS, error) { func (r *Repo) headFS() (fs.FS, error) {
_, headTree, err := r.head() head, err := r.GetGitHead()
if errors.Is(err, plumbing.ErrReferenceNotFound) { if errors.Is(err, ErrNoHead) {
bfs, err := r.billyFilesystem() bfs, err := r.billyFilesystem()
if err != nil { 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 return fs.FromBillyFilesystem(bfs), nil
} else if err != nil { } else if err != nil {
return nil, fmt.Errorf("could not get HEAD tree: %w", err) 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
} }

View File

@ -122,7 +122,7 @@ func (h *harness) checkout(branch plumbing.ReferenceName) {
h.t.Fatal(err) h.t.Fatal(err)
} }
head, _, err := h.repo.head() head, err := h.repo.GetGitHead()
if err != nil { if err != nil {
h.t.Fatal(err) h.t.Fatal(err)
} }
@ -130,7 +130,7 @@ func (h *harness) checkout(branch plumbing.ReferenceName) {
_, err = h.repo.GitRepo.Branch(branch.Short()) _, err = h.repo.GitRepo.Branch(branch.Short())
if errors.Is(err, git.ErrBranchNotFound) { if errors.Is(err, git.ErrBranchNotFound) {
err = w.Checkout(&git.CheckoutOptions{ err = w.Checkout(&git.CheckoutOptions{
Hash: head.Hash, Hash: head.GitCommit.Hash,
Branch: branch, Branch: branch,
Create: true, Create: true,
}) })
@ -166,9 +166,7 @@ func (h *harness) tryCommit(
shouldSucceed bool, shouldSucceed bool,
commit Commit, commit Commit,
accountID string, accountSig sigcred.SignifierInterface, accountID string, accountSig sigcred.SignifierInterface,
) ( ) GitCommit {
Commit, plumbing.Hash,
) {
if accountSig != nil { if accountSig != nil {
var err error var err error
if commit, err = h.repo.AccreditCommit(commit, accountSig); err != nil { 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 { if err != nil {
h.t.Fatalf("failed to commit ChangeCommit: %v", err) 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) 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 { 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 { } else if shouldSucceed {
return commit, hash return gitCommit
} else if !shouldSucceed && err == nil { } 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 if gitCommit.GitCommit.NumParents() == 0 {
// to reset to h.t.Fatalf("unverifiable commit %q has no parents, but it should", gitCommit.GitCommit.NumParents())
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)
} }
h.reset(commitObj.ParentHashes[0], git.HardReset) h.reset(gitCommit.GitCommit.ParentHashes[0], git.HardReset)
return commit, hash return gitCommit
} }
func (h *harness) changeCommit( func (h *harness) changeCommit(
msg string, msg string,
accountID string, accountID string,
sig sigcred.SignifierInterface, sig sigcred.SignifierInterface,
) ( ) GitCommit {
Commit, plumbing.Hash,
) {
commit, err := h.repo.NewCommitChange(msg) commit, err := h.repo.NewCommitChange(msg)
if err != nil { if err != nil {
h.t.Fatalf("creating ChangeCommit: %v", err) h.t.Fatalf("creating ChangeCommit: %v", err)
@ -304,9 +295,15 @@ func TestThisRepoStillVerifies(t *testing.T) {
t.Fatalf("error opening repo: %v", err) t.Fatalf("error opening repo: %v", err)
} }
headCommit, _, err := repo.head() headGitCommit, err := repo.GetGitHead()
if err != nil { 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() checkedOutBranch, err := repo.CheckedOutBranch()
@ -314,9 +311,7 @@ func TestThisRepoStillVerifies(t *testing.T) {
t.Fatalf("error determining checked out branch: %v", err) t.Fatalf("error determining checked out branch: %v", err)
} }
for _, hash := range headCommit.ParentHashes { if err := repo.VerifyCommits(checkedOutBranch, allCommits); err != nil {
if err := repo.VerifyCommit(checkedOutBranch, hash); err != nil { t.Fatal(err)
t.Fatalf("error verifying commit %q of branch %q: %v", hash, checkedOutBranch, err)
}
} }
} }