Completely refactor naming of everything, in light of new SPEC

---
type: change
description: |-
  Completely refactor naming of everything, in light of new SPEC

  Writing the SPEC shed some light on just how weakly a lot of concepts, like
  "commit", had been defined, and prompted the delineation of a lot of things
  along specific lines (commit vs payload, repo vs project). This commit makes the
  code reflect the SPEC much better in quite a few ways:

  * Repo is now Project
  * Commit is now Payload
  * GitCommit is now just Commit
  * Hash is now Fingerprint
  * A lot of minor fields got renamed
  * All the XXXInterface types are now just XXX, and their old XXX type is now
    XXXUnion.

  More than likely there's still some comments and variable names that have
  slipped passed, but overall I feel like I got most of the changes.
fingerprint: AKkDC5BKhKbfXzZQ/F4KquHeMgVvcNxgLmkZFz/nP/tY
credentials:
- type: pgp_signature
  pub_key_id: 95C46FA6A41148AC
  body: iQIzBAABAgAdFiEEJ6tQKp6olvZKJ0lwlcRvpqQRSKwFAl6l7aYACgkQlcRvpqQRSKxFrA//VQ+f8B6pwGS3ORB4VVBnHvvJTGZvAYTvB0fHuHJx2EreR4FwjhaNakk5ClkwbO7WFMq++2OV4xIkvzwswLdbXZF0IHx3wScQM59v4vIkR4V9Lj5p1aGGhQna52uIKugF2gTqKdU4tqYzmBjDND/c2XDwCN5CwTwwnAHXUSSsHxviiPUYPWV5wzFP7uyRW0ZeK8Isv7QECKRXlsDjcSJa+g+jc091FG/jG9Dkai8fbDbW8YXj7W3ALaXgXWEBJMrgQxZcJJRjgCvLY72FIIrUBquu3FepiyzMtZ0yaIvi4NmGCsYqIv00NcMvMtD7iwhOCZn10Sku4wvaKJ8YBMRduhqC99fnr/ZDW0/HvTNcL7GKx11GjwtmzkJgwsHFPy3zX+kMdF4m3WgtoeI0GwEsBXXZE2C49yAk3Mb/3puegl3a1PPMvOabTzo7Xm6xpWkI6gISChI7My71H3EuKZWhkb+IubPmMvJJXIdVxHnsHPz2dl/BZXLgpfVdEgQa2qWeXtYI4NNm37pLl3gv92V4kka+Kr4gfdoq8mJ7aqvc9was35baJbHg4+fEVJG2Wj+2AQU+ncx3nAFzgYyMxwo9K8VuC4QdfRF4ImyxTnWkuokEn9H6JRrbkBDKIELj6vzdPmsjOUEQ4nsYX66/zSibFD7UvhQmdXFs8Gp8/Qq6g4M=
  account: mediocregopher
main
mediocregopher 3 years ago committed by Brian Picciano
parent 351048e9aa
commit b01fe1524a
  1. 31
      ROADMAP.md
  2. 25
      accessctl/access_control.go
  3. 47
      accessctl/access_control_test.go
  4. 57
      accessctl/filter.go
  5. 11
      accessctl/filter_logical.go
  6. 8
      accessctl/filter_logical_test.go
  7. 4
      accessctl/filter_pattern.go
  8. 2
      accessctl/filter_sig.go
  9. 4
      accessctl/filter_sig_test.go
  10. 18
      accessctl/filter_test.go
  11. 93
      cmd/dehub/cmd_commit.go
  12. 8
      cmd/dehub/cmd_hook.go
  13. 12
      cmd/dehub/cmd_misc.go
  14. 32
      cmd/dehub/cmd_util.go
  15. 17
      cmd/dehub/cmd_verify.go
  16. 2
      cmd/dehub/main.go
  17. 676
      commit.go
  18. 156
      commit_change.go
  19. 49
      commit_comment.go
  20. 81
      commit_credential.go
  21. 31
      config.go
  22. 4
      fingerprint.go
  23. 8
      fingerprint_test.go
  24. 604
      payload.go
  25. 171
      payload_change.go
  26. 81
      payload_change_test.go
  27. 43
      payload_comment.go
  28. 73
      payload_credential.go
  29. 12
      payload_credential_test.go
  30. 114
      payload_test.go
  31. 326
      project.go
  32. 105
      project_test.go
  33. 544
      repo.go
  34. 39
      sigcred/credential.go
  35. 6
      sigcred/credential_test.go
  36. 48
      sigcred/pgp.go
  37. 6
      sigcred/pgp_test.go
  38. 91
      sigcred/signifier.go
  39. 4
      yamlutil/yamlutil.go

@ -20,18 +20,18 @@ to accept help from people asking to help.
## Milestone: Versions
* Tag commits
* Add dehub version to the SPEC, make binary aware of it
* Add dehub version to payloads, make binary aware of it
* Figure out a release system?
## Milestone: Checkpoints
## Milestone: Prime commits
* Ability to set change commits as being a "checkpoint", so that they mark a new
root commit. A couple of considerations:
- Only a checkpoint on the main branch should be considered when determining
the project "root".
- Must be a flag on change commits, to allow hard-forks of projects where
the config file is completely replaced.
- Not sure if it should be subject to ACL or not.
(Cloning/remote management is probably a pre-requisite of this, so it's a good
thing it comes after IPFS support)
* Ability to specify which commit is prime.
* The prime commit is essentially the identifier of the entire project; even
if two project instances share a commit tree, if they are using a
different prime commit then they are not the same project.
## Milestone: Minimal plugin support
@ -39,7 +39,7 @@ to accept help from people asking to help.
* Conditions
* Signifiers
* Filters
* Commits???
* Payloads???
## Milestone: Minimal notifications support
@ -63,14 +63,6 @@ are things that could use doing anyway.
* Maybe coalesce the `accessctl`, `fs`, and `sigcred` packages back into the
root "dehub" package.
* Polish all error messages. A good error message has the following qualities:
* If wrapping an error which was returned from a sub-call:
* Uses `fmt.Errorf` with the `%w` format directive at the end.
* Phrased as if the sentence starts with the word "while", e.g. "opening
file: %w".
* Only includes information the caller of that function/method couldn't
already know.
* 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
@ -86,6 +78,3 @@ are things that could use doing anyway.
* Possibly save state locally in order to speed things along, such as
"account id" which probably isn't going to change often for a user.
* More/better tests
* Commits need much better test coverage.

