Refactor commit type and logic to account for future commit types
--- type: change message: |- Refactor commit type and logic to account for future commit types This commit introduces a CommitInterface which CommitChange (previously ChangeCommit) now implements. Additionally, now all commit messages will include a type field and a "---" separator. The code is written to still accept all the old commit messages used in this repo. Other than those changes, most of this is just rearranging existing code. change_hash: AHjWAUxCjXgOL0Sb+oQZc6TmuVgVHJn08zpGreYyChwx credentials: - type: pgp_signature pub_key_id: 95C46FA6A41148AC body: iQIzBAABAgAdFiEEJ6tQKp6olvZKJ0lwlcRvpqQRSKwFAl5gOuoACgkQlcRvpqQRSKxapA//bODd2IwX2D7nFBkEEd00ol1l4vaw7pgwCjqyQtyskeCZ5IH6H6PkYOSmU9DIBde+cGo35Oi5ynChmfnSatvUZ1dLRJqm8FfOGDw/IsccyYDd1iptj16Ckr6Bsht1XgFJNN10hufuAg77fRIwbGi003WCQdnrJZ1Xbtgex6a4rUqVFW+sjXAB1msmo5B5nabfm/ta0epptxhlINIY2qP5Vb+ftdb2lRUNQwkIEr5GErfAgN3sxYEfqQvLzr/007tEeVznyhdgww47awWsBaTEd6njycbWpphnNA8PbcgUGKDYsj3qLz/bCR1VQOdWgod8PLEru2O+uC2+72ssfT9NDn8HB/WrBKG4W4oykJIhXJ5um18/B9ZSxPMlTQv2Yr4nF7w+Sx3UynEdUTducbIOUq2K+JnI+Ln3gHe0yRw5UEfrzymR4f2Nfe1I13rJk2W7SVRDmOYidr0MwBlLs8tmnJFMWmWZtd1hpPOpyOyUF1jKgvMuR9ferb5niuc3Lk4trqDiaF+tjy/NmAv/7c+qiVmKKpVJVVqvT60TqBR9DTHjRGktcPFD50sc811Th+Xd9RdhzpIYM+0DT790FTf8E0hY6wm/NKTGplfqwBSNZk87SeIiFTu7sZWVpAaPz1vTmVGduC1oj3/Zlv6TzNrUAp3VwBepROBhZlHCHUr9tKg= account: mediocregopher
This commit is contained in:
parent
9bfd012221
commit
cf05b3a072
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
210
commit.go
210
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,59 +18,116 @@ 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)
|
||||
}
|
||||
return msg
|
||||
|
||||
// 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 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("failed to encode ChangeCommit message: %w", err)
|
||||
return nil, fmt.Errorf("could not cast Commit %+v to interface : %w", c, err)
|
||||
}
|
||||
|
||||
fullMsg := msgHead(cc.Message) + "\n\n" + string(changeCommitEncoded)
|
||||
return []byte(fullMsg), nil
|
||||
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("error marshaling commit %+v as yaml: %w", c, err)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
*cc = ccy.Val
|
||||
if !strings.HasPrefix(cc.Message, string(msgHead)) {
|
||||
return errors.New("encoded ChangeCommit is malformed, it might not be an encoded ChangeCommit")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
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
|
||||
// specified accountID as the author) and commits it to the current HEAD,
|
||||
// returning the hash of the commit.
|
||||
@ -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) {
|
||||
return fmt.Errorf("malformed change_hash in commit body, is %s but should be %s",
|
||||
base64.StdEncoding.EncodeToString(expectedChangeHash),
|
||||
base64.StdEncoding.EncodeToString(changeCommit.ChangeHash))
|
||||
commitInt, err := commit.Interface()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not cast commit %+v to interface: %w", commit, err)
|
||||
}
|
||||
|
||||
for _, cred := range changeCommit.Credentials {
|
||||
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(changeHash))
|
||||
}
|
||||
|
||||
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)
|
||||
|
64
commit_change.go
Normal file
64
commit_change.go
Normal file
@ -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
|
||||
}
|
105
commit_change_test.go
Normal file
105
commit_change_test.go
Normal file
@ -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))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
103
commit_test.go
103
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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
6
repo.go
6
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()
|
||||
|
22
repo_test.go
22
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)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user