Change all references to 'master' into 'trunk'

message: Change all references to 'master' into 'trunk'
change_hash: ABJwxLvHMmj63oJPIv/vNeRCIp1ZDZYuPLQT57x9K1lO
credentials:
- type: pgp_signature
  pub_key_id: 95C46FA6A41148AC
  body: iQIzBAABAgAdFiEEJ6tQKp6olvZKJ0lwlcRvpqQRSKwFAl5Je88ACgkQlcRvpqQRSKyxyw/7B51/vvtlxLan9Z6q6rh7uyGcFf6WpWGiRDIccAJHqzegGP4eAb8V4Jzi9H4JJ82TnAc+EUegs8ewRiOcWj6YkU463b5CgUSwnzYKm86K6SyHGW1WH9OxFIDEMzCaVsktMEc9iLMl0dJNzakhPzu+qy74pU5xlypjCBzRLFeuqmnf4M1fq4FAq6fCs7ZVB3LccyC0mhCWsS2eiuCE/mVQ7WROVpxj5tplp71jlX6ZtWU7qsgvQS2V8ggtTVpCT2WE4u6bnu1oYOpSs9g+sxKKOAHKvZfjAMLG9qM3pOl1J+44W2Ms/mtON0VUX7G1Q5XVcmM0hPopXsiWLiAslSOAOuL+LE5iLq4nz1RyIbVh0QakYr+4NJL6Yt0L2I6lNVnUS4SgmMr86n8ZCcCjDAs6g5d7Zchqp3S3EF3bbJuLi0ICoOCxTD2gNkjo2BGverI8APLTUpujQl+9W/sGmT2aEdTpruGYjIcwsRaGo8VMDFECdZply5Ng1FbBoohv+j3vdO05fRyNkvRu3CBxP2tUe39jLxUmpu62igGF2VjZptsK0bLdzDpQ5Hv6jSZjn+mPyFJ5w+EX2JCRYpjn2eaYYtI/IyULGTaftzcEZeZibEhd/wQJnsEJNqXpCilDKgZajZyYhgy7BSeT9xDf2d8qyFoqWpvFhshHcesbUfG2O+Y=
  account: mediocregopher
This commit is contained in:
mediocregopher 2020-02-16 10:28:59 -07:00
parent 181802ba0e
commit a5bee27892
9 changed files with 115 additions and 125 deletions

2
.gitignore vendored
View File

@ -1 +1 @@
dehub
/dehub

20
SPEC.md
View File

