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
|
## Milestone: Enough polish to show off the project
|
||||||
|
|
||||||
|
* Maybe coalesce the `accessctl`, `fs`, and `sigcred` packages back into the
|
||||||
|
root "dehub" package.
|
||||||
|
|
||||||
* Polish commands
|
* Polish commands
|
||||||
- New flag system, some kind of interactivity support (e.g. user doesn't
|
- 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
|
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)
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("could not construct change commit: %w", err)
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("could not commit change commit: %w", err)
|
return fmt.Errorf("could not commit change commit: %w", err)
|
||||||
}
|
}
|
||||||
@ -191,7 +196,7 @@ var subCmds = []subCmd{
|
|||||||
branchName = plumbing.NewBranchReferenceName(*branch)
|
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)
|
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-- {
|
for i := len(hashesToCheck) - 1; i >= 0; i-- {
|
||||||
hash := hashesToCheck[i]
|
hash := hashesToCheck[i]
|
||||||
fmt.Printf("Verifying change commit %q\n", hash)
|
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)
|
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/accessctl"
|
||||||
"dehub/fs"
|
"dehub/fs"
|
||||||
"dehub/sigcred"
|
"dehub/sigcred"
|
||||||
"dehub/yamlutil"
|
"dehub/typeobj"
|
||||||
"encoding"
|
"encoding"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -19,59 +18,116 @@ import (
|
|||||||
yaml "gopkg.in/yaml.v2"
|
yaml "gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ChangeCommit describes the structure of a change commit message.
|
// CommitInterface describes the methods which must be implemented by the
|
||||||
type ChangeCommit struct {
|
// different commit types.
|
||||||
Message string `yaml:"message"`
|
type CommitInterface interface {
|
||||||
ChangeHash yamlutil.Blob `yaml:"change_hash"`
|
// 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"`
|
Credentials []sigcred.Credential `yaml:"credentials"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ccYAML struct {
|
// MarshalYAML implements the yaml.Marshaler interface.
|
||||||
Val ChangeCommit `yaml:",inline"`
|
func (c Commit) MarshalYAML() (interface{}, error) {
|
||||||
|
return typeobj.MarshalYAML(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func msgHead(msg string) string {
|
// UnmarshalYAML implements the yaml.Unmarshaler interface.
|
||||||
i := strings.Index(msg, "\n")
|
func (c *Commit) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
if i > 0 {
|
return typeobj.UnmarshalYAML(c, unmarshal)
|
||||||
return msg[:i]
|
|
||||||
}
|
}
|
||||||
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
|
// MarshalText implements the encoding.TextMarshaler interface by returning the
|
||||||
// form the ChangeCommit object takes in the git commit message.
|
// form the Commit object takes in the git commit message.
|
||||||
func (cc ChangeCommit) MarshalText() ([]byte, error) {
|
func (c Commit) MarshalText() ([]byte, error) {
|
||||||
changeCommitEncoded, err := yaml.Marshal(ccYAML{cc})
|
commitInt, err := c.Interface()
|
||||||
if err != nil {
|
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)
|
msgHead, err := commitInt.MessageHead()
|
||||||
return []byte(fullMsg), nil
|
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
|
// UnmarshalText implements the encoding.TextUnmarshaler interface by decoding a
|
||||||
// ChangeCommit object which has been encoded into a git commit message.
|
// Commit object which has been encoded into a git commit message.
|
||||||
func (cc *ChangeCommit) UnmarshalText(msg []byte) error {
|
func (c *Commit) UnmarshalText(msg []byte) error {
|
||||||
i := bytes.Index(msg, []byte("\n"))
|
i := bytes.Index(msg, []byte("\n"))
|
||||||
if i < 0 {
|
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(msgBody, c); err != nil {
|
||||||
if err := yaml.Unmarshal(msg, &ccy); err != nil {
|
return fmt.Errorf("could not unmarshal Commit message from yaml: %w", err)
|
||||||
return fmt.Errorf("could not unmarshal ChangeCommit message: %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
|
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
|
// 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.
|
||||||
@ -116,50 +172,6 @@ func (r *Repo) HasStagedChanges() (bool, error) {
|
|||||||
return any, nil
|
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(
|
func (r *Repo) assertAccessControls(
|
||||||
accessCtls []accessctl.BranchAccessControl, creds []sigcred.Credential,
|
accessCtls []accessctl.BranchAccessControl, creds []sigcred.Credential,
|
||||||
branch plumbing.ReferenceName, from, to *object.Tree,
|
branch plumbing.ReferenceName, from, to *object.Tree,
|
||||||
@ -197,28 +209,23 @@ func (r *Repo) assertAccessControls(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifyChangeCommit verifies that the change commit at the given hash, which
|
// VerifyCommit verifies that the commit at the given hash, which is presumably
|
||||||
// is presumably on the given branch, is gucci.
|
// on the given branch, is gucci.
|
||||||
func (r *Repo) VerifyChangeCommit(branch plumbing.ReferenceName, h plumbing.Hash) error {
|
func (r *Repo) VerifyCommit(branch plumbing.ReferenceName, h plumbing.Hash) error {
|
||||||
commit, err := r.GitRepo.CommitObject(h)
|
commitObj, err := r.GitRepo.CommitObject(h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not retrieve commit object: %w", err)
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("could not retrieve tree object: %w", err)
|
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
|
sigTree := commitTree // only for root commit
|
||||||
parentTree := &object.Tree{}
|
parentTree := &object.Tree{}
|
||||||
if commit.NumParents() > 0 {
|
if commitObj.NumParents() > 0 {
|
||||||
parent, err := commit.Parent(0)
|
parent, err := commitObj.Parent(0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not retrieve parent of commit: %w", err)
|
return fmt.Errorf("could not retrieve parent of commit: %w", err)
|
||||||
} else if parentTree, err = r.GitRepo.TreeObject(parent.TreeHash); err != nil {
|
} 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)
|
sigFS := fs.FromTree(sigTree)
|
||||||
|
|
||||||
|
var commit Commit
|
||||||
|
if err := commit.UnmarshalText([]byte(commitObj.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("error loading config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = r.assertAccessControls(
|
err = r.assertAccessControls(
|
||||||
cfg.AccessControls, changeCommit.Credentials,
|
cfg.AccessControls, commit.Credentials,
|
||||||
branch, parentTree, commitTree,
|
branch, parentTree, commitTree,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to satisfy all access controls: %w", err)
|
return fmt.Errorf("failed to satisfy all access controls: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
expectedChangeHash := genChangeHash(nil, changeCommit.Message, parentTree, commitTree)
|
commitInt, err := commit.Interface()
|
||||||
if !bytes.Equal(changeCommit.ChangeHash, expectedChangeHash) {
|
if err != nil {
|
||||||
return fmt.Errorf("malformed change_hash in commit body, is %s but should be %s",
|
return fmt.Errorf("could not cast commit %+v to interface: %w", commit, err)
|
||||||
base64.StdEncoding.EncodeToString(expectedChangeHash),
|
|
||||||
base64.StdEncoding.EncodeToString(changeCommit.ChangeHash))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
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("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 (
|
import (
|
||||||
"dehub/sigcred"
|
"dehub/sigcred"
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
|
||||||
"gopkg.in/src-d/go-git.v4"
|
"gopkg.in/src-d/go-git.v4"
|
||||||
"gopkg.in/src-d/go-git.v4/plumbing"
|
"gopkg.in/src-d/go-git.v4/plumbing"
|
||||||
yaml "gopkg.in/yaml.v2"
|
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) {
|
func TestConfigChange(t *testing.T) {
|
||||||
h := newHarness(t)
|
h := newHarness(t)
|
||||||
|
|
||||||
@ -137,7 +38,7 @@ func TestConfigChange(t *testing.T) {
|
|||||||
h.stage(map[string]string{ConfigPath: string(cfgBody)})
|
h.stage(map[string]string{ConfigPath: string(cfgBody)})
|
||||||
_, badHash := h.changeCommit("add toot user", h.cfg.Accounts[1].ID, newSig)
|
_, 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")
|
t.Fatal("toot user shouldn't be able to add itself to config")
|
||||||
}
|
}
|
||||||
h.reset(hash, git.HardReset)
|
h.reset(hash, git.HardReset)
|
||||||
@ -153,7 +54,7 @@ func TestConfigChange(t *testing.T) {
|
|||||||
hashes = append(hashes, hash)
|
hashes = append(hashes, hash)
|
||||||
|
|
||||||
for i, hash := range hashes {
|
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)
|
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
|
// 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.headOrRawFS()
|
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)
|
||||||
}
|
}
|
||||||
|
6
repo.go
6
repo.go
@ -158,9 +158,9 @@ func (r *Repo) head() (*object.Commit, *object.Tree, error) {
|
|||||||
return headCommit, headTree, nil
|
return headCommit, headTree, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// headOrRawFS returns an FS based on the HEAD commit, or if there is no HEAD
|
// HeadFS returns an FS based on the HEAD commit, or if there is no HEAD commit
|
||||||
// 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) headOrRawFS() (fs.FS, error) {
|
func (r *Repo) HeadFS() (fs.FS, error) {
|
||||||
_, headTree, err := r.head()
|
_, headTree, err := r.head()
|
||||||
if errors.Is(err, plumbing.ErrReferenceNotFound) {
|
if errors.Is(err, plumbing.ErrReferenceNotFound) {
|
||||||
bfs, err := r.billyFilesystem()
|
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) {
|
func (h *harness) changeCommit(msg, accountID string, sig sigcred.SignifierInterface) (Commit, plumbing.Hash) {
|
||||||
tc, err := h.repo.NewChangeCommit(msg, accountID, sig)
|
commit, err := h.repo.NewCommitChange(msg)
|
||||||
if err != nil {
|
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 {
|
if err != nil {
|
||||||
h.t.Fatalf("failed to commit ChangeCommit: %v", err)
|
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) {
|
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)
|
_, hash1 := harness.changeCommit("ain't no laws", "toot", nil)
|
||||||
|
|
||||||
// verifying the first should work, but not the second.
|
// 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)
|
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)
|
t.Fatalf("second commit %q should not have been verified", hash1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,7 +218,7 @@ accounts:
|
|||||||
path: ".dehub/root.asc"
|
path: ".dehub/root.asc"
|
||||||
`})
|
`})
|
||||||
_, hash2 := harness.changeCommit("Fix the config!", "root", harness.sig)
|
_, 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)
|
t.Fatalf("config fix commit %q should be verifiable, but got: %v", hash2, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user