|
|
|
package dehub
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"dehub/accessctl"
|
|
|
|
"dehub/fs"
|
|
|
|
"dehub/sigcred"
|
|
|
|
"dehub/typeobj"
|
|
|
|
"encoding"
|
|
|
|
"encoding/base64"
|
|
|
|
"fmt"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"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"
|
|
|
|
)
|
|
|
|
|
|
|
|
// 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"`
|
|
|
|
Credential *CommitCredential `type:"credential"`
|
|
|
|
|
|
|
|
// 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"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// MarshalYAML implements the yaml.Marshaler interface.
|
|
|
|
func (c Commit) MarshalYAML() (interface{}, error) {
|
|
|
|
return typeobj.MarshalYAML(c)
|
|
|
|
}
|
|
|
|
|
|
|
|
// UnmarshalYAML implements the yaml.Unmarshaler interface.
|
|
|
|
func (c *Commit) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|
|
|
return typeobj.UnmarshalYAML(c, unmarshal)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Interface returns the CommitInterface instance encapsulated by this Commit
|
|
|
|
// object.
|
|
|
|
func (c Commit) Interface() (CommitInterface, error) {
|
|
|
|
el, _, err := typeobj.Element(c)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return el.(CommitInterface), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// MarshalText implements the encoding.TextMarshaler interface by returning the
|
|
|
|
// form the Commit object takes in the git commit message.
|
|
|
|
func (c Commit) MarshalText() ([]byte, error) {
|
|
|
|
commitInt, err := c.Interface()
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("could not cast Commit %+v to interface : %w", c, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
msgHead, err := commitInt.MessageHead()
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error constructing message head: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
msgBodyB, err := yaml.Marshal(c)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("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
|
|
|
|
// 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, it has no body", msg)
|
|
|
|
}
|
|
|
|
msgBody := msg[i:]
|
|
|
|
|
|
|
|
if err := yaml.Unmarshal(msgBody, c); err != nil {
|
|
|
|
return fmt.Errorf("could not unmarshal Commit message from yaml: %w", err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// AccreditCommit returns the given Commit with an appended Credential provided
|
|
|
|
// by the given SignifierInterface.
|
|
|
|
func (r *Repo) AccreditCommit(commit Commit, 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)
|
|
|
|
}
|
|
|
|
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.
|
|
|
|
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(),
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// HasStagedChanges returns true if there are file changes which have been
|
|
|
|
// staged (e.g. via "git add").
|
|
|
|
func (r *Repo) HasStagedChanges() (bool, error) {
|
|
|
|
w, err := r.GitRepo.Worktree()
|
|
|
|
if err != nil {
|
|
|
|
return false, fmt.Errorf("error retrieving worktree: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
status, err := w.Status()
|
|
|
|
if err != nil {
|
|
|
|
return false, fmt.Errorf("error retrieving worktree status: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var any bool
|
|
|
|
for _, fileStatus := range status {
|
|
|
|
if fileStatus.Staging != git.Unmodified {
|
|
|
|
any = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return any, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *Repo) assertAccessControls(
|
|
|
|
accessCtls []accessctl.BranchAccessControl, creds []sigcred.Credential,
|
|
|
|
branch plumbing.ReferenceName, from, to *object.Tree,
|
|
|
|
) error {
|
|
|
|
filesChanged, err := calcDiff(from, to)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
pathsChanged := make([]string, len(filesChanged))
|
|
|
|
for i := range filesChanged {
|
|
|
|
pathsChanged[i] = filesChanged[i].path
|
|
|
|
}
|
|
|
|
|
|
|
|
matchRes, err := accessctl.Match(accessCtls, accessctl.MatchInteractions{
|
|
|
|
Branch: branch.Short(),
|
|
|
|
FilePathsChanged: pathsChanged,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("could not determine applicable access controls: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, matchedAC := range matchRes.ChangeAccessControls {
|
|
|
|
ac := matchedAC.ChangeAccessControl
|
|
|
|
condInt, err := ac.Condition.Interface()
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("could not cast Condition of file path pattern %q to interface: %w",
|
|
|
|
ac.FilePathPattern, err)
|
|
|
|
} else if err := condInt.Satisfied(creds); err != nil {
|
|
|
|
return fmt.Errorf("access control of file path pattern %q not satisfied: %w\nFiles matched:\n%s",
|
|
|
|
ac.FilePathPattern, err, strings.Join(matchedAC.FilePaths, "\n"))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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(commitObj.TreeHash)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("could not retrieve tree object: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
sigTree := commitTree // only for root commit
|
|
|
|
parentTree := &object.Tree{}
|
|
|
|
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 {
|
|
|
|
return fmt.Errorf("could not retrieve tree object of parent %q: %w", parent.Hash, err)
|
|
|
|
}
|
|
|
|
sigTree = parentTree
|
|
|
|
}
|
|
|
|
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, commit.Credentials,
|
|
|
|
branch, parentTree, commitTree,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to satisfy all access controls: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
commitInt, err := commit.Interface()
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("could not cast commit %+v to interface: %w", commit, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
changeHash := commitInt.GetHash()
|
|
|
|
expectedChangeHash, err := commitInt.Hash(parentTree, commitTree)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error calculating expected change hash: %w", err)
|
|
|
|
} else if !bytes.Equal(changeHash, expectedChangeHash) {
|
|
|
|
return fmt.Errorf("malformed change_hash in commit body, is %s but should be %s",
|
|
|
|
base64.StdEncoding.EncodeToString(expectedChangeHash),
|
|
|
|
base64.StdEncoding.EncodeToString(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)
|
|
|
|
} else if err := sig.Verify(sigFS, expectedChangeHash, cred); err != nil {
|
|
|
|
return fmt.Errorf("error verifying credential %+v: %w", cred, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|