aboutsummaryrefslogtreecommitdiffstats
path: root/entity
diff options
context:
space:
mode:
authorMichael Muré <batolettre@gmail.com>2022-11-29 13:01:53 +0100
committerMichael Muré <batolettre@gmail.com>2022-11-29 13:01:53 +0100
commit4a341b5e1714a6a36ec7f5839a6a1b73571d4851 (patch)
tree261e108d1c9bd78e15e19379f611cfecb900fef5 /entity
parent0ac39a7ab5db077fcf0df827e32bf6e625e980da (diff)
downloadgit-bug-4a341b5e1714a6a36ec7f5839a6a1b73571d4851.tar.gz
WIP
Diffstat (limited to 'entity')
-rw-r--r--entity/dag/common_test.go12
-rw-r--r--entity/dag/entity.go69
-rw-r--r--entity/dag/entity_actions.go14
-rw-r--r--entity/dag/entity_actions_test.go34
-rw-r--r--entity/dag/entity_test.go12
-rw-r--r--entity/dag/example_test.go14
-rw-r--r--entity/interface.go2
7 files changed, 92 insertions, 65 deletions
diff --git a/entity/dag/common_test.go b/entity/dag/common_test.go
index f78b09e9..51acfa49 100644
--- a/entity/dag/common_test.go
+++ b/entity/dag/common_test.go
@@ -88,6 +88,18 @@ func unmarshaler(raw json.RawMessage, resolvers entity.Resolvers) (Operation, er
}
/*
+ Entity
+*/
+
+type Foo struct {
+ *Entity
+}
+
+func wrapper(e *Entity) *Foo {
+ return &Foo{Entity: e}
+}
+
+/*
Identities + repo + definition
*/
diff --git a/entity/dag/entity.go b/entity/dag/entity.go
index ca674ad7..03b97aa0 100644
--- a/entity/dag/entity.go
+++ b/entity/dag/entity.go
@@ -33,6 +33,19 @@ type Definition struct {
FormatVersion uint
}
+type Actions[EntityT entity.Interface] struct {
+ Wrap func(e *Entity) EntityT
+ New func() EntityT
+ Read func(repo repository.ClockedRepo, id entity.Id) (EntityT, error)
+ ReadWithResolver func(repo repository.ClockedRepo, resolvers entity.Resolvers, id entity.Id) (EntityT, error)
+ ReadAll func(repo repository.ClockedRepo) <-chan StreamedEntity[EntityT]
+ ListLocalIds func(repo repository.Repo) ([]entity.Id, error)
+ Fetch func(repo repository.Repo, remote string) (string, error)
+ Push func(repo repository.Repo, remote string) (string, error)
+ Pull func(repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, mergeAuthor identity.Interface) error
+ MergeAll func(repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, mergeAuthor identity.Interface) <-chan entity.MergeResult
+}
+
// Entity is a data structure stored in a chain of git objects, supporting actions like Push, Pull and Merge.
type Entity struct {
// A Lamport clock is a logical clock that allow to order event
@@ -59,32 +72,32 @@ func New(definition Definition) *Entity {
}
// Read will read and decode a stored local Entity from a repository
-func Read(def Definition, repo repository.ClockedRepo, resolvers entity.Resolvers, id entity.Id) (*Entity, error) {
+func Read[EntityT entity.Interface](def Definition, wrapper func(e *Entity) EntityT, repo repository.ClockedRepo, resolvers entity.Resolvers, id entity.Id) (EntityT, error) {
if err := id.Validate(); err != nil {
- return nil, errors.Wrap(err, "invalid id")
+ return *new(EntityT), errors.Wrap(err, "invalid id")
}
ref := fmt.Sprintf("refs/%s/%s", def.Namespace, id.String())
- return read(def, repo, resolvers, ref)
+ return read[EntityT](def, wrapper, repo, resolvers, ref)
}
// readRemote will read and decode a stored remote Entity from a repository
-func readRemote(def Definition, repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, id entity.Id) (*Entity, error) {
+func readRemote[EntityT entity.Interface](def Definition, wrapper func(e *Entity) EntityT, repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, id entity.Id) (EntityT, error) {
if err := id.Validate(); err != nil {
- return nil, errors.Wrap(err, "invalid id")
+ return *new(EntityT), errors.Wrap(err, "invalid id")
}
ref := fmt.Sprintf("refs/remotes/%s/%s/%s", def.Namespace, remote, id.String())
- return read(def, repo, resolvers, ref)
+ return read[EntityT](def, wrapper, repo, resolvers, ref)
}
// read fetch from git and decode an Entity at an arbitrary git reference.
-func read(def Definition, repo repository.ClockedRepo, resolvers entity.Resolvers, ref string) (*Entity, error) {
+func read[EntityT entity.Interface](def Definition, wrapper func(e *Entity) EntityT, repo repository.ClockedRepo, resolvers entity.Resolvers, ref string) (EntityT, error) {
rootHash, err := repo.ResolveRef(ref)
if err != nil {
- return nil, err
+ return *new(EntityT), err
}
// Perform a breadth-first search to get a topological order of the DAG where we discover the
@@ -104,7 +117,7 @@ func read(def Definition, repo repository.ClockedRepo, resolvers entity.Resolver
commit, err := repo.ReadCommit(hash)
if err != nil {
- return nil, err
+ return *new(EntityT), err
}
BFSOrder = append(BFSOrder, commit)
@@ -137,26 +150,26 @@ func read(def Definition, repo repository.ClockedRepo, resolvers entity.Resolver
// can have no parents. Said otherwise, the DAG need to have exactly
// one leaf.
if !isFirstCommit && len(commit.Parents) == 0 {
- return nil, fmt.Errorf("multiple leafs in the entity DAG")
+ return *new(EntityT), fmt.Errorf("multiple leafs in the entity DAG")
}
opp, err := readOperationPack(def, repo, resolvers, commit)
if err != nil {
- return nil, err
+ return *new(EntityT), err
}
err = opp.Validate()
if err != nil {
- return nil, err
+ return *new(EntityT), err
}
if isMerge && len(opp.Operations) > 0 {
- return nil, fmt.Errorf("merge commit cannot have operations")
+ return *new(EntityT), fmt.Errorf("merge commit cannot have operations")
}
// Check that the create lamport clock is set (not checked in Validate() as it's optional)
if isFirstCommit && opp.CreateTime <= 0 {
- return nil, fmt.Errorf("creation lamport time not set")
+ return *new(EntityT), fmt.Errorf("creation lamport time not set")
}
// make sure that the lamport clocks causality match the DAG topology
@@ -167,7 +180,7 @@ func read(def Definition, repo repository.ClockedRepo, resolvers entity.Resolver
}
if parentPack.EditTime >= opp.EditTime {
- return nil, fmt.Errorf("lamport clock ordering doesn't match the DAG")
+ return *new(EntityT), fmt.Errorf("lamport clock ordering doesn't match the DAG")
}
// to avoid an attack where clocks are pushed toward the uint64 rollover, make sure
@@ -175,7 +188,7 @@ func read(def Definition, repo repository.ClockedRepo, resolvers entity.Resolver
// we ignore merge commits here to allow merging after a loooong time without breaking anything,
// as long as there is one valid chain of small hops, it's fine.
if !isMerge && opp.EditTime-parentPack.EditTime > 1_000_000 {
- return nil, fmt.Errorf("lamport clock jumping too far in the future, likely an attack")
+ return *new(EntityT), fmt.Errorf("lamport clock jumping too far in the future, likely an attack")
}
}
@@ -187,11 +200,11 @@ func read(def Definition, repo repository.ClockedRepo, resolvers entity.Resolver
for _, opp := range oppMap {
err = repo.Witness(fmt.Sprintf(creationClockPattern, def.Namespace), opp.CreateTime)
if err != nil {
- return nil, err
+ return *new(EntityT), err
}
err = repo.Witness(fmt.Sprintf(editClockPattern, def.Namespace), opp.EditTime)
if err != nil {
- return nil, err
+ return *new(EntityT), err
}
}
@@ -232,13 +245,13 @@ func read(def Definition, repo repository.ClockedRepo, resolvers entity.Resolver
}
}
- return &Entity{
+ return wrapper(&Entity{
Definition: def,
ops: ops,
lastCommit: rootHash,
createTime: createTime,
editTime: editTime,
- }, nil
+ }), nil
}
// readClockNoCheck fetch from git, read and witness the clocks of an Entity at an arbitrary git reference.
@@ -293,14 +306,14 @@ func readClockNoCheck(def Definition, repo repository.ClockedRepo, ref string) e
return nil
}
-type StreamedEntity struct {
- Entity *Entity
+type StreamedEntity[EntityT entity.Interface] struct {
+ Entity EntityT
Err error
}
// ReadAll read and parse all local Entity
-func ReadAll(def Definition, repo repository.ClockedRepo, resolvers entity.Resolvers) <-chan StreamedEntity {
- out := make(chan StreamedEntity)
+func ReadAll[EntityT entity.Interface](def Definition, wrapper func(e *Entity) EntityT, repo repository.ClockedRepo, resolvers entity.Resolvers) <-chan StreamedEntity[EntityT] {
+ out := make(chan StreamedEntity[EntityT])
go func() {
defer close(out)
@@ -309,19 +322,19 @@ func ReadAll(def Definition, repo repository.ClockedRepo, resolvers entity.Resol
refs, err := repo.ListRefs(refPrefix)
if err != nil {
- out <- StreamedEntity{Err: err}
+ out <- StreamedEntity[EntityT]{Err: err}
return
}
for _, ref := range refs {
- e, err := read(def, repo, resolvers, ref)
+ e, err := read[EntityT](def, wrapper, repo, resolvers, ref)
if err != nil {
- out <- StreamedEntity{Err: err}
+ out <- StreamedEntity[EntityT]{Err: err}
return
}
- out <- StreamedEntity{Entity: e}
+ out <- StreamedEntity[EntityT]{Entity: e}
}
}()
diff --git a/entity/dag/entity_actions.go b/entity/dag/entity_actions.go
index c971f316..2a2bf87f 100644
--- a/entity/dag/entity_actions.go
+++ b/entity/dag/entity_actions.go
@@ -32,13 +32,13 @@ func Push(def Definition, repo repository.Repo, remote string) (string, error) {
// Pull will do a Fetch + MergeAll
// Contrary to MergeAll, this function will return an error if a merge fail.
-func Pull(def Definition, repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, author identity.Interface) error {
+func Pull[EntityT entity.Interface](def Definition, wrapper func(e *Entity) EntityT, repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, author identity.Interface) error {
_, err := Fetch(def, repo, remote)
if err != nil {
return err
}
- for merge := range MergeAll(def, repo, resolvers, remote, author) {
+ for merge := range MergeAll(def, wrapper, repo, resolvers, remote, author) {
if merge.Err != nil {
return merge.Err
}
@@ -68,7 +68,7 @@ func Pull(def Definition, repo repository.ClockedRepo, resolvers entity.Resolver
//
// Note: an author is necessary for the case where a merge commit is created, as this commit will
// have an author and may be signed if a signing key is available.
-func MergeAll(def Definition, repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, author identity.Interface) <-chan entity.MergeResult {
+func MergeAll[EntityT entity.Interface](def Definition, wrapper func(e *Entity) EntityT, repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, author identity.Interface) <-chan entity.MergeResult {
out := make(chan entity.MergeResult)
go func() {
@@ -82,7 +82,7 @@ func MergeAll(def Definition, repo repository.ClockedRepo, resolvers entity.Reso
}
for _, remoteRef := range remoteRefs {
- out <- merge(def, repo, resolvers, remoteRef, author)
+ out <- merge[EntityT](def, wrapper, repo, resolvers, remoteRef, author)
}
}()
@@ -91,14 +91,14 @@ func MergeAll(def Definition, repo repository.ClockedRepo, resolvers entity.Reso
// merge perform a merge to make sure a local Entity is up-to-date.
// See MergeAll for more details.
-func merge(def Definition, repo repository.ClockedRepo, resolvers entity.Resolvers, remoteRef string, author identity.Interface) entity.MergeResult {
+func merge[EntityT entity.Interface](def Definition, wrapper func(e *Entity) EntityT, repo repository.ClockedRepo, resolvers entity.Resolvers, remoteRef string, author identity.Interface) entity.MergeResult {
id := entity.RefToId(remoteRef)
if err := id.Validate(); err != nil {
return entity.NewMergeInvalidStatus(id, errors.Wrap(err, "invalid ref").Error())
}
- remoteEntity, err := read(def, repo, resolvers, remoteRef)
+ remoteEntity, err := read[EntityT](def, wrapper, repo, resolvers, remoteRef)
if err != nil {
return entity.NewMergeInvalidStatus(id,
errors.Wrapf(err, "remote %s is not readable", def.Typename).Error())
@@ -197,7 +197,7 @@ func merge(def Definition, repo repository.ClockedRepo, resolvers entity.Resolve
// an empty operationPack.
// First step is to collect those clocks.
- localEntity, err := read(def, repo, resolvers, localRef)
+ localEntity, err := read[EntityT](def, wrapper, repo, resolvers, localRef)
if err != nil {
return entity.NewMergeError(err, id)
}
diff --git a/entity/dag/entity_actions_test.go b/entity/dag/entity_actions_test.go
index e6888148..d93059de 100644
--- a/entity/dag/entity_actions_test.go
+++ b/entity/dag/entity_actions_test.go
@@ -11,10 +11,10 @@ import (
"github.com/MichaelMure/git-bug/repository"
)
-func allEntities(t testing.TB, bugs <-chan StreamedEntity) []*Entity {
+func allEntities(t testing.TB, bugs <-chan StreamedEntity[*Foo]) []*Foo {
t.Helper()
- var result []*Entity
+ var result []*Foo
for streamed := range bugs {
require.NoError(t, streamed.Err)
@@ -36,10 +36,10 @@ func TestEntityPushPull(t *testing.T) {
_, err = Push(def, repoA, "remote")
require.NoError(t, err)
- err = Pull(def, repoB, resolvers, "remote", id1)
+ err = Pull(def, wrapper, repoB, resolvers, "remote", id1)
require.NoError(t, err)
- entities := allEntities(t, ReadAll(def, repoB, resolvers))
+ entities := allEntities(t, ReadAll(def, wrapper, repoB, resolvers))
require.Len(t, entities, 1)
// B --> remote --> A
@@ -52,10 +52,10 @@ func TestEntityPushPull(t *testing.T) {
_, err = Push(def, repoB, "remote")
require.NoError(t, err)
- err = Pull(def, repoA, resolvers, "remote", id1)
+ err = Pull(def, wrapper, repoA, resolvers, "remote", id1)
require.NoError(t, err)
- entities = allEntities(t, ReadAll(def, repoB, resolvers))
+ entities = allEntities(t, ReadAll(def, wrapper, repoB, resolvers))
require.Len(t, entities, 2)
}
@@ -85,7 +85,7 @@ func TestListLocalIds(t *testing.T) {
listLocalIds(t, def, repoA, 2)
listLocalIds(t, def, repoB, 0)
- err = Pull(def, repoB, resolvers, "remote", id1)
+ err = Pull(def, wrapper, repoB, resolvers, "remote", id1)
require.NoError(t, err)
listLocalIds(t, def, repoA, 2)
@@ -228,7 +228,7 @@ func TestMerge(t *testing.T) {
_, err = Fetch(def, repoB, "remote")
require.NoError(t, err)
- results := MergeAll(def, repoB, resolvers, "remote", id1)
+ results := MergeAll(def, wrapper, repoB, resolvers, "remote", id1)
assertMergeResults(t, []entity.MergeResult{
{
@@ -246,7 +246,7 @@ func TestMerge(t *testing.T) {
// SCENARIO 2
// if the remote and local Entity have the same state, nothing is changed
- results = MergeAll(def, repoB, resolvers, "remote", id1)
+ results = MergeAll(def, wrapper, repoB, resolvers, "remote", id1)
assertMergeResults(t, []entity.MergeResult{
{
@@ -272,7 +272,7 @@ func TestMerge(t *testing.T) {
err = e2A.Commit(repoA)
require.NoError(t, err)
- results = MergeAll(def, repoA, resolvers, "remote", id1)
+ results = MergeAll(def, wrapper, repoA, resolvers, "remote", id1)
assertMergeResults(t, []entity.MergeResult{
{
@@ -297,7 +297,7 @@ func TestMerge(t *testing.T) {
_, err = Fetch(def, repoB, "remote")
require.NoError(t, err)
- results = MergeAll(def, repoB, resolvers, "remote", id1)
+ results = MergeAll(def, wrapper, repoB, resolvers, "remote", id1)
assertMergeResults(t, []entity.MergeResult{
{
@@ -324,10 +324,10 @@ func TestMerge(t *testing.T) {
err = e2A.Commit(repoA)
require.NoError(t, err)
- e1B, err := Read(def, repoB, resolvers, e1A.Id())
+ e1B, err := Read(def, wrapper, repoB, resolvers, e1A.Id())
require.NoError(t, err)
- e2B, err := Read(def, repoB, resolvers, e2A.Id())
+ e2B, err := Read(def, wrapper, repoB, resolvers, e2A.Id())
require.NoError(t, err)
e1B.Append(newOp1(id1, "barbarfoofoo"))
@@ -344,7 +344,7 @@ func TestMerge(t *testing.T) {
_, err = Fetch(def, repoB, "remote")
require.NoError(t, err)
- results = MergeAll(def, repoB, resolvers, "remote", id1)
+ results = MergeAll(def, wrapper, repoB, resolvers, "remote", id1)
assertMergeResults(t, []entity.MergeResult{
{
@@ -365,7 +365,7 @@ func TestMerge(t *testing.T) {
_, err = Fetch(def, repoA, "remote")
require.NoError(t, err)
- results = MergeAll(def, repoA, resolvers, "remote", id1)
+ results = MergeAll(def, wrapper, repoA, resolvers, "remote", id1)
assertMergeResults(t, []entity.MergeResult{
{
@@ -396,10 +396,10 @@ func TestRemove(t *testing.T) {
err = Remove(def, repoA, e.Id())
require.NoError(t, err)
- _, err = Read(def, repoA, resolvers, e.Id())
+ _, err = Read(def, wrapper, repoA, resolvers, e.Id())
require.Error(t, err)
- _, err = readRemote(def, repoA, resolvers, "remote", e.Id())
+ _, err = readRemote(def, wrapper, repoA, resolvers, "remote", e.Id())
require.Error(t, err)
// Remove is idempotent
diff --git a/entity/dag/entity_test.go b/entity/dag/entity_test.go
index e399b6c7..c457eb21 100644
--- a/entity/dag/entity_test.go
+++ b/entity/dag/entity_test.go
@@ -9,7 +9,7 @@ import (
func TestWriteRead(t *testing.T) {
repo, id1, id2, resolver, def := makeTestContext()
- entity := New(def)
+ entity := wrapper(New(def))
require.False(t, entity.NeedCommit())
entity.Append(newOp1(id1, "foo"))
@@ -24,16 +24,16 @@ func TestWriteRead(t *testing.T) {
require.NoError(t, entity.CommitAsNeeded(repo))
require.False(t, entity.NeedCommit())
- read, err := Read(def, repo, resolver, entity.Id())
+ read, err := Read(def, wrapper, repo, resolver, entity.Id())
require.NoError(t, err)
- assertEqualEntities(t, entity, read)
+ assertEqualEntities(t, entity.Entity, read.Entity)
}
func TestWriteReadMultipleAuthor(t *testing.T) {
repo, id1, id2, resolver, def := makeTestContext()
- entity := New(def)
+ entity := wrapper(New(def))
entity.Append(newOp1(id1, "foo"))
entity.Append(newOp2(id2, "bar"))
@@ -43,10 +43,10 @@ func TestWriteReadMultipleAuthor(t *testing.T) {
entity.Append(newOp2(id1, "foobar"))
require.NoError(t, entity.CommitAsNeeded(repo))
- read, err := Read(def, repo, resolver, entity.Id())
+ read, err := Read(def, wrapper, repo, resolver, entity.Id())
require.NoError(t, err)
- assertEqualEntities(t, entity, read)
+ assertEqualEntities(t, entity.Entity, read.Entity)
}
func assertEqualEntities(t *testing.T, a, b *Entity) {
diff --git a/entity/dag/example_test.go b/entity/dag/example_test.go
index b1511dc6..a263eb2b 100644
--- a/entity/dag/example_test.go
+++ b/entity/dag/example_test.go
@@ -200,7 +200,11 @@ type ProjectConfig struct {
}
func NewProjectConfig() *ProjectConfig {
- return &ProjectConfig{Entity: dag.New(def)}
+ return wrapper(dag.New(def))
+}
+
+func wrapper(e *dag.Entity) *ProjectConfig {
+ return &ProjectConfig{Entity: e}
}
// a Definition describes a few properties of the Entity, a sort of configuration to manipulate the
@@ -282,11 +286,7 @@ func (pc ProjectConfig) Compile() *Snapshot {
// Read is a helper to load a ProjectConfig from a Repository
func Read(repo repository.ClockedRepo, id entity.Id) (*ProjectConfig, error) {
- e, err := dag.Read(def, repo, simpleResolvers(repo), id)
- if err != nil {
- return nil, err
- }
- return &ProjectConfig{Entity: e}, nil
+ return dag.Read(def, wrapper, repo, simpleResolvers(repo), id)
}
func simpleResolvers(repo repository.ClockedRepo) entity.Resolvers {
@@ -331,7 +331,7 @@ func Example_entity() {
_ = confRene.Commit(repoRene)
// Isaac pull and read the config
- _ = dag.Pull(def, repoIsaac, simpleResolvers(repoIsaac), "origin", isaac)
+ _ = dag.Pull(def, wrapper, repoIsaac, simpleResolvers(repoIsaac), "origin", isaac)
confIsaac, _ := Read(repoIsaac, confRene.Id())
// Compile gives the current state of the config
diff --git a/entity/interface.go b/entity/interface.go
index 656d4dc6..572ba602 100644
--- a/entity/interface.go
+++ b/entity/interface.go
@@ -9,6 +9,8 @@ type Interface interface {
// the root of the entity.
// It is acceptable to use such a hash and keep mutating that data as long as Id() is not called.
Id() Id
+ // Validate check if the Entity data is valid
+ Validate() error
}
// type Commitable interface {