@ -37,8 +37,8 @@ var DefaultAccessControlsStr = `
filters:
- type: branch
pattern: main
- type: commit_type
commit_type: change
- type: payload_type
payload_type: change
- type: signature
any_account: true
count: 1
@ -66,8 +66,8 @@ type CommitRequest struct {
// It is required.
Branch string
// Credentials are the Credential objects attached to the commit.
Credentials []sigcred.Credential
// Credentials are the credentials attached to the commit.
Credentials []sigcred.CredentialUnion
// FilesChanged is the set of file paths (relative to the repo root) which
// have been modified in some way.
@ -97,27 +97,20 @@ const (
// AccessControl describes a set of Filters, and the Actions which should be
// taken on a CommitRequest if those Filters all match on the CommitRequest.
type AccessControl struct {
Action Action `yaml:"action"`
Filters []Filter `yaml:"filters"`
Action Action `yaml:"action"`
Filters []FilterUnion `yaml:"filters"`
}
// ActionForCommit returns what Action this AccessControl says to take for a
// given CommitRequest. It may return ActionNext if the request is not matched
// by the AccessControl's Filters.
func (ac AccessControl) ActionForCommit(req CommitRequest) (Action, error) {
for _, filter := range ac.Filters {
filterI, err := filter.Interface()
if err != nil {
return "", fmt.Errorf("casting %+v to a FilterInterface: %w", filter, err)
} else if err := filterI.MatchCommit(req); errors.As(err, new(ErrFilterNoMatch)) {
for _, filterUn := range ac.Filters {
if err := filterUn.Filter().MatchCommit(req); errors.As(err, new(ErrFilterNoMatch)) {
return ActionNext, nil
} else if err != nil {
// ignore the error here, if we could get the FilterInterface then
// we should be able to get the type.
filterTypeStr, _ := filter.Type()
return "", fmt.Errorf("matching commit using filter of type %q: %w", filterTypeStr, err)
return "", fmt.Errorf("matching commit using filter of type %q: %w", filterUn.Type(), err)
}
}
return ac.Action, nil

@ -1,9 +1,10 @@
package accessctl
import (
"dehub.dev/src/dehub.git/sigcred"
"errors"
"testing"
"dehub.dev/src/dehub.git/sigcred"
)
func TestAssertCanCommit(t *testing.T) {
@ -18,14 +19,14 @@ func TestAssertCanCommit(t *testing.T) {
acl: []AccessControl{
{
Action: ActionAllow,
Filters: []Filter{{
CommitType: &FilterCommitType{Type: "foo"},
Filters: []FilterUnion{{
PayloadType: &FilterPayloadType{Type: "foo"},
}},
},
{
Action: ActionDeny,
Filters: []Filter{{
CommitType: &FilterCommitType{Type: "foo"},
Filters: []FilterUnion{{
PayloadType: &FilterPayloadType{Type: "foo"},
}},
},
},
@ -37,14 +38,14 @@ func TestAssertCanCommit(t *testing.T) {
acl: []AccessControl{
{
Action: ActionDeny,
Filters: []Filter{{
CommitType: &FilterCommitType{Type: "foo"},
Filters: []FilterUnion{{
PayloadType: &FilterPayloadType{Type: "foo"},
}},
},
{
Action: ActionAllow,
Filters: []Filter{{
CommitType: &FilterCommitType{Type: "foo"},
Filters: []FilterUnion{{
PayloadType: &FilterPayloadType{Type: "foo"},
}},
},
},
@ -56,14 +57,14 @@ func TestAssertCanCommit(t *testing.T) {
acl: []AccessControl{
{
Action: ActionDeny,
Filters: []Filter{{
CommitType: &FilterCommitType{Type: "bar"},
Filters: []FilterUnion{{
PayloadType: &FilterPayloadType{Type: "bar"},
}},
},
{
Action: ActionAllow,
Filters: []Filter{{
CommitType: &FilterCommitType{Type: "foo"},
Filters: []FilterUnion{{
PayloadType: &FilterPayloadType{Type: "foo"},
}},
},
},
@ -75,14 +76,14 @@ func TestAssertCanCommit(t *testing.T) {
acl: []AccessControl{
{
Action: ActionDeny,
Filters: []Filter{{
CommitType: &FilterCommitType{Type: "bar"},
Filters: []FilterUnion{{
PayloadType: &FilterPayloadType{Type: "bar"},
}},
},
{
Action: ActionDeny,
Filters: []Filter{{
CommitType: &FilterCommitType{Type: "foo"},
Filters: []FilterUnion{{
PayloadType: &FilterPayloadType{Type: "foo"},
}},
},
},
@ -94,15 +95,15 @@ func TestAssertCanCommit(t *testing.T) {
acl: []AccessControl{
{
Action: ActionDeny,
Filters: []Filter{{
CommitType: &FilterCommitType{Type: "bar"},
Filters: []FilterUnion{{
PayloadType: &FilterPayloadType{Type: "bar"},
}},
},
},
req: CommitRequest{
Branch: "not_main",
Type: "foo",
Credentials: []sigcred.Credential{{
Credentials: []sigcred.CredentialUnion{{
PGPSignature: new(sigcred.CredentialPGPSignature),
AccountID: "a",
}},
@ -114,15 +115,15 @@ func TestAssertCanCommit(t *testing.T) {
acl: []AccessControl{
{
Action: ActionDeny,
Filters: []Filter{{
CommitType: &FilterCommitType{Type: "bar"},
Filters: []FilterUnion{{
PayloadType: &FilterPayloadType{Type: "bar"},
}},
},
},
req: CommitRequest{
Branch: "main",
Type: "foo",
Credentials: []sigcred.Credential{{
Credentials: []sigcred.CredentialUnion{{
PGPSignature: new(sigcred.CredentialPGPSignature),
AccountID: "a",
}},

@ -18,70 +18,73 @@ func (err ErrFilterNoMatch) Error() string {
return fmt.Sprintf("matching with filter: %s", err.Err.Error())
}
// FilterInterface describes the methods that all Filters must implement.
type FilterInterface interface {
// Filter describes the methods that all Filters must implement.
type Filter interface {
// MatchCommit returns nil if the CommitRequest is matched by the filter,
// otherwise it returns an error (ErrFilterNoMatch if the error is due to
// the CommitRequest).
MatchCommit(CommitRequest) error
}
// Filter represents an access control filter being defined in the Config. Only
// one of its fields may be filled at a time.
type Filter struct {
// FilterUnion represents an access control filter being defined in the Config.
// Only one of its fields may be filled at a time.
type FilterUnion struct {
Signature *FilterSignature `type:"signature"`
Branch *FilterBranch `type:"branch"`
FilesChanged *FilterFilesChanged `type:"files_changed"`
CommitType *FilterCommitType `type:"commit_type"`
PayloadType *FilterPayloadType `type:"payload_type"`
CommitAttributes *FilterCommitAttributes `type:"commit_attributes"`
Not *FilterNot `type:"not"`
}
// MarshalYAML implements the yaml.Marshaler interface.
func (f Filter) MarshalYAML() (interface{}, error) {
func (f FilterUnion) MarshalYAML() (interface{}, error) {
return typeobj.MarshalYAML(f)
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (f *Filter) UnmarshalYAML(unmarshal func(interface{}) error) error {
func (f *FilterUnion) UnmarshalYAML(unmarshal func(interface{}) error) error {
return typeobj.UnmarshalYAML(f, unmarshal)
}
// Interface returns the FilterInterface encapsulated by this Filter.
func (f Filter) Interface() (FilterInterface, error) {
// Filter returns the Filter encapsulated by this FilterUnion.
//
// This method will panic if a Filter field is not populated.
func (f FilterUnion) Filter() Filter {
el, _, err := typeobj.Element(f)
if err != nil {
return nil, err
panic(err)
}
return el.(FilterInterface), nil
return el.(Filter)
}
// Type returns a string describing what type of Filter this object
// encapsulates, based on which of its fields are filled in.
func (f Filter) Type() (string, error) {
// Type returns the Filter's type (as would be used in its YAML "type" field).
//
// This will panic if a Filter field is not populated.
func (f FilterUnion) Type() string {
_, typeStr, err := typeobj.Element(f)
if err != nil {
return "", err
panic(err)
}
return typeStr, nil
return typeStr
}
// FilterCommitType filters by what type of commit is being requested. Exactly
// FilterPayloadType filters by what type of payload is being requested. Exactly
// one of its fields should be filled.
type FilterCommitType struct {
Type string `yaml:"commit_type"`
Types []string `yaml:"commit_types"`
type FilterPayloadType struct {
Type string `yaml:"payload_type"`
Types []string `yaml:"payload_types"`
}
var _ FilterInterface = FilterCommitType{}
var _ Filter = FilterPayloadType{}
// MatchCommit implements the method for FilterInterface.
func (f FilterCommitType) MatchCommit(req CommitRequest) error {
func (f FilterPayloadType) MatchCommit(req CommitRequest) error {
switch {
case f.Type != "":
if f.Type != req.Type {
return ErrFilterNoMatch{
Err: fmt.Errorf("commit type %q does not match filter's type %q",
Err: fmt.Errorf("payload type %q does not match filter's type %q",
req.Type, f.Type),
}
}
@ -94,12 +97,12 @@ func (f FilterCommitType) MatchCommit(req CommitRequest) error {
}
}
return ErrFilterNoMatch{
Err: fmt.Errorf("commit type %q does not match any of filter's types %+v",
Err: fmt.Errorf("payload type %q does not match any of filter's types %+v",
req.Type, f.Types),
}
default:
return errors.New(`one of the following fields must be set: "commit_type", "commit_types"`)
return errors.New(`one of the following fields must be set: "payload_type", "payload_types"`)
}
}
@ -110,7 +113,7 @@ type FilterCommitAttributes struct {
NonFastForward bool `yaml:"non_fast_forward"`
}
var _ FilterInterface = FilterCommitAttributes{}
var _ Filter = FilterCommitAttributes{}
// MatchCommit implements the method for FilterInterface.
func (f FilterCommitAttributes) MatchCommit(req CommitRequest) error {

@ -2,24 +2,19 @@ package accessctl
import (
"errors"
"fmt"
)
// FilterNot wraps another Filter. If that filter matches, FilterNot does not
// match, and vice-versa.
type FilterNot struct {
Filter Filter `yaml:"filter"`
Filter FilterUnion `yaml:"filter"`
}
var _ FilterInterface = FilterNot{}
var _ Filter = FilterNot{}
// MatchCommit implements the method for FilterInterface.
func (f FilterNot) MatchCommit(req CommitRequest) error {
fI, err := f.Filter.Interface()
if err != nil {
return fmt.Errorf("casting %+v to a FilterInterface: %w", f.Filter, err)
} else if err := fI.MatchCommit(req); errors.As(err, new(ErrFilterNoMatch)) {
if err := f.Filter.Filter().MatchCommit(req); errors.As(err, new(ErrFilterNoMatch)) {
return nil
} else if err != nil {
return err

@ -7,8 +7,8 @@ func TestFilterNot(t *testing.T) {
{
descr: "sub-filter does match",
filter: FilterNot{
Filter: Filter{
CommitType: &FilterCommitType{Type: "foo"},
Filter: FilterUnion{
PayloadType: &FilterPayloadType{Type: "foo"},
},
},
req: CommitRequest{
@ -19,8 +19,8 @@ func TestFilterNot(t *testing.T) {
{
descr: "sub-filter does not match",
filter: FilterNot{
Filter: Filter{
CommitType: &FilterCommitType{Type: "foo"},
Filter: FilterUnion{
PayloadType: &FilterPayloadType{Type: "foo"},
},
},
req: CommitRequest{

@ -65,7 +65,7 @@ type FilterBranch struct {
StringMatcher StringMatcher `yaml:",inline"`
}
var _ FilterInterface = FilterBranch{}
var _ Filter = FilterBranch{}
// MatchCommit implements the method for FilterInterface.
func (f FilterBranch) MatchCommit(req CommitRequest) error {
@ -79,7 +79,7 @@ type FilterFilesChanged struct {
StringMatcher StringMatcher `yaml:",inline"`
}
var _ FilterInterface = FilterFilesChanged{}
var _ Filter = FilterFilesChanged{}
// MatchCommit implements the method for FilterInterface.
func (f FilterFilesChanged) MatchCommit(req CommitRequest) error {

@ -20,7 +20,7 @@ type FilterSignature struct {
Count string `yaml:"count,omitempty"`
}
var _ FilterInterface = FilterSignature{}
var _ Filter = FilterSignature{}
func (f FilterSignature) targetNum() (int, error) {
if f.Count == "" {

@ -8,7 +8,7 @@ import (
func TestFilterSignature(t *testing.T) {
mkReq := func(accountIDs ...string) CommitRequest {
creds := make([]sigcred.Credential, len(accountIDs))
creds := make([]sigcred.CredentialUnion, len(accountIDs))
for i := range accountIDs {
creds[i].PGPSignature = new(sigcred.CredentialPGPSignature)
creds[i].AccountID = accountIDs[i]
@ -106,7 +106,7 @@ func TestFilterSignature(t *testing.T) {
Any: true,
},
req: CommitRequest{
Credentials: []sigcred.Credential{
Credentials: []sigcred.CredentialUnion{
{PGPSignature: new(sigcred.CredentialPGPSignature)},
},
},

@ -8,7 +8,7 @@ import (
type filterCommitMatchTest struct {
descr string
filter FilterInterface
filter Filter
req CommitRequest
match bool
@ -38,7 +38,7 @@ func runCommitMatchTests(t *testing.T, tests []filterCommitMatchTest) {
}
}
func TestFilterCommitType(t *testing.T) {
func TestFilterPayloadType(t *testing.T) {
mkReq := func(commitType string) CommitRequest {
return CommitRequest{Type: commitType}
}
@ -46,7 +46,7 @@ func TestFilterCommitType(t *testing.T) {
runCommitMatchTests(t, []filterCommitMatchTest{
{
descr: "single match",
filter: FilterCommitType{
filter: FilterPayloadType{
Type: "foo",
},
req: mkReq("foo"),
@ -54,7 +54,7 @@ func TestFilterCommitType(t *testing.T) {
},
{
descr: "single no match",
filter: FilterCommitType{
filter: FilterPayloadType{
Type: "foo",
},
req: mkReq("bar"),
@ -62,7 +62,7 @@ func TestFilterCommitType(t *testing.T) {
},
{
descr: "multi match first",
filter: FilterCommitType{
filter: FilterPayloadType{
Types: []string{"foo", "bar"},
},
req: mkReq("foo"),
@ -70,7 +70,7 @@ func TestFilterCommitType(t *testing.T) {
},
{
descr: "multi match second",
filter: FilterCommitType{
filter: FilterPayloadType{
Types: []string{"foo", "bar"},
},
req: mkReq("bar"),
@ -78,7 +78,7 @@ func TestFilterCommitType(t *testing.T) {
},
{
descr: "multi no match",
filter: FilterCommitType{
filter: FilterPayloadType{
Types: []string{"foo", "bar"},
},
req: mkReq("baz"),
@ -119,7 +119,7 @@ func TestFilterCommitAttributes(t *testing.T) {
},
{
descr: "ff with inverted non-ff filter",
filter: FilterNot{Filter: Filter{
filter: FilterNot{Filter: FilterUnion{
CommitAttributes: &FilterCommitAttributes{NonFastForward: true},
}},
req: mkReq(false),
@ -127,7 +127,7 @@ func TestFilterCommitAttributes(t *testing.T) {
},
{
descr: "non-ff with inverted non-ff filter",
filter: FilterNot{Filter: Filter{
filter: FilterNot{Filter: FilterUnion{
CommitAttributes: &FilterCommitAttributes{NonFastForward: true},
}},
req: mkReq(true),

@ -17,14 +17,14 @@ func cmdCommit(ctx context.Context, cmd *dcmd.Cmd) {
accountID := flag.String("as", "", "Account to accredit commit with")
pgpKeyID := flag.String("anon-pgp-key", "", "ID of pgp key to sign with instead of using an account")
var repo repo
repo.initFlags(flag)
var proj proj
proj.initFlags(flag)
accreditAndCommit := func(commit dehub.Commit) error {
accreditAndCommit := func(payUn dehub.PayloadUnion) error {
var sigInt sigcred.SignifierInterface
var sig sigcred.Signifier
if *accountID != "" {
cfg, err := repo.LoadConfig()
cfg, err := proj.LoadConfig()
if err != nil {
return err
}
@ -43,30 +43,25 @@ func cmdCommit(ctx context.Context, cmd *dcmd.Cmd) {
return fmt.Errorf("account %q has %d signifiers, only one is supported right now", *accountID, l)
}
sig := account.Signifiers[0]
sigInt, err = sig.Interface(*accountID)
if err != nil {
return fmt.Errorf("casting %#v to SignifierInterface: %w", sig, err)
}
sig = account.Signifiers[0].Signifier(*accountID)
} else {
var err error
if sigInt, err = sigcred.LoadSignifierPGP(*pgpKeyID, true); err != nil {
if sig, err = sigcred.LoadSignifierPGP(*pgpKeyID, true); err != nil {
return fmt.Errorf("loading pgp key %q: %w", *pgpKeyID, err)
}
}
commit, err := repo.AccreditCommit(commit, sigInt)
payUn, err := proj.AccreditPayload(payUn, sig)
if err != nil {
return fmt.Errorf("accrediting commit: %w", err)
return fmt.Errorf("accrediting payload: %w", err)
}
gitCommit, err := repo.Commit(commit)
commit, err := proj.Commit(payUn)
if err != nil {
return fmt.Errorf("committing to git: %w", err)
}
fmt.Printf("committed to HEAD as %s\n", gitCommit.GitCommit.Hash)
fmt.Printf("committed to HEAD as %s\n", commit.Hash)
return nil
}
@ -76,12 +71,12 @@ func cmdCommit(ctx context.Context, cmd *dcmd.Cmd) {
return nil, errors.New("-as or -anon-pgp-key is required")
}
if err := repo.openRepo(); err != nil {
if err := proj.openProj(); err != nil {
return nil, err
}
var err error
if hasStaged, err = repo.HasStagedChanges(); err != nil {
if hasStaged, err = proj.HasStagedChanges(); err != nil {
return nil, fmt.Errorf("determining if any changes have been staged: %w", err)
}
return ctx, nil
@ -90,7 +85,7 @@ func cmdCommit(ctx context.Context, cmd *dcmd.Cmd) {
cmd.SubCmd("change", "Commit file changes",
func(ctx context.Context, cmd *dcmd.Cmd) {
flag := cmd.FlagSet()
msg := flag.String("msg", "", "Commit message")
description := flag.String("descr", "", "Description of changes")
amend := flag.Bool("amend", false, "Add changes to HEAD commit, amend its message, and re-accredit it")
cmd.Run(func() (context.Context, error) {
if !hasStaged && !*amend {
@ -99,28 +94,28 @@ func cmdCommit(ctx context.Context, cmd *dcmd.Cmd) {
var prevMsg string
if *amend {
oldHead, err := repo.softReset("change")
oldHead, err := proj.softReset("change")
if err != nil {
return nil, err
}
prevMsg = oldHead.Commit.Change.Message
prevMsg = oldHead.Payload.Change.Description
}
if *msg == "" {
if *description == "" {
var err error
if *msg, err = tmpFileMsg(defaultCommitFileMsgTpl, prevMsg); err != nil {
if *description, err = tmpFileMsg(defaultCommitFileMsgTpl, prevMsg); err != nil {
return nil, fmt.Errorf("error collecting commit message from user: %w", err)
} else if *msg == "" {
} else if *description == "" {
return nil, errors.New("empty commit message, not doing anything")
}
}
commit, err := repo.NewCommitChange(*msg)
payUn, err := proj.NewPayloadChange(*description)
if err != nil {
return nil, fmt.Errorf("could not construct change commit: %w", err)
return nil, fmt.Errorf("could not construct change payload: %w", err)
} else if err := accreditAndCommit(commit); err != nil {
} else if err := accreditAndCommit(payUn); err != nil {
return nil, err
}
return nil, nil
@ -141,31 +136,30 @@ func cmdCommit(ctx context.Context, cmd *dcmd.Cmd) {
return nil, errors.New("credential commit cannot have staged changes")
}
var credCommit dehub.Commit
var credPayUn dehub.PayloadUnion
if *rev != "" {
gitCommit, err := repo.GetGitRevision(plumbing.Revision(*rev))
commit, err := proj.GetCommitByRevision(plumbing.Revision(*rev))
if err != nil {
return nil, fmt.Errorf("resolving revision %q: %w", *rev, err)
}
gitCommits := []dehub.GitCommit{gitCommit}
if credCommit, err = repo.NewCommitCredentialFromChanges(gitCommits); err != nil {
if credPayUn, err = proj.NewPayloadCredentialFromChanges([]dehub.Commit{commit}); err != nil {
return nil, fmt.Errorf("constructing credential commit: %w", err)
}
} else {
gitCommits, err := repo.GetGitRevisionRange(
commits, err := proj.GetCommitRangeByRevision(
plumbing.Revision(*startRev),
plumbing.Revision(*endRev),
)
if err != nil {
return nil, fmt.Errorf("resolving revisions %q to %q: %w",
*startRev, *endRev, err)
} else if credCommit, err = repo.NewCommitCredentialFromChanges(gitCommits); err != nil {
} else if credPayUn, err = proj.NewPayloadCredentialFromChanges(commits); err != nil {
return nil, fmt.Errorf("constructing credential commit: %w", err)
}
}
if err := accreditAndCommit(credCommit); err != nil {
if err := accreditAndCommit(credPayUn); err != nil {
return nil, err
}
return nil, nil
@ -176,37 +170,37 @@ func cmdCommit(ctx context.Context, cmd *dcmd.Cmd) {
cmd.SubCmd("comment", "Commit a comment to a branch",
func(ctx context.Context, cmd *dcmd.Cmd) {
flag := cmd.FlagSet()
msg := flag.String("msg", "", "Comment message")
comment := flag.String("comment", "", "Comment message")
amend := flag.Bool("amend", false, "Amend the comment message currently in HEAD")
cmd.Run(func() (context.Context, error) {
if hasStaged {
return nil, errors.New("comment commit cannot have staged changes")
}
var prevMsg string
var prevComment string
if *amend {
oldHead, err := repo.softReset("comment")
oldHead, err := proj.softReset("comment")
if err != nil {
return nil, err
}
prevMsg = oldHead.Commit.Comment.Message
prevComment = oldHead.Payload.Comment.Comment
}
if *msg == "" {
if *comment == "" {
var err error
if *msg, err = tmpFileMsg(defaultCommitFileMsgTpl, prevMsg); err != nil {
if *comment, err = tmpFileMsg(defaultCommitFileMsgTpl, prevComment); err != nil {
return nil, fmt.Errorf("collecting comment message from user: %w", err)
} else if *msg == "" {
} else if *comment == "" {
return nil, errors.New("empty comment message, not doing anything")
}
}
commit, err := repo.NewCommitComment(*msg)
payUn, err := proj.NewPayloadComment(*comment)
if err != nil {
return nil, fmt.Errorf("constructing comment commit: %w", err)
}
return nil, accreditAndCommit(commit)
return nil, accreditAndCommit(payUn)
})
},
)
@ -220,8 +214,8 @@ func cmdCombine(ctx context.Context, cmd *dcmd.Cmd) {
startRev := flag.String("start", "", "Revision of the starting commit to combine")
endRev := flag.String("end", "", "Revision of the ending commit to combine")
var repo repo
repo.initFlags(flag)
var proj proj
proj.initFlags(flag)
cmd.Run(func() (context.Context, error) {
if *onto == "" ||
@ -230,11 +224,11 @@ func cmdCombine(ctx context.Context, cmd *dcmd.Cmd) {
return nil, errors.New("-onto, -start, and -end are required")
}
if err := repo.openRepo(); err != nil {
if err := proj.openProj(); err != nil {
return nil, err
}
commits, err := repo.GetGitRevisionRange(
commits, err := proj.GetCommitRangeByRevision(
plumbing.Revision(*startRev),
plumbing.Revision(*endRev),
)
@ -244,13 +238,12 @@ func cmdCombine(ctx context.Context, cmd *dcmd.Cmd) {
}
ontoBranch := plumbing.NewBranchReferenceName(*onto)
gitCommit, err := repo.CombineCommitChanges(commits, ontoBranch)
commit, err := proj.CombinePayloadChanges(commits, ontoBranch)
if err != nil {
return nil, err
}
fmt.Printf("new commit %q added to branch %q\n",
gitCommit.GitCommit.Hash, ontoBranch.Short())
fmt.Printf("new commit %q added to branch %q\n", commit.Hash, ontoBranch.Short())
return nil, nil
})
}

@ -18,15 +18,15 @@ func cmdHook(ctx context.Context, cmd *dcmd.Cmd) {
flag := cmd.FlagSet()
preRcv := flag.Bool("pre-receive", false, "Use dehub as a server-side pre-receive hook")
var repo repo
repo.initFlags(flag)
var proj proj
proj.initFlags(flag)
cmd.Run(func() (context.Context, error) {
if !*preRcv {
return nil, errors.New("must set the hook type")
}
if err := repo.openRepo(); err != nil {
if err := proj.openProj(); err != nil {
return nil, err
}
@ -54,7 +54,7 @@ func cmdHook(ctx context.Context, cmd *dcmd.Cmd) {
return nil, errors.New("deleting remote branches is not currently supported")
}
return nil, repo.VerifyCanSetBranchHEADTo(branchName, endHash)
return nil, proj.VerifyCanSetBranchHEADTo(branchName, endHash)
}
fmt.Println("All pushed commits have been verified, well done.")

@ -10,14 +10,14 @@ import (
func cmdInit(ctx context.Context, cmd *dcmd.Cmd) {
flag := cmd.FlagSet()
path := flag.String("path", ".", "Path to initialize the repo at")
bare := flag.Bool("bare", false, "Initialize the repo as a bare repository")
remote := flag.Bool("remote", false, "Configure the directory to allow it to be used as a remote endpoint")
path := flag.String("path", ".", "Path to initialize the project at")
bare := flag.Bool("bare", false, "Initialize the git repo as a bare repository")
remote := flag.Bool("remote", false, "Configure the git repo to allow it to be used as a remote endpoint")
cmd.Run(func() (context.Context, error) {
_, err := dehub.InitRepo(*path,
dehub.InitBare(*bare),
dehub.InitRemote(*remote),
_, err := dehub.InitProject(*path,
dehub.InitBareRepo(*bare),
dehub.InitRemoteRepo(*remote),
)
if err != nil {
return nil, fmt.Errorf("initializing repo at %q: %w", *path, err)

@ -10,19 +10,19 @@ import (
"gopkg.in/src-d/go-git.v4/plumbing"
)
type repo struct {
type proj struct {
bare bool
*dehub.Repo
*dehub.Project
}
func (r *repo) initFlags(flag *flag.FlagSet) {
flag.BoolVar(&r.bare, "bare", false, "If set then the repo being opened will be expected to be bare")
func (proj *proj) initFlags(flag *flag.FlagSet) {
flag.BoolVar(&proj.bare, "bare", false, "If set then the project being opened will be expected to have a bare git repo")
}
func (r *repo) openRepo() error {
func (proj *proj) openProj() error {
var err error
if r.Repo, err = dehub.OpenRepo(".", dehub.OpenBare(r.bare)); err != nil {
if proj.Project, err = dehub.OpenProject(".", dehub.OpenBareRepo(proj.bare)); err != nil {
wd, _ := os.Getwd()
return fmt.Errorf("opening repo at %q: %w", wd, err)
}
@ -31,19 +31,17 @@ func (r *repo) openRepo() error {
// softReset resets to HEAD^ (or to an orphaned index, if HEAD has no parents),
// returning the old HEAD.
func (r *repo) softReset(expType string) (dehub.GitCommit, error) {
head, err := r.GetGitHead()
func (proj *proj) softReset(expType string) (dehub.Commit, error) {
head, err := proj.GetHeadCommit()
if err != nil {
return head, fmt.Errorf("getting HEAD commit: %w", err)
} else if typ, err := head.Commit.Type(); err != nil {
return head, fmt.Errorf("determining commit type of HEAD:% w", err)
} else if expType != "" && typ != expType {
return head, fmt.Errorf("expected HEAD to be a %q commit, but found %q",
} else if typ := head.Payload.Type(); expType != "" && typ != expType {
return head, fmt.Errorf("expected HEAD to be have a %q payload, but found a %q payload",
expType, typ)
}
branchName, branchErr := r.ReferenceToBranchName(plumbing.HEAD)
numParents := head.GitCommit.NumParents()
branchName, branchErr := proj.ReferenceToBranchName(plumbing.HEAD)
numParents := head.Object.NumParents()
if numParents > 1 {
return head, errors.New("cannot reset to parent of a commit with multiple parents")
@ -55,7 +53,7 @@ func (r *repo) softReset(expType string) (dehub.GitCommit, error) {
// it and all of HEAD's changes will be in the index.
if branchErr != nil {
return head, branchErr
} else if err := r.GitRepo.Storer.RemoveReference(branchName); err != nil {
} else if err := proj.GitRepo.Storer.RemoveReference(branchName); err != nil {
return head, fmt.Errorf("removing reference %q: %w", branchName, err)
}
return head, nil
@ -68,9 +66,9 @@ func (r *repo) softReset(expType string) (dehub.GitCommit, error) {
return head, fmt.Errorf("resolving HEAD: %w", err)
}
parentHash := head.GitCommit.ParentHashes[0]
parentHash := head.Object.ParentHashes[0]
newHeadRef := plumbing.NewHashReference(refName, parentHash)
if err := r.GitRepo.Storer.SetReference(newHeadRef); err != nil {
if err := proj.GitRepo.Storer.SetReference(newHeadRef); err != nil {
return head, fmt.Errorf("storing reference %q: %w", newHeadRef, err)
}
return head, nil

@ -15,35 +15,34 @@ func cmdVerify(ctx context.Context, cmd *dcmd.Cmd) {
rev := flag.String("rev", "HEAD", "Revision of commit to verify")
branch := flag.String("branch", "", "Branch that the revision is on. If not given then the currently checked out branch is assumed")
var repo repo
repo.initFlags(flag)
var proj proj
proj.initFlags(flag)
cmd.Run(func() (context.Context, error) {
if err := repo.openRepo(); err != nil {
if err := proj.openProj(); err != nil {
return nil, err
}
gitCommit, err := repo.GetGitRevision(plumbing.Revision(*rev))
commit, err := proj.GetCommitByRevision(plumbing.Revision(*rev))
if err != nil {
return nil, fmt.Errorf("resolving revision %q: %w", *rev, err)
}
gitCommitHash := gitCommit.GitCommit.Hash
var branchName plumbing.ReferenceName
if *branch == "" {
if branchName, err = repo.ReferenceToBranchName(plumbing.HEAD); err != nil {
if branchName, err = proj.ReferenceToBranchName(plumbing.HEAD); err != nil {
return nil, fmt.Errorf("determining branch at HEAD: %w", err)
}
} else {
branchName = plumbing.NewBranchReferenceName(*branch)
}
if err := repo.VerifyCommits(branchName, []dehub.GitCommit{gitCommit}); err != nil {
if err := proj.VerifyCommits(branchName, []dehub.Commit{commit}); err != nil {
return nil, fmt.Errorf("could not verify commit at %q (%s): %w",
*rev, gitCommitHash, err)
*rev, commit.Hash, err)
}
fmt.Printf("commit at %q (%s) is good to go!\n", *rev, gitCommitHash)
fmt.Printf("commit at %q (%s) is good to go!\n", *rev, commit.Hash)
return nil, nil
})
}

@ -8,7 +8,7 @@ import (
func main() {
cmd := dcmd.New()
cmd.SubCmd("init", "Initialize a new repository in a directory", cmdInit)
cmd.SubCmd("init", "Initialize a new project in a directory", cmdInit)
cmd.SubCmd("commit", "Commits staged changes to the head of the current branch", cmdCommit)
cmd.SubCmd("verify", "Verifies one or more commits as having the proper credentials", cmdVerify)
cmd.SubCmd("hook", "Use dehub as a git hook", cmdHook)

@ -1,610 +1,222 @@
package dehub
import (
"bytes"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"reflect"
"sort"
"path/filepath"
"strings"
"time"
"dehub.dev/src/dehub.git/accessctl"
"dehub.dev/src/dehub.git/fs"
"dehub.dev/src/dehub.git/sigcred"
"dehub.dev/src/dehub.git/typeobj"
"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. None of the methods should modify the underlying
// object.
type CommitInterface interface {
// MessageHead returns the head of the commit message (i.e. the first line).
// The CommitCommon of the outer Commit is passed in for added context, if
// necessary.
MessageHead(CommitCommon) (string, error)
// ExpectedHash returns the raw hash which Signifiers can sign to accredit
// this commit. The ChangedFile objects given describe the file changes
// between the parent commit and this commit.
ExpectedHash([]ChangedFile) ([]byte, error)
// StoredHash returns the signable Hash embedded in the commit, which should
// hopefully correspond to the ExpectedHash.
StoredHash() []byte
}
// CommitCommon describes the fields common to all Commit objects.
type CommitCommon struct {
// 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"`
}
func (cc CommitCommon) credIDs() []string {
m := map[string]struct{}{}
for _, cred := range cc.Credentials {
if cred.AccountID != "" {
m[cred.AccountID] = struct{}{}
} else if cred.AnonID != "" {
m[cred.AnonID] = struct{}{}
}
}
s := make([]string, 0, len(m))
for id := range m {
s = append(s, id)
}
sort.Strings(s)
return s
}
func abbrevCommitMessage(msg string) string {
i := strings.Index(msg, "\n")
if i > 0 {
msg = msg[:i]
}
if len(msg) > 80 {
msg = msg[:80] + "..."
}
return msg
}
// Commit represents a single Commit which is being added to a branch. Only one
// field should be set on a Commit, unless otherwise noted.
// Commit wraps a single git commit object, and also contains various fields
// which are parsed out of it, including the payload. It is used as a
// convenience type, in place of having to manually retrieve and parse specific
// information out of commit objects.
type Commit struct {
Change *CommitChange `type:"change,default"`
Credential *CommitCredential `type:"credential"`
Comment *CommitComment `type:"comment"`
Common CommitCommon `yaml:",inline"`
}
// 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
}
// Type returns the Commit's type (as would be used in its YAML "type" field).
func (c Commit) Type() (string, error) {
_, typeStr, err := typeobj.Element(c)
if err != nil {
return "", err
}
return typeStr, 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(c.Common)
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)
}
Payload PayloadUnion
w := new(bytes.Buffer)
w.WriteString(msgHead)
w.WriteString("\n\n---\n")
w.Write(msgBodyB)
return w.Bytes(), nil
Hash plumbing.Hash
Object *object.Commit
TreeObject *object.Tree
}
// 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)
// GetCommit retrieves the Commit at the given hash, and all of its sub-data
// which can be pulled out of it.
func (proj *Project) GetCommit(h plumbing.Hash) (c Commit, err error) {
if c.Object, err = proj.GitRepo.CommitObject(h); err != nil {
return c, fmt.Errorf("getting git commit object: %w", err)
} else if c.TreeObject, err = proj.GitRepo.TreeObject(c.Object.TreeHash); err != nil {
return c, fmt.Errorf("getting git tree object %q: %w",
c.Object.TreeHash, err)
} else if c.Payload.UnmarshalText([]byte(c.Object.Message)); err != nil {
return c, fmt.Errorf("decoding commit message: %w", err)
}
msgBody := msg[i:]
if err := yaml.Unmarshal(msgBody, c); err != nil {
return fmt.Errorf("could not unmarshal Commit message from yaml: %w", err)
} else if reflect.DeepEqual(*c, Commit{}) {
// a basic check, but worthwhile
return errors.New("commit message is malformed, could not unmarshal yaml object")
}
return nil
c.Hash = c.Object.Hash
return
}
// 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)
}
// ErrHeadIsZero is used to indicate that HEAD resolves to the zero hash. An
// example of when this can happen is if the project was just initialized and
// has no commits, or if an orphan branch is checked out.
var ErrHeadIsZero = errors.New("HEAD resolves to the zero hash")
headFS, err := r.headFS()
// GetHeadCommit returns the Commit which is currently referenced by HEAD.
// This method may return ErrHeadIsZero if HEAD resolves to the zero hash.
func (proj *Project) GetHeadCommit() (Commit, error) {
headHash, err := proj.ReferenceToHash(plumbing.HEAD)
if err != nil {
return commit, fmt.Errorf("could not grab snapshot of HEAD fs: %w", err)
return Commit{}, fmt.Errorf("resolving HEAD: %w", err)
} else if headHash == plumbing.ZeroHash {
return Commit{}, ErrHeadIsZero
}
cred, err := sigInt.Sign(headFS, commitInt.StoredHash())
c, err := proj.GetCommit(headHash)
if err != nil {
return commit, fmt.Errorf("could not accredit change commit: %w", err)
return Commit{}, fmt.Errorf("getting commit %q: %w", headHash, err)
}
commit.Common.Credentials = append(commit.Common.Credentials, cred)
return commit, nil
return c, nil
}
// CommitBareParams are the parameters to the CommitBare method. All are
// required, unless otherwise noted.
type CommitBareParams struct {
Commit Commit
Author string
ParentHash plumbing.Hash // can be zero if the commit has no parents (Q_Q)
GitTree *object.Tree
}
// CommitBare constructs a git commit object and and stores it, returning the
// resulting GitCommit. This method does not interact with HEAD at all.
func (r *Repo) CommitBare(params CommitBareParams) (GitCommit, error) {
msgB, err := params.Commit.MarshalText()
if err != nil {
return GitCommit{}, fmt.Errorf("encoding %T to message string: %w",
params.Commit, err)
}
author := object.Signature{
Name: params.Author,
When: time.Now(),
}
commit := &object.Commit{