bug: migrate to the DAG entity structure!

Michael MurΓ© created

Change summary

api/graphql/resolvers/operations.go    |  12 
api/graphql/resolvers/query.go         |  13 
bridge/github/export.go                |   2 
bridge/github/import_test.go           |  27 
bridge/gitlab/export.go                |   2 
bridge/gitlab/import_test.go           |  27 
bridge/jira/export.go                  |   2 
bug/bug.go                             | 594 +++------------------------
bug/bug_actions.go                     | 106 ----
bug/bug_actions_test.go                | 394 ------------------
bug/bug_test.go                        | 190 --------
bug/clocks.go                          |  40 -
bug/err.go                             |  17 
bug/git_tree.go                        |  84 ---
bug/identity.go                        |  27 -
bug/interface.go                       |   8 
bug/op_add_comment.go                  |  14 
bug/op_add_comment_test.go             |   4 
bug/op_create.go                       |  16 
bug/op_create_test.go                  |   4 
bug/op_edit_comment.go                 |  10 
bug/op_edit_comment_test.go            |   4 
bug/op_label_change.go                 |  12 
bug/op_label_change_test.go            |   4 
bug/op_noop.go                         |   8 
bug/op_noop_test.go                    |   4 
bug/op_set_metadata.go                 |  21 
bug/op_set_metadata_test.go            |   4 
bug/op_set_status.go                   |  12 
bug/op_set_status_test.go              |   4 
bug/op_set_title.go                    |  24 
bug/op_set_title_test.go               |   4 
bug/operation.go                       | 152 +++++-
bug/operation_iterator.go              |  72 ---
bug/operation_iterator_test.go         |  79 ---
bug/operation_pack.go                  | 187 --------
bug/operation_pack_test.go             |  78 ---
bug/operation_test.go                  |   4 
bug/sorting.go                         |   8 
bug/with_snapshot.go                   |   6 
cache/bug_cache.go                     |   4 
cache/bug_excerpt.go                   |   2 
cache/repo_cache.go                    |   2 
cache/repo_cache_bug.go                |   2 
cache/repo_cache_common.go             |  15 
cache/repo_cache_test.go               |   4 
entity/TODO                            |   9 
entity/merge.go                        |   6 
go.sum                                 |   1 
misc/random_bugs/create_random_bugs.go |  46 --
tests/read_bugs_test.go                |   4 
51 files changed, 335 insertions(+), 2,040 deletions(-)

Detailed changes

api/graphql/resolvers/operations.go πŸ”—

