identity: somewhat getting closer !

Michael MurΓ© created

Change summary

bug/bug_actions_test.go        | 149 ++++++++++++++++++-----------------
bug/bug_test.go                |   6 -
bug/op_add_comment.go          |  54 ++++++++++++
bug/op_add_comment_test.go     |  25 ++++++
bug/op_create.go               |  59 +++++++++++++
bug/op_create_test.go          |  17 ++++
bug/op_edit_comment.go         |  58 +++++++++++++
bug/op_edit_comment_test.go    |  18 ++++
bug/op_label_change.go         |  53 ++++++++++++
bug/op_label_change_test.go    |  25 ++++++
bug/op_noop.go                 |  42 ++++++++++
bug/op_noop_test.go            |  25 ++++++
bug/op_set_metadata.go         |  54 ++++++++++++
bug/op_set_metadata_test.go    |  19 ++++
bug/op_set_status.go           |  49 +++++++++++
bug/op_set_status_test.go      |  25 ++++++
bug/op_set_title.go            |  53 ++++++++++++
bug/op_set_title_test.go       |  25 ++++++
bug/operation.go               |  36 +++++---
bug/operation_iterator_test.go |   8 +
bug/operation_pack.go          |   8 +
identity/bare.go               |  55 ++++++++++--
identity/bare_test.go          |  13 +++
identity/common.go             |  53 ++++++++++++
identity/identity.go           |  16 ---
identity/interface.go          |   5 +
util/git/hash.go               |   2 
27 files changed, 813 insertions(+), 139 deletions(-)

Detailed changes

bug/bug_actions_test.go πŸ”—

