aboutsummaryrefslogtreecommitdiffstats
path: root/entity/dag
diff options
context:
space:
mode:
Diffstat (limited to 'entity/dag')
-rw-r--r--entity/dag/common_test.go12
-rw-r--r--entity/dag/entity.go63
-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/dag/interface.go6
-rw-r--r--entity/dag/op_set_metadata_test.go6
-rw-r--r--entity/dag/operation.go9
9 files changed, 101 insertions, 69 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..2028e1b4 100644
--- a/entity/dag/entity.go
+++ b/entity/dag/entity.go
@@ -59,32 +59,35 @@ 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 == repository.ErrNotFound {
+ return *new(EntityT), entity.NewErrNotFound(def.Typename)
+ }
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 +107,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 +140,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 +170,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 +178,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 +190,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 +235,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.
@@ -247,6 +250,9 @@ func read(def Definition, repo repository.ClockedRepo, resolvers entity.Resolver
// operation blobs can be implemented instead.
func readClockNoCheck(def Definition, repo repository.ClockedRepo, ref string) error {
rootHash, err := repo.ResolveRef(ref)
+ if err == repository.ErrNotFound {
+ return entity.NewErrNotFound(def.Typename)
+ }
if err != nil {
return err
}
@@ -293,14 +299,9 @@ func readClockNoCheck(def Definition, repo repository.ClockedRepo, ref string) e
return nil
}
-type StreamedEntity struct {
- Entity *Entity
- 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 entity.StreamedEntity[EntityT] {
+ out := make(chan entity.StreamedEntity[EntityT])
go func() {
defer close(out)
@@ -309,19 +310,19 @@ func ReadAll(def Definition, repo repository.ClockedRepo, resolvers entity.Resol
refs, err := repo.ListRefs(refPrefix)
if err != nil {
- out <- StreamedEntity{Err: err}
+ out <- entity.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 <- entity.StreamedEntity[EntityT]{Err: err}
return
}
- out <- StreamedEntity{Entity: e}
+ out <- entity.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..fd219644 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 entity.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/dag/interface.go b/entity/dag/interface.go
index 613f60e6..80abaced 100644
--- a/entity/dag/interface.go
+++ b/entity/dag/interface.go
@@ -25,6 +25,10 @@ type Interface[SnapT Snapshot, OpT Operation] interface {
// Commit writes the staging area in Git and move the operations to the packs
Commit(repo repository.ClockedRepo) error
+ // CommitAsNeeded execute a Commit only if necessary. This function is useful to avoid getting an error if the Entity
+ // is already in sync with the repository.
+ CommitAsNeeded(repo repository.ClockedRepo) error
+
// FirstOp lookup for the very first operation of the Entity.
FirstOp() OpT
@@ -32,7 +36,7 @@ type Interface[SnapT Snapshot, OpT Operation] interface {
// For a valid Entity, should never be nil
LastOp() OpT
- // Compile a bug in an easily usable snapshot
+ // Compile an Entity in an easily usable snapshot
Compile() SnapT
// CreateLamportTime return the Lamport time of creation
diff --git a/entity/dag/op_set_metadata_test.go b/entity/dag/op_set_metadata_test.go
index f4f20e8e..07ece013 100644
--- a/entity/dag/op_set_metadata_test.go
+++ b/entity/dag/op_set_metadata_test.go
@@ -12,6 +12,8 @@ import (
"github.com/stretchr/testify/require"
)
+var _ Snapshot = &snapshotMock{}
+
type snapshotMock struct {
ops []Operation
}
@@ -20,6 +22,10 @@ func (s *snapshotMock) AllOperations() []Operation {
return s.ops
}
+func (s *snapshotMock) AppendOperation(op Operation) {
+ s.ops = append(s.ops, op)
+}
+
func TestSetMetadata(t *testing.T) {
snap := &snapshotMock{}
diff --git a/entity/dag/operation.go b/entity/dag/operation.go
index 1a778878..f50d91b6 100644
--- a/entity/dag/operation.go
+++ b/entity/dag/operation.go
@@ -63,6 +63,13 @@ type Operation interface {
setExtraMetadataImmutable(key string, value string)
}
+type OperationWithApply[SnapT Snapshot] interface {
+ Operation
+
+ // Apply the operation to a Snapshot to create the final state
+ Apply(snapshot SnapT)
+}
+
// OperationWithFiles is an optional extension for an Operation that has files dependency, stored in git.
type OperationWithFiles interface {
// GetFiles return the files needed by this operation
@@ -83,6 +90,8 @@ type OperationDoesntChangeSnapshot interface {
type Snapshot interface {
// AllOperations returns all the operations that have been applied to that snapshot, in order
AllOperations() []Operation
+ // AppendOperation add an operation in the list
+ AppendOperation(op Operation)
}
// OpBase implement the common feature that every Operation should support.