@ -65,23 +65,23 @@ access_controls:
count: 1
```
# Master commit
# Trunk Commit
All new commits being appended to the HEAD of the `master` branch are subject to
All new commits being appended to the HEAD of the `trunk` branch are subject to
the following requirements:
* Must conform to all requirements defined by the `access_controls` section of
the `config.yml`, as found in the HEAD. If the commit is the initial commit of
the repo then it instead uses the `config.yml` found in itself.
the branch then it instead uses the `config.yml` found in itself.
* Must not be a merge commit (this may be amended later, but at present it
simplifies implementation).
* The commit message must conform to the format and semantics defined below.
## Master Commit Message
## Trunk Commit Message
The commit message for a commit being appended to the HEAD of the `master`
The commit message for a commit being appended to the HEAD of the `trunk`
branch must conform to the following format: a single line (the message head)
giving a short description of the change, then two newlines, then a body which
is a yaml formatted string:
@ -169,7 +169,7 @@ access control requirements. The message head of these are arbitrary, but the
body must be formatted as such:
```yaml
# This object matches the one found in the `credentials` section of the master
# This object matches the one found in the `credentials` section of the trunk
# commit message.
type: pgp_signature
account_id: some_user_id ```
@ -184,10 +184,10 @@ new signature commit, if they agree with the new changes.
## Merging MRs
When an MR has accumulated enough meta commits to fulfuill access control
requirements it may be coalesced into a single commit destined for the master
branch. See the Master Commit Message sub-section for details on how commits in
the master branch must be formatted.
When an MR has accumulated enough meta commits to fulfill access control
requirements it may be coalesced into a single commit destined for the `trunk`
branch. See the Trunk Commit Message sub-section for details on how commit
messages in the `trunk` branch must be formatted.
# TODO

View File

@ -76,10 +76,16 @@ var subCmds = []subCmd{
return fmt.Errorf("could not cast %+v to SignifierInterface: %w", sig, err)
}
_, hash, err := sctx.repo().CommitMaster(*msg, *accountID, sigInt)
tc, err := sctx.repo().NewTrunkCommit(*msg, *accountID, sigInt)
if err != nil {
return err
return fmt.Errorf("could not construct trunk commit: %w", err)
}
hash, err := sctx.repo().Commit(tc, *accountID)
if err != nil {
return fmt.Errorf("could not commit trunk commit: %w", err)
}
fmt.Printf("changes committed to HEAD as %s\n", hash)
return nil
},
@ -96,7 +102,7 @@ var subCmds = []subCmd{
return fmt.Errorf("could not resolve revision %q: %w", *rev, err)
}
if err := sctx.repo().VerifyMasterCommit(*h); err != nil {
if err := sctx.repo().VerifyTrunkCommit(*h); err != nil {
return fmt.Errorf("could not verify commit at %q (%s): %w", *rev, *h, err)
}
@ -130,8 +136,8 @@ var subCmds = []subCmd{
return fmt.Errorf("malformed pre-receive hook stdin line %q", line)
}
if plumbing.ReferenceName(lineParts[2]) != plumbing.Master {
return fmt.Errorf("only commits to the master branch are allowed at the moment (tried to push to %q)", lineParts[2])
if plumbing.ReferenceName(lineParts[2]) != dehub.Trunk {
return fmt.Errorf("only commits to the trunk branch are allowed at the moment (tried to push to %q)", lineParts[2])
}
// the zeroRevision gets sent on the very first push
@ -183,9 +189,9 @@ var subCmds = []subCmd{
for i := len(hashesToCheck) - 1; i >= 0; i-- {
hash := hashesToCheck[i]
fmt.Printf("Verifying master commit %q\n", hash)
if err := sctx.repo().VerifyMasterCommit(hash); err != nil {
return fmt.Errorf("could not verify master commit %q", hash)
fmt.Printf("Verifying trunk commit %q\n", hash)
if err := sctx.repo().VerifyTrunkCommit(hash); err != nil {
return fmt.Errorf("could not verify trunk commit %q", hash)
}
}
fmt.Println("All pushed commits have been verified, well done.")

1
cmd/http-server/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
dehub

128
commit.go
View File

@ -6,6 +6,7 @@ import (
"dehub/fs"
"dehub/sigcred"
"dehub/yamlutil"
"encoding"
"encoding/base64"
"errors"
"fmt"
@ -18,16 +19,16 @@ import (
yaml "gopkg.in/yaml.v2"
)
// MasterCommit describes the structure of the object encoded into the git
// message of a commit in the master branch.
type MasterCommit struct {
// TrunkCommit describes the structure of the object encoded into the git
// message of a commit in the repo trunk.
type TrunkCommit struct {
Message string `yaml:"message"`
ChangeHash yamlutil.Blob `yaml:"change_hash"`
Credentials []sigcred.Credential `yaml:"credentials"`
}
type mcYAML struct {
Val MasterCommit `yaml:",inline"`
type tcYAML struct {
Val TrunkCommit `yaml:",inline"`
}
func msgHead(msg string) string {
@ -39,55 +40,73 @@ func msgHead(msg string) string {
}
// MarshalText implements the encoding.TextMarshaler interface by returning the
// form the MasterCommit object takes in the git commit message.
func (mc MasterCommit) MarshalText() ([]byte, error) {
masterCommitEncoded, err := yaml.Marshal(mcYAML{mc})
// form the TrunkCommit object takes in the git commit message.
func (tc TrunkCommit) MarshalText() ([]byte, error) {
trunkCommitEncoded, err := yaml.Marshal(tcYAML{tc})
if err != nil {
return nil, fmt.Errorf("failed to encode MasterCommit message: %w", err)
return nil, fmt.Errorf("failed to encode TrunkCommit message: %w", err)
}
fullMsg := msgHead(mc.Message) + "\n\n" + string(masterCommitEncoded)
fullMsg := msgHead(tc.Message) + "\n\n" + string(trunkCommitEncoded)
return []byte(fullMsg), nil
}
// UnmarshalText implements the encoding.TextUnmarshaler interface by decoding a
// MasterCommit object which has been encoded into a git commit message.
func (mc *MasterCommit) UnmarshalText(msg []byte) error {
// TrunkCommit object which has been encoded into a git commit message.
func (tc *TrunkCommit) UnmarshalText(msg []byte) error {
i := bytes.Index(msg, []byte("\n"))
if i < 0 {
return fmt.Errorf("commit message %q is malformed", msg)
}
msgHead, msg := msg[:i], msg[i:]
var mcy mcYAML
if err := yaml.Unmarshal(msg, &mcy); err != nil {
return fmt.Errorf("could not unmarshal MasterCommit message: %w", err)
var tcy tcYAML
if err := yaml.Unmarshal(msg, &tcy); err != nil {
return fmt.Errorf("could not unmarshal TrunkCommit message: %w", err)
}
*mc = mcy.Val
if !strings.HasPrefix(mc.Message, string(msgHead)) {
return errors.New("encoded MasterCommit is malformed, it might not be an encoded MasterCommit")
*tc = tcy.Val
if !strings.HasPrefix(tc.Message, string(msgHead)) {
return errors.New("encoded TrunkCommit is malformed, it might not be an encoded TrunkCommit")
}
return nil
}
// CommitMaster constructs a MasterCommit using the given SignifierInterface to
// create a Credential for it. It returns the commit's hash after having set it
// to HEAD.
//
// TODO this method is a prototype and does not reflect the method's final form.
func (r *Repo) CommitMaster(msg, accountID string, sig sigcred.SignifierInterface) (MasterCommit, plumbing.Hash, error) {
// Commit uses the given TextMarshaler to create a git commit object (with the
// specified accountID as the author) and commits it to the current HEAD,
// returning the hash of the commit.
func (r *Repo) Commit(m encoding.TextMarshaler, accountID string) (plumbing.Hash, error) {
msgB, err := m.MarshalText()
if err != nil {
return plumbing.ZeroHash, fmt.Errorf("error marshaling %T to string: %v", m, err)
}
w, err := r.GitRepo.Worktree()
if err != nil {
return plumbing.ZeroHash, fmt.Errorf("could not get git worktree: %w", err)
}
return w.Commit(string(msgB), &git.CommitOptions{
Author: &object.Signature{
Name: accountID,
When: time.Now(),
},
})
}
// NewTrunkCommit constructs a TrunkCommit using the given SignifierInterface to
// create a Credential for it.
func (r *Repo) NewTrunkCommit(msg, accountID string, sig sigcred.SignifierInterface) (TrunkCommit, error) {
_, headTree, err := r.head()
if errors.Is(err, plumbing.ErrReferenceNotFound) {
headTree = &object.Tree{}
} else if err != nil {
return MasterCommit{}, plumbing.ZeroHash, err
return TrunkCommit{}, err
}
_, stagedTree, err := fs.FromStagedChangesTree(r.GitRepo)
if err != nil {
return MasterCommit{}, plumbing.ZeroHash, err
return TrunkCommit{}, err
}
// this is necessarily different than headTree for the case of there being
@ -97,18 +116,18 @@ func (r *Repo) CommitMaster(msg, accountID string, sig sigcred.SignifierInterfac
// data might be).
sigFS, err := r.headOrRawFS()
if err != nil {
return MasterCommit{}, plumbing.ZeroHash, err
return TrunkCommit{}, err
}
cfg, err := r.loadConfig(sigFS)
if err != nil {
return MasterCommit{}, plumbing.ZeroHash, fmt.Errorf("could not load config: %w", err)
return TrunkCommit{}, fmt.Errorf("could not load config: %w", err)
}
changeHash := genChangeHash(nil, msg, headTree, stagedTree)
cred, err := sig.Sign(sigFS, changeHash)
if err != nil {
return MasterCommit{}, plumbing.ZeroHash, fmt.Errorf("failed to sign commit hash: %w", err)
return TrunkCommit{}, fmt.Errorf("failed to sign commit hash: %w", err)
}
cred.AccountID = accountID
@ -120,37 +139,14 @@ func (r *Repo) CommitMaster(msg, accountID string, sig sigcred.SignifierInterfac
headTree, stagedTree,
)
if err != nil {
return MasterCommit{}, plumbing.ZeroHash, fmt.Errorf("commit would not satisfy access controls: %w", err)
return TrunkCommit{}, fmt.Errorf("commit would not satisfy access controls: %w", err)
}
masterCommit := MasterCommit{
return TrunkCommit{
Message: msg,
ChangeHash: changeHash,
Credentials: []sigcred.Credential{cred},
}
masterCommitB, err := masterCommit.MarshalText()
if err != nil {
return masterCommit, plumbing.ZeroHash, err
}
w, err := r.GitRepo.Worktree()
if err != nil {
return masterCommit, plumbing.ZeroHash, fmt.Errorf("could not get git worktree: %w", err)
}
hash, err := w.Commit(string(masterCommitB), &git.CommitOptions{
Author: &object.Signature{
Name: accountID,
When: time.Now(),
},
})
if err != nil {
return masterCommit, hash, fmt.Errorf("failed to commit changed: %w", err)
}
return masterCommit, hash, nil
}, nil
}
func (r *Repo) assertAccessControls(
@ -185,9 +181,9 @@ func (r *Repo) assertAccessControls(
return nil
}
// VerifyMasterCommit verifies that the commit at the given hash, which is
// presumably on the master branch, is gucci.
func (r *Repo) VerifyMasterCommit(h plumbing.Hash) error {
// VerifyTrunkCommit verifies that the commit at the given hash, which is
// presumably on the repo trunk, is gucci.
func (r *Repo) VerifyTrunkCommit(h plumbing.Hash) error {
commit, err := r.GitRepo.CommitObject(h)
if err != nil {
return fmt.Errorf("could not retrieve commit object: %w", err)
@ -198,8 +194,8 @@ func (r *Repo) VerifyMasterCommit(h plumbing.Hash) error {
return fmt.Errorf("could not retrieve tree object: %w", err)
}
var masterCommit MasterCommit
if err := masterCommit.UnmarshalText([]byte(commit.Message)); err != nil {
var trunkCommit TrunkCommit
if err := trunkCommit.UnmarshalText([]byte(commit.Message)); err != nil {
return err
}
@ -222,21 +218,21 @@ func (r *Repo) VerifyMasterCommit(h plumbing.Hash) error {
}
err = r.assertAccessControls(
cfg.AccessControls, masterCommit.Credentials,
cfg.AccessControls, trunkCommit.Credentials,
parentTree, commitTree,
)
if err != nil {
return fmt.Errorf("failed to satisfy all access controls: %w", err)
}
expectedChangeHash := genChangeHash(nil, masterCommit.Message, parentTree, commitTree)
if !bytes.Equal(masterCommit.ChangeHash, expectedChangeHash) {
expectedChangeHash := genChangeHash(nil, trunkCommit.Message, parentTree, commitTree)
if !bytes.Equal(trunkCommit.ChangeHash, expectedChangeHash) {
return fmt.Errorf("malformed change_hash in commit body, is %s but should be %s",
base64.StdEncoding.EncodeToString(expectedChangeHash),
base64.StdEncoding.EncodeToString(masterCommit.ChangeHash))
base64.StdEncoding.EncodeToString(trunkCommit.ChangeHash))
}
for _, cred := range masterCommit.Credentials {
for _, cred := range trunkCommit.Credentials {
sig, err := r.signifierForCredential(sigFS, cred)
if err != nil {
return fmt.Errorf("error finding signifier for credential %+v: %w", cred, err)
@ -245,7 +241,5 @@ func (r *Repo) VerifyMasterCommit(h plumbing.Hash) error {
}
}
// TODO access controls
return nil
}

View File

@ -13,7 +13,7 @@ import (
yaml "gopkg.in/yaml.v2"
)
func TestMasterCommitVerify(t *testing.T) {
func TestTrunkCommitVerify(t *testing.T) {
type step struct {
msg string
msgHead string // defaults to msg
@ -81,10 +81,8 @@ func TestMasterCommitVerify(t *testing.T) {
h.stage(step.tree)
account := h.cfg.Accounts[0]
masterCommit, hash, err := h.repo.CommitMaster(step.msg, account.ID, h.sig)
if err != nil {
t.Fatalf("failed to make MasterCommit: %v", err)
} else if err := h.repo.VerifyMasterCommit(hash); err != nil {
trunkCommit, hash := h.trunkCommit(step.msg, account.ID, h.sig)
if err := h.repo.VerifyTrunkCommit(hash); err != nil {
t.Fatalf("could not verify hash %v: %v", hash, err)
}
@ -99,12 +97,12 @@ func TestMasterCommitVerify(t *testing.T) {
t.Fatalf("commit message %q does not start with expected head %q", commit.Message, step.msgHead)
}
var actualMasterCommit MasterCommit
if err := actualMasterCommit.UnmarshalText([]byte(commit.Message)); err != nil {
var actualTrunkCommit TrunkCommit
if err := actualTrunkCommit.UnmarshalText([]byte(commit.Message)); err != nil {
t.Fatalf("error unmarshaling commit body: %v", err)
} else if !reflect.DeepEqual(actualMasterCommit, masterCommit) {
t.Fatalf("returned master commit:\n%s\ndoes not match actual one:\n%s",
spew.Sdump(masterCommit), spew.Sdump(actualMasterCommit))
} else if !reflect.DeepEqual(actualTrunkCommit, trunkCommit) {
t.Fatalf("returned trunk commit:\n%s\ndoes not match actual one:\n%s",
spew.Sdump(trunkCommit), spew.Sdump(actualTrunkCommit))
}
}
})
@ -118,10 +116,7 @@ func TestConfigChange(t *testing.T) {
// commit the initial staged changes, which merely include the config and
// public key
_, hash, err := h.repo.CommitMaster("commit configuration", h.cfg.Accounts[0].ID, h.sig)
if err != nil {
t.Fatal(err)
}
_, hash := h.trunkCommit("commit configuration", h.cfg.Accounts[0].ID, h.sig)
hashes = append(hashes, hash)
// create a new account and add it to the configuration. It should not be
@ -142,28 +137,22 @@ func TestConfigChange(t *testing.T) {
}
h.stage(map[string]string{ConfigPath: string(cfgBody)})
_, _, err = h.repo.CommitMaster("add toot user", h.cfg.Accounts[1].ID, newSig)
_, err = h.repo.NewTrunkCommit("add toot user", h.cfg.Accounts[1].ID, newSig)
if aclErr := (accessctl.ErrConditionSignatureUnsatisfied{}); !errors.As(err, &aclErr) {
t.Fatalf("CommitMaster should have returned an ErrConditionSignatureUnsatisfied, but returned %v", err)
t.Fatalf("NewTrunkCommit should have returned an ErrConditionSignatureUnsatisfied, but returned %v", err)
}
// now add with the root user, this should work.
_, hash, err = h.repo.CommitMaster("add toot user", h.cfg.Accounts[0].ID, h.sig)
if err != nil {
t.Fatalf("got an unexpected error committing with root: %v", err)
}
_, hash = h.trunkCommit("add toot user", h.cfg.Accounts[0].ID, h.sig)
hashes = append(hashes, hash)
// _now_ the toot user should be able to do things.
h.stage(map[string]string{"foo/bar": "what a cool file"})
_, hash, err = h.repo.CommitMaster("add a cool file", h.cfg.Accounts[1].ID, newSig)
if err != nil {
t.Fatalf("got an unexpected error committing with toot: %v", err)
}
_, hash = h.trunkCommit("add a cool file", h.cfg.Accounts[1].ID, newSig)
hashes = append(hashes, hash)
for i, hash := range hashes {
if err := h.repo.VerifyMasterCommit(hash); err != nil {
if err := h.repo.VerifyTrunkCommit(hash); err != nil {
t.Fatalf("commit %d (%v) should have been verified but wasn't: %v", i, hash, err)
}
}

View File

@ -24,6 +24,9 @@ const (
var (
// ConfigPath defines the expected path to the Repo's configuration file.
ConfigPath = filepath.Join(DehubDir, "config.yml")
// Trunk defines the reference name of the trunk branch.
Trunk = plumbing.ReferenceName("refs/heads/trunk")
)
type repoOpts struct {

View File

@ -9,9 +9,7 @@ import (
"path/filepath"
"testing"
"gopkg.in/src-d/go-git.v4"
"gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/plumbing/object"
yaml "gopkg.in/yaml.v2"
)
@ -102,17 +100,16 @@ func (h *harness) stage(tree map[string]string) {
}
}
func (h *harness) commit(msg string) plumbing.Hash {
w, err := h.repo.GitRepo.Worktree()
func (h *harness) trunkCommit(msg, accountID string, sig sigcred.SignifierInterface) (TrunkCommit, plumbing.Hash) {
tc, err := h.repo.NewTrunkCommit(msg, accountID, sig)
if err != nil {
h.t.Fatal(err)
}
hash, err := w.Commit(msg, &git.CommitOptions{
Author: &object.Signature{Name: "god"},
})
if err != nil {
h.t.Fatal(err)
h.t.Fatalf("failed to make TrunkCommit: %v", err)
}
return hash
hash, err := h.repo.Commit(tc, accountID)
if err != nil {
h.t.Fatalf("failed to commit TrunkCommit: %v", err)
}
return tc, hash
}

View File

@ -3,8 +3,8 @@ package sigcred
import "dehub/typeobj"
// Credential represents a credential which has been attached to a commit which
// hopefully will allow it to be included in the master branch. Exactly one
// field tagged with "type" should be set.
// hopefully will allow it to be included in the trunk. Exactly one field tagged
// with "type" should be set.
type Credential struct {
PGPSignature *CredentialPGPSignature `type:"pgp_signature"`