diff options
author | Michael Muré <batolettre@gmail.com> | 2022-11-29 13:01:53 +0100 |
---|---|---|
committer | Michael Muré <batolettre@gmail.com> | 2022-11-29 13:01:53 +0100 |
commit | 4a341b5e1714a6a36ec7f5839a6a1b73571d4851 (patch) | |
tree | 261e108d1c9bd78e15e19379f611cfecb900fef5 /entity/dag | |
parent | 0ac39a7ab5db077fcf0df827e32bf6e625e980da (diff) | |
download | git-bug-4a341b5e1714a6a36ec7f5839a6a1b73571d4851.tar.gz |
WIP
Diffstat (limited to 'entity/dag')
-rw-r--r-- | entity/dag/common_test.go | 12 | ||||
-rw-r--r-- | entity/dag/entity.go | 69 | ||||
-rw-r--r-- | entity/dag/entity_actions.go | 14 | ||||
-rw-r--r-- | entity/dag/entity_actions_test.go | 34 | ||||
-rw-r--r-- | entity/dag/entity_test.go | 12 | ||||
-rw-r--r-- | entity/dag/example_test.go | 14 |
6 files changed, 90 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 |