@@ -77,17 +77,20 @@ func TestPushPull(t *testing.T) {
 	repoA, repoB, remote := setupRepos(t)
 	defer cleanupRepos(repoA, repoB, remote)
 
+	err := rene.Commit(repoA)
+	assert.NoError(t, err)
+
 	bug1, _, err := Create(rene, unix, "bug1", "message")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	err = bug1.Commit(repoA)
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 
 	// A --> remote --> B
 	_, err = Push(repoA, "origin")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 
 	err = Pull(repoB, "origin")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 
 	bugs := allBugs(t, ReadAllLocalBugs(repoB))
 
@@ -97,15 +100,15 @@ func TestPushPull(t *testing.T) {
 
 	// B --> remote --> A
 	bug2, _, err := Create(rene, unix, "bug2", "message")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	err = bug2.Commit(repoB)
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 
 	_, err = Push(repoB, "origin")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 
 	err = Pull(repoA, "origin")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 
 	bugs = allBugs(t, ReadAllLocalBugs(repoA))
 
@@ -140,37 +143,37 @@ func _RebaseTheirs(t testing.TB) {
 	defer cleanupRepos(repoA, repoB, remote)
 
 	bug1, _, err := Create(rene, unix, "bug1", "message")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	err = bug1.Commit(repoA)
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 
 	// A --> remote
 	_, err = Push(repoA, "origin")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 
 	// remote --> B
 	err = Pull(repoB, "origin")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 
 	bug2, err := ReadLocalBug(repoB, bug1.Id())
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 
 	_, err = AddComment(bug2, rene, unix, "message2")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	_, err = AddComment(bug2, rene, unix, "message3")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	_, err = AddComment(bug2, rene, unix, "message4")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	err = bug2.Commit(repoB)
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 
 	// B --> remote
 	_, err = Push(repoB, "origin")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 
 	// remote --> A
 	err = Pull(repoA, "origin")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 
 	bugs := allBugs(t, ReadAllLocalBugs(repoB))
 
@@ -179,7 +182,7 @@ func _RebaseTheirs(t testing.TB) {
 	}
 
 	bug3, err := ReadLocalBug(repoA, bug1.Id())
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 
 	if nbOps(bug3) != 4 {
 		t.Fatal("Unexpected number of operations")
@@ -201,48 +204,48 @@ func _RebaseOurs(t testing.TB) {
 	defer cleanupRepos(repoA, repoB, remote)
 
 	bug1, _, err := Create(rene, unix, "bug1", "message")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	err = bug1.Commit(repoA)
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 
 	// A --> remote
 	_, err = Push(repoA, "origin")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 
 	// remote --> B
 	err = Pull(repoB, "origin")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 
 	_, err = AddComment(bug1, rene, unix, "message2")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	_, err = AddComment(bug1, rene, unix, "message3")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	_, err = AddComment(bug1, rene, unix, "message4")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	err = bug1.Commit(repoA)
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 
 	_, err = AddComment(bug1, rene, unix, "message5")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	_, err = AddComment(bug1, rene, unix, "message6")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	_, err = AddComment(bug1, rene, unix, "message7")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	err = bug1.Commit(repoA)
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 
 	_, err = AddComment(bug1, rene, unix, "message8")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	_, err = AddComment(bug1, rene, unix, "message9")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	_, err = AddComment(bug1, rene, unix, "message10")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	err = bug1.Commit(repoA)
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 
 	// remote --> A
 	err = Pull(repoA, "origin")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 
 	bugs := allBugs(t, ReadAllLocalBugs(repoA))
 
@@ -251,7 +254,7 @@ func _RebaseOurs(t testing.TB) {
 	}
 
 	bug2, err := ReadLocalBug(repoA, bug1.Id())
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 
 	if nbOps(bug2) != 10 {
 		t.Fatal("Unexpected number of operations")
@@ -282,82 +285,82 @@ func _RebaseConflict(t testing.TB) {
 	defer cleanupRepos(repoA, repoB, remote)
 
 	bug1, _, err := Create(rene, unix, "bug1", "message")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	err = bug1.Commit(repoA)
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 
 	// A --> remote
 	_, err = Push(repoA, "origin")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 
 	// remote --> B
 	err = Pull(repoB, "origin")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 
 	_, err = AddComment(bug1, rene, unix, "message2")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	_, err = AddComment(bug1, rene, unix, "message3")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	_, err = AddComment(bug1, rene, unix, "message4")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	err = bug1.Commit(repoA)
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 
 	_, err = AddComment(bug1, rene, unix, "message5")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	_, err = AddComment(bug1, rene, unix, "message6")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	_, err = AddComment(bug1, rene, unix, "message7")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	err = bug1.Commit(repoA)
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 
 	_, err = AddComment(bug1, rene, unix, "message8")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	_, err = AddComment(bug1, rene, unix, "message9")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	_, err = AddComment(bug1, rene, unix, "message10")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	err = bug1.Commit(repoA)
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 
 	bug2, err := ReadLocalBug(repoB, bug1.Id())
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 
 	_, err = AddComment(bug2, rene, unix, "message11")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	_, err = AddComment(bug2, rene, unix, "message12")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	_, err = AddComment(bug2, rene, unix, "message13")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	err = bug2.Commit(repoB)
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 
 	_, err = AddComment(bug2, rene, unix, "message14")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	_, err = AddComment(bug2, rene, unix, "message15")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	_, err = AddComment(bug2, rene, unix, "message16")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	err = bug2.Commit(repoB)
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 
 	_, err = AddComment(bug2, rene, unix, "message17")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	_, err = AddComment(bug2, rene, unix, "message18")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	_, err = AddComment(bug2, rene, unix, "message19")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	err = bug2.Commit(repoB)
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 
 	// A --> remote
 	_, err = Push(repoA, "origin")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 
 	// remote --> B
 	err = Pull(repoB, "origin")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 
 	bugs := allBugs(t, ReadAllLocalBugs(repoB))
 
@@ -366,7 +369,7 @@ func _RebaseConflict(t testing.TB) {
 	}
 
 	bug3, err := ReadLocalBug(repoB, bug1.Id())
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 
 	if nbOps(bug3) != 19 {
 		t.Fatal("Unexpected number of operations")
@@ -374,11 +377,11 @@ func _RebaseConflict(t testing.TB) {
 
 	// B --> remote
 	_, err = Push(repoB, "origin")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 
 	// remote --> A
 	err = Pull(repoA, "origin")
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 
 	bugs = allBugs(t, ReadAllLocalBugs(repoA))
 
@@ -387,7 +390,7 @@ func _RebaseConflict(t testing.TB) {
 	}
 
 	bug4, err := ReadLocalBug(repoA, bug1.Id())
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 
 	if nbOps(bug4) != 19 {
 		t.Fatal("Unexpected number of operations")

bug/bug_test.go πŸ”—

@@ -2,7 +2,6 @@ package bug
 
 import (
 	"github.com/MichaelMure/git-bug/repository"
-	"github.com/go-test/deep"
 	"github.com/stretchr/testify/assert"
 
 	"testing"
@@ -87,8 +86,5 @@ func TestBugSerialisation(t *testing.T) {
 		}
 	}
 
-	deep.CompareUnexportedFields = true
-	if diff := deep.Equal(bug1, bug2); diff != nil {
-		t.Fatal(diff)
-	}
+	assert.Equal(t, bug1, bug2)
 }

bug/op_add_comment.go πŸ”—

@@ -1,10 +1,10 @@
 package bug
 
 import (
+	"encoding/json"
 	"fmt"
 
 	"github.com/MichaelMure/git-bug/identity"
-
 	"github.com/MichaelMure/git-bug/util/git"
 	"github.com/MichaelMure/git-bug/util/text"
 )
@@ -14,9 +14,9 @@ var _ Operation = &AddCommentOperation{}
 // AddCommentOperation will add a new comment in the bug
 type AddCommentOperation struct {
 	OpBase
-	Message string `json:"message"`
+	Message string
 	// TODO: change for a map[string]util.hash to store the filename ?
-	Files []git.Hash `json:"files"`
+	Files []git.Hash
 }
 
 func (op *AddCommentOperation) base() *OpBase {
@@ -67,6 +67,54 @@ func (op *AddCommentOperation) Validate() error {
 	return nil
 }
 
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *AddCommentOperation) MarshalJSON() ([]byte, error) {
+	base, err := json.Marshal(op.OpBase)
+	if err != nil {
+		return nil, err
+	}
+
+	// revert back to a flat map to be able to add our own fields
+	var data map[string]interface{}
+	if err := json.Unmarshal(base, &data); err != nil {
+		return nil, err
+	}
+
+	data["message"] = op.Message
+	data["files"] = op.Files
+
+	return json.Marshal(data)
+}
+
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *AddCommentOperation) UnmarshalJSON(data []byte) error {
+	// Unmarshal OpBase and the op separately
+
+	base := OpBase{}
+	err := json.Unmarshal(data, &base)
+	if err != nil {
+		return err
+	}
+
+	aux := struct {
+		Message string     `json:"message"`
+		Files   []git.Hash `json:"files"`
+	}{}
+
+	err = json.Unmarshal(data, &aux)
+	if err != nil {
+		return err
+	}
+
+	op.OpBase = base
+	op.Message = aux.Message
+	op.Files = aux.Files
+
+	return nil
+}
+
 // Sign post method for gqlgen
 func (op *AddCommentOperation) IsAuthored() {}
 

bug/op_add_comment_test.go πŸ”—

@@ -0,0 +1,25 @@
+package bug
+
+import (
+	"encoding/json"
+	"testing"
+	"time"
+
+	"github.com/MichaelMure/git-bug/identity"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestAddCommentSerialize(t *testing.T) {
+	var rene = identity.NewBare("RenΓ© Descartes", "rene@descartes.fr")
+	unix := time.Now().Unix()
+	before := NewAddCommentOp(rene, unix, "message", nil)
+
+	data, err := json.Marshal(before)
+	assert.NoError(t, err)
+
+	var after AddCommentOperation
+	err = json.Unmarshal(data, &after)
+	assert.NoError(t, err)
+
+	assert.Equal(t, before, &after)
+}

bug/op_create.go πŸ”—

@@ -1,11 +1,11 @@
 package bug
 
 import (
+	"encoding/json"
 	"fmt"
 	"strings"
 
 	"github.com/MichaelMure/git-bug/identity"
-
 	"github.com/MichaelMure/git-bug/util/git"
 	"github.com/MichaelMure/git-bug/util/text"
 )
@@ -15,9 +15,9 @@ var _ Operation = &CreateOperation{}
 // CreateOperation define the initial creation of a bug
 type CreateOperation struct {
 	OpBase
-	Title   string     `json:"title"`
-	Message string     `json:"message"`
-	Files   []git.Hash `json:"files"`
+	Title   string
+	Message string
+	Files   []git.Hash
 }
 
 func (op *CreateOperation) base() *OpBase {
@@ -83,6 +83,57 @@ func (op *CreateOperation) Validate() error {
 	return nil
 }
 
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *CreateOperation) MarshalJSON() ([]byte, error) {
+	base, err := json.Marshal(op.OpBase)
+	if err != nil {
+		return nil, err
+	}
+
+	// revert back to a flat map to be able to add our own fields
+	var data map[string]interface{}
+	if err := json.Unmarshal(base, &data); err != nil {
+		return nil, err
+	}
+
+	data["title"] = op.Title
+	data["message"] = op.Message
+	data["files"] = op.Files
+
+	return json.Marshal(data)
+}
+
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *CreateOperation) UnmarshalJSON(data []byte) error {
+	// Unmarshal OpBase and the op separately
+
+	base := OpBase{}
+	err := json.Unmarshal(data, &base)
+	if err != nil {
+		return err
+	}
+
+	aux := struct {
+		Title   string     `json:"title"`
+		Message string     `json:"message"`
+		Files   []git.Hash `json:"files"`
+	}{}
+
+	err = json.Unmarshal(data, &aux)
+	if err != nil {
+		return err
+	}
+
+	op.OpBase = base
+	op.Title = aux.Title
+	op.Message = aux.Message
+	op.Files = aux.Files
+
+	return nil
+}
+
 // Sign post method for gqlgen
 func (op *CreateOperation) IsAuthored() {}
 

bug/op_create_test.go πŸ”—

@@ -1,11 +1,13 @@
 package bug
 
 import (
+	"encoding/json"
 	"testing"
 	"time"
 
 	"github.com/MichaelMure/git-bug/identity"
 	"github.com/go-test/deep"
+	"github.com/stretchr/testify/assert"
 )
 
 func TestCreate(t *testing.T) {
@@ -45,3 +47,18 @@ func TestCreate(t *testing.T) {
 		t.Fatal(diff)
 	}
 }
+
+func TestCreateSerialize(t *testing.T) {
+	var rene = identity.NewBare("RenΓ© Descartes", "rene@descartes.fr")
+	unix := time.Now().Unix()
+	before := NewCreateOp(rene, unix, "title", "message", nil)
+
+	data, err := json.Marshal(before)
+	assert.NoError(t, err)
+
+	var after CreateOperation
+	err = json.Unmarshal(data, &after)
+	assert.NoError(t, err)
+
+	assert.Equal(t, before, &after)
+}

bug/op_edit_comment.go πŸ”—

@@ -1,6 +1,7 @@
 package bug
 
 import (
+	"encoding/json"
 	"fmt"
 
 	"github.com/MichaelMure/git-bug/identity"
@@ -14,9 +15,9 @@ var _ Operation = &EditCommentOperation{}
 // EditCommentOperation will change a comment in the bug
 type EditCommentOperation struct {
 	OpBase
-	Target  git.Hash   `json:"target"`
-	Message string     `json:"message"`
-	Files   []git.Hash `json:"files"`
+	Target  git.Hash
+	Message string
+	Files   []git.Hash
 }
 
 func (op *EditCommentOperation) base() *OpBase {
@@ -94,6 +95,57 @@ func (op *EditCommentOperation) Validate() error {
 	return nil
 }
 
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *EditCommentOperation) MarshalJSON() ([]byte, error) {
+	base, err := json.Marshal(op.OpBase)
+	if err != nil {
+		return nil, err
+	}
+
+	// revert back to a flat map to be able to add our own fields
+	var data map[string]interface{}
+	if err := json.Unmarshal(base, &data); err != nil {
+		return nil, err
+	}
+
+	data["target"] = op.Target
+	data["message"] = op.Message
+	data["files"] = op.Files
+
+	return json.Marshal(data)
+}
+
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *EditCommentOperation) UnmarshalJSON(data []byte) error {
+	// Unmarshal OpBase and the op separately
+
+	base := OpBase{}
+	err := json.Unmarshal(data, &base)
+	if err != nil {
+		return err
+	}
+
+	aux := struct {
+		Target  git.Hash   `json:"target"`
+		Message string     `json:"message"`
+		Files   []git.Hash `json:"files"`
+	}{}
+
+	err = json.Unmarshal(data, &aux)
+	if err != nil {
+		return err
+	}
+
+	op.OpBase = base
+	op.Target = aux.Target
+	op.Message = aux.Message
+	op.Files = aux.Files
+
+	return nil
+}
+
 // Sign post method for gqlgen
 func (op *EditCommentOperation) IsAuthored() {}
 

bug/op_edit_comment_test.go πŸ”—

@@ -1,11 +1,12 @@
 package bug
 
 import (
+	"encoding/json"
 	"testing"
 	"time"
 
 	"github.com/MichaelMure/git-bug/identity"
-	"gotest.tools/assert"
+	"github.com/stretchr/testify/assert"
 )
 
 func TestEdit(t *testing.T) {
@@ -49,3 +50,18 @@ func TestEdit(t *testing.T) {
 	assert.Equal(t, snapshot.Comments[0].Message, "create edited")
 	assert.Equal(t, snapshot.Comments[1].Message, "comment edited")
 }
+
+func TestEditCommentSerialize(t *testing.T) {
+	var rene = identity.NewBare("RenΓ© Descartes", "rene@descartes.fr")
+	unix := time.Now().Unix()
+	before := NewEditCommentOp(rene, unix, "target", "message", nil)
+
+	data, err := json.Marshal(before)
+	assert.NoError(t, err)
+
+	var after EditCommentOperation
+	err = json.Unmarshal(data, &after)
+	assert.NoError(t, err)
+
+	assert.Equal(t, before, &after)
+}

bug/op_label_change.go πŸ”—

@@ -1,6 +1,7 @@
 package bug
 
 import (
+	"encoding/json"
 	"fmt"
 	"sort"
 
@@ -15,8 +16,8 @@ var _ Operation = &LabelChangeOperation{}
 // LabelChangeOperation define a Bug operation to add or remove labels
 type LabelChangeOperation struct {
 	OpBase
-	Added   []Label `json:"added"`
-	Removed []Label `json:"removed"`
+	Added   []Label
+	Removed []Label
 }
 
 func (op *LabelChangeOperation) base() *OpBase {
@@ -99,6 +100,54 @@ func (op *LabelChangeOperation) Validate() error {
 	return nil
 }
 
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *LabelChangeOperation) MarshalJSON() ([]byte, error) {
+	base, err := json.Marshal(op.OpBase)
+	if err != nil {
+		return nil, err
+	}
+
+	// revert back to a flat map to be able to add our own fields
+	var data map[string]interface{}
+	if err := json.Unmarshal(base, &data); err != nil {
+		return nil, err
+	}
+
+	data["added"] = op.Added
+	data["removed"] = op.Removed
+
+	return json.Marshal(data)
+}
+
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *LabelChangeOperation) UnmarshalJSON(data []byte) error {
+	// Unmarshal OpBase and the op separately
+
+	base := OpBase{}
+	err := json.Unmarshal(data, &base)
+	if err != nil {
+		return err
+	}
+
+	aux := struct {
+		Added   []Label `json:"added"`
+		Removed []Label `json:"removed"`
+	}{}
+
+	err = json.Unmarshal(data, &aux)
+	if err != nil {
+		return err
+	}
+
+	op.OpBase = base
+	op.Added = aux.Added
+	op.Removed = aux.Removed
+
+	return nil
+}
+
 // Sign post method for gqlgen
 func (op *LabelChangeOperation) IsAuthored() {}
 

bug/op_label_change_test.go πŸ”—

@@ -0,0 +1,25 @@
+package bug
+
+import (
+	"encoding/json"
+	"testing"
+	"time"
+
+	"github.com/MichaelMure/git-bug/identity"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestLabelChangeSerialize(t *testing.T) {
+	var rene = identity.NewBare("RenΓ© Descartes", "rene@descartes.fr")
+	unix := time.Now().Unix()
+	before := NewLabelChangeOperation(rene, unix, []Label{"added"}, []Label{"removed"})
+
+	data, err := json.Marshal(before)
+	assert.NoError(t, err)
+
+	var after LabelChangeOperation
+	err = json.Unmarshal(data, &after)
+	assert.NoError(t, err)
+
+	assert.Equal(t, before, &after)
+}

bug/op_noop.go πŸ”—

@@ -1,6 +1,8 @@
 package bug
 
 import (
+	"encoding/json"
+
 	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/util/git"
 )
@@ -30,6 +32,46 @@ func (op *NoOpOperation) Validate() error {
 	return opBaseValidate(op, NoOpOp)
 }
 
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *NoOpOperation) MarshalJSON() ([]byte, error) {
+	base, err := json.Marshal(op.OpBase)
+	if err != nil {
+		return nil, err
+	}
+
+	// revert back to a flat map to be able to add our own fields
+	var data map[string]interface{}
+	if err := json.Unmarshal(base, &data); err != nil {
+		return nil, err
+	}
+
+	return json.Marshal(data)
+}
+
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *NoOpOperation) UnmarshalJSON(data []byte) error {
+	// Unmarshal OpBase and the op separately
+
+	base := OpBase{}
+	err := json.Unmarshal(data, &base)
+	if err != nil {
+		return err
+	}
+
+	aux := struct{}{}
+
+	err = json.Unmarshal(data, &aux)
+	if err != nil {
+		return err
+	}
+
+	op.OpBase = base
+
+	return nil
+}
+
 // Sign post method for gqlgen
 func (op *NoOpOperation) IsAuthored() {}
 

bug/op_noop_test.go πŸ”—

@@ -0,0 +1,25 @@
+package bug
+
+import (
+	"encoding/json"
+	"testing"
+	"time"
+
+	"github.com/MichaelMure/git-bug/identity"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNoopSerialize(t *testing.T) {
+	var rene = identity.NewBare("RenΓ© Descartes", "rene@descartes.fr")
+	unix := time.Now().Unix()
+	before := NewNoOpOp(rene, unix)
+
+	data, err := json.Marshal(before)
+	assert.NoError(t, err)
+
+	var after NoOpOperation
+	err = json.Unmarshal(data, &after)
+	assert.NoError(t, err)
+
+	assert.Equal(t, before, &after)
+}

bug/op_set_metadata.go πŸ”—

@@ -1,6 +1,8 @@
 package bug
 
 import (
+	"encoding/json"
+
 	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/util/git"
 )
@@ -9,8 +11,8 @@ var _ Operation = &SetMetadataOperation{}
 
 type SetMetadataOperation struct {
 	OpBase
-	Target      git.Hash          `json:"target"`
-	NewMetadata map[string]string `json:"new_metadata"`
+	Target      git.Hash
+	NewMetadata map[string]string
 }
 
 func (op *SetMetadataOperation) base() *OpBase {
@@ -56,6 +58,54 @@ func (op *SetMetadataOperation) Validate() error {
 	return nil
 }
 
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *SetMetadataOperation) MarshalJSON() ([]byte, error) {
+	base, err := json.Marshal(op.OpBase)
+	if err != nil {
+		return nil, err
+	}
+
+	// revert back to a flat map to be able to add our own fields
+	var data map[string]interface{}
+	if err := json.Unmarshal(base, &data); err != nil {
+		return nil, err
+	}
+
+	data["target"] = op.Target
+	data["new_metadata"] = op.NewMetadata
+
+	return json.Marshal(data)
+}
+
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *SetMetadataOperation) UnmarshalJSON(data []byte) error {
+	// Unmarshal OpBase and the op separately
+
+	base := OpBase{}
+	err := json.Unmarshal(data, &base)
+	if err != nil {
+		return err
+	}
+
+	aux := struct {
+		Target      git.Hash          `json:"target"`
+		NewMetadata map[string]string `json:"new_metadata"`
+	}{}
+
+	err = json.Unmarshal(data, &aux)
+	if err != nil {
+		return err
+	}
+
+	op.OpBase = base
+	op.Target = aux.Target
+	op.NewMetadata = aux.NewMetadata
+
+	return nil
+}
+
 // Sign post method for gqlgen
 func (op *SetMetadataOperation) IsAuthored() {}
 

bug/op_set_metadata_test.go πŸ”—

@@ -1,6 +1,7 @@
 package bug
 
 import (
+	"encoding/json"
 	"testing"
 	"time"
 
@@ -94,3 +95,21 @@ func TestSetMetadata(t *testing.T) {
 	assert.Equal(t, commentMetadata["key2"], "value2")
 	assert.Equal(t, commentMetadata["key3"], "value3")
 }
+
+func TestSetMetadataSerialize(t *testing.T) {
+	var rene = identity.NewBare("RenΓ© Descartes", "rene@descartes.fr")
+	unix := time.Now().Unix()
+	before := NewSetMetadataOp(rene, unix, "message", map[string]string{
+		"key1": "value1",
+		"key2": "value2",
+	})
+
+	data, err := json.Marshal(before)
+	assert.NoError(t, err)
+
+	var after SetMetadataOperation
+	err = json.Unmarshal(data, &after)
+	assert.NoError(t, err)
+
+	assert.Equal(t, before, &after)
+}

bug/op_set_status.go πŸ”—

@@ -1,6 +1,8 @@
 package bug
 
 import (
+	"encoding/json"
+
 	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/util/git"
 	"github.com/pkg/errors"
@@ -11,7 +13,7 @@ var _ Operation = &SetStatusOperation{}
 // SetStatusOperation will change the status of a bug
 type SetStatusOperation struct {
 	OpBase
-	Status Status `json:"status"`
+	Status Status
 }
 
 func (op *SetStatusOperation) base() *OpBase {
@@ -54,6 +56,51 @@ func (op *SetStatusOperation) Validate() error {
 	return nil
 }
 
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *SetStatusOperation) MarshalJSON() ([]byte, error) {
+	base, err := json.Marshal(op.OpBase)
+	if err != nil {
+		return nil, err
+	}
+
+	// revert back to a flat map to be able to add our own fields
+	var data map[string]interface{}
+	if err := json.Unmarshal(base, &data); err != nil {
+		return nil, err
+	}
+
+	data["status"] = op.Status
+
+	return json.Marshal(data)
+}
+
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *SetStatusOperation) UnmarshalJSON(data []byte) error {
+	// Unmarshal OpBase and the op separately
+
+	base := OpBase{}
+	err := json.Unmarshal(data, &base)
+	if err != nil {
+		return err
+	}
+
+	aux := struct {
+		Status Status `json:"status"`
+	}{}
+
+	err = json.Unmarshal(data, &aux)
+	if err != nil {
+		return err
+	}
+
+	op.OpBase = base
+	op.Status = aux.Status
+
+	return nil
+}
+
 // Sign post method for gqlgen
 func (op *SetStatusOperation) IsAuthored() {}
 

bug/op_set_status_test.go πŸ”—

@@ -0,0 +1,25 @@
+package bug
+
+import (
+	"encoding/json"
+	"testing"
+	"time"
+
+	"github.com/MichaelMure/git-bug/identity"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestSetStatusSerialize(t *testing.T) {
+	var rene = identity.NewBare("RenΓ© Descartes", "rene@descartes.fr")
+	unix := time.Now().Unix()
+	before := NewSetStatusOp(rene, unix, ClosedStatus)
+
+	data, err := json.Marshal(before)
+	assert.NoError(t, err)
+
+	var after SetStatusOperation
+	err = json.Unmarshal(data, &after)
+	assert.NoError(t, err)
+
+	assert.Equal(t, before, &after)
+}

bug/op_set_title.go πŸ”—

@@ -1,6 +1,7 @@
 package bug
 
 import (
+	"encoding/json"
 	"fmt"
 	"strings"
 
@@ -15,8 +16,8 @@ var _ Operation = &SetTitleOperation{}
 // SetTitleOperation will change the title of a bug
 type SetTitleOperation struct {
 	OpBase
-	Title string `json:"title"`
-	Was   string `json:"was"`
+	Title string
+	Was   string
 }
 
 func (op *SetTitleOperation) base() *OpBase {
@@ -76,6 +77,54 @@ func (op *SetTitleOperation) Validate() error {
 	return nil
 }
 
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *SetTitleOperation) MarshalJSON() ([]byte, error) {
+	base, err := json.Marshal(op.OpBase)
+	if err != nil {
+		return nil, err
+	}
+
+	// revert back to a flat map to be able to add our own fields
+	var data map[string]interface{}
+	if err := json.Unmarshal(base, &data); err != nil {
+		return nil, err
+	}
+
+	data["title"] = op.Title
+	data["was"] = op.Was
+
+	return json.Marshal(data)
+}
+
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *SetTitleOperation) UnmarshalJSON(data []byte) error {
+	// Unmarshal OpBase and the op separately
+
+	base := OpBase{}
+	err := json.Unmarshal(data, &base)
+	if err != nil {
+		return err
+	}
+
+	aux := struct {
+		Title string `json:"title"`
+		Was   string `json:"was"`
+	}{}
+
+	err = json.Unmarshal(data, &aux)
+	if err != nil {
+		return err
+	}
+
+	op.OpBase = base
+	op.Title = aux.Title
+	op.Was = aux.Was
+
+	return nil
+}
+
 // Sign post method for gqlgen
 func (op *SetTitleOperation) IsAuthored() {}
 

bug/op_set_title_test.go πŸ”—

@@ -0,0 +1,25 @@
+package bug
+
+import (
+	"encoding/json"
+	"testing"
+	"time"
+
+	"github.com/MichaelMure/git-bug/identity"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestSetTitleSerialize(t *testing.T) {
+	var rene = identity.NewBare("RenΓ© Descartes", "rene@descartes.fr")
+	unix := time.Now().Unix()
+	before := NewSetTitleOp(rene, unix, "title", "was")
+
+	data, err := json.Marshal(before)
+	assert.NoError(t, err)
+
+	var after SetTitleOperation
+	err = json.Unmarshal(data, &after)
+	assert.NoError(t, err)
+
+	assert.Equal(t, before, &after)
+}

bug/operation.go πŸ”—

@@ -76,8 +76,6 @@ func hashOperation(op Operation) (git.Hash, error) {
 	return base.hash, nil
 }
 
-// TODO: serialization with identity
-
 // OpBase implement the common code for all operations
 type OpBase struct {
 	OperationType OperationType
@@ -100,28 +98,40 @@ func newOpBase(opType OperationType, author identity.Interface, unixTime int64)
 	}
 }
 
-type opBaseJson struct {
-	OperationType OperationType     `json:"type"`
-	UnixTime      int64             `json:"timestamp"`
-	Metadata      map[string]string `json:"metadata,omitempty"`
-}
-
-func (op *OpBase) MarshalJSON() ([]byte, error) {
-	return json.Marshal(opBaseJson{
+func (op OpBase) MarshalJSON() ([]byte, error) {
+	return json.Marshal(struct {
+		OperationType OperationType      `json:"type"`
+		Author        identity.Interface `json:"author"`
+		UnixTime      int64              `json:"timestamp"`
+		Metadata      map[string]string  `json:"metadata,omitempty"`
+	}{
 		OperationType: op.OperationType,
+		Author:        op.Author,
 		UnixTime:      op.UnixTime,
 		Metadata:      op.Metadata,
 	})
 }
 
 func (op *OpBase) UnmarshalJSON(data []byte) error {
-	aux := opBaseJson{}
+	aux := struct {
+		OperationType OperationType     `json:"type"`
+		Author        json.RawMessage   `json:"author"`
+		UnixTime      int64             `json:"timestamp"`
+		Metadata      map[string]string `json:"metadata,omitempty"`
+	}{}
 
 	if err := json.Unmarshal(data, &aux); err != nil {
 		return err
 	}
 
+	// delegate the decoding of the identity
+	author, err := identity.UnmarshalJSON(aux.Author)
+	if err != nil {
+		return err
+	}
+
 	op.OperationType = aux.OperationType
+	op.Author = author
 	op.UnixTime = aux.UnixTime
 	op.Metadata = aux.Metadata
 
@@ -149,10 +159,6 @@ func opBaseValidate(op Operation, opType OperationType) error {
 		return fmt.Errorf("incorrect operation type (expected: %v, actual: %v)", opType, op.base().OperationType)
 	}
 
-	if _, err := op.Hash(); err != nil {
-		return errors.Wrap(err, "op is not serializable")
-	}
-
 	if op.GetUnixTime() == 0 {
 		return fmt.Errorf("time not set")
 	}

bug/operation_iterator_test.go πŸ”—

@@ -3,6 +3,8 @@ package bug
 import (
 	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/repository"
+	"github.com/stretchr/testify/assert"
+
 	"testing"
 	"time"
 )
@@ -29,13 +31,15 @@ func TestOpIterator(t *testing.T) {
 	bug1.Append(addCommentOp)
 	bug1.Append(setStatusOp)
 	bug1.Append(labelChangeOp)
-	bug1.Commit(mockRepo)
+	err := bug1.Commit(mockRepo)
+	assert.NoError(t, err)
 
 	// second pack
 	bug1.Append(setTitleOp)
 	bug1.Append(setTitleOp)
 	bug1.Append(setTitleOp)
-	bug1.Commit(mockRepo)
+	err = bug1.Commit(mockRepo)
+	assert.NoError(t, err)
 
 	// staging
 	bug1.Append(setTitleOp)

bug/operation_pack.go πŸ”—

@@ -139,6 +139,14 @@ func (opp *OperationPack) Validate() error {
 // Write will serialize and store the OperationPack as a git blob and return
 // its hash
 func (opp *OperationPack) Write(repo repository.Repo) (git.Hash, error) {
+	// First, make sure that all the identities are properly Commit as well
+	for _, op := range opp.Operations {
+		err := op.base().Author.Commit(repo)
+		if err != nil {
+			return "", err
+		}
+	}
+
 	data, err := json.Marshal(opp)
 
 	if err != nil {

identity/bare.go πŸ”—

@@ -1,20 +1,25 @@
 package identity
 
 import (
+	"crypto/sha256"
 	"encoding/json"
 	"fmt"
 	"strings"
 
+	"github.com/MichaelMure/git-bug/repository"
 	"github.com/MichaelMure/git-bug/util/lamport"
 	"github.com/MichaelMure/git-bug/util/text"
 )
 
+var _ Interface = &Bare{}
+
 // Bare is a very minimal identity, designed to be fully embedded directly along
 // other data.
 //
 // in particular, this identity is designed to be compatible with the handling of
 // identities in the early version of git-bug.
 type Bare struct {
+	id        string
 	name      string
 	email     string
 	login     string
@@ -36,7 +41,7 @@ type bareIdentityJson struct {
 	AvatarUrl string `json:"avatar_url,omitempty"`
 }
 
-func (i Bare) MarshalJSON() ([]byte, error) {
+func (i *Bare) MarshalJSON() ([]byte, error) {
 	return json.Marshal(bareIdentityJson{
 		Name:      i.name,
 		Email:     i.email,
@@ -45,7 +50,7 @@ func (i Bare) MarshalJSON() ([]byte, error) {
 	})
 }
 
-func (i Bare) UnmarshalJSON(data []byte) error {
+func (i *Bare) UnmarshalJSON(data []byte) error {
 	aux := bareIdentityJson{}
 
 	if err := json.Unmarshal(data, &aux); err != nil {
@@ -60,35 +65,54 @@ func (i Bare) UnmarshalJSON(data []byte) error {
 	return nil
 }
 
-func (i Bare) Name() string {
+func (i *Bare) Id() string {
+	// We don't have a proper ID at hand, so let's hash all the data to get one.
+	// Hopefully the
+
+	if i.id != "" {
+		return i.id
+	}
+
+	data, err := json.Marshal(i)
+	if err != nil {
+		panic(err)
+	}
+
+	h := fmt.Sprintf("%x", sha256.New().Sum(data)[:16])
+	i.id = string(h)
+
+	return i.id
+}
+
+func (i *Bare) Name() string {
 	return i.name
 }
 
-func (i Bare) Email() string {
+func (i *Bare) Email() string {
 	return i.email
 }
 
-func (i Bare) Login() string {
+func (i *Bare) Login() string {
 	return i.login
 }
 
-func (i Bare) AvatarUrl() string {
+func (i *Bare) AvatarUrl() string {
 	return i.avatarUrl
 }
 
 // Keys return the last version of the valid keys
-func (i Bare) Keys() []Key {
+func (i *Bare) Keys() []Key {
 	return []Key{}
 }
 
 // ValidKeysAtTime return the set of keys valid at a given lamport time
-func (i Bare) ValidKeysAtTime(time lamport.Time) []Key {
+func (i *Bare) ValidKeysAtTime(time lamport.Time) []Key {
 	return []Key{}
 }
 
 // DisplayName return a non-empty string to display, representing the
 // identity, based on the non-empty values.
-func (i Bare) DisplayName() string {
+func (i *Bare) DisplayName() string {
 	switch {
 	case i.name == "" && i.login != "":
 		return i.login
@@ -102,7 +126,7 @@ func (i Bare) DisplayName() string {
 }
 
 // Match tell is the Person match the given query string
-func (i Bare) Match(query string) bool {
+func (i *Bare) Match(query string) bool {
 	query = strings.ToLower(query)
 
 	return strings.Contains(strings.ToLower(i.name), query) ||
@@ -110,7 +134,7 @@ func (i Bare) Match(query string) bool {
 }
 
 // Validate check if the Identity data is valid
-func (i Bare) Validate() error {
+func (i *Bare) Validate() error {
 	if text.Empty(i.name) && text.Empty(i.login) {
 		return fmt.Errorf("either name or login should be set")
 	}
@@ -146,8 +170,15 @@ func (i Bare) Validate() error {
 	return nil
 }
 
+// Write the identity into the Repository. In particular, this ensure that
+// the Id is properly set.
+func (i *Bare) Commit(repo repository.Repo) error {
+	// Nothing to do, everything is directly embedded
+	return nil
+}
+
 // IsProtected return true if the chain of git commits started to be signed.
 // If that's the case, only signed commit with a valid key for this identity can be added.
-func (i Bare) IsProtected() bool {
+func (i *Bare) IsProtected() bool {
 	return false
 }

identity/bare_test.go πŸ”—

@@ -0,0 +1,13 @@
+package identity
+
+import (
+	"testing"
+
+	"github.com/magiconair/properties/assert"
+)
+
+func TestBare_Id(t *testing.T) {
+	i := NewBare("name", "email")
+	id := i.Id()
+	assert.Equal(t, "7b226e616d65223a226e616d65222c22", id)
+}

identity/common.go πŸ”—

@@ -0,0 +1,53 @@
+package identity
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"strings"
+)
+
+var ErrIdentityNotExist = errors.New("identity doesn't exist")
+
+type ErrMultipleMatch struct {
+	Matching []string
+}
+
+func (e ErrMultipleMatch) Error() string {
+	return fmt.Sprintf("Multiple matching identities found:\n%s", strings.Join(e.Matching, "\n"))
+}
+
+// Custom unmarshaling function to allow package user to delegate
+// the decoding of an Identity and distinguish between an Identity
+// and a Bare.
+//
+// If the given message has a "id" field, it's considered being a proper Identity.
+func UnmarshalJSON(raw json.RawMessage) (Interface, error) {
+	// First try to decode as a normal Identity
+	var i Identity
+
+	err := json.Unmarshal(raw, &i)
+	if err == nil && i.id != "" {
+		return &i, nil
+	}
+
+	// abort if we have an error other than the wrong type
+	if _, ok := err.(*json.UnmarshalTypeError); err != nil && !ok {
+		return nil, err
+	}
+
+	// Fallback on a legacy Bare identity
+	var b Bare
+
+	err = json.Unmarshal(raw, &b)
+	if err == nil && (b.name != "" || b.login != "") {
+		return &b, nil
+	}
+
+	// abort if we have an error other than the wrong type
+	if _, ok := err.(*json.UnmarshalTypeError); err != nil && !ok {
+		return nil, err
+	}
+
+	return nil, fmt.Errorf("unknown identity type")
+}

identity/identity.go πŸ”—

@@ -16,16 +16,6 @@ const identityRefPattern = "refs/identities/"
 const versionEntryName = "version"
 const identityConfigKey = "git-bug.identity"
 
-var ErrIdentityNotExist = errors.New("identity doesn't exist")
-
-type ErrMultipleMatch struct {
-	Matching []string
-}
-
-func (e ErrMultipleMatch) Error() string {
-	return fmt.Sprintf("Multiple matching identities found:\n%s", strings.Join(e.Matching, "\n"))
-}
-
 var _ Interface = &Identity{}
 
 type Identity struct {
@@ -85,8 +75,6 @@ func (i *Identity) UnmarshalJSON(data []byte) error {
 	return nil
 }
 
-// TODO: load/write from OpBase
-
 // Read load an Identity from the identities data available in git
 func Read(repo repository.Repo, id string) (*Identity, error) {
 	i := &Identity{
@@ -230,7 +218,9 @@ func (i *Identity) AddVersion(version *Version) {
 	i.Versions = append(i.Versions, version)
 }
 
-func (i *Identity) Commit(repo repository.ClockedRepo) error {
+// Write the identity into the Repository. In particular, this ensure that
+// the Id is properly set.
+func (i *Identity) Commit(repo repository.Repo) error {
 	// Todo: check for mismatch between memory and commited data
 
 	var lastCommit git.Hash = ""

identity/interface.go πŸ”—

@@ -1,6 +1,7 @@
 package identity
 
 import (
+	"github.com/MichaelMure/git-bug/repository"
 	"github.com/MichaelMure/git-bug/util/lamport"
 )
 
@@ -28,6 +29,10 @@ type Interface interface {
 	// Validate check if the Identity data is valid
 	Validate() error
 
+	// Write the identity into the Repository. In particular, this ensure that
+	// the Id is properly set.
+	Commit(repo repository.Repo) error
+
 	// IsProtected return true if the chain of git commits started to be signed.
 	// If that's the case, only signed commit with a valid key for this identity can be added.
 	IsProtected() bool

util/git/hash.go πŸ”—

@@ -30,7 +30,7 @@ func (h *Hash) UnmarshalGQL(v interface{}) error {
 
 // MarshalGQL implement the Marshaler interface for gqlgen
 func (h Hash) MarshalGQL(w io.Writer) {
-	w.Write([]byte(`"` + h.String() + `"`))
+	_, _ = w.Write([]byte(`"` + h.String() + `"`))
 }
 
 // IsValid tell if the hash is valid