diff --git a/ROADMAP.md b/ROADMAP.md index a774c46..f82a797 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -20,6 +20,9 @@ set, only a sequence of milestones and the requirements to hit them. ## Milestone: Enough polish to show off the project +* Maybe coalesce the `accessctl`, `fs`, and `sigcred` packages back into the + root "dehub" package. + * Polish commands - New flag system, some kind of interactivity support (e.g. user doesn't specify required argument, give them a prompt on the CLI to input it diff --git a/cmd/dehub/main.go b/cmd/dehub/main.go index b133b62..547733e 100644 --- a/cmd/dehub/main.go +++ b/cmd/dehub/main.go @@ -155,12 +155,17 @@ var subCmds = []subCmd{ return fmt.Errorf("could not cast %+v to SignifierInterface: %w", sig, err) } - tc, err := sctx.repo().NewChangeCommit(*msg, *accountID, sigInt) + commit, err := sctx.repo().NewCommitChange(*msg) if err != nil { return fmt.Errorf("could not construct change commit: %w", err) } - hash, err := sctx.repo().Commit(tc, *accountID) + commit, err = sctx.repo().AccreditCommit(commit, *accountID, sigInt) + if err != nil { + return fmt.Errorf("could not accredit commit: %w", err) + } + + hash, err := sctx.repo().Commit(commit, *accountID) if err != nil { return fmt.Errorf("could not commit change commit: %w", err) } @@ -191,7 +196,7 @@ var subCmds = []subCmd{ branchName = plumbing.NewBranchReferenceName(*branch) } - if err := sctx.repo().VerifyChangeCommit(branchName, *h); err != nil { + if err := sctx.repo().VerifyCommit(branchName, *h); err != nil { return fmt.Errorf("could not verify commit at %q (%s): %w", *rev, *h, err) } @@ -278,7 +283,7 @@ var subCmds = []subCmd{ for i := len(hashesToCheck) - 1; i >= 0; i-- { hash := hashesToCheck[i] fmt.Printf("Verifying change commit %q\n", hash) - if err := sctx.repo().VerifyChangeCommit(branchName, hash); err != nil { + if err := sctx.repo().VerifyCommit(branchName, hash); err != nil { return fmt.Errorf("could not verify change commit %q: %w", hash, err) } } diff --git a/commit.go b/commit.go index b2a812f..3450f8c 100644 --- a/commit.go +++ b/commit.go @@ -5,10 +5,9 @@ import ( "dehub/accessctl" "dehub/fs" "dehub/sigcred" - "dehub/yamlutil" + "dehub/typeobj" "encoding" "encoding/base64" - "errors" "fmt" "strings" "time" @@ -19,57 +18,114 @@ import ( yaml "gopkg.in/yaml.v2" ) -// ChangeCommit describes the structure of a change commit message. -type ChangeCommit struct { - Message string `yaml:"message"` - ChangeHash yamlutil.Blob `yaml:"change_hash"` +// CommitInterface describes the methods which must be implemented by the +// different commit types. +type CommitInterface interface { + // MessageHead returns the head of the commit message (i.e. the first line). + MessageHead() (string, error) + + // Hash returns the raw hash which Signifiers can sign to accredit this + // commit. The tree objects given describe the filesystem state of the + // parent commit, and the filesystem state of this commit. + // + // This method should _not_ change any fields on the commit. + Hash(parent, this *object.Tree) ([]byte, error) + + // GetHash returns the signable Hash embedded in the commit, which should + // hopefully correspond to the Commit's Credentials. + GetHash() []byte +} + +// Commit represents a single Commit which is being added to a branch. Only one +// field should be set on a Commit, unless otherwise noted. +type Commit struct { + Change *CommitChange `type:"change,default"` + + // Credentials represent all created Credentials for this commit, and can be + // set on all Commit objects regardless of other fields being set. Credentials []sigcred.Credential `yaml:"credentials"` } -type ccYAML struct { - Val ChangeCommit `yaml:",inline"` +// MarshalYAML implements the yaml.Marshaler interface. +func (c Commit) MarshalYAML() (interface{}, error) { + return typeobj.MarshalYAML(c) } -func msgHead(msg string) string { - i := strings.Index(msg, "\n") - if i > 0 { - return msg[:i] +// UnmarshalYAML implements the yaml.Unmarshaler interface. +func (c *Commit) UnmarshalYAML(unmarshal func(interface{}) error) error { + return typeobj.UnmarshalYAML(c, unmarshal) +} + +// Interface returns the CommitInterface instance encapsulated by this Commit +// object. +func (c Commit) Interface() (CommitInterface, error) { + el, _, err := typeobj.Element(c) + if err != nil { + return nil, err } - return msg + return el.(CommitInterface), nil } // MarshalText implements the encoding.TextMarshaler interface by returning the -// form the ChangeCommit object takes in the git commit message. -func (cc ChangeCommit) MarshalText() ([]byte, error) { - changeCommitEncoded, err := yaml.Marshal(ccYAML{cc}) +// form the Commit object takes in the git commit message. +func (c Commit) MarshalText() ([]byte, error) { + commitInt, err := c.Interface() + if err != nil { + return nil, fmt.Errorf("could not cast Commit %+v to interface : %w", c, err) + } + + msgHead, err := commitInt.MessageHead() + if err != nil { + return nil, fmt.Errorf("error constructing message head: %w", err) + } + + msgBodyB, err := yaml.Marshal(c) if err != nil { - return nil, fmt.Errorf("failed to encode ChangeCommit message: %w", err) + return nil, fmt.Errorf("error marshaling commit %+v as yaml: %w", c, err) } - fullMsg := msgHead(cc.Message) + "\n\n" + string(changeCommitEncoded) - return []byte(fullMsg), nil + w := new(bytes.Buffer) + w.WriteString(msgHead) + w.WriteString("\n\n---\n") + w.Write(msgBodyB) + return w.Bytes(), nil } // UnmarshalText implements the encoding.TextUnmarshaler interface by decoding a -// ChangeCommit object which has been encoded into a git commit message. -func (cc *ChangeCommit) UnmarshalText(msg []byte) error { +// Commit object which has been encoded into a git commit message. +func (c *Commit) UnmarshalText(msg []byte) error { i := bytes.Index(msg, []byte("\n")) if i < 0 { - return fmt.Errorf("commit message %q is malformed", msg) + return fmt.Errorf("commit message %q is malformed, it has no body", msg) } - msgHead, msg := msg[:i], msg[i:] + msgBody := msg[i:] - var ccy ccYAML - if err := yaml.Unmarshal(msg, &ccy); err != nil { - return fmt.Errorf("could not unmarshal ChangeCommit message: %w", err) + if err := yaml.Unmarshal(msgBody, c); err != nil { + return fmt.Errorf("could not unmarshal Commit message from yaml: %w", err) } + return nil +} - *cc = ccy.Val - if !strings.HasPrefix(cc.Message, string(msgHead)) { - return errors.New("encoded ChangeCommit is malformed, it might not be an encoded ChangeCommit") +// AccreditCommit returns the given Commit with an appended Credential provided +// by the given account and its Signifier. +func (r *Repo) AccreditCommit(commit Commit, accountID string, sigInt sigcred.SignifierInterface) (Commit, error) { + commitInt, err := commit.Interface() + if err != nil { + return commit, fmt.Errorf("could not cast commit %+v to interface: %w", commit, err) } - return nil + headFS, err := r.HeadFS() + if err != nil { + return commit, fmt.Errorf("could not grab snapshot of HEAD fs: %w", err) + } + + cred, err := sigInt.Sign(headFS, commitInt.GetHash()) + if err != nil { + return commit, fmt.Errorf("could not accreddit change commit: %w", err) + } + cred.AccountID = accountID + commit.Credentials = append(commit.Credentials, cred) + return commit, nil } // Commit uses the given TextMarshaler to create a git commit object (with the @@ -116,50 +172,6 @@ func (r *Repo) HasStagedChanges() (bool, error) { return any, nil } -// NewChangeCommit constructs a ChangeCommit. If sig is given then it is used to -// create a Credential for the ChangeCommit. -func (r *Repo) NewChangeCommit(msg, accountID string, sig sigcred.SignifierInterface) (ChangeCommit, error) { - _, headTree, err := r.head() - if errors.Is(err, plumbing.ErrReferenceNotFound) { - headTree = &object.Tree{} - } else if err != nil { - return ChangeCommit{}, err - } - - _, stagedTree, err := fs.FromStagedChangesTree(r.GitRepo) - if err != nil { - return ChangeCommit{}, err - } - - changeHash := genChangeHash(nil, msg, headTree, stagedTree) - - var creds []sigcred.Credential - if sig != nil { - // this is necessarily different than headTree for the case of there - // being no HEAD (ie it's the first commit). In that case we want - // headTree to be empty (because it's being used to generate the change - // hash), but we want the signifier to use the raw fs (because that's - // where the signifier's data might be). - sigFS, err := r.headOrRawFS() - if err != nil { - return ChangeCommit{}, err - } - - cred, err := sig.Sign(sigFS, changeHash) - if err != nil { - return ChangeCommit{}, fmt.Errorf("failed to sign commit hash: %w", err) - } - cred.AccountID = accountID - creds = append(creds, cred) - } - - return ChangeCommit{ - Message: msg, - ChangeHash: changeHash, - Credentials: creds, - }, nil -} - func (r *Repo) assertAccessControls( accessCtls []accessctl.BranchAccessControl, creds []sigcred.Credential, branch plumbing.ReferenceName, from, to *object.Tree, @@ -197,28 +209,23 @@ func (r *Repo) assertAccessControls( return nil } -// VerifyChangeCommit verifies that the change commit at the given hash, which -// is presumably on the given branch, is gucci. -func (r *Repo) VerifyChangeCommit(branch plumbing.ReferenceName, h plumbing.Hash) error { - commit, err := r.GitRepo.CommitObject(h) +// 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 { + commitObj, err := r.GitRepo.CommitObject(h) if err != nil { return fmt.Errorf("could not retrieve commit object: %w", err) } - commitTree, err := r.GitRepo.TreeObject(commit.TreeHash) + commitTree, err := r.GitRepo.TreeObject(commitObj.TreeHash) if err != nil { return fmt.Errorf("could not retrieve tree object: %w", err) } - var changeCommit ChangeCommit - if err := changeCommit.UnmarshalText([]byte(commit.Message)); err != nil { - return err - } - sigTree := commitTree // only for root commit parentTree := &object.Tree{} - if commit.NumParents() > 0 { - parent, err := commit.Parent(0) + if commitObj.NumParents() > 0 { + parent, err := commitObj.Parent(0) if err != nil { return fmt.Errorf("could not retrieve parent of commit: %w", err) } else if parentTree, err = r.GitRepo.TreeObject(parent.TreeHash); err != nil { @@ -228,27 +235,40 @@ func (r *Repo) VerifyChangeCommit(branch plumbing.ReferenceName, h plumbing.Hash } sigFS := fs.FromTree(sigTree) + var commit Commit + if err := commit.UnmarshalText([]byte(commitObj.Message)); err != nil { + return err + } + cfg, err := r.loadConfig(sigFS) if err != nil { return fmt.Errorf("error loading config: %w", err) } err = r.assertAccessControls( - cfg.AccessControls, changeCommit.Credentials, + cfg.AccessControls, commit.Credentials, branch, parentTree, commitTree, ) if err != nil { return fmt.Errorf("failed to satisfy all access controls: %w", err) } - expectedChangeHash := genChangeHash(nil, changeCommit.Message, parentTree, commitTree) - if !bytes.Equal(changeCommit.ChangeHash, expectedChangeHash) { + commitInt, err := commit.Interface() + if err != nil { + return fmt.Errorf("could not cast commit %+v to interface: %w", commit, err) + } + + changeHash := commitInt.GetHash() + expectedChangeHash, err := commitInt.Hash(parentTree, commitTree) + if err != nil { + return fmt.Errorf("error 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(changeCommit.ChangeHash)) + base64.StdEncoding.EncodeToString(changeHash)) } - for _, cred := range changeCommit.Credentials { + for _, cred := range commit.Credentials { sig, err := r.signifierForCredential(sigFS, cred) if err != nil { return fmt.Errorf("error finding signifier for credential %+v: %w", cred, err) diff --git a/commit_change.go b/commit_change.go new file mode 100644 index 0000000..b7b4c17 --- /dev/null +++ b/commit_change.go @@ -0,0 +1,64 @@ +package dehub + +import ( + "dehub/fs" + "dehub/yamlutil" + "errors" + "strings" + + "gopkg.in/src-d/go-git.v4/plumbing" + "gopkg.in/src-d/go-git.v4/plumbing/object" +) + +// CommitChange describes the structure of a change commit message. +type CommitChange struct { + Message string `yaml:"message"` + ChangeHash yamlutil.Blob `yaml:"change_hash"` +} + +var _ CommitInterface = CommitChange{} + +// NewCommitChange constructs a Commit populated with a 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 + } + + _, stagedTree, err := fs.FromStagedChangesTree(r.GitRepo) + if err != nil { + return Commit{}, err + } + + cc := CommitChange{Message: msg} + if cc.ChangeHash, err = cc.Hash(headTree, stagedTree); err != nil { + return Commit{}, err + } + + return Commit{ + Change: &cc, + }, nil +} + +// MessageHead implements the method for the CommitInterface interface. +func (cc CommitChange) MessageHead() (string, error) { + i := strings.Index(cc.Message, "\n") + if i > 0 { + return cc.Message[:i], nil + } + return cc.Message, nil +} + +// Hash implements the method for the CommitInterface interface. +func (cc CommitChange) Hash(parent, this *object.Tree) ([]byte, error) { + return genChangeHash(nil, cc.Message, parent, this), nil +} + +// GetHash implements the method for the CommitInterface interface. +func (cc CommitChange) GetHash() []byte { + return cc.ChangeHash +} diff --git a/commit_change_test.go b/commit_change_test.go new file mode 100644 index 0000000..313d11c --- /dev/null +++ b/commit_change_test.go @@ -0,0 +1,105 @@ +package dehub + +import ( + "reflect" + "strings" + "testing" + + "github.com/davecgh/go-spew/spew" +) + +func TestChangeCommitVerify(t *testing.T) { + type step struct { + msg string + msgHead string // defaults to msg + tree map[string]string + } + testCases := []struct { + descr string + steps []step + }{ + { + descr: "single commit", + steps: []step{ + { + msg: "first commit", + tree: map[string]string{"a": "0", "b": "1"}, + }, + }, + }, + { + descr: "multiple commits", + steps: []step{ + { + msg: "first commit", + tree: map[string]string{"a": "0", "b": "1"}, + }, + { + msg: "second commit, changing a", + tree: map[string]string{"a": "1"}, + }, + { + msg: "third commit, empty", + }, + { + msg: "fourth commit, adding c, removing b", + tree: map[string]string{"b": "", "c": "2"}, + }, + }, + }, + { + descr: "big body commits", + steps: []step{ + { + msg: "first commit, single line but with newline\n", + }, + { + msg: "second commit, single line but with two newlines\n\n", + msgHead: "second commit, single line but with two newlines\n\n", + }, + { + msg: "third commit, multi-line with one newline\nanother line!", + msgHead: "third commit, multi-line with one newline\n\n", + }, + { + msg: "fourth commit, multi-line with two newlines\n\nanother line!", + msgHead: "fourth commit, multi-line with two newlines\n\n", + }, + }, + }, + } + + for _, test := range testCases { + t.Run(test.descr, func(t *testing.T) { + h := newHarness(t) + for _, step := range test.steps { + h.stage(step.tree) + account := h.cfg.Accounts[0] + + commit, hash := h.changeCommit(step.msg, account.ID, h.sig) + if err := h.repo.VerifyCommit(MainRefName, hash); err != nil { + t.Fatalf("could not verify hash %v: %v", hash, err) + } + + commitObj, err := h.repo.GitRepo.CommitObject(hash) + 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" + } + + if !strings.HasPrefix(commitObj.Message, step.msgHead) { + t.Fatalf("commit message %q does not start with expected head %q", commitObj.Message, step.msgHead) + } + + var actualCommit Commit + if err := actualCommit.UnmarshalText([]byte(commitObj.Message)); err != nil { + t.Fatalf("error unmarshaling commit body: %v", err) + } else if !reflect.DeepEqual(actualCommit, commit) { + t.Fatalf("returned change commit:\n%s\ndoes not match actual one:\n%s", + spew.Sdump(commit), spew.Sdump(actualCommit)) + } + } + }) + } +} diff --git a/commit_test.go b/commit_test.go index 87eb9a8..f50f92e 100644 --- a/commit_test.go +++ b/commit_test.go @@ -2,112 +2,13 @@ package dehub import ( "dehub/sigcred" - "reflect" - "strings" "testing" - "github.com/davecgh/go-spew/spew" "gopkg.in/src-d/go-git.v4" "gopkg.in/src-d/go-git.v4/plumbing" yaml "gopkg.in/yaml.v2" ) -func TestChangeCommitVerify(t *testing.T) { - type step struct { - msg string - msgHead string // defaults to msg - tree map[string]string - } - testCases := []struct { - descr string - steps []step - }{ - { - descr: "single commit", - steps: []step{ - { - msg: "first commit", - tree: map[string]string{"a": "0", "b": "1"}, - }, - }, - }, - { - descr: "multiple commits", - steps: []step{ - { - msg: "first commit", - tree: map[string]string{"a": "0", "b": "1"}, - }, - { - msg: "second commit, changing a", - tree: map[string]string{"a": "1"}, - }, - { - msg: "third commit, empty", - }, - { - msg: "fourth commit, adding c, removing b", - tree: map[string]string{"b": "", "c": "2"}, - }, - }, - }, - { - descr: "big body commits", - steps: []step{ - { - msg: "first commit, single line but with newline\n", - }, - { - msg: "second commit, single line but with two newlines\n\n", - msgHead: "second commit, single line but with two newlines\n\n", - }, - { - msg: "third commit, multi-line with one newline\nanother line!", - msgHead: "third commit, multi-line with one newline\n\n", - }, - { - msg: "fourth commit, multi-line with two newlines\n\nanother line!", - msgHead: "fourth commit, multi-line with two newlines\n\n", - }, - }, - }, - } - - for _, test := range testCases { - t.Run(test.descr, func(t *testing.T) { - h := newHarness(t) - for _, step := range test.steps { - h.stage(step.tree) - account := h.cfg.Accounts[0] - - changeCommit, hash := h.changeCommit(step.msg, account.ID, h.sig) - if err := h.repo.VerifyChangeCommit(MainRefName, hash); err != nil { - t.Fatalf("could not verify hash %v: %v", hash, err) - } - - commit, err := h.repo.GitRepo.CommitObject(hash) - 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" - } - - if !strings.HasPrefix(commit.Message, step.msgHead) { - t.Fatalf("commit message %q does not start with expected head %q", commit.Message, step.msgHead) - } - - var actualChangeCommit ChangeCommit - if err := actualChangeCommit.UnmarshalText([]byte(commit.Message)); err != nil { - t.Fatalf("error unmarshaling commit body: %v", err) - } else if !reflect.DeepEqual(actualChangeCommit, changeCommit) { - t.Fatalf("returned change commit:\n%s\ndoes not match actual one:\n%s", - spew.Sdump(changeCommit), spew.Sdump(actualChangeCommit)) - } - } - }) - } -} - func TestConfigChange(t *testing.T) { h := newHarness(t) @@ -137,7 +38,7 @@ func TestConfigChange(t *testing.T) { h.stage(map[string]string{ConfigPath: string(cfgBody)}) _, badHash := h.changeCommit("add toot user", h.cfg.Accounts[1].ID, newSig) - if err := h.repo.VerifyChangeCommit(MainRefName, badHash); err == nil { + if err := h.repo.VerifyCommit(MainRefName, badHash); err == nil { t.Fatal("toot user shouldn't be able to add itself to config") } h.reset(hash, git.HardReset) @@ -153,7 +54,7 @@ func TestConfigChange(t *testing.T) { hashes = append(hashes, hash) for i, hash := range hashes { - if err := h.repo.VerifyChangeCommit(MainRefName, hash); err != nil { + 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) } } diff --git a/config.go b/config.go index c95209c..637fa80 100644 --- a/config.go +++ b/config.go @@ -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.headOrRawFS() + headFS, err := r.HeadFS() if err != nil { return Config{}, fmt.Errorf("error retrieving repo HEAD: %w", err) } diff --git a/repo.go b/repo.go index 731b4b4..b7ee3b0 100644 --- a/repo.go +++ b/repo.go @@ -158,9 +158,9 @@ func (r *Repo) head() (*object.Commit, *object.Tree, error) { return headCommit, headTree, nil } -// headOrRawFS 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) headOrRawFS() (fs.FS, error) { +// 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) { bfs, err := r.billyFilesystem() diff --git a/repo_test.go b/repo_test.go index 6bd8f1f..7501b81 100644 --- a/repo_test.go +++ b/repo_test.go @@ -107,18 +107,24 @@ func (h *harness) stage(tree map[string]string) { } } -func (h *harness) changeCommit(msg, accountID string, sig sigcred.SignifierInterface) (ChangeCommit, plumbing.Hash) { - tc, err := h.repo.NewChangeCommit(msg, accountID, sig) +func (h *harness) changeCommit(msg, accountID string, sig sigcred.SignifierInterface) (Commit, plumbing.Hash) { + commit, err := h.repo.NewCommitChange(msg) if err != nil { - h.t.Fatalf("failed to make ChangeCommit: %v", err) + h.t.Fatalf("failed to create CommitChange: %v", err) } - hash, err := h.repo.Commit(tc, accountID) + if sig != nil { + if commit, err = h.repo.AccreditCommit(commit, accountID, sig); err != nil { + h.t.Fatalf("failed to accredit commit: %v", err) + } + } + + hash, err := h.repo.Commit(commit, accountID) if err != nil { h.t.Fatalf("failed to commit ChangeCommit: %v", err) } - return tc, hash + return commit, hash } func (h *harness) reset(to plumbing.Hash, mode git.ResetMode) { @@ -193,9 +199,9 @@ access_controls: _, hash1 := harness.changeCommit("ain't no laws", "toot", nil) // verifying the first should work, but not the second. - if err := harness.repo.VerifyChangeCommit(MainRefName, hash0); err != nil { + if err := harness.repo.VerifyCommit(MainRefName, hash0); err != nil { t.Fatalf("first commit %q should be verifiable, but got: %v", hash0, err) - } else if err := harness.repo.VerifyChangeCommit(MainRefName, hash1); err == nil { + } else if err := harness.repo.VerifyCommit(MainRefName, hash1); err == nil { t.Fatalf("second commit %q should not have been verified", hash1) } @@ -212,7 +218,7 @@ accounts: path: ".dehub/root.asc" `}) _, hash2 := harness.changeCommit("Fix the config!", "root", harness.sig) - if err := harness.repo.VerifyChangeCommit(MainRefName, hash2); err != nil { + if err := harness.repo.VerifyCommit(MainRefName, hash2); err != nil { t.Fatalf("config fix commit %q should be verifiable, but got: %v", hash2, err) } }