force all commits to be ancestors of the root of main
--- type: change message: |- force all commits to be ancestors of the root of main Without this commit it would be possible to just start branches willy-nilly on any dehub-remote, each with an independent config. Despite this obvious benefit, there's two significant problems which will need to be solved at a later date: - If an account is removed from the config, then that account will still be able to create and push branches based off commits older than that config change. This can be solved by adding a "checkpoint" flag to change commits, making them the new root. This would also enable hard-forks. - Currently there's no way to specify a custom branch name in lieu of "main". I don't know where this configuration would even go, so I don't know how to solve it atm. change_hash: ANc9dlX1IhMQcuSAAnsAoVWBg5v02mPwcoPn9LBOBAbL credentials: - type: pgp_signature pub_key_id: 95C46FA6A41148AC body: iQIzBAABAgAdFiEEJ6tQKp6olvZKJ0lwlcRvpqQRSKwFAl6KI08ACgkQlcRvpqQRSKwrBRAAt8u95nX/BYjgV4yFv/GQ69miWMidFqR84xAk4ZexiUct50Adf4O8fLpf0iIN1TWNItz3d3CM28h+N5+6ewbaM+zMhmiZ13xz4U/vxKyQ2f3LRpj5x8QdEjPEPzKXEIha+6TzBwYA3ijnRgksUKuDEB0tfgA0YDHeWxaVFSgi/F3Uupu1qphiKqytRI5XGimsVoMgI2CGk3RyzchYzLPJTPeHPocSPGT+FmSNk/hh58DGZ2oIfB4bV93xcW87O1RnwAlTNINp1Opsiq2vA7qKMTbqgDl47XaAgctmbpvVfPSbUhQn3MPkV62YvyA9Ft83ls80GNf4RoYQk5szBpTkCsoEaYGZ9xxTuxH6tuAviBR8aj7UZ5SA7FrxQLTe273971nIK1Ei8P6ms4s4Air0eN34GG7yVdhv692xfGDe2E0Td/PnsHx4el8CbK+pJ6O05o7uSU+855IlNPn8gaka1govfwKdn+AsDxKkk4jIakiYB03L3hDr/pGbu1Mm9hNPMDOegKwMfEmuEaQs6tGhYmkIVcFLxOxV/20JvwGWkmPUPFpNCfS9QfVfqhRS5hjJDQvaDDBtk5e8afEUahTSaqx1pqCMU1joFZT7HSrhsldnTiKBIjobrnuAHPi2Lp8EdoSN+Lgp5jqC2wPEpU72Sh9grINrcr8JE9mXDMuFygs= account: mediocregopher
This commit is contained in:
parent
54af1ee510
commit
ca4887bf07
11
ROADMAP.md
11
ROADMAP.md
@ -9,7 +9,6 @@ set, only a sequence of milestones and the requirements to hit them.
|
|||||||
Must be able to feel good about showing the project publicly, as well as be able
|
Must be able to feel good about showing the project publicly, as well as be able
|
||||||
to accept help from people asking to help.
|
to accept help from people asking to help.
|
||||||
|
|
||||||
* Restrict new branches so that they must be ancestors of main.
|
|
||||||
* Fast-forward perms on branches (so they can be deleted)
|
* Fast-forward perms on branches (so they can be deleted)
|
||||||
* Ammending commits.
|
* Ammending commits.
|
||||||
* Figure out commit range syntax, use that everywhere.
|
* Figure out commit range syntax, use that everywhere.
|
||||||
@ -33,6 +32,16 @@ to accept help from people asking to help.
|
|||||||
* Add dehub version to the SPEC, make binary aware of it
|
* Add dehub version to the SPEC, make binary aware of it
|
||||||
* Figure out a release system?
|
* Figure out a release system?
|
||||||
|
|
||||||
|
## Milestone: Checkpoints
|
||||||
|
|
||||||
|
* 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.
|
||||||
|
|
||||||
## Milestone: Minimal plugin support
|
## Milestone: Minimal plugin support
|
||||||
|
|
||||||
* SPEC and implement. Things which should be pluggable, initially:
|
* SPEC and implement. Things which should be pluggable, initially:
|
||||||
|
157
commit.go
157
commit.go
@ -2,10 +2,6 @@ package dehub
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"dehub.dev/src/dehub.git/accessctl"
|
|
||||||
"dehub.dev/src/dehub.git/fs"
|
|
||||||
"dehub.dev/src/dehub.git/sigcred"
|
|
||||||
"dehub.dev/src/dehub.git/typeobj"
|
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -14,6 +10,11 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"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"
|
||||||
"gopkg.in/src-d/go-git.v4/plumbing"
|
"gopkg.in/src-d/go-git.v4/plumbing"
|
||||||
"gopkg.in/src-d/go-git.v4/plumbing/object"
|
"gopkg.in/src-d/go-git.v4/plumbing/object"
|
||||||
@ -288,74 +289,49 @@ func (r *Repo) HasStagedChanges() (bool, error) {
|
|||||||
return any, nil
|
return any, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type verificationCtx struct {
|
|
||||||
commit *object.Commit
|
|
||||||
commitTree, parentTree *object.Tree
|
|
||||||
}
|
|
||||||
|
|
||||||
// non-gophers gonna hate on this method, but I say it's fine
|
|
||||||
func (r *Repo) verificationCtx(h plumbing.Hash) (vctx verificationCtx, err error) {
|
|
||||||
if vctx.commit, err = r.GitRepo.CommitObject(h); err != nil {
|
|
||||||
return vctx, fmt.Errorf("retrieving commit object: %w", err)
|
|
||||||
|
|
||||||
} else if vctx.commitTree, err = r.GitRepo.TreeObject(vctx.commit.TreeHash); err != nil {
|
|
||||||
return vctx, fmt.Errorf("retrieving commit tree object %q: %w",
|
|
||||||
vctx.commit.TreeHash, err)
|
|
||||||
|
|
||||||
} else if parent, err := vctx.commit.Parent(0); err != nil {
|
|
||||||
return vctx, fmt.Errorf("retrieving commit parent: %w", err)
|
|
||||||
|
|
||||||
} else if vctx.parentTree, err = r.GitRepo.TreeObject(parent.TreeHash); err != nil {
|
|
||||||
return vctx, fmt.Errorf("retrieving commit parent tree object %q: %w",
|
|
||||||
parent.Hash, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return vctx, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Repo) assertAccessControls(
|
|
||||||
acl []accessctl.AccessControl,
|
|
||||||
commit Commit, vctx verificationCtx, branch plumbing.ReferenceName,
|
|
||||||
) (err error) {
|
|
||||||
filesChanged, err := calcDiff(vctx.parentTree, vctx.commitTree)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("calculating diff from tree %q to tree %q: %w",
|
|
||||||
vctx.parentTree.Hash, vctx.commitTree.Hash, err)
|
|
||||||
|
|
||||||
} else if len(filesChanged) > 0 && commit.Change == nil {
|
|
||||||
return errors.New("files changes but commit is not a change commit")
|
|
||||||
}
|
|
||||||
|
|
||||||
pathsChanged := make([]string, len(filesChanged))
|
|
||||||
for i := range filesChanged {
|
|
||||||
pathsChanged[i] = filesChanged[i].path
|
|
||||||
}
|
|
||||||
|
|
||||||
commitType, err := commit.Type()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("determining type of commit %+v: %w", commit, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return accessctl.AssertCanCommit(acl, accessctl.CommitRequest{
|
|
||||||
Type: commitType,
|
|
||||||
Branch: branch.Short(),
|
|
||||||
Credentials: commit.Common.Credentials,
|
|
||||||
FilesChanged: pathsChanged,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// VerifyCommits verifies that the given commits, which are presumably on the
|
// VerifyCommits verifies that the given commits, which are presumably on the
|
||||||
// given branch, are gucci.
|
// given branch, are gucci.
|
||||||
func (r *Repo) VerifyCommits(branch plumbing.ReferenceName, gitCommits []GitCommit) error {
|
func (r *Repo) VerifyCommits(branch plumbing.ReferenceName, gitCommits []GitCommit) error {
|
||||||
|
|
||||||
|
// First determine the root of the main branch. All commits need to be an
|
||||||
|
// ancestor of it.
|
||||||
|
var root plumbing.Hash
|
||||||
|
mainGitCommit, err := r.GetGitRevision(plumbing.Revision(MainRefName))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("retrieving commit at HEAD of main: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rootCommit := mainGitCommit.GitCommit
|
||||||
|
for {
|
||||||
|
if rootCommit.NumParents() == 0 {
|
||||||
|
break
|
||||||
|
} else if rootCommit.NumParents() > 1 {
|
||||||
|
return fmt.Errorf("commit %q in main branch has more than one parent", root)
|
||||||
|
} else if rootCommit, err = rootCommit.Parent(0); err != nil {
|
||||||
|
return fmt.Errorf("retrieving parent commit of %q: %w", root, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for i, gitCommit := range gitCommits {
|
for i, gitCommit := range gitCommits {
|
||||||
// It's not a requirement that the given GitCommits are in ancestral
|
// It's not a requirement that the given GitCommits are in ancestral
|
||||||
// order, but usually they are, so we can help verifyCommit not have to
|
// order, but usually they are, so we can help verifyCommit not have to
|
||||||
// calculate the parentTree if the previous commit is the parent of this
|
// calculate the parentTree if the previous commit is the parent of this
|
||||||
// one.
|
// one, and not have to determine that each commit is an ancestor of
|
||||||
|
// main manually.
|
||||||
var parentTree *object.Tree
|
var parentTree *object.Tree
|
||||||
if i > 0 && gitCommits[i-1].GitCommit.Hash == gitCommit.GitCommit.ParentHashes[0] {
|
if i > 0 && gitCommits[i-1].GitCommit.Hash == gitCommit.GitCommit.ParentHashes[0] {
|
||||||
parentTree = gitCommits[i-1].GitTree
|
parentTree = gitCommits[i-1].GitTree
|
||||||
|
|
||||||
|
} else if gitCommit.GitCommit.Hash == rootCommit.Hash {
|
||||||
|
// looking at the root commit itself, assume it's ok
|
||||||
|
|
||||||
|
} else if isAncestor, err := rootCommit.IsAncestor(gitCommit.GitCommit); err != nil {
|
||||||
|
return fmt.Errorf("determining if %q is an ancestor of %q (root of main): %w",
|
||||||
|
gitCommit.GitCommit.Hash, rootCommit.Hash, err)
|
||||||
|
|
||||||
|
} else if !isAncestor {
|
||||||
|
return fmt.Errorf("%q is not an ancestor of %q (root of main)",
|
||||||
|
gitCommit.GitCommit.Hash, rootCommit.Hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.verifyCommit(branch, gitCommit, parentTree); err != nil {
|
if err := r.verifyCommit(branch, gitCommit, parentTree); err != nil {
|
||||||
@ -394,17 +370,11 @@ func (r *Repo) verifyCommit(branch plumbing.ReferenceName, gitCommit GitCommit,
|
|||||||
return fmt.Errorf("retrieving parent tree of commit: %w", err)
|
return fmt.Errorf("retrieving parent tree of commit: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
vctx := verificationCtx{
|
|
||||||
commit: gitCommit.GitCommit,
|
|
||||||
commitTree: gitCommit.GitTree,
|
|
||||||
parentTree: parentTree,
|
|
||||||
}
|
|
||||||
|
|
||||||
var sigFS fs.FS
|
var sigFS fs.FS
|
||||||
if gitCommit.Root() {
|
if gitCommit.Root() {
|
||||||
sigFS = fs.FromTree(vctx.commitTree)
|
sigFS = fs.FromTree(gitCommit.GitTree)
|
||||||
} else {
|
} else {
|
||||||
sigFS = fs.FromTree(vctx.parentTree)
|
sigFS = fs.FromTree(parentTree)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := r.loadConfig(sigFS)
|
cfg, err := r.loadConfig(sigFS)
|
||||||
@ -413,26 +383,53 @@ func (r *Repo) verifyCommit(branch plumbing.ReferenceName, gitCommit GitCommit,
|
|||||||
gitCommit.GitCommit.ParentHashes[0], err)
|
gitCommit.GitCommit.ParentHashes[0], err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = r.assertAccessControls(cfg.AccessControls, gitCommit.Commit, vctx, branch)
|
// assert access controls
|
||||||
|
filesChanged, err := calcDiff(parentTree, gitCommit.GitTree)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("enforcing access controls: %w", err)
|
return fmt.Errorf("calculating diff from tree %q to tree %q: %w",
|
||||||
|
parentTree.Hash, gitCommit.GitTree.Hash, err)
|
||||||
|
|
||||||
|
} else if len(filesChanged) > 0 && gitCommit.Commit.Change == nil {
|
||||||
|
return errors.New("files changes but commit is not a change commit")
|
||||||
}
|
}
|
||||||
|
|
||||||
changeHash := gitCommit.Interface.GetHash()
|
pathsChanged := make([]string, len(filesChanged))
|
||||||
expectedChangeHash, err := gitCommit.Interface.Hash(vctx.parentTree, vctx.commitTree)
|
for i := range filesChanged {
|
||||||
if err != nil {
|
pathsChanged[i] = filesChanged[i].path
|
||||||
return fmt.Errorf("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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
commitType, err := gitCommit.Commit.Type()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("determining type of commit %+v: %w", gitCommit.Commit, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = accessctl.AssertCanCommit(cfg.AccessControls, accessctl.CommitRequest{
|
||||||
|
Type: commitType,
|
||||||
|
Branch: branch.Short(),
|
||||||
|
Credentials: gitCommit.Commit.Common.Credentials,
|
||||||
|
FilesChanged: pathsChanged,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("asserting access controls: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure the hash is what it's expected to be
|
||||||
|
commitHash := gitCommit.Interface.GetHash()
|
||||||
|
expectedCommitHash, err := gitCommit.Interface.Hash(parentTree, gitCommit.GitTree)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("calculating expected commit hash: %w", err)
|
||||||
|
} else if !bytes.Equal(commitHash, expectedCommitHash) {
|
||||||
|
return fmt.Errorf("unexpected hash in commit body, is %s but should be %s",
|
||||||
|
base64.StdEncoding.EncodeToString(expectedCommitHash),
|
||||||
|
base64.StdEncoding.EncodeToString(commitHash))
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify all credentials
|
||||||
for _, cred := range gitCommit.Commit.Common.Credentials {
|
for _, cred := range gitCommit.Commit.Common.Credentials {
|
||||||
sig, err := r.signifierForCredential(sigFS, cred)
|
sig, err := r.signifierForCredential(sigFS, cred)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("finding signifier for credential %+v: %w", cred, err)
|
return fmt.Errorf("finding signifier for credential %+v: %w", cred, err)
|
||||||
} else if err := sig.Verify(sigFS, expectedChangeHash, cred); err != nil {
|
} else if err := sig.Verify(sigFS, expectedCommitHash, cred); err != nil {
|
||||||
return fmt.Errorf("verifying credential %+v: %w", cred, err)
|
return fmt.Errorf("verifying credential %+v: %w", cred, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
package dehub
|
package dehub
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"dehub.dev/src/dehub.git/sigcred"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"dehub.dev/src/dehub.git/sigcred"
|
||||||
|
|
||||||
|
"gopkg.in/src-d/go-git.v4/plumbing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConfigChange(t *testing.T) {
|
func TestConfigChange(t *testing.T) {
|
||||||
@ -46,3 +49,38 @@ func TestConfigChange(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMainAncestryRequirement(t *testing.T) {
|
||||||
|
otherBranch := plumbing.NewBranchReferenceName("other")
|
||||||
|
t.Run("empty repo", func(t *testing.T) {
|
||||||
|
h := newHarness(t)
|
||||||
|
h.checkout(otherBranch)
|
||||||
|
|
||||||
|
// stage and try to add to the "other" branch, it shouldn't work though
|
||||||
|
h.stageCfg()
|
||||||
|
badCommit, err := h.repo.NewCommitChange("starting new branch at other")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("creating CommitChange: %v", err)
|
||||||
|
}
|
||||||
|
h.tryCommit(false, badCommit, h.cfg.Accounts[0].ID, h.sig)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("new branch, single commit", func(t *testing.T) {
|
||||||
|
h := newHarness(t)
|
||||||
|
h.stageCfg()
|
||||||
|
h.changeCommit("add cfg", h.cfg.Accounts[0].ID, h.sig)
|
||||||
|
|
||||||
|
// set HEAD to this other branch which doesn't really exist
|
||||||
|
ref := plumbing.NewSymbolicReference(plumbing.HEAD, otherBranch)
|
||||||
|
if err := h.repo.GitRepo.Storer.SetReference(ref); err != nil {
|
||||||
|
h.t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
h.stageCfg()
|
||||||
|
badCommit, err := h.repo.NewCommitChange("starting new branch at other")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("creating CommitChange: %v", err)
|
||||||
|
}
|
||||||
|
h.tryCommit(false, badCommit, h.cfg.Accounts[0].ID, h.sig)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
21
repo_test.go
21
repo_test.go
@ -2,7 +2,6 @@ package dehub
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"dehub.dev/src/dehub.git/sigcred"
|
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
@ -10,6 +9,8 @@ import (
|
|||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"dehub.dev/src/dehub.git/sigcred"
|
||||||
|
|
||||||
"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"
|
||||||
@ -100,13 +101,22 @@ func (h *harness) stageCfg() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *harness) checkout(branch plumbing.ReferenceName) {
|
func (h *harness) checkout(branch plumbing.ReferenceName) {
|
||||||
|
|
||||||
w, err := h.repo.GitRepo.Worktree()
|
w, err := h.repo.GitRepo.Worktree()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.t.Fatal(err)
|
h.t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
head, err := h.repo.GetGitHead()
|
head, err := h.repo.GetGitHead()
|
||||||
if err != nil {
|
if errors.Is(err, ErrHeadIsZero) {
|
||||||
|
// if HEAD is resolvable to any hash than the Checkout method doesn't
|
||||||
|
// work, just set HEAD manually.
|
||||||
|
ref := plumbing.NewSymbolicReference(plumbing.HEAD, branch)
|
||||||
|
if err := h.repo.GitRepo.Storer.SetReference(ref); err != nil {
|
||||||
|
h.t.Fatal(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
h.t.Fatal(err)
|
h.t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,11 +186,12 @@ func (h *harness) tryCommit(
|
|||||||
h.t.Fatalf("verifying commit %q should have failed", gitCommit.GitCommit.Hash)
|
h.t.Fatalf("verifying commit %q should have failed", gitCommit.GitCommit.Hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
if gitCommit.GitCommit.NumParents() == 0 {
|
var parentHash plumbing.Hash
|
||||||
h.t.Fatalf("unverifiable commit %q has no parents, but it should", gitCommit.GitCommit.Hash)
|
if gitCommit.GitCommit.NumParents() > 0 {
|
||||||
|
parentHash = gitCommit.GitCommit.ParentHashes[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
h.reset(gitCommit.GitCommit.ParentHashes[0], git.HardReset)
|
h.reset(parentHash, git.HardReset)
|
||||||
return gitCommit
|
return gitCommit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user