diff options
-rw-r--r-- | bug/bug_actions_test.go | 149 | ||||
-rw-r--r-- | bug/bug_test.go | 6 | ||||
-rw-r--r-- | bug/op_add_comment.go | 54 | ||||
-rw-r--r-- | bug/op_add_comment_test.go | 25 | ||||
-rw-r--r-- | bug/op_create.go | 59 | ||||
-rw-r--r-- | bug/op_create_test.go | 17 | ||||
-rw-r--r-- | bug/op_edit_comment.go | 58 | ||||
-rw-r--r-- | bug/op_edit_comment_test.go | 18 | ||||
-rw-r--r-- | bug/op_label_change.go | 53 | ||||
-rw-r--r-- | bug/op_label_change_test.go | 25 | ||||
-rw-r--r-- | bug/op_noop.go | 42 | ||||
-rw-r--r-- | bug/op_noop_test.go | 25 | ||||
-rw-r--r-- | bug/op_set_metadata.go | 54 | ||||
-rw-r--r-- | bug/op_set_metadata_test.go | 19 | ||||
-rw-r--r-- | bug/op_set_status.go | 49 | ||||
-rw-r--r-- | bug/op_set_status_test.go | 25 | ||||
-rw-r--r-- | bug/op_set_title.go | 53 | ||||
-rw-r--r-- | bug/op_set_title_test.go | 25 | ||||
-rw-r--r-- | bug/operation.go | 36 | ||||
-rw-r--r-- | bug/operation_iterator_test.go | 8 | ||||
-rw-r--r-- | bug/operation_pack.go | 8 | ||||
-rw-r--r-- | identity/bare.go | 55 | ||||
-rw-r--r-- | identity/bare_test.go | 13 | ||||
-rw-r--r-- | identity/common.go | 53 | ||||
-rw-r--r-- | identity/identity.go | 16 | ||||
-rw-r--r-- | identity/interface.go | 5 | ||||
-rw-r--r-- | util/git/hash.go | 2 |
27 files changed, 813 insertions, 139 deletions
diff --git a/bug/bug_actions_test.go b/bug/bug_actions_test.go index 4327ae58..95ca01c9 100644 --- a/bug/bug_actions_test.go +++ b/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") diff --git a/bug/bug_test.go b/bug/bug_test.go index 0fd373d5..41a5b03d 100644 --- a/bug/bug_test.go +++ b/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) } diff --git a/bug/op_add_comment.go b/bug/op_add_comment.go index 23a10419..ba5d611e 100644 --- a/bug/op_add_comment.go +++ b/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() {} diff --git a/bug/op_add_comment_test.go b/bug/op_add_comment_test.go new file mode 100644 index 00000000..a38d0228 --- /dev/null +++ b/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) +} diff --git a/bug/op_create.go b/bug/op_create.go index 01b2bf03..1d157e67 100644 --- a/bug/op_create.go +++ b/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() {} diff --git a/bug/op_create_test.go b/bug/op_create_test.go index aff58acc..31693a4a 100644 --- a/bug/op_create_test.go +++ b/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) +} diff --git a/bug/op_edit_comment.go b/bug/op_edit_comment.go index 9e0afc02..3ff16653 100644 --- a/bug/op_edit_comment.go +++ b/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() {} diff --git a/bug/op_edit_comment_test.go b/bug/op_edit_comment_test.go index 7eee2fc1..dbdf341d 100644 --- a/bug/op_edit_comment_test.go +++ b/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) +} diff --git a/bug/op_label_change.go b/bug/op_label_change.go index 5d0b6a78..b0dd2c33 100644 --- a/bug/op_label_change.go +++ b/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() {} diff --git a/bug/op_label_change_test.go b/bug/op_label_change_test.go new file mode 100644 index 00000000..f5550b72 --- /dev/null +++ b/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) +} diff --git a/bug/op_noop.go b/bug/op_noop.go index 410799b3..fbc112a8 100644 --- a/bug/op_noop.go +++ b/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() {} diff --git a/bug/op_noop_test.go b/bug/op_noop_test.go new file mode 100644 index 00000000..385bc914 --- /dev/null +++ b/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) +} diff --git a/bug/op_set_metadata.go b/bug/op_set_metadata.go index e18f1cb6..57b78667 100644 --- a/bug/op_set_metadata.go +++ b/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() {} diff --git a/bug/op_set_metadata_test.go b/bug/op_set_metadata_test.go index 6e62c9a3..847164f3 100644 --- a/bug/op_set_metadata_test.go +++ b/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) +} diff --git a/bug/op_set_status.go b/bug/op_set_status.go index 9fc64e52..6deb1675 100644 --- a/bug/op_set_status.go +++ b/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() {} diff --git a/bug/op_set_status_test.go b/bug/op_set_status_test.go new file mode 100644 index 00000000..2506b947 --- /dev/null +++ b/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) +} diff --git a/bug/op_set_title.go b/bug/op_set_title.go index 3b253c06..ae6484c6 100644 --- a/bug/op_set_title.go +++ b/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() {} diff --git a/bug/op_set_title_test.go b/bug/op_set_title_test.go new file mode 100644 index 00000000..1f730596 --- /dev/null +++ b/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) +} diff --git a/bug/operation.go b/bug/operation.go index 8dec5644..cc5b0007 100644 --- a/bug/operation.go +++ b/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") } diff --git a/bug/operation_iterator_test.go b/bug/operation_iterator_test.go index b8e1bf09..e1aa8911 100644 --- a/bug/operation_iterator_test.go +++ b/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) diff --git a/bug/operation_pack.go b/bug/operation_pack.go index fc395d90..18b2a478 100644 --- a/bug/operation_pack.go +++ b/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 { diff --git a/identity/bare.go b/identity/bare.go index 24f30f9f..729dc2e0 100644 --- a/identity/bare.go +++ b/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 } diff --git a/identity/bare_test.go b/identity/bare_test.go new file mode 100644 index 00000000..1458107a --- /dev/null +++ b/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) +} diff --git a/identity/common.go b/identity/common.go new file mode 100644 index 00000000..32dd3d9e --- /dev/null +++ b/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") +} diff --git a/identity/identity.go b/identity/identity.go index 313e3fd7..38729e37 100644 --- a/identity/identity.go +++ b/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 = "" diff --git a/identity/interface.go b/identity/interface.go index 6489efbe..c784a7a6 100644 --- a/identity/interface.go +++ b/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 diff --git a/util/git/hash.go b/util/git/hash.go index 401e6edc..d9160d75 100644 --- a/util/git/hash.go +++ b/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 |