@@ -19,7 +19,7 @@ func (createOperationResolver) ID(_ context.Context, obj *bug.CreateOperation) (
 }
 
 func (createOperationResolver) Author(_ context.Context, obj *bug.CreateOperation) (models.IdentityWrapper, error) {
-	return models.NewLoadedIdentity(obj.Author), nil
+	return models.NewLoadedIdentity(obj.Author()), nil
 }
 
 func (createOperationResolver) Date(_ context.Context, obj *bug.CreateOperation) (*time.Time, error) {
@@ -36,7 +36,7 @@ func (addCommentOperationResolver) ID(_ context.Context, obj *bug.AddCommentOper
 }
 
 func (addCommentOperationResolver) Author(_ context.Context, obj *bug.AddCommentOperation) (models.IdentityWrapper, error) {
-	return models.NewLoadedIdentity(obj.Author), nil
+	return models.NewLoadedIdentity(obj.Author()), nil
 }
 
 func (addCommentOperationResolver) Date(_ context.Context, obj *bug.AddCommentOperation) (*time.Time, error) {
@@ -57,7 +57,7 @@ func (editCommentOperationResolver) Target(_ context.Context, obj *bug.EditComme
 }
 
 func (editCommentOperationResolver) Author(_ context.Context, obj *bug.EditCommentOperation) (models.IdentityWrapper, error) {
-	return models.NewLoadedIdentity(obj.Author), nil
+	return models.NewLoadedIdentity(obj.Author()), nil
 }
 
 func (editCommentOperationResolver) Date(_ context.Context, obj *bug.EditCommentOperation) (*time.Time, error) {
@@ -74,7 +74,7 @@ func (labelChangeOperationResolver) ID(_ context.Context, obj *bug.LabelChangeOp
 }
 
 func (labelChangeOperationResolver) Author(_ context.Context, obj *bug.LabelChangeOperation) (models.IdentityWrapper, error) {
-	return models.NewLoadedIdentity(obj.Author), nil
+	return models.NewLoadedIdentity(obj.Author()), nil
 }
 
 func (labelChangeOperationResolver) Date(_ context.Context, obj *bug.LabelChangeOperation) (*time.Time, error) {
@@ -91,7 +91,7 @@ func (setStatusOperationResolver) ID(_ context.Context, obj *bug.SetStatusOperat
 }
 
 func (setStatusOperationResolver) Author(_ context.Context, obj *bug.SetStatusOperation) (models.IdentityWrapper, error) {
-	return models.NewLoadedIdentity(obj.Author), nil
+	return models.NewLoadedIdentity(obj.Author()), nil
 }
 
 func (setStatusOperationResolver) Date(_ context.Context, obj *bug.SetStatusOperation) (*time.Time, error) {
@@ -112,7 +112,7 @@ func (setTitleOperationResolver) ID(_ context.Context, obj *bug.SetTitleOperatio
 }
 
 func (setTitleOperationResolver) Author(_ context.Context, obj *bug.SetTitleOperation) (models.IdentityWrapper, error) {
-	return models.NewLoadedIdentity(obj.Author), nil
+	return models.NewLoadedIdentity(obj.Author()), nil
 }
 
 func (setTitleOperationResolver) Date(_ context.Context, obj *bug.SetTitleOperation) (*time.Time, error) {

api/graphql/resolvers/query.go πŸ”—

@@ -14,19 +14,6 @@ type rootQueryResolver struct {
 	cache *cache.MultiRepoCache
 }
 
-func (r rootQueryResolver) DefaultRepository(_ context.Context) (*models.Repository, error) {
-	repo, err := r.cache.DefaultRepo()
-
-	if err != nil {
-		return nil, err
-	}
-
-	return &models.Repository{
-		Cache: r.cache,
-		Repo:  repo,
-	}, nil
-}
-
 func (r rootQueryResolver) Repository(_ context.Context, ref *string) (*models.Repository, error) {
 	var repo *cache.RepoCache
 	var err error

bridge/github/export.go πŸ”—

@@ -294,7 +294,7 @@ func (ge *githubExporter) exportBug(ctx context.Context, b *cache.BugCache, out
 			continue
 		}
 
-		opAuthor := op.GetAuthor()
+		opAuthor := op.Author()
 		client, err := ge.getClientForIdentity(opAuthor.Id())
 		if err != nil {
 			continue

bridge/github/import_test.go πŸ”—

@@ -182,29 +182,24 @@ func TestGithubImporter(t *testing.T) {
 
 			for i, op := range tt.bug.Operations {
 				require.IsType(t, ops[i], op)
+				require.Equal(t, op.Author().Name(), ops[i].Author().Name())
 
-				switch op.(type) {
+				switch op := op.(type) {
 				case *bug.CreateOperation:
-					require.Equal(t, op.(*bug.CreateOperation).Title, ops[i].(*bug.CreateOperation).Title)
-					require.Equal(t, op.(*bug.CreateOperation).Message, ops[i].(*bug.CreateOperation).Message)
-					require.Equal(t, op.(*bug.CreateOperation).Author.Name(), ops[i].(*bug.CreateOperation).Author.Name())
+					require.Equal(t, op.Title, ops[i].(*bug.CreateOperation).Title)
+					require.Equal(t, op.Message, ops[i].(*bug.CreateOperation).Message)
 				case *bug.SetStatusOperation:
-					require.Equal(t, op.(*bug.SetStatusOperation).Status, ops[i].(*bug.SetStatusOperation).Status)
-					require.Equal(t, op.(*bug.SetStatusOperation).Author.Name(), ops[i].(*bug.SetStatusOperation).Author.Name())
+					require.Equal(t, op.Status, ops[i].(*bug.SetStatusOperation).Status)
 				case *bug.SetTitleOperation:
-					require.Equal(t, op.(*bug.SetTitleOperation).Was, ops[i].(*bug.SetTitleOperation).Was)
-					require.Equal(t, op.(*bug.SetTitleOperation).Title, ops[i].(*bug.SetTitleOperation).Title)
-					require.Equal(t, op.(*bug.SetTitleOperation).Author.Name(), ops[i].(*bug.SetTitleOperation).Author.Name())
+					require.Equal(t, op.Was, ops[i].(*bug.SetTitleOperation).Was)
+					require.Equal(t, op.Title, ops[i].(*bug.SetTitleOperation).Title)
 				case *bug.LabelChangeOperation:
-					require.ElementsMatch(t, op.(*bug.LabelChangeOperation).Added, ops[i].(*bug.LabelChangeOperation).Added)
-					require.ElementsMatch(t, op.(*bug.LabelChangeOperation).Removed, ops[i].(*bug.LabelChangeOperation).Removed)
-					require.Equal(t, op.(*bug.LabelChangeOperation).Author.Name(), ops[i].(*bug.LabelChangeOperation).Author.Name())
+					require.ElementsMatch(t, op.Added, ops[i].(*bug.LabelChangeOperation).Added)
+					require.ElementsMatch(t, op.Removed, ops[i].(*bug.LabelChangeOperation).Removed)
 				case *bug.AddCommentOperation:
-					require.Equal(t, op.(*bug.AddCommentOperation).Message, ops[i].(*bug.AddCommentOperation).Message)
-					require.Equal(t, op.(*bug.AddCommentOperation).Author.Name(), ops[i].(*bug.AddCommentOperation).Author.Name())
+					require.Equal(t, op.Message, ops[i].(*bug.AddCommentOperation).Message)
 				case *bug.EditCommentOperation:
-					require.Equal(t, op.(*bug.EditCommentOperation).Message, ops[i].(*bug.EditCommentOperation).Message)
-					require.Equal(t, op.(*bug.EditCommentOperation).Author.Name(), ops[i].(*bug.EditCommentOperation).Author.Name())
+					require.Equal(t, op.Message, ops[i].(*bug.EditCommentOperation).Message)
 
 				default:
 					panic("unknown operation type")

bridge/gitlab/export.go πŸ”—

@@ -267,7 +267,7 @@ func (ge *gitlabExporter) exportBug(ctx context.Context, b *cache.BugCache, out
 			continue
 		}
 
-		opAuthor := op.GetAuthor()
+		opAuthor := op.Author()
 		client, err := ge.getIdentityClient(opAuthor.Id())
 		if err != nil {
 			continue

bridge/gitlab/import_test.go πŸ”—

@@ -138,29 +138,24 @@ func TestGitlabImport(t *testing.T) {
 			for i, op := range tt.bug.Operations {
 
 				require.IsType(t, ops[i], op)
+				require.Equal(t, op.Author().Name(), ops[i].Author().Name())
 
-				switch op.(type) {
+				switch op := op.(type) {
 				case *bug.CreateOperation:
-					require.Equal(t, op.(*bug.CreateOperation).Title, ops[i].(*bug.CreateOperation).Title)
-					require.Equal(t, op.(*bug.CreateOperation).Message, ops[i].(*bug.CreateOperation).Message)
-					require.Equal(t, op.(*bug.CreateOperation).Author.Name(), ops[i].(*bug.CreateOperation).Author.Name())
+					require.Equal(t, op.Title, ops[i].(*bug.CreateOperation).Title)
+					require.Equal(t, op.Message, ops[i].(*bug.CreateOperation).Message)
 				case *bug.SetStatusOperation:
-					require.Equal(t, op.(*bug.SetStatusOperation).Status, ops[i].(*bug.SetStatusOperation).Status)
-					require.Equal(t, op.(*bug.SetStatusOperation).Author.Name(), ops[i].(*bug.SetStatusOperation).Author.Name())
+					require.Equal(t, op.Status, ops[i].(*bug.SetStatusOperation).Status)
 				case *bug.SetTitleOperation:
-					require.Equal(t, op.(*bug.SetTitleOperation).Was, ops[i].(*bug.SetTitleOperation).Was)
-					require.Equal(t, op.(*bug.SetTitleOperation).Title, ops[i].(*bug.SetTitleOperation).Title)
-					require.Equal(t, op.(*bug.SetTitleOperation).Author.Name(), ops[i].(*bug.SetTitleOperation).Author.Name())
+					require.Equal(t, op.Was, ops[i].(*bug.SetTitleOperation).Was)
+					require.Equal(t, op.Title, ops[i].(*bug.SetTitleOperation).Title)
 				case *bug.LabelChangeOperation:
-					require.ElementsMatch(t, op.(*bug.LabelChangeOperation).Added, ops[i].(*bug.LabelChangeOperation).Added)
-					require.ElementsMatch(t, op.(*bug.LabelChangeOperation).Removed, ops[i].(*bug.LabelChangeOperation).Removed)
-					require.Equal(t, op.(*bug.LabelChangeOperation).Author.Name(), ops[i].(*bug.LabelChangeOperation).Author.Name())
+					require.ElementsMatch(t, op.Added, ops[i].(*bug.LabelChangeOperation).Added)
+					require.ElementsMatch(t, op.Removed, ops[i].(*bug.LabelChangeOperation).Removed)
 				case *bug.AddCommentOperation:
-					require.Equal(t, op.(*bug.AddCommentOperation).Message, ops[i].(*bug.AddCommentOperation).Message)
-					require.Equal(t, op.(*bug.AddCommentOperation).Author.Name(), ops[i].(*bug.AddCommentOperation).Author.Name())
+					require.Equal(t, op.Message, ops[i].(*bug.AddCommentOperation).Message)
 				case *bug.EditCommentOperation:
-					require.Equal(t, op.(*bug.EditCommentOperation).Message, ops[i].(*bug.EditCommentOperation).Message)
-					require.Equal(t, op.(*bug.EditCommentOperation).Author.Name(), ops[i].(*bug.EditCommentOperation).Author.Name())
+					require.Equal(t, op.Message, ops[i].(*bug.EditCommentOperation).Message)
 
 				default:
 					panic("unknown operation type")

bridge/jira/export.go πŸ”—

@@ -309,7 +309,7 @@ func (je *jiraExporter) exportBug(ctx context.Context, b *cache.BugCache, out ch
 			continue
 		}
 
-		opAuthor := op.GetAuthor()
+		opAuthor := op.Author()
 		client, err := je.getClientForIdentity(opAuthor.Id())
 		if err != nil {
 			out <- core.NewExportError(

bug/bug.go πŸ”—

@@ -2,222 +2,62 @@
 package bug
 
 import (
-	"encoding/json"
 	"fmt"
 
-	"github.com/pkg/errors"
-
 	"github.com/MichaelMure/git-bug/entity"
+	"github.com/MichaelMure/git-bug/entity/dag"
 	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/repository"
-	"github.com/MichaelMure/git-bug/util/lamport"
 )
 
-const bugsRefPattern = "refs/bugs/"
-const bugsRemoteRefPattern = "refs/remotes/%s/bugs/"
-
-const opsEntryName = "ops"
-const mediaEntryName = "media"
-
-const createClockEntryPrefix = "create-clock-"
-const createClockEntryPattern = "create-clock-%d"
-const editClockEntryPrefix = "edit-clock-"
-const editClockEntryPattern = "edit-clock-%d"
-
-const creationClockName = "bug-create"
-const editClockName = "bug-edit"
+var _ Interface = &Bug{}
+var _ entity.Interface = &Bug{}
 
-var ErrBugNotExist = errors.New("bug doesn't exist")
+// 1: original format
+// 2: no more legacy identities
+// 3: Ids are generated from the create operation serialized data instead of from the first git commit
+// 4: with DAG entity framework
+const formatVersion = 4
 
-func NewErrMultipleMatchBug(matching []entity.Id) *entity.ErrMultipleMatch {
-	return entity.NewErrMultipleMatch("bug", matching)
+var def = dag.Definition{
+	Typename:             "bug",
+	Namespace:            "bugs",
+	OperationUnmarshaler: operationUnmarshaller,
+	FormatVersion:        formatVersion,
 }
 
-func NewErrMultipleMatchOp(matching []entity.Id) *entity.ErrMultipleMatch {
-	return entity.NewErrMultipleMatch("operation", matching)
-}
-
-var _ Interface = &Bug{}
-var _ entity.Interface = &Bug{}
+var ClockLoader = dag.ClockLoader(def)
 
 // Bug hold the data of a bug thread, organized in a way close to
 // how it will be persisted inside Git. This is the data structure
 // used to merge two different version of the same Bug.
 type Bug struct {
-	// A Lamport clock is a logical clock that allow to order event
-	// inside a distributed system.
-	// It must be the first field in this struct due to https://github.com/golang/go/issues/599
-	createTime lamport.Time
-	editTime   lamport.Time
-
-	lastCommit repository.Hash
-
-	// all the committed operations
-	packs []OperationPack
-
-	// a temporary pack of operations used for convenience to pile up new operations
-	// before a commit
-	staging OperationPack
+	*dag.Entity
 }
 
 // NewBug create a new Bug
 func NewBug() *Bug {
-	// No logical clock yet
-	return &Bug{}
-}
-
-// ReadLocal will read a local bug from its hash
-func ReadLocal(repo repository.ClockedRepo, id entity.Id) (*Bug, error) {
-	ref := bugsRefPattern + id.String()
-	return read(repo, identity.NewSimpleResolver(repo), ref)
-}
-
-// ReadLocalWithResolver will read a local bug from its hash
-func ReadLocalWithResolver(repo repository.ClockedRepo, identityResolver identity.Resolver, id entity.Id) (*Bug, error) {
-	ref := bugsRefPattern + id.String()
-	return read(repo, identityResolver, ref)
-}
-
-// ReadRemote will read a remote bug from its hash
-func ReadRemote(repo repository.ClockedRepo, remote string, id entity.Id) (*Bug, error) {
-	ref := fmt.Sprintf(bugsRemoteRefPattern, remote) + id.String()
-	return read(repo, identity.NewSimpleResolver(repo), ref)
-}
-
-// ReadRemoteWithResolver will read a remote bug from its hash
-func ReadRemoteWithResolver(repo repository.ClockedRepo, identityResolver identity.Resolver, remote string, id entity.Id) (*Bug, error) {
-	ref := fmt.Sprintf(bugsRemoteRefPattern, remote) + id.String()
-	return read(repo, identityResolver, ref)
-}
-
-// read will read and parse a Bug from git
-func read(repo repository.ClockedRepo, identityResolver identity.Resolver, ref string) (*Bug, error) {
-	id := entity.RefToId(ref)
-
-	if err := id.Validate(); err != nil {
-		return nil, errors.Wrap(err, "invalid ref ")
-	}
-
-	hashes, err := repo.ListCommits(ref)
-	if err != nil {
-		return nil, ErrBugNotExist
-	}
-	if len(hashes) == 0 {
-		return nil, fmt.Errorf("empty bug")
-	}
-
-	bug := Bug{}
-
-	// Load each OperationPack
-	for _, hash := range hashes {
-		tree, err := readTree(repo, hash)
-		if err != nil {
-			return nil, err
-		}
-
-		// Due to rebase, edit Lamport time are not necessarily ordered
-		if tree.editTime > bug.editTime {
-			bug.editTime = tree.editTime
-		}
-
-		// Update the clocks
-		err = repo.Witness(creationClockName, bug.createTime)
-		if err != nil {
-			return nil, errors.Wrap(err, "failed to update create lamport clock")
-		}
-		err = repo.Witness(editClockName, bug.editTime)
-		if err != nil {
-			return nil, errors.Wrap(err, "failed to update edit lamport clock")
-		}
-
-		data, err := repo.ReadData(tree.opsEntry.Hash)
-		if err != nil {
-			return nil, errors.Wrap(err, "failed to read git blob data")
-		}
-
-		opp := &OperationPack{}
-		err = json.Unmarshal(data, &opp)
-		if err != nil {
-			return nil, errors.Wrap(err, "failed to decode OperationPack json")
-		}
-
-		// tag the pack with the commit hash
-		opp.commitHash = hash
-		bug.lastCommit = hash
-
-		// if it's the first OperationPack read
-		if len(bug.packs) == 0 {
-			bug.createTime = tree.createTime
-		}
-
-		bug.packs = append(bug.packs, *opp)
-	}
-
-	// Bug Id is the Id of the first operation
-	if len(bug.packs[0].Operations) == 0 {
-		return nil, fmt.Errorf("first OperationPack is empty")
-	}
-	if id != bug.packs[0].Operations[0].Id() {
-		return nil, fmt.Errorf("bug ID doesn't match the first operation ID")
+	return &Bug{
+		Entity: dag.New(def),
 	}
+}
 
-	// Make sure that the identities are properly loaded
-	err = bug.EnsureIdentities(identityResolver)
+// Read will read a bug from a repository
+func Read(repo repository.ClockedRepo, id entity.Id) (*Bug, error) {
+	e, err := dag.Read(def, repo, identity.NewSimpleResolver(repo), id)
 	if err != nil {
 		return nil, err
 	}
-
-	return &bug, nil
+	return &Bug{Entity: e}, nil
 }
 
-// RemoveBug will remove a local bug from its entity.Id
-func RemoveBug(repo repository.ClockedRepo, id entity.Id) error {
-	var fullMatches []string
-
-	refs, err := repo.ListRefs(bugsRefPattern + id.String())
+// ReadWithResolver will read a bug from its Id, with a custom identity.Resolver
+func ReadWithResolver(repo repository.ClockedRepo, identityResolver identity.Resolver, id entity.Id) (*Bug, error) {
+	e, err := dag.Read(def, repo, identityResolver, id)
 	if err != nil {
-		return err
-	}
-	if len(refs) > 1 {
-		return NewErrMultipleMatchBug(entity.RefsToIds(refs))
-	}
-	if len(refs) == 1 {
-		// we have the bug locally
-		fullMatches = append(fullMatches, refs[0])
-	}
-
-	remotes, err := repo.GetRemotes()
-	if err != nil {
-		return err
-	}
-
-	for remote := range remotes {
-		remotePrefix := fmt.Sprintf(bugsRemoteRefPattern+id.String(), remote)
-		remoteRefs, err := repo.ListRefs(remotePrefix)
-		if err != nil {
-			return err
-		}
-		if len(remoteRefs) > 1 {
-			return NewErrMultipleMatchBug(entity.RefsToIds(refs))
-		}
-		if len(remoteRefs) == 1 {
-			// found the bug in a remote
-			fullMatches = append(fullMatches, remoteRefs[0])
-		}
-	}
-
-	if len(fullMatches) == 0 {
-		return ErrBugNotExist
-	}
-
-	for _, ref := range fullMatches {
-		err = repo.RemoveRef(ref)
-		if err != nil {
-			return err
-		}
+		return nil, err
 	}
-
-	return nil
+	return &Bug{Entity: e}, nil
 }
 
 type StreamedBug struct {
@@ -225,50 +65,33 @@ type StreamedBug struct {
 	Err error
 }
 
-// ReadAllLocal read and parse all local bugs
-func ReadAllLocal(repo repository.ClockedRepo) <-chan StreamedBug {
-	return readAll(repo, identity.NewSimpleResolver(repo), bugsRefPattern)
+// ReadAll read and parse all local bugs
+func ReadAll(repo repository.ClockedRepo) <-chan StreamedBug {
+	return readAll(repo, identity.NewSimpleResolver(repo))
 }
 
-// ReadAllLocalWithResolver read and parse all local bugs
-func ReadAllLocalWithResolver(repo repository.ClockedRepo, identityResolver identity.Resolver) <-chan StreamedBug {
-	return readAll(repo, identityResolver, bugsRefPattern)
-}
-
-// ReadAllRemote read and parse all remote bugs for a given remote
-func ReadAllRemote(repo repository.ClockedRepo, remote string) <-chan StreamedBug {
-	refPrefix := fmt.Sprintf(bugsRemoteRefPattern, remote)
-	return readAll(repo, identity.NewSimpleResolver(repo), refPrefix)
-}
-
-// ReadAllRemoteWithResolver read and parse all remote bugs for a given remote
-func ReadAllRemoteWithResolver(repo repository.ClockedRepo, identityResolver identity.Resolver, remote string) <-chan StreamedBug {
-	refPrefix := fmt.Sprintf(bugsRemoteRefPattern, remote)
-	return readAll(repo, identityResolver, refPrefix)
+// ReadAllWithResolver read and parse all local bugs
+func ReadAllWithResolver(repo repository.ClockedRepo, identityResolver identity.Resolver) <-chan StreamedBug {
+	return readAll(repo, identityResolver)
 }
 
 // Read and parse all available bug with a given ref prefix
-func readAll(repo repository.ClockedRepo, identityResolver identity.Resolver, refPrefix string) <-chan StreamedBug {
+func readAll(repo repository.ClockedRepo, identityResolver identity.Resolver) <-chan StreamedBug {
 	out := make(chan StreamedBug)
 
 	go func() {
 		defer close(out)
 
-		refs, err := repo.ListRefs(refPrefix)
-		if err != nil {
-			out <- StreamedBug{Err: err}
-			return
-		}
-
-		for _, ref := range refs {
-			b, err := read(repo, identityResolver, ref)
-
-			if err != nil {
-				out <- StreamedBug{Err: err}
-				return
+		for streamedEntity := range dag.ReadAll(def, repo, identityResolver) {
+			if streamedEntity.Err != nil {
+				out <- StreamedBug{
+					Err: streamedEntity.Err,
+				}
+			} else {
+				out <- StreamedBug{
+					Bug: &Bug{Entity: streamedEntity.Entity},
+				}
 			}
-
-			out <- StreamedBug{Bug: b}
 		}
 	}()
 
@@ -277,345 +100,78 @@ func readAll(repo repository.ClockedRepo, identityResolver identity.Resolver, re
 
 // ListLocalIds list all the available local bug ids
 func ListLocalIds(repo repository.Repo) ([]entity.Id, error) {
-	refs, err := repo.ListRefs(bugsRefPattern)
-	if err != nil {
-		return nil, err
-	}
-
-	return entity.RefsToIds(refs), nil
+	return dag.ListLocalIds(def, repo)
 }
 
 // Validate check if the Bug data is valid
 func (bug *Bug) Validate() error {
-	// non-empty
-	if len(bug.packs) == 0 && bug.staging.IsEmpty() {
-		return fmt.Errorf("bug has no operations")
-	}
-
-	// check if each pack and operations are valid
-	for _, pack := range bug.packs {
-		if err := pack.Validate(); err != nil {
-			return err
-		}
-	}
-
-	// check if staging is valid if needed
-	if !bug.staging.IsEmpty() {
-		if err := bug.staging.Validate(); err != nil {
-			return errors.Wrap(err, "staging")
-		}
+	if err := bug.Entity.Validate(); err != nil {
+		return err
 	}
 
 	// The very first Op should be a CreateOp
 	firstOp := bug.FirstOp()
-	if firstOp == nil || firstOp.base().OperationType != CreateOp {
+	if firstOp == nil || firstOp.Type() != CreateOp {
 		return fmt.Errorf("first operation should be a Create op")
 	}
 
 	// Check that there is no more CreateOp op
-	// Check that there is no colliding operation's ID
-	it := NewOperationIterator(bug)
-	createCount := 0
-	ids := make(map[entity.Id]struct{})
-	for it.Next() {
-		if it.Value().base().OperationType == CreateOp {
-			createCount++
+	for i, op := range bug.Operations() {
+		if i == 0 {
+			continue
 		}
-		if _, ok := ids[it.Value().Id()]; ok {
-			return fmt.Errorf("id collision: %s", it.Value().Id())
+		if op.Type() == CreateOp {
+			return fmt.Errorf("only one Create op allowed")
 		}
-		ids[it.Value().Id()] = struct{}{}
-	}
-
-	if createCount != 1 {
-		return fmt.Errorf("only one Create op allowed")
 	}
 
 	return nil
 }
 
-// Append an operation into the staging area, to be committed later
+// Append add a new Operation to the Bug
 func (bug *Bug) Append(op Operation) {
-	if len(bug.packs) == 0 && len(bug.staging.Operations) == 0 {
-		if op.base().OperationType != CreateOp {
-			panic("first operation should be a Create")
-		}
-	}
-	bug.staging.Append(op)
+	bug.Entity.Append(op)
 }
 
-// Commit write the staging area in Git and move the operations to the packs
-func (bug *Bug) Commit(repo repository.ClockedRepo) error {
-	if !bug.NeedCommit() {
-		return fmt.Errorf("can't commit a bug with no pending operation")
-	}
-
-	if err := bug.Validate(); err != nil {
-		return errors.Wrap(err, "can't commit a bug with invalid data")
-	}
-
-	// update clocks
-	var err error
-	bug.editTime, err = repo.Increment(editClockName)
-	if err != nil {
-		return err
-	}
-	if bug.lastCommit == "" {
-		bug.createTime, err = repo.Increment(creationClockName)
-		if err != nil {
-			return err
-		}
+// Operations return the ordered operations
+func (bug *Bug) Operations() []Operation {
+	source := bug.Entity.Operations()
+	result := make([]Operation, len(source))
+	for i, op := range source {
+		result[i] = op.(Operation)
 	}
-
-	// Write the Ops as a Git blob containing the serialized array
-	hash, err := bug.staging.Write(repo)
-	if err != nil {
-		return err
-	}
-
-	// Make a Git tree referencing this blob
-	tree := []repository.TreeEntry{
-		// the last pack of ops
-		{ObjectType: repository.Blob, Hash: hash, Name: opsEntryName},
-	}
-
-	// Store the logical clocks as well
-	// --> edit clock for each OperationPack/commits
-	// --> create clock only for the first OperationPack/commits
-	//
-	// To avoid having one blob for each clock value, clocks are serialized
-	// directly into the entry name
-	emptyBlobHash, err := repo.StoreData([]byte{})
-	if err != nil {
-		return err
-	}
-	tree = append(tree, repository.TreeEntry{
-		ObjectType: repository.Blob,
-		Hash:       emptyBlobHash,
-		Name:       fmt.Sprintf(editClockEntryPattern, bug.editTime),
-	})
-	if bug.lastCommit == "" {
-		tree = append(tree, repository.TreeEntry{
-			ObjectType: repository.Blob,
-			Hash:       emptyBlobHash,
-			Name:       fmt.Sprintf(createClockEntryPattern, bug.createTime),
-		})
-	}
-
-	// Reference, if any, all the files required by the ops
-	// Git will check that they actually exist in the storage and will make sure
-	// to push/pull them as needed.
-	mediaTree := makeMediaTree(bug.staging)
-	if len(mediaTree) > 0 {
-		mediaTreeHash, err := repo.StoreTree(mediaTree)
-		if err != nil {
-			return err
-		}
-		tree = append(tree, repository.TreeEntry{
-			ObjectType: repository.Tree,
-			Hash:       mediaTreeHash,
-			Name:       mediaEntryName,
-		})
-	}
-
-	// Store the tree
-	hash, err = repo.StoreTree(tree)
-	if err != nil {
-		return err
-	}
-
-	// Write a Git commit referencing the tree, with the previous commit as parent
-	if bug.lastCommit != "" {
-		hash, err = repo.StoreCommit(hash, bug.lastCommit)
-	} else {
-		hash, err = repo.StoreCommit(hash)
-	}
-	if err != nil {
-		return err
-	}
-
-	bug.lastCommit = hash
-	bug.staging.commitHash = hash
-	bug.packs = append(bug.packs, bug.staging)
-	bug.staging = OperationPack{}
-
-	// Create or update the Git reference for this bug
-	// When pushing later, the remote will ensure that this ref update
-	// is fast-forward, that is no data has been overwritten
-	ref := fmt.Sprintf("%s%s", bugsRefPattern, bug.Id().String())
-	return repo.UpdateRef(ref, hash)
+	return result
 }
 
-func (bug *Bug) CommitAsNeeded(repo repository.ClockedRepo) error {
-	if !bug.NeedCommit() {
-		return nil
-	}
-	return bug.Commit(repo)
-}
-
-func (bug *Bug) NeedCommit() bool {
-	return !bug.staging.IsEmpty()
-}
-
-// Merge a different version of the same bug by rebasing operations of this bug
-// that are not present in the other on top of the chain of operations of the
-// other version.
-func (bug *Bug) Merge(repo repository.Repo, other Interface) (bool, error) {
-	var otherBug = bugFromInterface(other)
-
-	// Note: a faster merge should be possible without actually reading and parsing
-	// all operations pack of our side.
-	// Reading the other side is still necessary to validate remote data, at least
-	// for new operations
-
-	if bug.Id() != otherBug.Id() {
-		return false, errors.New("merging unrelated bugs is not supported")
-	}
-
-	if len(otherBug.staging.Operations) > 0 {
-		return false, errors.New("merging a bug with a non-empty staging is not supported")
-	}
-
-	if bug.lastCommit == "" || otherBug.lastCommit == "" {
-		return false, errors.New("can't merge a bug that has never been stored")
-	}
-
-	ancestor, err := repo.FindCommonAncestor(bug.lastCommit, otherBug.lastCommit)
-	if err != nil {
-		return false, errors.Wrap(err, "can't find common ancestor")
-	}
-
-	ancestorIndex := 0
-	newPacks := make([]OperationPack, 0, len(bug.packs))
-
-	// Find the root of the rebase
-	for i, pack := range bug.packs {
-		newPacks = append(newPacks, pack)
-
-		if pack.commitHash == ancestor {
-			ancestorIndex = i
-			break
-		}
-	}
-
-	if len(otherBug.packs) == ancestorIndex+1 {
-		// Nothing to rebase, return early
-		return false, nil
-	}
-
-	// get other bug's extra packs
-	for i := ancestorIndex + 1; i < len(otherBug.packs); i++ {
-		// clone is probably not necessary
-		newPack := otherBug.packs[i].Clone()
-
-		newPacks = append(newPacks, newPack)
-		bug.lastCommit = newPack.commitHash
-	}
-
-	// rebase our extra packs
-	for i := ancestorIndex + 1; i < len(bug.packs); i++ {
-		pack := bug.packs[i]
-
-		// get the referenced git tree
-		treeHash, err := repo.GetTreeHash(pack.commitHash)
-
-		if err != nil {
-			return false, err
-		}
-
-		// create a new commit with the correct ancestor
-		hash, err := repo.StoreCommit(treeHash, bug.lastCommit)
-
-		if err != nil {
-			return false, err
-		}
-
-		// replace the pack
-		newPack := pack.Clone()
-		newPack.commitHash = hash
-		newPacks = append(newPacks, newPack)
-
-		// update the bug
-		bug.lastCommit = hash
+// Compile a bug in a easily usable snapshot
+func (bug *Bug) Compile() Snapshot {
+	snap := Snapshot{
+		id:     bug.Id(),
+		Status: OpenStatus,
 	}
 
-	bug.packs = newPacks
-
-	// Update the git ref
-	err = repo.UpdateRef(bugsRefPattern+bug.Id().String(), bug.lastCommit)
-	if err != nil {
-		return false, err
+	for _, op := range bug.Operations() {
+		op.Apply(&snap)
+		snap.Operations = append(snap.Operations, op)
 	}
 
-	return true, nil
-}
-
-// Id return the Bug identifier
-func (bug *Bug) Id() entity.Id {
-	// id is the id of the first operation
-	return bug.FirstOp().Id()
-}
-
-// CreateLamportTime return the Lamport time of creation
-func (bug *Bug) CreateLamportTime() lamport.Time {
-	return bug.createTime
-}
-
-// EditLamportTime return the Lamport time of the last edit
-func (bug *Bug) EditLamportTime() lamport.Time {
-	return bug.editTime
+	return snap
 }
 
 // Lookup for the very first operation of the bug.
 // For a valid Bug, this operation should be a CreateOp
 func (bug *Bug) FirstOp() Operation {
-	for _, pack := range bug.packs {
-		for _, op := range pack.Operations {
-			return op
-		}
+	if fo := bug.Entity.FirstOp(); fo != nil {
+		return fo.(Operation)
 	}
-
-	if !bug.staging.IsEmpty() {
-		return bug.staging.Operations[0]
-	}
-
 	return nil
 }
 
 // Lookup for the very last operation of the bug.
 // For a valid Bug, should never be nil
 func (bug *Bug) LastOp() Operation {
-	if !bug.staging.IsEmpty() {
-		return bug.staging.Operations[len(bug.staging.Operations)-1]
+	if lo := bug.Entity.LastOp(); lo != nil {
+		return lo.(Operation)
 	}
-
-	if len(bug.packs) == 0 {
-		return nil
-	}
-
-	lastPack := bug.packs[len(bug.packs)-1]
-
-	if len(lastPack.Operations) == 0 {
-		return nil
-	}
-
-	return lastPack.Operations[len(lastPack.Operations)-1]
-}
-
-// Compile a bug in a easily usable snapshot
-func (bug *Bug) Compile() Snapshot {
-	snap := Snapshot{
-		id:     bug.Id(),
-		Status: OpenStatus,
-	}
-
-	it := NewOperationIterator(bug)
-
-	for it.Next() {
-		op := it.Value()
-		op.Apply(&snap)
-		snap.Operations = append(snap.Operations, op)
-	}
-
-	return snap
+	return nil
 }

bug/bug_actions.go πŸ”—

@@ -1,12 +1,10 @@
 package bug
 
 import (
-	"fmt"
-	"strings"
-
 	"github.com/pkg/errors"
 
 	"github.com/MichaelMure/git-bug/entity"
+	"github.com/MichaelMure/git-bug/entity/dag"
 	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/repository"
 )
@@ -14,23 +12,23 @@ import (
 // Fetch retrieve updates from a remote
 // This does not change the local bugs state
 func Fetch(repo repository.Repo, remote string) (string, error) {
-	return repo.FetchRefs(remote, "bugs")
+	return dag.Fetch(def, repo, remote)
 }
 
 // Push update a remote with the local changes
 func Push(repo repository.Repo, remote string) (string, error) {
-	return repo.PushRefs(remote, "bugs")
+	return dag.Push(def, repo, remote)
 }
 
 // Pull will do a Fetch + MergeAll
 // This function will return an error if a merge fail
-func Pull(repo repository.ClockedRepo, remote string) error {
+func Pull(repo repository.ClockedRepo, remote string, author identity.Interface) error {
 	_, err := Fetch(repo, remote)
 	if err != nil {
 		return err
 	}
 
-	for merge := range MergeAll(repo, remote) {
+	for merge := range MergeAll(repo, remote, author) {
 		if merge.Err != nil {
 			return merge.Err
 		}
@@ -42,95 +40,19 @@ func Pull(repo repository.ClockedRepo, remote string) error {
 	return nil
 }
 
-// MergeAll will merge all the available remote bug:
-//
-// - If the remote has new commit, the local bug is updated to match the same history
-//   (fast-forward update)
-// - if the local bug has new commits but the remote don't, nothing is changed
-// - if both local and remote bug have new commits (that is, we have a concurrent edition),
-//   new local commits are rewritten at the head of the remote history (that is, a rebase)
-func MergeAll(repo repository.ClockedRepo, remote string) <-chan entity.MergeResult {
-	out := make(chan entity.MergeResult)
-
+// MergeAll will merge all the available remote bug
+// 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(repo repository.ClockedRepo, remote string, author identity.Interface) <-chan entity.MergeResult {
 	// no caching for the merge, we load everything from git even if that means multiple
 	// copy of the same entity in memory. The cache layer will intercept the results to
 	// invalidate entities if necessary.
 	identityResolver := identity.NewSimpleResolver(repo)
 
-	go func() {
-		defer close(out)
-
-		remoteRefSpec := fmt.Sprintf(bugsRemoteRefPattern, remote)
-		remoteRefs, err := repo.ListRefs(remoteRefSpec)
-		if err != nil {
-			out <- entity.MergeResult{Err: err}
-			return
-		}
-
-		for _, remoteRef := range remoteRefs {
-			refSplit := strings.Split(remoteRef, "/")
-			id := entity.Id(refSplit[len(refSplit)-1])
-
-			if err := id.Validate(); err != nil {
-				out <- entity.NewMergeInvalidStatus(id, errors.Wrap(err, "invalid ref").Error())
-				continue
-			}
-
-			remoteBug, err := read(repo, identityResolver, remoteRef)
-
-			if err != nil {
-				out <- entity.NewMergeInvalidStatus(id, errors.Wrap(err, "remote bug is not readable").Error())
-				continue
-			}
-
-			// Check for error in remote data
-			if err := remoteBug.Validate(); err != nil {
-				out <- entity.NewMergeInvalidStatus(id, errors.Wrap(err, "remote bug is invalid").Error())
-				continue
-			}
-
-			localRef := bugsRefPattern + remoteBug.Id().String()
-			localExist, err := repo.RefExist(localRef)
-
-			if err != nil {
-				out <- entity.NewMergeError(err, id)
-				continue
-			}
-
-			// the bug is not local yet, simply create the reference
-			if !localExist {
-				err := repo.CopyRef(remoteRef, localRef)
-
-				if err != nil {
-					out <- entity.NewMergeError(err, id)
-					return
-				}
-
-				out <- entity.NewMergeNewStatus(id, remoteBug)
-				continue
-			}
-
-			localBug, err := read(repo, identityResolver, localRef)
-
-			if err != nil {
-				out <- entity.NewMergeError(errors.Wrap(err, "local bug is not readable"), id)
-				return
-			}
-
-			updated, err := localBug.Merge(repo, remoteBug)
-
-			if err != nil {
-				out <- entity.NewMergeInvalidStatus(id, errors.Wrap(err, "merge failed").Error())
-				return
-			}
-
-			if updated {
-				out <- entity.NewMergeUpdatedStatus(id, localBug)
-			} else {
-				out <- entity.NewMergeNothingStatus(id)
-			}
-		}
-	}()
+	return dag.MergeAll(def, repo, identityResolver, remote, author)
+}
 
-	return out
+// RemoveBug will remove a local bug from its entity.Id
+func RemoveBug(repo repository.ClockedRepo, id entity.Id) error {
+	return dag.Remove(def, repo, id)
 }

bug/bug_actions_test.go πŸ”—

@@ -1,394 +0,0 @@
-package bug
-
-import (
-	"testing"
-	"time"
-
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-
-	"github.com/MichaelMure/git-bug/identity"
-	"github.com/MichaelMure/git-bug/repository"
-)
-
-func TestPushPull(t *testing.T) {
-	repoA, repoB, remote := repository.SetupGoGitReposAndRemote()
-	defer repository.CleanupTestRepos(repoA, repoB, remote)
-
-	reneA, err := identity.NewIdentity(repoA, "RenΓ© Descartes", "rene@descartes.fr")
-	require.NoError(t, err)
-	err = reneA.Commit(repoA)
-	require.NoError(t, err)
-
-	bug1, _, err := Create(reneA, time.Now().Unix(), "bug1", "message")
-	require.NoError(t, err)
-	assert.True(t, bug1.NeedCommit())
-	err = bug1.Commit(repoA)
-	require.NoError(t, err)
-	assert.False(t, bug1.NeedCommit())
-
-	// distribute the identity
-	_, err = identity.Push(repoA, "origin")
-	require.NoError(t, err)
-	err = identity.Pull(repoB, "origin")
-	require.NoError(t, err)
-
-	// A --> remote --> B
-	_, err = Push(repoA, "origin")
-	require.NoError(t, err)
-
-	err = Pull(repoB, "origin")
-	require.NoError(t, err)
-
-	bugs := allBugs(t, ReadAllLocal(repoB))
-
-	if len(bugs) != 1 {
-		t.Fatal("Unexpected number of bugs")
-	}
-
-	// B --> remote --> A
-	reneB, err := identity.ReadLocal(repoA, reneA.Id())
-	require.NoError(t, err)
-
-	bug2, _, err := Create(reneB, time.Now().Unix(), "bug2", "message")
-	require.NoError(t, err)
-	err = bug2.Commit(repoB)
-	require.NoError(t, err)
-
-	_, err = Push(repoB, "origin")
-	require.NoError(t, err)
-
-	err = Pull(repoA, "origin")
-	require.NoError(t, err)
-
-	bugs = allBugs(t, ReadAllLocal(repoA))
-
-	if len(bugs) != 2 {
-		t.Fatal("Unexpected number of bugs")
-	}
-}
-
-func allBugs(t testing.TB, bugs <-chan StreamedBug) []*Bug {
-	var result []*Bug
-	for streamed := range bugs {
-		if streamed.Err != nil {
-			t.Fatal(streamed.Err)
-		}
-		result = append(result, streamed.Bug)
-	}
-	return result
-}
-
-func TestRebaseTheirs(t *testing.T) {
-	_RebaseTheirs(t)
-}
-
-func BenchmarkRebaseTheirs(b *testing.B) {
-	for n := 0; n < b.N; n++ {
-		_RebaseTheirs(b)
-	}
-}
-
-func _RebaseTheirs(t testing.TB) {
-	repoA, repoB, remote := repository.SetupGoGitReposAndRemote()
-	defer repository.CleanupTestRepos(repoA, repoB, remote)
-
-	reneA, err := identity.NewIdentity(repoA, "RenΓ© Descartes", "rene@descartes.fr")
-	require.NoError(t, err)
-	err = reneA.Commit(repoA)
-	require.NoError(t, err)
-
-	bug1, _, err := Create(reneA, time.Now().Unix(), "bug1", "message")
-	require.NoError(t, err)
-	assert.True(t, bug1.NeedCommit())
-	err = bug1.Commit(repoA)
-	require.NoError(t, err)
-	assert.False(t, bug1.NeedCommit())
-
-	// distribute the identity
-	_, err = identity.Push(repoA, "origin")
-	require.NoError(t, err)
-	err = identity.Pull(repoB, "origin")
-	require.NoError(t, err)
-
-	// A --> remote
-
-	_, err = Push(repoA, "origin")
-	require.NoError(t, err)
-
-	// remote --> B
-	err = Pull(repoB, "origin")
-	require.NoError(t, err)
-
-	bug2, err := ReadLocal(repoB, bug1.Id())
-	require.NoError(t, err)
-	assert.False(t, bug2.NeedCommit())
-
-	reneB, err := identity.ReadLocal(repoA, reneA.Id())
-	require.NoError(t, err)
-
-	_, err = AddComment(bug2, reneB, time.Now().Unix(), "message2")
-	require.NoError(t, err)
-	assert.True(t, bug2.NeedCommit())
-	_, err = AddComment(bug2, reneB, time.Now().Unix(), "message3")
-	require.NoError(t, err)
-	_, err = AddComment(bug2, reneB, time.Now().Unix(), "message4")
-	require.NoError(t, err)
-	err = bug2.Commit(repoB)
-	require.NoError(t, err)
-	assert.False(t, bug2.NeedCommit())
-
-	// B --> remote
-	_, err = Push(repoB, "origin")
-	require.NoError(t, err)
-
-	// remote --> A
-	err = Pull(repoA, "origin")
-	require.NoError(t, err)
-
-	bugs := allBugs(t, ReadAllLocal(repoB))
-
-	if len(bugs) != 1 {
-		t.Fatal("Unexpected number of bugs")
-	}
-
-	bug3, err := ReadLocal(repoA, bug1.Id())
-	require.NoError(t, err)
-
-	if nbOps(bug3) != 4 {
-		t.Fatal("Unexpected number of operations")
-	}
-}
-
-func TestRebaseOurs(t *testing.T) {
-	_RebaseOurs(t)
-}
-
-func BenchmarkRebaseOurs(b *testing.B) {
-	for n := 0; n < b.N; n++ {
-		_RebaseOurs(b)
-	}
-}
-
-func _RebaseOurs(t testing.TB) {
-	repoA, repoB, remote := repository.SetupGoGitReposAndRemote()
-	defer repository.CleanupTestRepos(repoA, repoB, remote)
-
-	reneA, err := identity.NewIdentity(repoA, "RenΓ© Descartes", "rene@descartes.fr")
-	require.NoError(t, err)
-	err = reneA.Commit(repoA)
-	require.NoError(t, err)
-
-	bug1, _, err := Create(reneA, time.Now().Unix(), "bug1", "message")
-	require.NoError(t, err)
-	err = bug1.Commit(repoA)
-	require.NoError(t, err)
-
-	// distribute the identity
-	_, err = identity.Push(repoA, "origin")
-	require.NoError(t, err)
-	err = identity.Pull(repoB, "origin")
-	require.NoError(t, err)
-
-	// A --> remote
-	_, err = Push(repoA, "origin")
-	require.NoError(t, err)
-
-	// remote --> B
-	err = Pull(repoB, "origin")
-	require.NoError(t, err)
-
-	_, err = AddComment(bug1, reneA, time.Now().Unix(), "message2")
-	require.NoError(t, err)
-	_, err = AddComment(bug1, reneA, time.Now().Unix(), "message3")
-	require.NoError(t, err)
-	_, err = AddComment(bug1, reneA, time.Now().Unix(), "message4")
-	require.NoError(t, err)
-	err = bug1.Commit(repoA)
-	require.NoError(t, err)
-
-	_, err = AddComment(bug1, reneA, time.Now().Unix(), "message5")
-	require.NoError(t, err)
-	_, err = AddComment(bug1, reneA, time.Now().Unix(), "message6")
-	require.NoError(t, err)
-	_, err = AddComment(bug1, reneA, time.Now().Unix(), "message7")
-	require.NoError(t, err)
-	err = bug1.Commit(repoA)
-	require.NoError(t, err)
-
-	_, err = AddComment(bug1, reneA, time.Now().Unix(), "message8")
-	require.NoError(t, err)
-	_, err = AddComment(bug1, reneA, time.Now().Unix(), "message9")
-	require.NoError(t, err)
-	_, err = AddComment(bug1, reneA, time.Now().Unix(), "message10")
-	require.NoError(t, err)
-	err = bug1.Commit(repoA)
-	require.NoError(t, err)
-
-	// remote --> A
-	err = Pull(repoA, "origin")
-	require.NoError(t, err)
-
-	bugs := allBugs(t, ReadAllLocal(repoA))
-
-	if len(bugs) != 1 {
-		t.Fatal("Unexpected number of bugs")
-	}
-
-	bug2, err := ReadLocal(repoA, bug1.Id())
-	require.NoError(t, err)
-
-	if nbOps(bug2) != 10 {
-		t.Fatal("Unexpected number of operations")
-	}
-}
-
-func nbOps(b *Bug) int {
-	it := NewOperationIterator(b)
-	counter := 0
-	for it.Next() {
-		counter++
-	}
-	return counter
-}
-
-func TestRebaseConflict(t *testing.T) {
-	_RebaseConflict(t)
-}
-
-func BenchmarkRebaseConflict(b *testing.B) {
-	for n := 0; n < b.N; n++ {
-		_RebaseConflict(b)
-	}
-}
-
-func _RebaseConflict(t testing.TB) {
-	repoA, repoB, remote := repository.SetupGoGitReposAndRemote()
-	defer repository.CleanupTestRepos(repoA, repoB, remote)
-
-	reneA, err := identity.NewIdentity(repoA, "RenΓ© Descartes", "rene@descartes.fr")
-	require.NoError(t, err)
-	err = reneA.Commit(repoA)
-	require.NoError(t, err)
-
-	bug1, _, err := Create(reneA, time.Now().Unix(), "bug1", "message")
-	require.NoError(t, err)
-	err = bug1.Commit(repoA)
-	require.NoError(t, err)
-
-	// distribute the identity
-	_, err = identity.Push(repoA, "origin")
-	require.NoError(t, err)
-	err = identity.Pull(repoB, "origin")
-	require.NoError(t, err)
-
-	// A --> remote
-	_, err = Push(repoA, "origin")
-	require.NoError(t, err)
-
-	// remote --> B
-	err = Pull(repoB, "origin")
-	require.NoError(t, err)
-
-	_, err = AddComment(bug1, reneA, time.Now().Unix(), "message2")
-	require.NoError(t, err)
-	_, err = AddComment(bug1, reneA, time.Now().Unix(), "message3")
-	require.NoError(t, err)
-	_, err = AddComment(bug1, reneA, time.Now().Unix(), "message4")
-	require.NoError(t, err)
-	err = bug1.Commit(repoA)
-	require.NoError(t, err)
-
-	_, err = AddComment(bug1, reneA, time.Now().Unix(), "message5")
-	require.NoError(t, err)
-	_, err = AddComment(bug1, reneA, time.Now().Unix(), "message6")
-	require.NoError(t, err)
-	_, err = AddComment(bug1, reneA, time.Now().Unix(), "message7")
-	require.NoError(t, err)
-	err = bug1.Commit(repoA)
-	require.NoError(t, err)
-
-	_, err = AddComment(bug1, reneA, time.Now().Unix(), "message8")
-	require.NoError(t, err)
-	_, err = AddComment(bug1, reneA, time.Now().Unix(), "message9")
-	require.NoError(t, err)
-	_, err = AddComment(bug1, reneA, time.Now().Unix(), "message10")
-	require.NoError(t, err)
-	err = bug1.Commit(repoA)
-	require.NoError(t, err)
-
-	bug2, err := ReadLocal(repoB, bug1.Id())
-	require.NoError(t, err)
-
-	reneB, err := identity.ReadLocal(repoA, reneA.Id())
-	require.NoError(t, err)
-
-	_, err = AddComment(bug2, reneB, time.Now().Unix(), "message11")
-	require.NoError(t, err)
-	_, err = AddComment(bug2, reneB, time.Now().Unix(), "message12")
-	require.NoError(t, err)
-	_, err = AddComment(bug2, reneB, time.Now().Unix(), "message13")
-	require.NoError(t, err)
-	err = bug2.Commit(repoB)
-	require.NoError(t, err)
-
-	_, err = AddComment(bug2, reneB, time.Now().Unix(), "message14")
-	require.NoError(t, err)
-	_, err = AddComment(bug2, reneB, time.Now().Unix(), "message15")
-	require.NoError(t, err)
-	_, err = AddComment(bug2, reneB, time.Now().Unix(), "message16")
-	require.NoError(t, err)
-	err = bug2.Commit(repoB)
-	require.NoError(t, err)
-
-	_, err = AddComment(bug2, reneB, time.Now().Unix(), "message17")
-	require.NoError(t, err)
-	_, err = AddComment(bug2, reneB, time.Now().Unix(), "message18")
-	require.NoError(t, err)
-	_, err = AddComment(bug2, reneB, time.Now().Unix(), "message19")
-	require.NoError(t, err)
-	err = bug2.Commit(repoB)
-	require.NoError(t, err)
-
-	// A --> remote
-	_, err = Push(repoA, "origin")
-	require.NoError(t, err)
-
-	// remote --> B
-	err = Pull(repoB, "origin")
-	require.NoError(t, err)
-
-	bugs := allBugs(t, ReadAllLocal(repoB))
-
-	if len(bugs) != 1 {
-		t.Fatal("Unexpected number of bugs")
-	}
-
-	bug3, err := ReadLocal(repoB, bug1.Id())
-	require.NoError(t, err)
-
-	if nbOps(bug3) != 19 {
-		t.Fatal("Unexpected number of operations")
-	}
-
-	// B --> remote
-	_, err = Push(repoB, "origin")
-	require.NoError(t, err)
-
-	// remote --> A
-	err = Pull(repoA, "origin")
-	require.NoError(t, err)
-
-	bugs = allBugs(t, ReadAllLocal(repoA))
-
-	if len(bugs) != 1 {
-		t.Fatal("Unexpected number of bugs")
-	}
-
-	bug4, err := ReadLocal(repoA, bug1.Id())
-	require.NoError(t, err)
-
-	if nbOps(bug4) != 19 {
-		t.Fatal("Unexpected number of operations")
-	}
-}

bug/bug_test.go πŸ”—

@@ -1,190 +0,0 @@
-package bug
-
-import (
-	"fmt"
-	"testing"
-	"time"
-
-	"github.com/stretchr/testify/require"
-
-	"github.com/MichaelMure/git-bug/identity"
-	"github.com/MichaelMure/git-bug/repository"
-)
-
-func TestBugId(t *testing.T) {
-	repo := repository.NewMockRepo()
-
-	bug1 := NewBug()
-
-	rene, err := identity.NewIdentity(repo, "RenΓ© Descartes", "rene@descartes.fr")
-	require.NoError(t, err)
-	err = rene.Commit(repo)
-	require.NoError(t, err)
-
-	createOp := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil)
-
-	bug1.Append(createOp)
-
-	err = bug1.Commit(repo)
-
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	bug1.Id()
-}
-
-func TestBugValidity(t *testing.T) {
-	repo := repository.NewMockRepo()
-
-	bug1 := NewBug()
-
-	rene, err := identity.NewIdentity(repo, "RenΓ© Descartes", "rene@descartes.fr")
-	require.NoError(t, err)
-	err = rene.Commit(repo)
-	require.NoError(t, err)
-
-	createOp := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil)
-
-	if bug1.Validate() == nil {
-		t.Fatal("Empty bug should be invalid")
-	}
-
-	bug1.Append(createOp)
-
-	if bug1.Validate() != nil {
-		t.Fatal("Bug with just a CreateOp should be valid")
-	}
-
-	err = bug1.Commit(repo)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	bug1.Append(createOp)
-
-	if bug1.Validate() == nil {
-		t.Fatal("Bug with multiple CreateOp should be invalid")
-	}
-
-	err = bug1.Commit(repo)
-	if err == nil {
-		t.Fatal("Invalid bug should not commit")
-	}
-}
-
-func TestBugCommitLoad(t *testing.T) {
-	repo := repository.NewMockRepo()
-
-	bug1 := NewBug()
-
-	rene, err := identity.NewIdentity(repo, "RenΓ© Descartes", "rene@descartes.fr")
-	require.NoError(t, err)
-	err = rene.Commit(repo)
-	require.NoError(t, err)
-
-	createOp := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil)
-	setTitleOp := NewSetTitleOp(rene, time.Now().Unix(), "title2", "title1")
-	addCommentOp := NewAddCommentOp(rene, time.Now().Unix(), "message2", nil)
-
-	bug1.Append(createOp)
-	bug1.Append(setTitleOp)
-
-	require.True(t, bug1.NeedCommit())
-
-	err = bug1.Commit(repo)
-	require.Nil(t, err)
-	require.False(t, bug1.NeedCommit())
-
-	bug2, err := ReadLocal(repo, bug1.Id())
-	require.NoError(t, err)
-	equivalentBug(t, bug1, bug2)
-
-	// add more op
-
-	bug1.Append(addCommentOp)
-
-	require.True(t, bug1.NeedCommit())
-
-	err = bug1.Commit(repo)
-	require.Nil(t, err)
-	require.False(t, bug1.NeedCommit())
-
-	bug3, err := ReadLocal(repo, bug1.Id())
-	require.NoError(t, err)
-	equivalentBug(t, bug1, bug3)
-}
-
-func equivalentBug(t *testing.T, expected, actual *Bug) {
-	require.Equal(t, len(expected.packs), len(actual.packs))
-
-	for i := range expected.packs {
-		for j := range expected.packs[i].Operations {
-			actual.packs[i].Operations[j].base().id = expected.packs[i].Operations[j].base().id
-		}
-	}
-
-	require.Equal(t, expected, actual)
-}
-
-func TestBugRemove(t *testing.T) {
-	repo := repository.CreateGoGitTestRepo(false)
-	remoteA := repository.CreateGoGitTestRepo(true)
-	remoteB := repository.CreateGoGitTestRepo(true)
-	defer repository.CleanupTestRepos(repo, remoteA, remoteB)
-
-	err := repo.AddRemote("remoteA", remoteA.GetLocalRemote())
-	require.NoError(t, err)
-
-	err = repo.AddRemote("remoteB", remoteB.GetLocalRemote())
-	require.NoError(t, err)
-
-	// generate a bunch of bugs
-	rene, err := identity.NewIdentity(repo, "RenΓ© Descartes", "rene@descartes.fr")
-	require.NoError(t, err)
-	err = rene.Commit(repo)
-	require.NoError(t, err)
-
-	for i := 0; i < 100; i++ {
-		b := NewBug()
-		createOp := NewCreateOp(rene, time.Now().Unix(), "title", fmt.Sprintf("message%v", i), nil)
-		b.Append(createOp)
-		err = b.Commit(repo)
-		require.NoError(t, err)
-	}
-
-	// and one more for testing
-	b := NewBug()
-	createOp := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil)
-	b.Append(createOp)
-	err = b.Commit(repo)
-	require.NoError(t, err)
-
-	_, err = Push(repo, "remoteA")
-	require.NoError(t, err)
-
-	_, err = Push(repo, "remoteB")
-	require.NoError(t, err)
-
-	_, err = Fetch(repo, "remoteA")
-	require.NoError(t, err)
-
-	_, err = Fetch(repo, "remoteB")
-	require.NoError(t, err)
-
-	err = RemoveBug(repo, b.Id())
-	require.NoError(t, err)
-
-	_, err = ReadLocal(repo, b.Id())
-	require.Error(t, ErrBugNotExist, err)
-
-	_, err = ReadRemote(repo, "remoteA", b.Id())
-	require.Error(t, ErrBugNotExist, err)
-
-	_, err = ReadRemote(repo, "remoteB", b.Id())
-	require.Error(t, ErrBugNotExist, err)
-
-	ids, err := ListLocalIds(repo)
-	require.NoError(t, err)
-	require.Len(t, ids, 100)
-}

bug/clocks.go πŸ”—

@@ -1,40 +0,0 @@
-package bug
-
-import (
-	"github.com/MichaelMure/git-bug/identity"
-	"github.com/MichaelMure/git-bug/repository"
-)
-
-// ClockLoader is the repository.ClockLoader for the Bug entity
-var ClockLoader = repository.ClockLoader{
-	Clocks: []string{creationClockName, editClockName},
-	Witnesser: func(repo repository.ClockedRepo) error {
-		// We don't care about the actual identity so an IdentityStub will do
-		resolver := identity.NewStubResolver()
-		for b := range ReadAllLocalWithResolver(repo, resolver) {
-			if b.Err != nil {
-				return b.Err
-			}
-
-			createClock, err := repo.GetOrCreateClock(creationClockName)
-			if err != nil {
-				return err
-			}
-			err = createClock.Witness(b.Bug.createTime)
-			if err != nil {
-				return err
-			}
-
-			editClock, err := repo.GetOrCreateClock(editClockName)
-			if err != nil {
-				return err
-			}
-			err = editClock.Witness(b.Bug.editTime)
-			if err != nil {
-				return err
-			}
-		}
-
-		return nil
-	},
-}

bug/err.go πŸ”—

@@ -0,0 +1,17 @@
+package bug
+
+import (
+	"errors"
+
+	"github.com/MichaelMure/git-bug/entity"
+)
+
+var ErrBugNotExist = errors.New("bug doesn't exist")
+
+func NewErrMultipleMatchBug(matching []entity.Id) *entity.ErrMultipleMatch {
+	return entity.NewErrMultipleMatch("bug", matching)
+}
+
+func NewErrMultipleMatchOp(matching []entity.Id) *entity.ErrMultipleMatch {
+	return entity.NewErrMultipleMatch("operation", matching)
+}

bug/git_tree.go πŸ”—

@@ -1,84 +0,0 @@
-package bug
-
-import (
-	"fmt"
-	"strings"
-
-	"github.com/pkg/errors"
-
-	"github.com/MichaelMure/git-bug/repository"
-	"github.com/MichaelMure/git-bug/util/lamport"
-)
-
-type gitTree struct {
-	opsEntry   repository.TreeEntry
-	createTime lamport.Time
-	editTime   lamport.Time
-}
-
-func readTree(repo repository.RepoData, hash repository.Hash) (*gitTree, error) {
-	tree := &gitTree{}
-
-	entries, err := repo.ReadTree(hash)
-	if err != nil {
-		return nil, errors.Wrap(err, "can't list git tree entries")
-	}
-
-	opsFound := false
-
-	for _, entry := range entries {
-		if entry.Name == opsEntryName {
-			tree.opsEntry = entry
-			opsFound = true
-			continue
-		}
-		if strings.HasPrefix(entry.Name, createClockEntryPrefix) {
-			n, err := fmt.Sscanf(entry.Name, createClockEntryPattern, &tree.createTime)
-			if err != nil {
-				return nil, errors.Wrap(err, "can't read create lamport time")
-			}
-			if n != 1 {
-				return nil, fmt.Errorf("could not parse create time lamport value")
-			}
-		}
-		if strings.HasPrefix(entry.Name, editClockEntryPrefix) {
-			n, err := fmt.Sscanf(entry.Name, editClockEntryPattern, &tree.editTime)
-			if err != nil {
-				return nil, errors.Wrap(err, "can't read edit lamport time")
-			}
-			if n != 1 {
-				return nil, fmt.Errorf("could not parse edit time lamport value")
-			}
-		}
-	}
-
-	if !opsFound {
-		return nil, errors.New("invalid tree, missing the ops entry")
-	}
-
-	return tree, nil
-}
-
-func makeMediaTree(pack OperationPack) []repository.TreeEntry {
-	var tree []repository.TreeEntry
-	counter := 0
-	added := make(map[repository.Hash]interface{})
-
-	for _, ops := range pack.Operations {
-		for _, file := range ops.GetFiles() {
-			if _, has := added[file]; !has {
-				tree = append(tree, repository.TreeEntry{
-					ObjectType: repository.Blob,
-					Hash:       file,
-					// The name is not important here, we only need to
-					// reference the blob.
-					Name: fmt.Sprintf("file%d", counter),
-				})
-				counter++
-				added[file] = struct{}{}
-			}
-		}
-	}
-
-	return tree
-}

bug/identity.go πŸ”—

@@ -1,27 +0,0 @@
-package bug
-
-import (
-	"github.com/MichaelMure/git-bug/identity"
-)
-
-// EnsureIdentities walk the graph of operations and make sure that all Identity
-// are properly loaded. That is, it replace all the IdentityStub with the full
-// Identity, loaded through a Resolver.
-func (bug *Bug) EnsureIdentities(resolver identity.Resolver) error {
-	it := NewOperationIterator(bug)
-
-	for it.Next() {
-		op := it.Value()
-		base := op.base()
-
-		if stub, ok := base.Author.(*identity.IdentityStub); ok {
-			i, err := resolver.ResolveIdentity(stub.Id())
-			if err != nil {
-				return err
-			}
-
-			base.Author = i
-		}
-	}
-	return nil
-}

bug/interface.go πŸ”—

@@ -16,17 +16,15 @@ type Interface interface {
 	// Append an operation into the staging area, to be committed later
 	Append(op Operation)
 
+	// Operations return the ordered operations
+	Operations() []Operation
+
 	// Indicate that the in-memory state changed and need to be commit in the repository
 	NeedCommit() bool
 
 	// Commit write the staging area in Git and move the operations to the packs
 	Commit(repo repository.ClockedRepo) error
 
-	// Merge a different version of the same bug by rebasing operations of this bug
-	// that are not present in the other on top of the chain of operations of the
-	// other version.
-	Merge(repo repository.Repo, other Interface) (bool, error)
-
 	// Lookup for the very first operation of the bug.
 	// For a valid Bug, this operation should be a CreateOp
 	FirstOp() Operation

bug/op_add_comment.go πŸ”—

@@ -24,23 +24,19 @@ type AddCommentOperation struct {
 // Sign-post method for gqlgen
 func (op *AddCommentOperation) IsOperation() {}
 
-func (op *AddCommentOperation) base() *OpBase {
-	return &op.OpBase
-}
-
 func (op *AddCommentOperation) Id() entity.Id {
-	return idOperation(op)
+	return idOperation(op, &op.OpBase)
 }
 
 func (op *AddCommentOperation) Apply(snapshot *Snapshot) {
-	snapshot.addActor(op.Author)
-	snapshot.addParticipant(op.Author)
+	snapshot.addActor(op.Author_)
+	snapshot.addParticipant(op.Author_)
 
 	commentId := entity.CombineIds(snapshot.Id(), op.Id())
 	comment := Comment{
 		id:       commentId,
 		Message:  op.Message,
-		Author:   op.Author,
+		Author:   op.Author_,
 		Files:    op.Files,
 		UnixTime: timestamp.Timestamp(op.UnixTime),
 	}
@@ -59,7 +55,7 @@ func (op *AddCommentOperation) GetFiles() []repository.Hash {
 }
 
 func (op *AddCommentOperation) Validate() error {
-	if err := opBaseValidate(op, AddCommentOp); err != nil {
+	if err := op.OpBase.Validate(op, AddCommentOp); err != nil {
 		return err
 	}
 

bug/op_add_comment_test.go πŸ”—

@@ -32,8 +32,8 @@ func TestAddCommentSerialize(t *testing.T) {
 	before.Id()
 
 	// Replace the identity stub with the real thing
-	assert.Equal(t, rene.Id(), after.base().Author.Id())
-	after.Author = rene
+	assert.Equal(t, rene.Id(), after.Author().Id())
+	after.Author_ = rene
 
 	assert.Equal(t, before, &after)
 }

bug/op_create.go πŸ”—

@@ -30,12 +30,8 @@ type CreateOperation struct {
 // Sign-post method for gqlgen
 func (op *CreateOperation) IsOperation() {}
 
-func (op *CreateOperation) base() *OpBase {
-	return &op.OpBase
-}
-
 func (op *CreateOperation) Id() entity.Id {
-	return idOperation(op)
+	return idOperation(op, &op.OpBase)
 }
 
 // OVERRIDE
@@ -61,8 +57,8 @@ func (op *CreateOperation) Apply(snapshot *Snapshot) {
 
 	snapshot.id = op.Id()
 
-	snapshot.addActor(op.Author)
-	snapshot.addParticipant(op.Author)
+	snapshot.addActor(op.Author_)
+	snapshot.addParticipant(op.Author_)
 
 	snapshot.Title = op.Title
 
@@ -70,12 +66,12 @@ func (op *CreateOperation) Apply(snapshot *Snapshot) {
 	comment := Comment{
 		id:       commentId,
 		Message:  op.Message,
-		Author:   op.Author,
+		Author:   op.Author_,
 		UnixTime: timestamp.Timestamp(op.UnixTime),
 	}
 
 	snapshot.Comments = []Comment{comment}
-	snapshot.Author = op.Author
+	snapshot.Author = op.Author_
 	snapshot.CreateTime = op.Time()
 
 	snapshot.Timeline = []TimelineItem{
@@ -90,7 +86,7 @@ func (op *CreateOperation) GetFiles() []repository.Hash {
 }
 
 func (op *CreateOperation) Validate() error {
-	if err := opBaseValidate(op, CreateOp); err != nil {
+	if err := op.OpBase.Validate(op, CreateOp); err != nil {
 		return err
 	}
 

bug/op_create_test.go πŸ”—

@@ -79,8 +79,8 @@ func TestCreateSerialize(t *testing.T) {
 	before.Id()
 
 	// Replace the identity stub with the real thing
-	require.Equal(t, rene.Id(), after.base().Author.Id())
-	after.Author = rene
+	require.Equal(t, rene.Id(), after.Author().Id())
+	after.Author_ = rene
 
 	require.Equal(t, before, &after)
 }

bug/op_edit_comment.go πŸ”—

@@ -27,19 +27,15 @@ type EditCommentOperation struct {
 // Sign-post method for gqlgen
 func (op *EditCommentOperation) IsOperation() {}
 
-func (op *EditCommentOperation) base() *OpBase {
-	return &op.OpBase
-}
-
 func (op *EditCommentOperation) Id() entity.Id {
-	return idOperation(op)
+	return idOperation(op, &op.OpBase)
 }
 
 func (op *EditCommentOperation) Apply(snapshot *Snapshot) {
 	// Todo: currently any message can be edited, even by a different author
 	// crypto signature are needed.
 
-	snapshot.addActor(op.Author)
+	snapshot.addActor(op.Author_)
 
 	var target TimelineItem
 
@@ -85,7 +81,7 @@ func (op *EditCommentOperation) GetFiles() []repository.Hash {
 }
 
 func (op *EditCommentOperation) Validate() error {
-	if err := opBaseValidate(op, EditCommentOp); err != nil {
+	if err := op.OpBase.Validate(op, EditCommentOp); err != nil {
 		return err
 	}
 

bug/op_edit_comment_test.go πŸ”—

@@ -97,8 +97,8 @@ func TestEditCommentSerialize(t *testing.T) {
 	before.Id()
 
 	// Replace the identity stub with the real thing
-	require.Equal(t, rene.Id(), after.base().Author.Id())
-	after.Author = rene
+	require.Equal(t, rene.Id(), after.Author().Id())
+	after.Author_ = rene
 
 	require.Equal(t, before, &after)
 }

bug/op_label_change.go πŸ”—

@@ -24,17 +24,13 @@ type LabelChangeOperation struct {
 // Sign-post method for gqlgen
 func (op *LabelChangeOperation) IsOperation() {}
 
-func (op *LabelChangeOperation) base() *OpBase {
-	return &op.OpBase
-}
-
 func (op *LabelChangeOperation) Id() entity.Id {
-	return idOperation(op)
+	return idOperation(op, &op.OpBase)
 }
 
 // Apply apply the operation
 func (op *LabelChangeOperation) Apply(snapshot *Snapshot) {
-	snapshot.addActor(op.Author)
+	snapshot.addActor(op.Author_)
 
 	// Add in the set
 AddLoop:
@@ -66,7 +62,7 @@ AddLoop:
 
 	item := &LabelChangeTimelineItem{
 		id:       op.Id(),
-		Author:   op.Author,
+		Author:   op.Author_,
 		UnixTime: timestamp.Timestamp(op.UnixTime),
 		Added:    op.Added,
 		Removed:  op.Removed,
@@ -76,7 +72,7 @@ AddLoop:
 }
 
 func (op *LabelChangeOperation) Validate() error {
-	if err := opBaseValidate(op, LabelChangeOp); err != nil {
+	if err := op.OpBase.Validate(op, LabelChangeOp); err != nil {
 		return err
 	}
 

bug/op_label_change_test.go πŸ”—

@@ -31,8 +31,8 @@ func TestLabelChangeSerialize(t *testing.T) {
 	before.Id()
 
 	// Replace the identity stub with the real thing
-	require.Equal(t, rene.Id(), after.base().Author.Id())
-	after.Author = rene
+	require.Equal(t, rene.Id(), after.Author().Id())
+	after.Author_ = rene
 
 	require.Equal(t, before, &after)
 }

bug/op_noop.go πŸ”—

@@ -19,12 +19,8 @@ type NoOpOperation struct {
 // Sign-post method for gqlgen
 func (op *NoOpOperation) IsOperation() {}
 
-func (op *NoOpOperation) base() *OpBase {
-	return &op.OpBase
-}
-
 func (op *NoOpOperation) Id() entity.Id {
-	return idOperation(op)
+	return idOperation(op, &op.OpBase)
 }
 
 func (op *NoOpOperation) Apply(snapshot *Snapshot) {
@@ -32,7 +28,7 @@ func (op *NoOpOperation) Apply(snapshot *Snapshot) {
 }
 
 func (op *NoOpOperation) Validate() error {
-	return opBaseValidate(op, NoOpOp)
+	return op.OpBase.Validate(op, NoOpOp)
 }
 
 // UnmarshalJSON is a two step JSON unmarshaling

bug/op_noop_test.go πŸ”—

@@ -33,8 +33,8 @@ func TestNoopSerialize(t *testing.T) {
 	before.Id()
 
 	// Replace the identity stub with the real thing
-	assert.Equal(t, rene.Id(), after.base().Author.Id())
-	after.Author = rene
+	assert.Equal(t, rene.Id(), after.Author().Id())
+	after.Author_ = rene
 
 	assert.Equal(t, before, &after)
 }

bug/op_set_metadata.go πŸ”—

@@ -20,38 +20,25 @@ type SetMetadataOperation struct {
 // Sign-post method for gqlgen
 func (op *SetMetadataOperation) IsOperation() {}
 
-func (op *SetMetadataOperation) base() *OpBase {
-	return &op.OpBase
-}
-
 func (op *SetMetadataOperation) Id() entity.Id {
-	return idOperation(op)
+	return idOperation(op, &op.OpBase)
 }
 
 func (op *SetMetadataOperation) Apply(snapshot *Snapshot) {
 	for _, target := range snapshot.Operations {
 		if target.Id() == op.Target {
-			base := target.base()
-
-			if base.extraMetadata == nil {
-				base.extraMetadata = make(map[string]string)
-			}
-
 			// Apply the metadata in an immutable way: if a metadata already
 			// exist, it's not possible to override it.
-			for key, val := range op.NewMetadata {
-				if _, exist := base.extraMetadata[key]; !exist {
-					base.extraMetadata[key] = val
-				}
+			for key, value := range op.NewMetadata {
+				target.setExtraMetadataImmutable(key, value)
 			}
-
 			return
 		}
 	}
 }
 
 func (op *SetMetadataOperation) Validate() error {
-	if err := opBaseValidate(op, SetMetadataOp); err != nil {
+	if err := op.OpBase.Validate(op, SetMetadataOp); err != nil {
 		return err
 	}
 

bug/op_set_metadata_test.go πŸ”—

@@ -120,8 +120,8 @@ func TestSetMetadataSerialize(t *testing.T) {
 	before.Id()
 
 	// Replace the identity stub with the real thing
-	require.Equal(t, rene.Id(), after.base().Author.Id())
-	after.Author = rene
+	require.Equal(t, rene.Id(), after.Author().Id())
+	after.Author_ = rene
 
 	require.Equal(t, before, &after)
 }

bug/op_set_status.go πŸ”—

@@ -21,21 +21,17 @@ type SetStatusOperation struct {
 // Sign-post method for gqlgen
 func (op *SetStatusOperation) IsOperation() {}
 
-func (op *SetStatusOperation) base() *OpBase {
-	return &op.OpBase
-}
-
 func (op *SetStatusOperation) Id() entity.Id {
-	return idOperation(op)
+	return idOperation(op, &op.OpBase)
 }
 
 func (op *SetStatusOperation) Apply(snapshot *Snapshot) {
 	snapshot.Status = op.Status
-	snapshot.addActor(op.Author)
+	snapshot.addActor(op.Author_)
 
 	item := &SetStatusTimelineItem{
 		id:       op.Id(),
-		Author:   op.Author,
+		Author:   op.Author_,
 		UnixTime: timestamp.Timestamp(op.UnixTime),
 		Status:   op.Status,
 	}
@@ -44,7 +40,7 @@ func (op *SetStatusOperation) Apply(snapshot *Snapshot) {
 }
 
 func (op *SetStatusOperation) Validate() error {
-	if err := opBaseValidate(op, SetStatusOp); err != nil {
+	if err := op.OpBase.Validate(op, SetStatusOp); err != nil {
 		return err
 	}
 

bug/op_set_status_test.go πŸ”—

@@ -31,8 +31,8 @@ func TestSetStatusSerialize(t *testing.T) {
 	before.Id()
 
 	// Replace the identity stub with the real thing
-	require.Equal(t, rene.Id(), after.base().Author.Id())
-	after.Author = rene
+	require.Equal(t, rene.Id(), after.Author().Id())
+	after.Author_ = rene
 
 	require.Equal(t, before, &after)
 }

bug/op_set_title.go πŸ”—

@@ -24,21 +24,17 @@ type SetTitleOperation struct {
 // Sign-post method for gqlgen
 func (op *SetTitleOperation) IsOperation() {}
 
-func (op *SetTitleOperation) base() *OpBase {
-	return &op.OpBase
-}
-
 func (op *SetTitleOperation) Id() entity.Id {
-	return idOperation(op)
+	return idOperation(op, &op.OpBase)
 }
 
 func (op *SetTitleOperation) Apply(snapshot *Snapshot) {
 	snapshot.Title = op.Title
-	snapshot.addActor(op.Author)
+	snapshot.addActor(op.Author_)
 
 	item := &SetTitleTimelineItem{
 		id:       op.Id(),
-		Author:   op.Author,
+		Author:   op.Author_,
 		UnixTime: timestamp.Timestamp(op.UnixTime),
 		Title:    op.Title,
 		Was:      op.Was,
@@ -48,7 +44,7 @@ func (op *SetTitleOperation) Apply(snapshot *Snapshot) {
 }
 
 func (op *SetTitleOperation) Validate() error {
-	if err := opBaseValidate(op, SetTitleOp); err != nil {
+	if err := op.OpBase.Validate(op, SetTitleOp); err != nil {
 		return err
 	}
 
@@ -132,19 +128,17 @@ func (s *SetTitleTimelineItem) IsAuthored() {}
 
 // Convenience function to apply the operation
 func SetTitle(b Interface, author identity.Interface, unixTime int64, title string) (*SetTitleOperation, error) {
-	it := NewOperationIterator(b)
-
-	var lastTitleOp Operation
-	for it.Next() {
-		op := it.Value()
-		if op.base().OperationType == SetTitleOp {
+	var lastTitleOp *SetTitleOperation
+	for _, op := range b.Operations() {
+		switch op := op.(type) {
+		case *SetTitleOperation:
 			lastTitleOp = op
 		}
 	}
 
 	var was string
 	if lastTitleOp != nil {
-		was = lastTitleOp.(*SetTitleOperation).Title
+		was = lastTitleOp.Title
 	} else {
 		was = b.FirstOp().(*CreateOperation).Title
 	}

bug/op_set_title_test.go πŸ”—

@@ -31,8 +31,8 @@ func TestSetTitleSerialize(t *testing.T) {
 	before.Id()
 
 	// Replace the identity stub with the real thing
-	require.Equal(t, rene.Id(), after.base().Author.Id())
-	after.Author = rene
+	require.Equal(t, rene.Id(), after.Author().Id())
+	after.Author_ = rene
 
 	require.Equal(t, before, &after)
 }

bug/operation.go πŸ”—

@@ -8,6 +8,7 @@ import (
 	"github.com/pkg/errors"
 
 	"github.com/MichaelMure/git-bug/entity"
+	"github.com/MichaelMure/git-bug/entity/dag"
 	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/repository"
 )
@@ -29,34 +30,31 @@ const (
 
 // Operation define the interface to fulfill for an edit operation of a Bug
 type Operation interface {
-	// base return the OpBase of the Operation, for package internal use
-	base() *OpBase
-	// Id return the identifier of the operation, to be used for back references
-	Id() entity.Id
+	dag.Operation
+
+	// Type return the type of the operation
+	Type() OperationType
+
 	// Time return the time when the operation was added
 	Time() time.Time
 	// GetFiles return the files needed by this operation
 	GetFiles() []repository.Hash
 	// Apply the operation to a Snapshot to create the final state
 	Apply(snapshot *Snapshot)
-	// Validate check if the operation is valid (ex: a title is a single line)
-	Validate() error
 	// SetMetadata store arbitrary metadata about the operation
 	SetMetadata(key string, value string)
 	// GetMetadata retrieve arbitrary metadata about the operation
 	GetMetadata(key string) (string, bool)
 	// AllMetadata return all metadata for this operation
 	AllMetadata() map[string]string
-	// GetAuthor return the author identity
-	GetAuthor() identity.Interface
+
+	setExtraMetadataImmutable(key string, value string)
 
 	// sign-post method for gqlgen
 	IsOperation()
 }
 
-func idOperation(op Operation) entity.Id {
-	base := op.base()
-
+func idOperation(op Operation, base *OpBase) entity.Id {
 	if base.id == "" {
 		// something went really wrong
 		panic("op's id not set")
@@ -77,10 +75,69 @@ func idOperation(op Operation) entity.Id {
 	return base.id
 }
 
+func operationUnmarshaller(author identity.Interface, raw json.RawMessage) (dag.Operation, error) {
+	var t struct {
+		OperationType OperationType `json:"type"`
+	}
+
+	if err := json.Unmarshal(raw, &t); err != nil {
+		return nil, err
+	}
+
+	var op Operation
+
+	switch t.OperationType {
+	case AddCommentOp:
+		op = &AddCommentOperation{}
+	case CreateOp:
+		op = &CreateOperation{}
+	case EditCommentOp:
+		op = &EditCommentOperation{}
+	case LabelChangeOp:
+		op = &LabelChangeOperation{}
+	case NoOpOp:
+		op = &NoOpOperation{}
+	case SetMetadataOp:
+		op = &SetMetadataOperation{}
+	case SetStatusOp:
+		op = &SetStatusOperation{}
+	case SetTitleOp:
+		op = &SetTitleOperation{}
+	default:
+		panic(fmt.Sprintf("unknown operation type %v", t.OperationType))
+	}
+
+	err := json.Unmarshal(raw, &op)
+	if err != nil {
+		return nil, err
+	}
+
+	switch op := op.(type) {
+	case *AddCommentOperation:
+		op.Author_ = author
+	case *CreateOperation:
+		op.Author_ = author
+	case *LabelChangeOperation:
+		op.Author_ = author
+	case *NoOpOperation:
+		op.Author_ = author
+	case *SetMetadataOperation:
+		op.Author_ = author
+	case *SetStatusOperation:
+		op.Author_ = author
+	case *SetTitleOperation:
+		op.Author_ = author
+	default:
+		panic(fmt.Sprintf("unknown operation type %T", op))
+	}
+
+	return op, nil
+}
+
 // OpBase implement the common code for all operations
 type OpBase struct {
 	OperationType OperationType      `json:"type"`
-	Author        identity.Interface `json:"author"`
+	Author_       identity.Interface `json:"author"`
 	// TODO: part of the data model upgrade, this should eventually be a timestamp + lamport
 	UnixTime int64             `json:"timestamp"`
 	Metadata map[string]string `json:"metadata,omitempty"`
@@ -95,15 +152,15 @@ type OpBase struct {
 func newOpBase(opType OperationType, author identity.Interface, unixTime int64) OpBase {
 	return OpBase{
 		OperationType: opType,
-		Author:        author,
+		Author_:       author,
 		UnixTime:      unixTime,
 		id:            entity.UnsetId,
 	}
 }
 
-func (op *OpBase) UnmarshalJSON(data []byte) error {
+func (base *OpBase) UnmarshalJSON(data []byte) error {
 	// Compute the Id when loading the op from disk.
-	op.id = entity.DeriveId(data)
+	base.id = entity.DeriveId(data)
 
 	aux := struct {
 		OperationType OperationType     `json:"type"`
@@ -122,39 +179,43 @@ func (op *OpBase) UnmarshalJSON(data []byte) error {
 		return err
 	}
 
-	op.OperationType = aux.OperationType
-	op.Author = author
-	op.UnixTime = aux.UnixTime
-	op.Metadata = aux.Metadata
+	base.OperationType = aux.OperationType
+	base.Author_ = author
+	base.UnixTime = aux.UnixTime
+	base.Metadata = aux.Metadata
 
 	return nil
 }
 
+func (base *OpBase) Type() OperationType {
+	return base.OperationType
+}
+
 // Time return the time when the operation was added
-func (op *OpBase) Time() time.Time {
-	return time.Unix(op.UnixTime, 0)
+func (base *OpBase) Time() time.Time {
+	return time.Unix(base.UnixTime, 0)
 }
 
 // GetFiles return the files needed by this operation
-func (op *OpBase) GetFiles() []repository.Hash {
+func (base *OpBase) GetFiles() []repository.Hash {
 	return nil
 }
 
 // Validate check the OpBase for errors
-func opBaseValidate(op Operation, opType OperationType) error {
-	if op.base().OperationType != opType {
-		return fmt.Errorf("incorrect operation type (expected: %v, actual: %v)", opType, op.base().OperationType)
+func (base *OpBase) Validate(op Operation, opType OperationType) error {
+	if base.OperationType != opType {
+		return fmt.Errorf("incorrect operation type (expected: %v, actual: %v)", opType, base.OperationType)
 	}
 
 	if op.Time().Unix() == 0 {
 		return fmt.Errorf("time not set")
 	}
 
-	if op.base().Author == nil {
+	if base.Author_ == nil {
 		return fmt.Errorf("author not set")
 	}
 
-	if err := op.base().Author.Validate(); err != nil {
+	if err := op.Author().Validate(); err != nil {
 		return errors.Wrap(err, "author")
 	}
 
@@ -168,46 +229,55 @@ func opBaseValidate(op Operation, opType OperationType) error {
 }
 
 // SetMetadata store arbitrary metadata about the operation
-func (op *OpBase) SetMetadata(key string, value string) {
-	if op.Metadata == nil {
-		op.Metadata = make(map[string]string)
+func (base *OpBase) SetMetadata(key string, value string) {
+	if base.Metadata == nil {
+		base.Metadata = make(map[string]string)
 	}
 
-	op.Metadata[key] = value
-	op.id = entity.UnsetId
+	base.Metadata[key] = value
+	base.id = entity.UnsetId
 }
 
 // GetMetadata retrieve arbitrary metadata about the operation
-func (op *OpBase) GetMetadata(key string) (string, bool) {
-	val, ok := op.Metadata[key]
+func (base *OpBase) GetMetadata(key string) (string, bool) {
+	val, ok := base.Metadata[key]
 
 	if ok {
 		return val, true
 	}
 
 	// extraMetadata can't replace the original operations value if any
-	val, ok = op.extraMetadata[key]
+	val, ok = base.extraMetadata[key]
 
 	return val, ok
 }
 
 // AllMetadata return all metadata for this operation
-func (op *OpBase) AllMetadata() map[string]string {
+func (base *OpBase) AllMetadata() map[string]string {
 	result := make(map[string]string)
 
-	for key, val := range op.extraMetadata {
+	for key, val := range base.extraMetadata {
 		result[key] = val
 	}
 
 	// Original metadata take precedence
-	for key, val := range op.Metadata {
+	for key, val := range base.Metadata {
 		result[key] = val
 	}
 
 	return result
 }
 
-// GetAuthor return author identity
-func (op *OpBase) GetAuthor() identity.Interface {
-	return op.Author
+func (base *OpBase) setExtraMetadataImmutable(key string, value string) {
+	if base.extraMetadata == nil {
+		base.extraMetadata = make(map[string]string)
+	}
+	if _, exist := base.extraMetadata[key]; !exist {
+		base.extraMetadata[key] = value
+	}
+}
+
+// Author return author identity
+func (base *OpBase) Author() identity.Interface {
+	return base.Author_
 }

bug/operation_iterator.go πŸ”—

@@ -1,72 +0,0 @@
-package bug
-
-type OperationIterator struct {
-	bug       *Bug
-	packIndex int
-	opIndex   int
-}
-
-func NewOperationIterator(bug Interface) *OperationIterator {
-	return &OperationIterator{
-		bug:       bugFromInterface(bug),
-		packIndex: 0,
-		opIndex:   -1,
-	}
-}
-
-func (it *OperationIterator) Next() bool {
-	// Special case of the staging area
-	if it.packIndex == len(it.bug.packs) {
-		pack := it.bug.staging
-		it.opIndex++
-		return it.opIndex < len(pack.Operations)
-	}
-
-	if it.packIndex >= len(it.bug.packs) {
-		return false
-	}
-
-	pack := it.bug.packs[it.packIndex]
-
-	it.opIndex++
-
-	if it.opIndex < len(pack.Operations) {
-		return true
-	}
-
-	// Note: this iterator doesn't handle the empty pack case
-	it.opIndex = 0
-	it.packIndex++
-
-	// Special case of the non-empty staging area
-	if it.packIndex == len(it.bug.packs) && len(it.bug.staging.Operations) > 0 {
-		return true
-	}
-
-	return it.packIndex < len(it.bug.packs)
-}
-
-func (it *OperationIterator) Value() Operation {
-	// Special case of the staging area
-	if it.packIndex == len(it.bug.packs) {
-		pack := it.bug.staging
-
-		if it.opIndex >= len(pack.Operations) {
-			panic("Iterator is not valid anymore")
-		}
-
-		return pack.Operations[it.opIndex]
-	}
-
-	if it.packIndex >= len(it.bug.packs) {
-		panic("Iterator is not valid anymore")
-	}
-
-	pack := it.bug.packs[it.packIndex]
-
-	if it.opIndex >= len(pack.Operations) {
-		panic("Iterator is not valid anymore")
-	}
-
-	return pack.Operations[it.opIndex]
-}

bug/operation_iterator_test.go πŸ”—

@@ -1,79 +0,0 @@
-package bug
-
-import (
-	"fmt"
-	"testing"
-	"time"
-
-	"github.com/stretchr/testify/require"
-
-	"github.com/MichaelMure/git-bug/identity"
-	"github.com/MichaelMure/git-bug/repository"
-)
-
-func ExampleOperationIterator() {
-	b := NewBug()
-
-	// add operations
-
-	it := NewOperationIterator(b)
-
-	for it.Next() {
-		// do something with each operations
-		_ = it.Value()
-	}
-}
-
-func TestOpIterator(t *testing.T) {
-	repo := repository.NewMockRepo()
-
-	rene, err := identity.NewIdentity(repo, "RenΓ© Descartes", "rene@descartes.fr")
-	require.NoError(t, err)
-	err = rene.Commit(repo)
-	require.NoError(t, err)
-
-	unix := time.Now().Unix()
-
-	createOp := NewCreateOp(rene, unix, "title", "message", nil)
-	addCommentOp := NewAddCommentOp(rene, unix, "message2", nil)
-	setStatusOp := NewSetStatusOp(rene, unix, ClosedStatus)
-	labelChangeOp := NewLabelChangeOperation(rene, unix, []Label{"added"}, []Label{"removed"})
-
-	var i int
-	genTitleOp := func() Operation {
-		i++
-		return NewSetTitleOp(rene, unix, fmt.Sprintf("title%d", i), "")
-	}
-
-	bug1 := NewBug()
-
-	// first pack
-	bug1.Append(createOp)
-	bug1.Append(addCommentOp)
-	bug1.Append(setStatusOp)
-	bug1.Append(labelChangeOp)
-	err = bug1.Commit(repo)
-	require.NoError(t, err)
-
-	// second pack
-	bug1.Append(genTitleOp())
-	bug1.Append(genTitleOp())
-	bug1.Append(genTitleOp())
-	err = bug1.Commit(repo)
-	require.NoError(t, err)
-
-	// staging
-	bug1.Append(genTitleOp())
-	bug1.Append(genTitleOp())
-	bug1.Append(genTitleOp())
-
-	it := NewOperationIterator(bug1)
-
-	counter := 0
-	for it.Next() {
-		_ = it.Value()
-		counter++
-	}
-
-	require.Equal(t, 10, counter)
-}

bug/operation_pack.go πŸ”—

@@ -1,187 +0,0 @@
-package bug
-
-import (
-	"encoding/json"
-	"fmt"
-
-	"github.com/pkg/errors"
-
-	"github.com/MichaelMure/git-bug/entity"
-	"github.com/MichaelMure/git-bug/repository"
-)
-
-// 1: original format
-// 2: no more legacy identities
-// 3: Ids are generated from the create operation serialized data instead of from the first git commit
-const formatVersion = 3
-
-// OperationPack represent an ordered set of operation to apply
-// to a Bug. These operations are stored in a single Git commit.
-//
-// These commits will be linked together in a linear chain of commits
-// inside Git to form the complete ordered chain of operation to
-// apply to get the final state of the Bug
-type OperationPack struct {
-	Operations []Operation
-
-	// Private field so not serialized
-	commitHash repository.Hash
-}
-
-func (opp *OperationPack) MarshalJSON() ([]byte, error) {
-	return json.Marshal(struct {
-		Version    uint        `json:"version"`
-		Operations []Operation `json:"ops"`
-	}{
-		Version:    formatVersion,
-		Operations: opp.Operations,
-	})
-}
-
-func (opp *OperationPack) UnmarshalJSON(data []byte) error {
-	aux := struct {
-		Version    uint              `json:"version"`
-		Operations []json.RawMessage `json:"ops"`
-	}{}
-
-	if err := json.Unmarshal(data, &aux); err != nil {
-		return err
-	}
-
-	if aux.Version < formatVersion {
-		return entity.NewErrOldFormatVersion(aux.Version)
-	}
-	if aux.Version > formatVersion {
-		return entity.NewErrNewFormatVersion(aux.Version)
-	}
-
-	for _, raw := range aux.Operations {
-		var t struct {
-			OperationType OperationType `json:"type"`
-		}
-
-		if err := json.Unmarshal(raw, &t); err != nil {
-			return err
-		}
-
-		// delegate to specialized unmarshal function
-		op, err := opp.unmarshalOp(raw, t.OperationType)
-		if err != nil {
-			return err
-		}
-
-		opp.Operations = append(opp.Operations, op)
-	}
-
-	return nil
-}
-
-func (opp *OperationPack) unmarshalOp(raw []byte, _type OperationType) (Operation, error) {
-	switch _type {
-	case AddCommentOp:
-		op := &AddCommentOperation{}
-		err := json.Unmarshal(raw, &op)
-		return op, err
-	case CreateOp:
-		op := &CreateOperation{}
-		err := json.Unmarshal(raw, &op)
-		return op, err
-	case EditCommentOp:
-		op := &EditCommentOperation{}
-		err := json.Unmarshal(raw, &op)
-		return op, err
-	case LabelChangeOp:
-		op := &LabelChangeOperation{}
-		err := json.Unmarshal(raw, &op)
-		return op, err
-	case NoOpOp:
-		op := &NoOpOperation{}
-		err := json.Unmarshal(raw, &op)
-		return op, err
-	case SetMetadataOp:
-		op := &SetMetadataOperation{}
-		err := json.Unmarshal(raw, &op)
-		return op, err
-	case SetStatusOp:
-		op := &SetStatusOperation{}
-		err := json.Unmarshal(raw, &op)
-		return op, err
-	case SetTitleOp:
-		op := &SetTitleOperation{}
-		err := json.Unmarshal(raw, &op)
-		return op, err
-	default:
-		return nil, fmt.Errorf("unknown operation type %v", _type)
-	}
-}
-
-// Append a new operation to the pack
-func (opp *OperationPack) Append(op Operation) {
-	opp.Operations = append(opp.Operations, op)
-}
-
-// IsEmpty tell if the OperationPack is empty
-func (opp *OperationPack) IsEmpty() bool {
-	return len(opp.Operations) == 0
-}
-
-// IsValid tell if the OperationPack is considered valid
-func (opp *OperationPack) Validate() error {
-	if opp.IsEmpty() {
-		return fmt.Errorf("empty")
-	}
-
-	for _, op := range opp.Operations {
-		if err := op.Validate(); err != nil {
-			return errors.Wrap(err, "op")
-		}
-	}
-
-	return nil
-}
-
-// Write will serialize and store the OperationPack as a git blob and return
-// its hash
-func (opp *OperationPack) Write(repo repository.ClockedRepo) (repository.Hash, error) {
-	// make sure we don't write invalid data
-	err := opp.Validate()
-	if err != nil {
-		return "", errors.Wrap(err, "validation error")
-	}
-
-	// First, make sure that all the identities are properly Commit as well
-	// TODO: this might be downgraded to "make sure it exist in git" but then, what make
-	// sure no data is lost on identities ?
-	for _, op := range opp.Operations {
-		if op.base().Author.NeedCommit() {
-			return "", fmt.Errorf("identity need commmit")
-		}
-	}
-
-	data, err := json.Marshal(opp)
-	if err != nil {
-		return "", err
-	}
-
-	hash, err := repo.StoreData(data)
-	if err != nil {
-		return "", err
-	}
-
-	return hash, nil
-}
-
-// Make a deep copy
-func (opp *OperationPack) Clone() OperationPack {
-
-	clone := OperationPack{
-		Operations: make([]Operation, len(opp.Operations)),
-		commitHash: opp.commitHash,
-	}
-
-	for i, op := range opp.Operations {
-		clone.Operations[i] = op
-	}
-
-	return clone
-}

bug/operation_pack_test.go πŸ”—

@@ -1,78 +0,0 @@
-package bug
-
-import (
-	"encoding/json"
-	"testing"
-	"time"
-
-	"github.com/stretchr/testify/require"
-
-	"github.com/MichaelMure/git-bug/identity"
-	"github.com/MichaelMure/git-bug/repository"
-)
-
-func TestOperationPackSerialize(t *testing.T) {
-	opp := &OperationPack{}
-
-	repo := repository.NewMockRepo()
-
-	rene, err := identity.NewIdentity(repo, "RenΓ© Descartes", "rene@descartes.fr")
-	require.NoError(t, err)
-
-	createOp := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil)
-	setTitleOp := NewSetTitleOp(rene, time.Now().Unix(), "title2", "title1")
-	addCommentOp := NewAddCommentOp(rene, time.Now().Unix(), "message2", nil)
-	setStatusOp := NewSetStatusOp(rene, time.Now().Unix(), ClosedStatus)
-	labelChangeOp := NewLabelChangeOperation(rene, time.Now().Unix(), []Label{"added"}, []Label{"removed"})
-
-	opp.Append(createOp)
-	opp.Append(setTitleOp)
-	opp.Append(addCommentOp)
-	opp.Append(setStatusOp)
-	opp.Append(labelChangeOp)
-
-	opMeta := NewSetTitleOp(rene, time.Now().Unix(), "title3", "title2")
-	opMeta.SetMetadata("key", "value")
-	opp.Append(opMeta)
-
-	require.Equal(t, 1, len(opMeta.Metadata))
-
-	opFile := NewAddCommentOp(rene, time.Now().Unix(), "message", []repository.Hash{
-		"abcdef",
-		"ghijkl",
-	})
-	opp.Append(opFile)
-
-	require.Equal(t, 2, len(opFile.Files))
-
-	data, err := json.Marshal(opp)
-	require.NoError(t, err)
-
-	var opp2 *OperationPack
-	err = json.Unmarshal(data, &opp2)
-	require.NoError(t, err)
-
-	ensureIds(opp)
-	ensureAuthors(t, opp, opp2)
-
-	require.Equal(t, opp, opp2)
-}
-
-func ensureIds(opp *OperationPack) {
-	for _, op := range opp.Operations {
-		op.Id()
-	}
-}
-
-func ensureAuthors(t *testing.T, opp1 *OperationPack, opp2 *OperationPack) {
-	require.Equal(t, len(opp1.Operations), len(opp2.Operations))
-	for i := 0; i < len(opp1.Operations); i++ {
-		op1 := opp1.Operations[i]
-		op2 := opp2.Operations[i]
-
-		// ensure we have equivalent authors (IdentityStub vs Identity) then
-		// enforce equality
-		require.Equal(t, op1.base().Author.Id(), op2.base().Author.Id())
-		op1.base().Author = op2.base().Author
-	}
-}

bug/operation_test.go πŸ”—

@@ -45,7 +45,7 @@ func TestValidate(t *testing.T) {
 		NewSetStatusOp(makeIdentity(t, "RenΓ© \nDescartes", "rene@descartes.fr"), unix, ClosedStatus),
 		NewSetStatusOp(makeIdentity(t, "RenΓ© Descartes", "rene@\ndescartes.fr"), unix, ClosedStatus),
 		&CreateOperation{OpBase: OpBase{
-			Author:        rene,
+			Author_:       rene,
 			UnixTime:      0,
 			OperationType: CreateOp,
 		},
@@ -121,7 +121,7 @@ func TestID(t *testing.T) {
 		require.NoError(t, id2.Validate())
 		require.Equal(t, id1, id2)
 
-		b2, err := ReadLocal(repo, b.Id())
+		b2, err := Read(repo, b.Id())
 		require.NoError(t, err)
 
 		op3 := b2.FirstOp()

bug/sorting.go πŸ”—

@@ -7,11 +7,11 @@ func (b BugsByCreationTime) Len() int {
 }
 
 func (b BugsByCreationTime) Less(i, j int) bool {
-	if b[i].createTime < b[j].createTime {
+	if b[i].CreateLamportTime() < b[j].CreateLamportTime() {
 		return true
 	}
 
-	if b[i].createTime > b[j].createTime {
+	if b[i].CreateLamportTime() > b[j].CreateLamportTime() {
 		return false
 	}
 
@@ -35,11 +35,11 @@ func (b BugsByEditTime) Len() int {
 }
 
 func (b BugsByEditTime) Less(i, j int) bool {
-	if b[i].editTime < b[j].editTime {
+	if b[i].EditLamportTime() < b[j].EditLamportTime() {
 		return true
 	}
 
-	if b[i].editTime > b[j].editTime {
+	if b[i].EditLamportTime() > b[j].EditLamportTime() {
 		return false
 	}
 

bug/with_snapshot.go πŸ”—

@@ -50,9 +50,3 @@ func (b *WithSnapshot) Commit(repo repository.ClockedRepo) error {
 	b.snap.id = b.Bug.Id()
 	return nil
 }
-
-// Merge intercept Bug.Merge() and clear the snapshot
-func (b *WithSnapshot) Merge(repo repository.Repo, other Interface) (bool, error) {
-	b.snap = nil
-	return b.Bug.Merge(repo, other)
-}

cache/bug_cache.go πŸ”—

@@ -51,9 +51,7 @@ func (c *BugCache) ResolveOperationWithMetadata(key string, value string) (entit
 	// preallocate but empty
 	matching := make([]entity.Id, 0, 5)
 
-	it := bug.NewOperationIterator(c.bug)
-	for it.Next() {
-		op := it.Value()
+	for _, op := range c.bug.Operations() {
 		opValue, ok := op.GetMetadata(key)
 		if ok && value == opValue {
 			matching = append(matching, op.Id())

cache/bug_excerpt.go πŸ”—

@@ -87,7 +87,7 @@ func NewBugExcerpt(b bug.Interface, snap *bug.Snapshot) *BugExcerpt {
 	}
 
 	switch snap.Author.(type) {
-	case *identity.Identity, *IdentityCache:
+	case *identity.Identity, *identity.IdentityStub, *IdentityCache:
 		e.AuthorId = snap.Author.Id()
 	default:
 		panic("unhandled identity type")

cache/repo_cache.go πŸ”—

@@ -195,7 +195,7 @@ func (c *RepoCache) buildCache() error {
 
 	c.bugExcerpts = make(map[entity.Id]*BugExcerpt)
 
-	allBugs := bug.ReadAllLocal(c.repo)
+	allBugs := bug.ReadAll(c.repo)
 
 	// wipe the index just to be sure
 	err := c.repo.ClearBleveIndex("bug")

cache/repo_cache_bug.go πŸ”—

@@ -151,7 +151,7 @@ func (c *RepoCache) ResolveBug(id entity.Id) (*BugCache, error) {
 	}
 	c.muBug.RUnlock()
 
-	b, err := bug.ReadLocalWithResolver(c.repo, newIdentityCacheResolver(c), id)
+	b, err := bug.ReadWithResolver(c.repo, newIdentityCacheResolver(c), id)
 	if err != nil {
 		return nil, err
 	}

cache/repo_cache_common.go πŸ”—

@@ -95,6 +95,12 @@ func (c *RepoCache) MergeAll(remote string) <-chan entity.MergeResult {
 	go func() {
 		defer close(out)
 
+		author, err := c.GetUserIdentity()
+		if err != nil {
+			out <- entity.NewMergeError(err, "")
+			return
+		}
+
 		results := identity.MergeAll(c.repo, remote)
 		for result := range results {
 			out <- result
@@ -112,7 +118,7 @@ func (c *RepoCache) MergeAll(remote string) <-chan entity.MergeResult {
 			}
 		}
 
-		results = bug.MergeAll(c.repo, remote)
+		results = bug.MergeAll(c.repo, remote, author)
 		for result := range results {
 			out <- result
 
@@ -130,11 +136,10 @@ func (c *RepoCache) MergeAll(remote string) <-chan entity.MergeResult {
 			}
 		}
 
-		err := c.write()
-
-		// No easy way out here ..
+		err = c.write()
 		if err != nil {
-			panic(err)
+			out <- entity.NewMergeError(err, "")
+			return
 		}
 	}()
 

cache/repo_cache_test.go πŸ”—

@@ -123,6 +123,10 @@ func TestPushPull(t *testing.T) {
 	require.NoError(t, err)
 	err = cacheA.SetUserIdentity(reneA)
 	require.NoError(t, err)
+	isaacB, err := cacheB.NewIdentity("Isaac Newton", "isaac@newton.uk")
+	require.NoError(t, err)
+	err = cacheB.SetUserIdentity(isaacB)
+	require.NoError(t, err)
 
 	// distribute the identity
 	_, err = cacheA.Push("origin")

entity/TODO πŸ”—

@@ -1,9 +0,0 @@
-- is the pack Lamport clock really useful vs only topological sort?
-  - topological order is enforced on the clocks, so what's the point?
-  - is EditTime equivalent to PackTime? no, avoid the gaps. Is it better?
-    --> PackTime is contained within a bug and might avoid extreme reordering?
-- how to do commit signature?
-- how to avoid id collision between Operations?
-- write tests for actions
-- migrate Bug to the new structure
-- migrate Identity to the new structure?

entity/merge.go πŸ”—

@@ -42,13 +42,15 @@ func (mr MergeResult) String() string {
 	case MergeStatusNothing:
 		return "nothing to do"
 	case MergeStatusError:
-		return fmt.Sprintf("merge error on %s: %s", mr.Id, mr.Err.Error())
+		if mr.Id != "" {
+			return fmt.Sprintf("merge error on %s: %s", mr.Id, mr.Err.Error())
+		}
+		return fmt.Sprintf("merge error: %s", mr.Err.Error())
 	default:
 		panic("unknown merge status")
 	}
 }
 
-// TODO: Interface --> *Entity ?
 func NewMergeNewStatus(id Id, entity Interface) MergeResult {
 	return MergeResult{
 		Id:     id,

go.sum πŸ”—

@@ -575,6 +575,7 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
 golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
 golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=

misc/random_bugs/create_random_bugs.go πŸ”—

@@ -111,52 +111,6 @@ func generateRandomBugsWithSeed(opts Options, seed int64) []*bug.Bug {
 	return result
 }
 
-func GenerateRandomOperationPacks(packNumber int, opNumber int) []*bug.OperationPack {
-	return GenerateRandomOperationPacksWithSeed(packNumber, opNumber, time.Now().UnixNano())
-}
-
-func GenerateRandomOperationPacksWithSeed(packNumber int, opNumber int, seed int64) []*bug.OperationPack {
-	// Note: this is a bit crude, only generate a Create + Comments
-
-	panic("this piece of code needs to be updated to make sure that the identities " +
-		"are properly commit before usage. That is, generateRandomPersons() need to be called.")
-
-	rand.Seed(seed)
-	fake.Seed(seed)
-
-	result := make([]*bug.OperationPack, packNumber)
-
-	for i := 0; i < packNumber; i++ {
-		opp := &bug.OperationPack{}
-
-		var op bug.Operation
-
-		op = bug.NewCreateOp(
-			randomPerson(),
-			time.Now().Unix(),
-			fake.Sentence(),
-			paragraphs(),
-			nil,
-		)
-
-		opp.Append(op)
-
-		for j := 0; j < opNumber-1; j++ {
-			op = bug.NewAddCommentOp(
-				randomPerson(),
-				time.Now().Unix(),
-				paragraphs(),
-				nil,
-			)
-			opp.Append(op)
-		}
-
-		result[i] = opp
-	}
-
-	return result
-}
-
 func person(repo repository.RepoClock) (*identity.Identity, error) {
 	return identity.NewIdentity(repo, fake.FullName(), fake.EmailAddress())
 }

tests/read_bugs_test.go πŸ”—

@@ -14,7 +14,7 @@ func TestReadBugs(t *testing.T) {
 
 	random_bugs.FillRepoWithSeed(repo, 15, 42)
 
-	bugs := bug.ReadAllLocal(repo)
+	bugs := bug.ReadAll(repo)
 	for b := range bugs {
 		if b.Err != nil {
 			t.Fatal(b.Err)
@@ -30,7 +30,7 @@ func benchmarkReadBugs(bugNumber int, t *testing.B) {
 	t.ResetTimer()
 
 	for n := 0; n < t.N; n++ {
-		bugs := bug.ReadAllLocal(repo)
+		bugs := bug.ReadAll(repo)
 		for b := range bugs {
 			if b.Err != nil {
 				t.Fatal(b.Err)