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