bug: Id from first operation data, not git + remove root link

Michael Muré created

Change summary

bug/bug.go                     | 233 +++++++++++------------------------
bug/bug_actions_test.go        |  20 +-
bug/bug_test.go                |  28 ++-
bug/git_tree.go                |  84 ++++++++++++
bug/op_add_comment_test.go     |   4 
bug/op_create.go               |  26 +++
bug/op_create_test.go          |  26 ++-
bug/op_edit_comment_test.go    |  59 ++++----
bug/op_label_change_test.go    |  14 -
bug/op_noop_test.go            |   4 
bug/op_set_metadata_test.go    |  51 +++----
bug/op_set_status_test.go      |  14 -
bug/op_set_title_test.go       |  14 -
bug/operation_iterator_test.go |  11 
bug/operation_pack.go          |   5 
bug/operation_pack_test.go     |  15 +-
bug/operation_test.go          |  32 +++-
entity/refs.go                 |   4 
identity/identity.go           |   4 
19 files changed, 341 insertions(+), 307 deletions(-)

Detailed changes

bug/bug.go đź”—

@@ -4,7 +4,6 @@ package bug
 import (
 	"encoding/json"
 	"fmt"
-	"strings"
 
 	"github.com/pkg/errors"
 
@@ -18,7 +17,6 @@ const bugsRefPattern = "refs/bugs/"
 const bugsRemoteRefPattern = "refs/remotes/%s/bugs/"
 
 const opsEntryName = "ops"
-const rootEntryName = "root"
 const mediaEntryName = "media"
 
 const createClockEntryPrefix = "create-clock-"
@@ -57,7 +55,6 @@ type Bug struct {
 	id entity.Id
 
 	lastCommit repository.Hash
-	rootPack   repository.Hash
 
 	// all the committed operations
 	packs []OperationPack
@@ -71,7 +68,7 @@ type Bug struct {
 func NewBug() *Bug {
 	// No id yet
 	// No logical clock yet
-	return &Bug{}
+	return &Bug{id: entity.UnsetId}
 }
 
 // ReadLocal will read a local bug from its hash
@@ -100,122 +97,77 @@ func ReadRemoteWithResolver(repo repository.ClockedRepo, identityResolver identi
 
 // read will read and parse a Bug from git
 func read(repo repository.ClockedRepo, identityResolver identity.Resolver, ref string) (*Bug, error) {
-	refSplit := strings.Split(ref, "/")
-	id := entity.Id(refSplit[len(refSplit)-1])
+	id := entity.RefToId(ref)
 
 	if err := id.Validate(); err != nil {
 		return nil, errors.Wrap(err, "invalid ref ")
 	}
 
 	hashes, err := repo.ListCommits(ref)
-
-	// TODO: this is not perfect, it might be a command invoke error
 	if err != nil {
 		return nil, ErrBugNotExist
 	}
+	if len(hashes) == 0 {
+		return nil, fmt.Errorf("empty bug")
+	}
 
 	bug := Bug{
-		id:       id,
-		editTime: 0,
+		id: id,
 	}
 
 	// Load each OperationPack
 	for _, hash := range hashes {
-		entries, err := repo.ReadTree(hash)
+		tree, err := readTree(repo, hash)
 		if err != nil {
-			return nil, errors.Wrap(err, "can't list git tree entries")
-		}
-
-		bug.lastCommit = hash
-
-		var opsEntry repository.TreeEntry
-		opsFound := false
-		var rootEntry repository.TreeEntry
-		rootFound := false
-		var createTime uint64
-		var editTime uint64
-
-		for _, entry := range entries {
-			if entry.Name == opsEntryName {
-				opsEntry = entry
-				opsFound = true
-				continue
-			}
-			if entry.Name == rootEntryName {
-				rootEntry = entry
-				rootFound = true
-			}
-			if strings.HasPrefix(entry.Name, createClockEntryPrefix) {
-				n, err := fmt.Sscanf(entry.Name, createClockEntryPattern, &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, &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")
-		}
-		if !rootFound {
-			return nil, errors.New("invalid tree, missing the root entry")
-		}
-
-		if bug.rootPack == "" {
-			bug.rootPack = rootEntry.Hash
-			bug.createTime = lamport.Time(createTime)
+			return nil, err
 		}
 
 		// Due to rebase, edit Lamport time are not necessarily ordered
-		if editTime > uint64(bug.editTime) {
-			bug.editTime = lamport.Time(editTime)
+		if tree.editTime > bug.editTime {
+			bug.editTime = tree.editTime
 		}
 
 		// Update the clocks
-		createClock, err := repo.GetOrCreateClock(creationClockName)
+		err = repo.Witness(creationClockName, bug.createTime)
 		if err != nil {
-			return nil, err
-		}
-		if err := createClock.Witness(bug.createTime); err != nil {
 			return nil, errors.Wrap(err, "failed to update create lamport clock")
 		}
-		editClock, err := repo.GetOrCreateClock(editClockName)
+		err = repo.Witness(editClockName, bug.editTime)
 		if err != nil {
-			return nil, err
-		}
-		if err := editClock.Witness(bug.editTime); err != nil {
 			return nil, errors.Wrap(err, "failed to update edit lamport clock")
 		}
 
-		data, err := repo.ReadData(opsEntry.Hash)
+		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 bug.id != bug.packs[0].Operations[0].Id() {
+		return nil, fmt.Errorf("bug ID doesn't match the first operation ID")
+	}
+
 	// Make sure that the identities are properly loaded
 	err = bug.EnsureIdentities(identityResolver)
 	if err != nil {
@@ -367,8 +319,8 @@ func (bug *Bug) Validate() error {
 		return fmt.Errorf("first operation should be a Create op")
 	}
 
-	// The bug Id should be the hash of the first commit
-	if len(bug.packs) > 0 && string(bug.packs[0].commitHash) != bug.id.String() {
+	// The bug Id should be the id of the first operation
+	if bug.FirstOp().Id() != bug.id {
 		return fmt.Errorf("bug id should be the first commit hash")
 	}
 
@@ -396,12 +348,17 @@ func (bug *Bug) Validate() error {
 
 // Append an operation into the staging area, to be committed later
 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.id = op.Id()
+	}
 	bug.staging.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")
 	}
@@ -410,38 +367,29 @@ func (bug *Bug) Commit(repo repository.ClockedRepo) error {
 		return errors.Wrap(err, "can't commit a bug with invalid data")
 	}
 
-	// Write the Ops as a Git blob containing the serialized array
-	hash, err := bug.staging.Write(repo)
+	// 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
+		}
+	}
 
-	if bug.rootPack == "" {
-		bug.rootPack = hash
+	// 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},
-		// always the first pack of ops (might be the same)
-		{ObjectType: repository.Blob, Hash: bug.rootPack, Name: rootEntryName},
-	}
-
-	// 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 logical clocks as well
@@ -454,31 +402,12 @@ func (bug *Bug) Commit(repo repository.ClockedRepo) error {
 	if err != nil {
 		return err
 	}
-
-	editClock, err := repo.GetOrCreateClock(editClockName)
-	if err != nil {
-		return err
-	}
-	bug.editTime, err = editClock.Increment()
-	if err != nil {
-		return err
-	}
-
 	tree = append(tree, repository.TreeEntry{
 		ObjectType: repository.Blob,
 		Hash:       emptyBlobHash,
 		Name:       fmt.Sprintf(editClockEntryPattern, bug.editTime),
 	})
 	if bug.lastCommit == "" {
-		createClock, err := repo.GetOrCreateClock(creationClockName)
-		if err != nil {
-			return err
-		}
-		bug.createTime, err = createClock.Increment()
-		if err != nil {
-			return err
-		}
-
 		tree = append(tree, repository.TreeEntry{
 			ObjectType: repository.Blob,
 			Hash:       emptyBlobHash,
@@ -486,6 +415,22 @@ func (bug *Bug) Commit(repo repository.ClockedRepo) error {
 		})
 	}
 
+	// 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 {
@@ -498,33 +443,25 @@ func (bug *Bug) Commit(repo repository.ClockedRepo) error {
 	} 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{}
 
-	// if it was the first commit, use the commit hash as bug id
-	if bug.id == "" {
-		bug.id = entity.Id(hash)
+	// if it was the first commit, use the Id of the first op (create)
+	if bug.id == "" || bug.id == entity.UnsetId {
+		bug.id = bug.packs[0].Operations[0].Id()
 	}
 
 	// 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)
-	err = repo.UpdateRef(ref, hash)
-
-	if err != nil {
-		return err
-	}
-
-	bug.staging.commitHash = hash
-	bug.packs = append(bug.packs, bug.staging)
-	bug.staging = OperationPack{}
-
-	return nil
+	return repo.UpdateRef(ref, hash)
 }
 
 func (bug *Bug) CommitAsNeeded(repo repository.ClockedRepo) error {
@@ -538,30 +475,6 @@ func (bug *Bug) NeedCommit() bool {
 	return !bug.staging.IsEmpty()
 }
 
-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
-}
-
 // 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.
@@ -657,9 +570,9 @@ func (bug *Bug) Merge(repo repository.Repo, other Interface) (bool, error) {
 
 // Id return the Bug identifier
 func (bug *Bug) Id() entity.Id {
-	if bug.id == "" {
+	if bug.id == "" || bug.id == entity.UnsetId {
 		// simply panic as it would be a coding error
-		// (using an id of a bug not stored yet)
+		// (using an id of a bug without operation yet)
 		panic("no id yet")
 	}
 	return bug.id

bug/bug_actions_test.go đź”—

@@ -15,8 +15,9 @@ func TestPushPull(t *testing.T) {
 	repoA, repoB, remote := repository.SetupReposAndRemote()
 	defer repository.CleanupTestRepos(repoA, repoB, remote)
 
-	reneA := identity.NewIdentity("René Descartes", "rene@descartes.fr")
-	err := reneA.Commit(repoA)
+	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")
@@ -92,8 +93,9 @@ func _RebaseTheirs(t testing.TB) {
 	repoA, repoB, remote := repository.SetupReposAndRemote()
 	defer repository.CleanupTestRepos(repoA, repoB, remote)
 
-	reneA := identity.NewIdentity("René Descartes", "rene@descartes.fr")
-	err := reneA.Commit(repoA)
+	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")
@@ -172,8 +174,9 @@ func _RebaseOurs(t testing.TB) {
 	repoA, repoB, remote := repository.SetupReposAndRemote()
 	defer repository.CleanupTestRepos(repoA, repoB, remote)
 
-	reneA := identity.NewIdentity("René Descartes", "rene@descartes.fr")
-	err := reneA.Commit(repoA)
+	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")
@@ -263,8 +266,9 @@ func _RebaseConflict(t testing.TB) {
 	repoA, repoB, remote := repository.SetupReposAndRemote()
 	defer repository.CleanupTestRepos(repoA, repoB, remote)
 
-	reneA := identity.NewIdentity("René Descartes", "rene@descartes.fr")
-	err := reneA.Commit(repoA)
+	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")

bug/bug_test.go đź”—

@@ -12,19 +12,20 @@ import (
 )
 
 func TestBugId(t *testing.T) {
-	mockRepo := repository.NewMockRepo()
+	repo := repository.NewMockRepo()
 
 	bug1 := NewBug()
 
-	rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
-	err := rene.Commit(mockRepo)
+	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(mockRepo)
+	err = bug1.Commit(repo)
 
 	if err != nil {
 		t.Fatal(err)
@@ -34,12 +35,13 @@ func TestBugId(t *testing.T) {
 }
 
 func TestBugValidity(t *testing.T) {
-	mockRepo := repository.NewMockRepo()
+	repo := repository.NewMockRepo()
 
 	bug1 := NewBug()
 
-	rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
-	err := rene.Commit(mockRepo)
+	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)
@@ -54,7 +56,7 @@ func TestBugValidity(t *testing.T) {
 		t.Fatal("Bug with just a CreateOp should be valid")
 	}
 
-	err = bug1.Commit(mockRepo)
+	err = bug1.Commit(repo)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -65,7 +67,7 @@ func TestBugValidity(t *testing.T) {
 		t.Fatal("Bug with multiple CreateOp should be invalid")
 	}
 
-	err = bug1.Commit(mockRepo)
+	err = bug1.Commit(repo)
 	if err == nil {
 		t.Fatal("Invalid bug should not commit")
 	}
@@ -76,8 +78,9 @@ func TestBugCommitLoad(t *testing.T) {
 
 	bug1 := NewBug()
 
-	rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
-	err := rene.Commit(repo)
+	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)
@@ -137,7 +140,8 @@ func TestBugRemove(t *testing.T) {
 	require.NoError(t, err)
 
 	// generate a bunch of bugs
-	rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
+	rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
+	require.NoError(t, err)
 	err = rene.Commit(repo)
 	require.NoError(t, err)
 

bug/git_tree.go đź”—

@@ -0,0 +1,84 @@
+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/op_add_comment_test.go đź”—

@@ -14,8 +14,8 @@ import (
 
 func TestAddCommentSerialize(t *testing.T) {
 	repo := repository.NewMockRepo()
-	rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
-	err := rene.Commit(repo)
+
+	rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
 	require.NoError(t, err)
 
 	unix := time.Now().Unix()

bug/op_create.go đź”—

@@ -1,6 +1,7 @@
 package bug
 
 import (
+	"crypto/rand"
 	"encoding/json"
 	"fmt"
 	"strings"
@@ -17,6 +18,10 @@ var _ Operation = &CreateOperation{}
 // CreateOperation define the initial creation of a bug
 type CreateOperation struct {
 	OpBase
+	// mandatory random bytes to ensure a better randomness of the data of the first
+	// operation of a bug, used to later generate the ID
+	// len(Nonce) should be > 20 and < 64 bytes
+	Nonce   []byte            `json:"nonce"`
 	Title   string            `json:"title"`
 	Message string            `json:"message"`
 	Files   []repository.Hash `json:"files"`
@@ -66,14 +71,19 @@ func (op *CreateOperation) Validate() error {
 		return err
 	}
 
+	if len(op.Nonce) > 64 {
+		return fmt.Errorf("create nonce is too big")
+	}
+	if len(op.Nonce) < 20 {
+		return fmt.Errorf("create nonce is too small")
+	}
+
 	if text.Empty(op.Title) {
 		return fmt.Errorf("title is empty")
 	}
-
 	if strings.Contains(op.Title, "\n") {
 		return fmt.Errorf("title should be a single line")
 	}
-
 	if !text.Safe(op.Title) {
 		return fmt.Errorf("title is not fully printable")
 	}
@@ -98,6 +108,7 @@ func (op *CreateOperation) UnmarshalJSON(data []byte) error {
 	}
 
 	aux := struct {
+		Nonce   []byte            `json:"nonce"`
 		Title   string            `json:"title"`
 		Message string            `json:"message"`
 		Files   []repository.Hash `json:"files"`
@@ -109,6 +120,7 @@ func (op *CreateOperation) UnmarshalJSON(data []byte) error {
 	}
 
 	op.OpBase = base
+	op.Nonce = aux.Nonce
 	op.Title = aux.Title
 	op.Message = aux.Message
 	op.Files = aux.Files
@@ -119,9 +131,19 @@ func (op *CreateOperation) UnmarshalJSON(data []byte) error {
 // Sign post method for gqlgen
 func (op *CreateOperation) IsAuthored() {}
 
+func makeNonce(len int) []byte {
+	result := make([]byte, len)
+	_, err := rand.Read(result)
+	if err != nil {
+		panic(err)
+	}
+	return result
+}
+
 func NewCreateOp(author identity.Interface, unixTime int64, title, message string, files []repository.Hash) *CreateOperation {
 	return &CreateOperation{
 		OpBase:  newOpBase(CreateOp, author, unixTime),
+		Nonce:   makeNonce(20),
 		Title:   title,
 		Message: message,
 		Files:   files,

bug/op_create_test.go đź”—

@@ -5,17 +5,21 @@ import (
 	"testing"
 	"time"
 
+	"github.com/stretchr/testify/require"
+
 	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/repository"
 	"github.com/MichaelMure/git-bug/util/timestamp"
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
 )
 
 func TestCreate(t *testing.T) {
 	snapshot := Snapshot{}
 
-	rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
+	repo := repository.NewMockRepoClock()
+
+	rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
+	require.NoError(t, err)
+
 	unix := time.Now().Unix()
 
 	create := NewCreateOp(rene, unix, "title", "message", nil)
@@ -23,7 +27,7 @@ func TestCreate(t *testing.T) {
 	create.Apply(&snapshot)
 
 	id := create.Id()
-	assert.NoError(t, id.Validate())
+	require.NoError(t, id.Validate())
 
 	comment := Comment{
 		id:       id,
@@ -48,31 +52,31 @@ func TestCreate(t *testing.T) {
 		},
 	}
 
-	assert.Equal(t, expected, snapshot)
+	require.Equal(t, expected, snapshot)
 }
 
 func TestCreateSerialize(t *testing.T) {
 	repo := repository.NewMockRepo()
-	rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
-	err := rene.Commit(repo)
+
+	rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
 	require.NoError(t, err)
 
 	unix := time.Now().Unix()
 	before := NewCreateOp(rene, unix, "title", "message", nil)
 
 	data, err := json.Marshal(before)
-	assert.NoError(t, err)
+	require.NoError(t, err)
 
 	var after CreateOperation
 	err = json.Unmarshal(data, &after)
-	assert.NoError(t, err)
+	require.NoError(t, err)
 
 	// enforce creating the ID
 	before.Id()
 
 	// Replace the identity stub with the real thing
-	assert.Equal(t, rene.Id(), after.base().Author.Id())
+	require.Equal(t, rene.Id(), after.base().Author.Id())
 	after.Author = rene
 
-	assert.Equal(t, before, &after)
+	require.Equal(t, before, &after)
 }

bug/op_edit_comment_test.go đź”—

@@ -5,7 +5,6 @@ import (
 	"testing"
 	"time"
 
-	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 
 	"github.com/MichaelMure/git-bug/identity"
@@ -16,8 +15,8 @@ func TestEdit(t *testing.T) {
 	snapshot := Snapshot{}
 
 	repo := repository.NewMockRepo()
-	rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
-	err := rene.Commit(repo)
+
+	rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
 	require.NoError(t, err)
 
 	unix := time.Now().Unix()
@@ -47,59 +46,59 @@ func TestEdit(t *testing.T) {
 	edit := NewEditCommentOp(rene, unix, id1, "create edited", nil)
 	edit.Apply(&snapshot)
 
-	assert.Equal(t, len(snapshot.Timeline), 4)
-	assert.Equal(t, len(snapshot.Timeline[0].(*CreateTimelineItem).History), 2)
-	assert.Equal(t, len(snapshot.Timeline[1].(*AddCommentTimelineItem).History), 1)
-	assert.Equal(t, len(snapshot.Timeline[3].(*AddCommentTimelineItem).History), 1)
-	assert.Equal(t, snapshot.Comments[0].Message, "create edited")
-	assert.Equal(t, snapshot.Comments[1].Message, "comment 1")
-	assert.Equal(t, snapshot.Comments[2].Message, "comment 2")
+	require.Equal(t, len(snapshot.Timeline), 4)
+	require.Equal(t, len(snapshot.Timeline[0].(*CreateTimelineItem).History), 2)
+	require.Equal(t, len(snapshot.Timeline[1].(*AddCommentTimelineItem).History), 1)
+	require.Equal(t, len(snapshot.Timeline[3].(*AddCommentTimelineItem).History), 1)
+	require.Equal(t, snapshot.Comments[0].Message, "create edited")
+	require.Equal(t, snapshot.Comments[1].Message, "comment 1")
+	require.Equal(t, snapshot.Comments[2].Message, "comment 2")
 
 	edit2 := NewEditCommentOp(rene, unix, id2, "comment 1 edited", nil)
 	edit2.Apply(&snapshot)
 
-	assert.Equal(t, len(snapshot.Timeline), 4)
-	assert.Equal(t, len(snapshot.Timeline[0].(*CreateTimelineItem).History), 2)
-	assert.Equal(t, len(snapshot.Timeline[1].(*AddCommentTimelineItem).History), 2)
-	assert.Equal(t, len(snapshot.Timeline[3].(*AddCommentTimelineItem).History), 1)
-	assert.Equal(t, snapshot.Comments[0].Message, "create edited")
-	assert.Equal(t, snapshot.Comments[1].Message, "comment 1 edited")
-	assert.Equal(t, snapshot.Comments[2].Message, "comment 2")
+	require.Equal(t, len(snapshot.Timeline), 4)
+	require.Equal(t, len(snapshot.Timeline[0].(*CreateTimelineItem).History), 2)
+	require.Equal(t, len(snapshot.Timeline[1].(*AddCommentTimelineItem).History), 2)
+	require.Equal(t, len(snapshot.Timeline[3].(*AddCommentTimelineItem).History), 1)
+	require.Equal(t, snapshot.Comments[0].Message, "create edited")
+	require.Equal(t, snapshot.Comments[1].Message, "comment 1 edited")
+	require.Equal(t, snapshot.Comments[2].Message, "comment 2")
 
 	edit3 := NewEditCommentOp(rene, unix, id3, "comment 2 edited", nil)
 	edit3.Apply(&snapshot)
 
-	assert.Equal(t, len(snapshot.Timeline), 4)
-	assert.Equal(t, len(snapshot.Timeline[0].(*CreateTimelineItem).History), 2)
-	assert.Equal(t, len(snapshot.Timeline[1].(*AddCommentTimelineItem).History), 2)
-	assert.Equal(t, len(snapshot.Timeline[3].(*AddCommentTimelineItem).History), 2)
-	assert.Equal(t, snapshot.Comments[0].Message, "create edited")
-	assert.Equal(t, snapshot.Comments[1].Message, "comment 1 edited")
-	assert.Equal(t, snapshot.Comments[2].Message, "comment 2 edited")
+	require.Equal(t, len(snapshot.Timeline), 4)
+	require.Equal(t, len(snapshot.Timeline[0].(*CreateTimelineItem).History), 2)
+	require.Equal(t, len(snapshot.Timeline[1].(*AddCommentTimelineItem).History), 2)
+	require.Equal(t, len(snapshot.Timeline[3].(*AddCommentTimelineItem).History), 2)
+	require.Equal(t, snapshot.Comments[0].Message, "create edited")
+	require.Equal(t, snapshot.Comments[1].Message, "comment 1 edited")
+	require.Equal(t, snapshot.Comments[2].Message, "comment 2 edited")
 }
 
 func TestEditCommentSerialize(t *testing.T) {
 	repo := repository.NewMockRepo()
-	rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
-	err := rene.Commit(repo)
+
+	rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
 	require.NoError(t, err)
 
 	unix := time.Now().Unix()
 	before := NewEditCommentOp(rene, unix, "target", "message", nil)
 
 	data, err := json.Marshal(before)
-	assert.NoError(t, err)
+	require.NoError(t, err)
 
 	var after EditCommentOperation
 	err = json.Unmarshal(data, &after)
-	assert.NoError(t, err)
+	require.NoError(t, err)
 
 	// enforce creating the ID
 	before.Id()
 
 	// Replace the identity stub with the real thing
-	assert.Equal(t, rene.Id(), after.base().Author.Id())
+	require.Equal(t, rene.Id(), after.base().Author.Id())
 	after.Author = rene
 
-	assert.Equal(t, before, &after)
+	require.Equal(t, before, &after)
 }

bug/op_label_change_test.go đź”—

@@ -9,32 +9,30 @@ import (
 
 	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/repository"
-
-	"github.com/stretchr/testify/assert"
 )
 
 func TestLabelChangeSerialize(t *testing.T) {
 	repo := repository.NewMockRepo()
-	rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
-	err := rene.Commit(repo)
+
+	rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
 	require.NoError(t, err)
 
 	unix := time.Now().Unix()
 	before := NewLabelChangeOperation(rene, unix, []Label{"added"}, []Label{"removed"})
 
 	data, err := json.Marshal(before)
-	assert.NoError(t, err)
+	require.NoError(t, err)
 
 	var after LabelChangeOperation
 	err = json.Unmarshal(data, &after)
-	assert.NoError(t, err)
+	require.NoError(t, err)
 
 	// enforce creating the ID
 	before.Id()
 
 	// Replace the identity stub with the real thing
-	assert.Equal(t, rene.Id(), after.base().Author.Id())
+	require.Equal(t, rene.Id(), after.base().Author.Id())
 	after.Author = rene
 
-	assert.Equal(t, before, &after)
+	require.Equal(t, before, &after)
 }

bug/op_noop_test.go đź”—

@@ -15,8 +15,8 @@ import (
 
 func TestNoopSerialize(t *testing.T) {
 	repo := repository.NewMockRepo()
-	rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
-	err := rene.Commit(repo)
+
+	rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
 	require.NoError(t, err)
 
 	unix := time.Now().Unix()

bug/op_set_metadata_test.go đź”—

@@ -8,7 +8,6 @@ import (
 	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/repository"
 
-	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
@@ -16,8 +15,8 @@ func TestSetMetadata(t *testing.T) {
 	snapshot := Snapshot{}
 
 	repo := repository.NewMockRepo()
-	rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
-	err := rene.Commit(repo)
+
+	rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
 	require.NoError(t, err)
 
 	unix := time.Now().Unix()
@@ -47,15 +46,15 @@ func TestSetMetadata(t *testing.T) {
 	snapshot.Operations = append(snapshot.Operations, op1)
 
 	createMetadata := snapshot.Operations[0].AllMetadata()
-	assert.Equal(t, len(createMetadata), 2)
+	require.Equal(t, len(createMetadata), 2)
 	// original key is not overrided
-	assert.Equal(t, createMetadata["key"], "value")
+	require.Equal(t, createMetadata["key"], "value")
 	// new key is set
-	assert.Equal(t, createMetadata["key2"], "value")
+	require.Equal(t, createMetadata["key2"], "value")
 
 	commentMetadata := snapshot.Operations[1].AllMetadata()
-	assert.Equal(t, len(commentMetadata), 1)
-	assert.Equal(t, commentMetadata["key2"], "value2")
+	require.Equal(t, len(commentMetadata), 1)
+	require.Equal(t, commentMetadata["key2"], "value2")
 
 	op2 := NewSetMetadataOp(rene, unix, id2, map[string]string{
 		"key2": "value",
@@ -66,16 +65,16 @@ func TestSetMetadata(t *testing.T) {
 	snapshot.Operations = append(snapshot.Operations, op2)
 
 	createMetadata = snapshot.Operations[0].AllMetadata()
-	assert.Equal(t, len(createMetadata), 2)
-	assert.Equal(t, createMetadata["key"], "value")
-	assert.Equal(t, createMetadata["key2"], "value")
+	require.Equal(t, len(createMetadata), 2)
+	require.Equal(t, createMetadata["key"], "value")
+	require.Equal(t, createMetadata["key2"], "value")
 
 	commentMetadata = snapshot.Operations[1].AllMetadata()
-	assert.Equal(t, len(commentMetadata), 2)
+	require.Equal(t, len(commentMetadata), 2)
 	// original key is not overrided
-	assert.Equal(t, commentMetadata["key2"], "value2")
+	require.Equal(t, commentMetadata["key2"], "value2")
 	// new key is set
-	assert.Equal(t, commentMetadata["key3"], "value3")
+	require.Equal(t, commentMetadata["key3"], "value3")
 
 	op3 := NewSetMetadataOp(rene, unix, id1, map[string]string{
 		"key":  "override",
@@ -86,22 +85,22 @@ func TestSetMetadata(t *testing.T) {
 	snapshot.Operations = append(snapshot.Operations, op3)
 
 	createMetadata = snapshot.Operations[0].AllMetadata()
-	assert.Equal(t, len(createMetadata), 2)
+	require.Equal(t, len(createMetadata), 2)
 	// original key is not overrided
-	assert.Equal(t, createMetadata["key"], "value")
+	require.Equal(t, createMetadata["key"], "value")
 	// previously set key is not overrided
-	assert.Equal(t, createMetadata["key2"], "value")
+	require.Equal(t, createMetadata["key2"], "value")
 
 	commentMetadata = snapshot.Operations[1].AllMetadata()
-	assert.Equal(t, len(commentMetadata), 2)
-	assert.Equal(t, commentMetadata["key2"], "value2")
-	assert.Equal(t, commentMetadata["key3"], "value3")
+	require.Equal(t, len(commentMetadata), 2)
+	require.Equal(t, commentMetadata["key2"], "value2")
+	require.Equal(t, commentMetadata["key3"], "value3")
 }
 
 func TestSetMetadataSerialize(t *testing.T) {
 	repo := repository.NewMockRepo()
-	rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
-	err := rene.Commit(repo)
+
+	rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
 	require.NoError(t, err)
 
 	unix := time.Now().Unix()
@@ -111,18 +110,18 @@ func TestSetMetadataSerialize(t *testing.T) {
 	})
 
 	data, err := json.Marshal(before)
-	assert.NoError(t, err)
+	require.NoError(t, err)
 
 	var after SetMetadataOperation
 	err = json.Unmarshal(data, &after)
-	assert.NoError(t, err)
+	require.NoError(t, err)
 
 	// enforce creating the ID
 	before.Id()
 
 	// Replace the identity stub with the real thing
-	assert.Equal(t, rene.Id(), after.base().Author.Id())
+	require.Equal(t, rene.Id(), after.base().Author.Id())
 	after.Author = rene
 
-	assert.Equal(t, before, &after)
+	require.Equal(t, before, &after)
 }

bug/op_set_status_test.go đź”—

@@ -9,32 +9,30 @@ import (
 
 	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/repository"
-
-	"github.com/stretchr/testify/assert"
 )
 
 func TestSetStatusSerialize(t *testing.T) {
 	repo := repository.NewMockRepo()
-	rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
-	err := rene.Commit(repo)
+
+	rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
 	require.NoError(t, err)
 
 	unix := time.Now().Unix()
 	before := NewSetStatusOp(rene, unix, ClosedStatus)
 
 	data, err := json.Marshal(before)
-	assert.NoError(t, err)
+	require.NoError(t, err)
 
 	var after SetStatusOperation
 	err = json.Unmarshal(data, &after)
-	assert.NoError(t, err)
+	require.NoError(t, err)
 
 	// enforce creating the ID
 	before.Id()
 
 	// Replace the identity stub with the real thing
-	assert.Equal(t, rene.Id(), after.base().Author.Id())
+	require.Equal(t, rene.Id(), after.base().Author.Id())
 	after.Author = rene
 
-	assert.Equal(t, before, &after)
+	require.Equal(t, before, &after)
 }

bug/op_set_title_test.go đź”—

@@ -9,32 +9,30 @@ import (
 
 	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/repository"
-
-	"github.com/stretchr/testify/assert"
 )
 
 func TestSetTitleSerialize(t *testing.T) {
 	repo := repository.NewMockRepo()
-	rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
-	err := rene.Commit(repo)
+
+	rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
 	require.NoError(t, err)
 
 	unix := time.Now().Unix()
 	before := NewSetTitleOp(rene, unix, "title", "was")
 
 	data, err := json.Marshal(before)
-	assert.NoError(t, err)
+	require.NoError(t, err)
 
 	var after SetTitleOperation
 	err = json.Unmarshal(data, &after)
-	assert.NoError(t, err)
+	require.NoError(t, err)
 
 	// enforce creating the ID
 	before.Id()
 
 	// Replace the identity stub with the real thing
-	assert.Equal(t, rene.Id(), after.base().Author.Id())
+	require.Equal(t, rene.Id(), after.base().Author.Id())
 	after.Author = rene
 
-	assert.Equal(t, before, &after)
+	require.Equal(t, before, &after)
 }

bug/operation_iterator_test.go đź”—

@@ -25,10 +25,11 @@ func ExampleOperationIterator() {
 }
 
 func TestOpIterator(t *testing.T) {
-	mockRepo := repository.NewMockRepo()
+	repo := repository.NewMockRepo()
 
-	rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
-	err := rene.Commit(mockRepo)
+	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()
@@ -51,14 +52,14 @@ func TestOpIterator(t *testing.T) {
 	bug1.Append(addCommentOp)
 	bug1.Append(setStatusOp)
 	bug1.Append(labelChangeOp)
-	err = bug1.Commit(mockRepo)
+	err = bug1.Commit(repo)
 	require.NoError(t, err)
 
 	// second pack
 	bug1.Append(genTitleOp())
 	bug1.Append(genTitleOp())
 	bug1.Append(genTitleOp())
-	err = bug1.Commit(mockRepo)
+	err = bug1.Commit(repo)
 	require.NoError(t, err)
 
 	// staging

bug/operation_pack.go đź”—

@@ -12,7 +12,8 @@ import (
 
 // 1: original format
 // 2: no more legacy identities
-const formatVersion = 2
+// 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.
@@ -158,13 +159,11 @@ func (opp *OperationPack) Write(repo repository.ClockedRepo) (repository.Hash, e
 	}
 
 	data, err := json.Marshal(opp)
-
 	if err != nil {
 		return "", err
 	}
 
 	hash, err := repo.StoreData(data)
-
 	if err != nil {
 		return "", err
 	}

bug/operation_pack_test.go đź”—

@@ -5,7 +5,6 @@ import (
 	"testing"
 	"time"
 
-	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 
 	"github.com/MichaelMure/git-bug/identity"
@@ -16,8 +15,8 @@ func TestOperationPackSerialize(t *testing.T) {
 	opp := &OperationPack{}
 
 	repo := repository.NewMockRepo()
-	rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
-	err := rene.Commit(repo)
+
+	rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
 	require.NoError(t, err)
 
 	createOp := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil)
@@ -36,7 +35,7 @@ func TestOperationPackSerialize(t *testing.T) {
 	opMeta.SetMetadata("key", "value")
 	opp.Append(opMeta)
 
-	assert.Equal(t, 1, len(opMeta.Metadata))
+	require.Equal(t, 1, len(opMeta.Metadata))
 
 	opFile := NewAddCommentOp(rene, time.Now().Unix(), "message", []repository.Hash{
 		"abcdef",
@@ -44,19 +43,19 @@ func TestOperationPackSerialize(t *testing.T) {
 	})
 	opp.Append(opFile)
 
-	assert.Equal(t, 2, len(opFile.Files))
+	require.Equal(t, 2, len(opFile.Files))
 
 	data, err := json.Marshal(opp)
-	assert.NoError(t, err)
+	require.NoError(t, err)
 
 	var opp2 *OperationPack
 	err = json.Unmarshal(data, &opp2)
-	assert.NoError(t, err)
+	require.NoError(t, err)
 
 	ensureIds(opp)
 	ensureAuthors(t, opp, opp2)
 
-	assert.Equal(t, opp, opp2)
+	require.Equal(t, opp, opp2)
 }
 
 func ensureIds(opp *OperationPack) {

bug/operation_test.go đź”—

@@ -11,7 +11,16 @@ import (
 )
 
 func TestValidate(t *testing.T) {
-	rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
+	repo := repository.NewMockRepoClock()
+
+	makeIdentity := func(t *testing.T, name, email string) *identity.Identity {
+		i, err := identity.NewIdentity(repo, name, email)
+		require.NoError(t, err)
+		return i
+	}
+
+	rene := makeIdentity(t, "René Descartes", "rene@descartes.fr")
+
 	unix := time.Now().Unix()
 
 	good := []Operation{
@@ -30,11 +39,11 @@ func TestValidate(t *testing.T) {
 
 	bad := []Operation{
 		// opbase
-		NewSetStatusOp(identity.NewIdentity("", "rene@descartes.fr"), unix, ClosedStatus),
-		NewSetStatusOp(identity.NewIdentity("René Descartes\u001b", "rene@descartes.fr"), unix, ClosedStatus),
-		NewSetStatusOp(identity.NewIdentity("René Descartes", "rene@descartes.fr\u001b"), unix, ClosedStatus),
-		NewSetStatusOp(identity.NewIdentity("René \nDescartes", "rene@descartes.fr"), unix, ClosedStatus),
-		NewSetStatusOp(identity.NewIdentity("René Descartes", "rene@\ndescartes.fr"), unix, ClosedStatus),
+		NewSetStatusOp(makeIdentity(t, "", "rene@descartes.fr"), unix, ClosedStatus),
+		NewSetStatusOp(makeIdentity(t, "René Descartes\u001b", "rene@descartes.fr"), unix, ClosedStatus),
+		NewSetStatusOp(makeIdentity(t, "René Descartes", "rene@descartes.fr\u001b"), unix, ClosedStatus),
+		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,
 			UnixTime:      0,
@@ -68,7 +77,11 @@ func TestValidate(t *testing.T) {
 }
 
 func TestMetadata(t *testing.T) {
-	rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
+	repo := repository.NewMockRepoClock()
+
+	rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
+	require.NoError(t, err)
+
 	op := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil)
 
 	op.SetMetadata("key", "value")
@@ -88,8 +101,9 @@ func TestID(t *testing.T) {
 	}
 
 	for _, repo := range repos {
-		rene := identity.NewIdentity("René Descartes", "rene@descartes.fr")
-		err := rene.Commit(repo)
+		rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
+		require.NoError(t, err)
+		err = rene.Commit(repo)
 		require.NoError(t, err)
 
 		b, op, err := Create(rene, time.Now().Unix(), "title", "message")

entity/refs.go đź”—

@@ -6,13 +6,13 @@ func RefsToIds(refs []string) []Id {
 	ids := make([]Id, len(refs))
 
 	for i, ref := range refs {
-		ids[i] = refToId(ref)
+		ids[i] = RefToId(ref)
 	}
 
 	return ids
 }
 
-func refToId(ref string) Id {
+func RefToId(ref string) Id {
 	split := strings.Split(ref, "/")
 	return Id(split[len(split)-1])
 }

identity/identity.go đź”—

@@ -5,7 +5,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"reflect"
-	"strings"
 
 	"github.com/pkg/errors"
 
@@ -102,8 +101,7 @@ func ReadRemote(repo repository.Repo, remote string, id string) (*Identity, erro
 
 // read will load and parse an identity from git
 func read(repo repository.Repo, ref string) (*Identity, error) {
-	refSplit := strings.Split(ref, "/")
-	id := entity.Id(refSplit[len(refSplit)-1])
+	id := entity.RefToId(ref)
 
 	if err := id.Validate(); err != nil {
 		return nil, errors.Wrap(err, "invalid ref")