aboutsummaryrefslogtreecommitdiffstats
path: root/bug
diff options
context:
space:
mode:
authorMichael Muré <batolettre@gmail.com>2022-08-01 17:16:45 +0200
committerGitHub <noreply@github.com>2022-08-01 17:16:45 +0200
commitcd52872475f1b39f3fb6546606c1e78afb6c08e3 (patch)
tree9f04b1e1ee362e210e74aaa324d950058ed14bf2 /bug
parent2ade8fb1d570ddcb4aedc9386af46d208b129daa (diff)
parent56966fec5562c3a0e23340d0fbe754626c3beb64 (diff)
downloadgit-bug-cd52872475f1b39f3fb6546606c1e78afb6c08e3.tar.gz
Merge pull request #835 from MichaelMure/op-base
entity/dag: proper base operation for simplified implementation
Diffstat (limited to 'bug')
-rw-r--r--bug/bug.go16
-rw-r--r--bug/comment.go2
-rw-r--r--bug/interface.go29
-rw-r--r--bug/op_add_comment.go68
-rw-r--r--bug/op_add_comment_test.go33
-rw-r--r--bug/op_create.go103
-rw-r--r--bug/op_create_test.go30
-rw-r--r--bug/op_edit_comment.go70
-rw-r--r--bug/op_edit_comment_test.go30
-rw-r--r--bug/op_label_change.go81
-rw-r--r--bug/op_label_change_test.go37
-rw-r--r--bug/op_noop.go77
-rw-r--r--bug/op_noop_test.go39
-rw-r--r--bug/op_set_metadata.go105
-rw-r--r--bug/op_set_metadata_test.go126
-rw-r--r--bug/op_set_status.go61
-rw-r--r--bug/op_set_status_test.go31
-rw-r--r--bug/op_set_title.go64
-rw-r--r--bug/op_set_title_test.go31
-rw-r--r--bug/operation.go240
-rw-r--r--bug/operation_test.go11
-rw-r--r--bug/snapshot.go11
-rw-r--r--bug/with_snapshot.go9
23 files changed, 205 insertions, 1099 deletions
diff --git a/bug/bug.go b/bug/bug.go
index 48269a91..dce30f76 100644
--- a/bug/bug.go
+++ b/bug/bug.go
@@ -44,11 +44,7 @@ func NewBug() *Bug {
// Read will read a bug from a repository
func Read(repo repository.ClockedRepo, id entity.Id) (*Bug, error) {
- e, err := dag.Read(def, repo, identity.NewSimpleResolver(repo), id)
- if err != nil {
- return nil, err
- }
- return &Bug{Entity: e}, nil
+ return ReadWithResolver(repo, identity.NewSimpleResolver(repo), id)
}
// ReadWithResolver will read a bug from its Id, with a custom identity.Resolver
@@ -144,21 +140,21 @@ func (bug *Bug) Operations() []Operation {
}
// Compile a bug in a easily usable snapshot
-func (bug *Bug) Compile() Snapshot {
- snap := Snapshot{
+func (bug *Bug) Compile() *Snapshot {
+ snap := &Snapshot{
id: bug.Id(),
Status: OpenStatus,
}
for _, op := range bug.Operations() {
- op.Apply(&snap)
+ op.Apply(snap)
snap.Operations = append(snap.Operations, op)
}
return snap
}
-// Lookup for the very first operation of the bug.
+// FirstOp lookup for the very first operation of the bug.
// For a valid Bug, this operation should be a CreateOp
func (bug *Bug) FirstOp() Operation {
if fo := bug.Entity.FirstOp(); fo != nil {
@@ -167,7 +163,7 @@ func (bug *Bug) FirstOp() Operation {
return nil
}
-// Lookup for the very last operation of the bug.
+// LastOp lookup for the very last operation of the bug.
// For a valid Bug, should never be nil
func (bug *Bug) LastOp() Operation {
if lo := bug.Entity.LastOp(); lo != nil {
diff --git a/bug/comment.go b/bug/comment.go
index c1cfc7e5..03d58da5 100644
--- a/bug/comment.go
+++ b/bug/comment.go
@@ -41,5 +41,5 @@ func (c Comment) FormatTime() string {
return c.UnixTime.Time().Format("Mon Jan 2 15:04:05 2006 +0200")
}
-// Sign post method for gqlgen
+// IsAuthored is a sign post method for gqlgen
func (c Comment) IsAuthored() {}
diff --git a/bug/interface.go b/bug/interface.go
index e71496a9..2ae31fd1 100644
--- a/bug/interface.go
+++ b/bug/interface.go
@@ -7,34 +7,34 @@ import (
)
type Interface interface {
- // Id return the Bug identifier
+ // Id returns the Bug identifier
Id() entity.Id
- // Validate check if the Bug data is valid
+ // Validate checks if the Bug data is valid
Validate() error
// Append an operation into the staging area, to be committed later
Append(op Operation)
- // Operations return the ordered operations
+ // Operations returns the ordered operations
Operations() []Operation
- // Indicate that the in-memory state changed and need to be commit in the repository
+ // NeedCommit indicates that the in-memory state changed and need to be commit in the repository
NeedCommit() bool
- // Commit write the staging area in Git and move the operations to the packs
+ // Commit writes the staging area in Git and move the operations to the packs
Commit(repo repository.ClockedRepo) error
- // Lookup for the very first operation of the bug.
+ // FirstOp lookup for the very first operation of the bug.
// For a valid Bug, this operation should be a CreateOp
FirstOp() Operation
- // Lookup for the very last operation of the bug.
+ // LastOp lookup for the very last operation of the bug.
// For a valid Bug, should never be nil
LastOp() Operation
- // Compile a bug in a easily usable snapshot
- Compile() Snapshot
+ // Compile a bug in an easily usable snapshot
+ Compile() *Snapshot
// CreateLamportTime return the Lamport time of creation
CreateLamportTime() lamport.Time
@@ -42,14 +42,3 @@ type Interface interface {
// EditLamportTime return the Lamport time of the last edit
EditLamportTime() lamport.Time
}
-
-func bugFromInterface(bug Interface) *Bug {
- switch bug := bug.(type) {
- case *Bug:
- return bug
- case *WithSnapshot:
- return bug.Bug
- default:
- panic("missing type case")
- }
-}
diff --git a/bug/op_add_comment.go b/bug/op_add_comment.go
index 8eb937cf..eddd585a 100644
--- a/bug/op_add_comment.go
+++ b/bug/op_add_comment.go
@@ -1,7 +1,6 @@
package bug
import (
- "encoding/json"
"fmt"
"github.com/MichaelMure/git-bug/entity"
@@ -17,24 +16,24 @@ var _ dag.OperationWithFiles = &AddCommentOperation{}
// AddCommentOperation will add a new comment in the bug
type AddCommentOperation struct {
- OpBase
+ dag.OpBase
Message string `json:"message"`
// TODO: change for a map[string]util.hash to store the filename ?
Files []repository.Hash `json:"files"`
}
func (op *AddCommentOperation) Id() entity.Id {
- return idOperation(op, &op.OpBase)
+ return dag.IdOperation(op, &op.OpBase)
}
func (op *AddCommentOperation) Apply(snapshot *Snapshot) {
- snapshot.addActor(op.Author_)
- snapshot.addParticipant(op.Author_)
+ snapshot.addActor(op.Author())
+ snapshot.addParticipant(op.Author())
comment := Comment{
id: entity.CombineIds(snapshot.Id(), op.Id()),
Message: op.Message,
- Author: op.Author_,
+ Author: op.Author(),
Files: op.Files,
UnixTime: timestamp.Timestamp(op.UnixTime),
}
@@ -64,64 +63,31 @@ func (op *AddCommentOperation) Validate() error {
return nil
}
-// UnmarshalJSON is a two-steps JSON unmarshalling
-// This workaround is necessary to avoid the inner OpBase.MarshalJSON
-// overriding the outer op's 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 []repository.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() {}
-
func NewAddCommentOp(author identity.Interface, unixTime int64, message string, files []repository.Hash) *AddCommentOperation {
return &AddCommentOperation{
- OpBase: newOpBase(AddCommentOp, author, unixTime),
+ OpBase: dag.NewOpBase(AddCommentOp, author, unixTime),
Message: message,
Files: files,
}
}
-// CreateTimelineItem replace a AddComment operation in the Timeline and hold its edition history
+// AddCommentTimelineItem hold a comment in the timeline
type AddCommentTimelineItem struct {
CommentTimelineItem
}
-// Sign post method for gqlgen
+// IsAuthored is a sign post method for gqlgen
func (a *AddCommentTimelineItem) IsAuthored() {}
-// Convenience function to apply the operation
-func AddComment(b Interface, author identity.Interface, unixTime int64, message string) (*AddCommentOperation, error) {
- return AddCommentWithFiles(b, author, unixTime, message, nil)
-}
-
-func AddCommentWithFiles(b Interface, author identity.Interface, unixTime int64, message string, files []repository.Hash) (*AddCommentOperation, error) {
- addCommentOp := NewAddCommentOp(author, unixTime, message, files)
- if err := addCommentOp.Validate(); err != nil {
+// AddComment is a convenience function to add a comment to a bug
+func AddComment(b Interface, author identity.Interface, unixTime int64, message string, files []repository.Hash, metadata map[string]string) (*AddCommentOperation, error) {
+ op := NewAddCommentOp(author, unixTime, message, files)
+ for key, val := range metadata {
+ op.SetMetadata(key, val)
+ }
+ if err := op.Validate(); err != nil {
return nil, err
}
- b.Append(addCommentOp)
- return addCommentOp, nil
+ b.Append(op)
+ return op, nil
}
diff --git a/bug/op_add_comment_test.go b/bug/op_add_comment_test.go
index 446bdb18..efdf7601 100644
--- a/bug/op_add_comment_test.go
+++ b/bug/op_add_comment_test.go
@@ -1,37 +1,18 @@
package bug
import (
- "encoding/json"
"testing"
- "time"
-
- "github.com/stretchr/testify/require"
+ "github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/repository"
)
func TestAddCommentSerialize(t *testing.T) {
- repo := repository.NewMockRepo()
-
- rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
- require.NoError(t, err)
-
- unix := time.Now().Unix()
- before := NewAddCommentOp(rene, unix, "message", nil)
-
- data, err := json.Marshal(before)
- require.NoError(t, err)
-
- var after AddCommentOperation
- err = json.Unmarshal(data, &after)
- require.NoError(t, err)
-
- // enforce creating the ID
- before.Id()
-
- // Replace the identity as it's not serialized
- after.Author_ = rene
-
- require.Equal(t, before, &after)
+ dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *AddCommentOperation {
+ return NewAddCommentOp(author, unixTime, "message", nil)
+ })
+ dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *AddCommentOperation {
+ return NewAddCommentOp(author, unixTime, "message", []repository.Hash{"hash1", "hash2"})
+ })
}
diff --git a/bug/op_create.go b/bug/op_create.go
index 9c972195..ca4f3d8a 100644
--- a/bug/op_create.go
+++ b/bug/op_create.go
@@ -1,7 +1,6 @@
package bug
import (
- "encoding/json"
"fmt"
"github.com/MichaelMure/git-bug/entity"
@@ -17,53 +16,38 @@ var _ dag.OperationWithFiles = &CreateOperation{}
// CreateOperation define the initial creation of a bug
type CreateOperation struct {
- OpBase
+ dag.OpBase
Title string `json:"title"`
Message string `json:"message"`
Files []repository.Hash `json:"files"`
}
func (op *CreateOperation) Id() entity.Id {
- return idOperation(op, &op.OpBase)
-}
-
-// OVERRIDE
-func (op *CreateOperation) SetMetadata(key string, value string) {
- // sanity check: we make sure we are not in the following scenario:
- // - the bug is created with a first operation
- // - Id() is used
- // - metadata are added, which will change the Id
- // - Id() is used again
-
- if op.id != entity.UnsetId {
- panic("usage of Id() after changing the first operation")
- }
-
- op.OpBase.SetMetadata(key, value)
+ return dag.IdOperation(op, &op.OpBase)
}
func (op *CreateOperation) Apply(snapshot *Snapshot) {
// sanity check: will fail when adding a second Create
if snapshot.id != "" && snapshot.id != entity.UnsetId && snapshot.id != op.Id() {
- panic("adding a second Create operation")
+ return
}
snapshot.id = op.Id()
- snapshot.addActor(op.Author_)
- snapshot.addParticipant(op.Author_)
+ snapshot.addActor(op.Author())
+ snapshot.addParticipant(op.Author())
snapshot.Title = op.Title
comment := Comment{
id: entity.CombineIds(snapshot.Id(), op.Id()),
Message: op.Message,
- Author: op.Author_,
+ Author: op.Author(),
UnixTime: timestamp.Timestamp(op.UnixTime),
}
snapshot.Comments = []Comment{comment}
- snapshot.Author = op.Author_
+ snapshot.Author = op.Author()
snapshot.CreateTime = op.Time()
snapshot.Timeline = []TimelineItem{
@@ -82,13 +66,6 @@ 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")
}
@@ -103,45 +80,9 @@ func (op *CreateOperation) Validate() error {
return nil
}
-// UnmarshalJSON is a two step JSON unmarshalling
-// This workaround is necessary to avoid the inner OpBase.MarshalJSON
-// overriding the outer op's 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 {
- Nonce []byte `json:"nonce"`
- Title string `json:"title"`
- Message string `json:"message"`
- Files []repository.Hash `json:"files"`
- }{}
-
- err = json.Unmarshal(data, &aux)
- if err != nil {
- return err
- }
-
- op.OpBase = base
- op.Nonce = aux.Nonce
- op.Title = aux.Title
- op.Message = aux.Message
- op.Files = aux.Files
-
- return nil
-}
-
-// Sign post method for gqlgen
-func (op *CreateOperation) IsAuthored() {}
-
func NewCreateOp(author identity.Interface, unixTime int64, title, message string, files []repository.Hash) *CreateOperation {
return &CreateOperation{
- OpBase: newOpBase(CreateOp, author, unixTime),
+ OpBase: dag.NewOpBase(CreateOp, author, unixTime),
Title: title,
Message: message,
Files: files,
@@ -153,23 +94,19 @@ type CreateTimelineItem struct {
CommentTimelineItem
}
-// Sign post method for gqlgen
+// IsAuthored is a sign post method for gqlgen
func (c *CreateTimelineItem) IsAuthored() {}
-// Convenience function to apply the operation
-func Create(author identity.Interface, unixTime int64, title, message string) (*Bug, *CreateOperation, error) {
- return CreateWithFiles(author, unixTime, title, message, nil)
-}
-
-func CreateWithFiles(author identity.Interface, unixTime int64, title, message string, files []repository.Hash) (*Bug, *CreateOperation, error) {
- newBug := NewBug()
- createOp := NewCreateOp(author, unixTime, title, message, files)
-
- if err := createOp.Validate(); err != nil {
- return nil, createOp, err
+// Create is a convenience function to create a bug
+func Create(author identity.Interface, unixTime int64, title, message string, files []repository.Hash, metadata map[string]string) (*Bug, *CreateOperation, error) {
+ b := NewBug()
+ op := NewCreateOp(author, unixTime, title, message, files)
+ for key, val := range metadata {
+ op.SetMetadata(key, val)
}
-
- newBug.Append(createOp)
-
- return newBug, createOp, nil
+ if err := op.Validate(); err != nil {
+ return nil, op, err
+ }
+ b.Append(op)
+ return b, op, nil
}
diff --git a/bug/op_create_test.go b/bug/op_create_test.go
index 7696d065..478bc9d4 100644
--- a/bug/op_create_test.go
+++ b/bug/op_create_test.go
@@ -1,13 +1,13 @@
package bug
import (
- "encoding/json"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/MichaelMure/git-bug/entity"
+ "github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/repository"
"github.com/MichaelMure/git-bug/util/timestamp"
@@ -58,26 +58,10 @@ func TestCreate(t *testing.T) {
}
func TestCreateSerialize(t *testing.T) {
- repo := repository.NewMockRepo()
-
- 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)
- require.NoError(t, err)
-
- var after CreateOperation
- err = json.Unmarshal(data, &after)
- require.NoError(t, err)
-
- // enforce creating the ID
- before.Id()
-
- // Replace the identity as it's not serialized
- after.Author_ = rene
-
- require.Equal(t, before, &after)
+ dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *CreateOperation {
+ return NewCreateOp(author, unixTime, "title", "message", nil)
+ })
+ dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *CreateOperation {
+ return NewCreateOp(author, unixTime, "title", "message", []repository.Hash{"hash1", "hash2"})
+ })
}
diff --git a/bug/op_edit_comment.go b/bug/op_edit_comment.go
index 4740f37b..70a362de 100644
--- a/bug/op_edit_comment.go
+++ b/bug/op_edit_comment.go
@@ -1,7 +1,6 @@
package bug
import (
- "encoding/json"
"fmt"
"github.com/pkg/errors"
@@ -20,14 +19,14 @@ var _ dag.OperationWithFiles = &EditCommentOperation{}
// EditCommentOperation will change a comment in the bug
type EditCommentOperation struct {
- OpBase
+ dag.OpBase
Target entity.Id `json:"target"`
Message string `json:"message"`
Files []repository.Hash `json:"files"`
}
func (op *EditCommentOperation) Id() entity.Id {
- return idOperation(op, &op.OpBase)
+ return dag.IdOperation(op, &op.OpBase)
}
func (op *EditCommentOperation) Apply(snapshot *Snapshot) {
@@ -68,7 +67,7 @@ func (op *EditCommentOperation) Apply(snapshot *Snapshot) {
return
}
- snapshot.addActor(op.Author_)
+ snapshot.addActor(op.Author())
// Updating the corresponding comment
@@ -101,43 +100,9 @@ func (op *EditCommentOperation) Validate() error {
return nil
}
-// UnmarshalJSON is two steps JSON unmarshalling
-// This workaround is necessary to avoid the inner OpBase.MarshalJSON
-// overriding the outer op's 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 entity.Id `json:"target"`
- Message string `json:"message"`
- Files []repository.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() {}
-
func NewEditCommentOp(author identity.Interface, unixTime int64, target entity.Id, message string, files []repository.Hash) *EditCommentOperation {
return &EditCommentOperation{
- OpBase: newOpBase(EditCommentOp, author, unixTime),
+ OpBase: dag.NewOpBase(EditCommentOp, author, unixTime),
Target: target,
Message: message,
Files: files,
@@ -145,27 +110,20 @@ func NewEditCommentOp(author identity.Interface, unixTime int64, target entity.I
}
// EditComment is a convenience function to apply the operation
-func EditComment(b Interface, author identity.Interface, unixTime int64, target entity.Id, message string) (*EditCommentOperation, error) {
- return EditCommentWithFiles(b, author, unixTime, target, message, nil)
-}
-
-func EditCommentWithFiles(b Interface, author identity.Interface, unixTime int64, target entity.Id, message string, files []repository.Hash) (*EditCommentOperation, error) {
- editCommentOp := NewEditCommentOp(author, unixTime, target, message, files)
- if err := editCommentOp.Validate(); err != nil {
+func EditComment(b Interface, author identity.Interface, unixTime int64, target entity.Id, message string, files []repository.Hash, metadata map[string]string) (*EditCommentOperation, error) {
+ op := NewEditCommentOp(author, unixTime, target, message, files)
+ for key, val := range metadata {
+ op.SetMetadata(key, val)
+ }
+ if err := op.Validate(); err != nil {
return nil, err
}
- b.Append(editCommentOp)
- return editCommentOp, nil
+ b.Append(op)
+ return op, nil
}
// EditCreateComment is a convenience function to edit the body of a bug (the first comment)
-func EditCreateComment(b Interface, author identity.Interface, unixTime int64, message string) (*EditCommentOperation, error) {
- createOp := b.FirstOp().(*CreateOperation)
- return EditComment(b, author, unixTime, createOp.Id(), message)
-}
-
-// EditCreateCommentWithFiles is a convenience function to edit the body of a bug (the first comment)
-func EditCreateCommentWithFiles(b Interface, author identity.Interface, unixTime int64, message string, files []repository.Hash) (*EditCommentOperation, error) {
+func EditCreateComment(b Interface, author identity.Interface, unixTime int64, message string, files []repository.Hash, metadata map[string]string) (*EditCommentOperation, error) {
createOp := b.FirstOp().(*CreateOperation)
- return EditCommentWithFiles(b, author, unixTime, createOp.Id(), message, files)
+ return EditComment(b, author, unixTime, createOp.Id(), message, files, metadata)
}
diff --git a/bug/op_edit_comment_test.go b/bug/op_edit_comment_test.go
index 62034a0b..780483e4 100644
--- a/bug/op_edit_comment_test.go
+++ b/bug/op_edit_comment_test.go
@@ -1,12 +1,12 @@
package bug
import (
- "encoding/json"
"testing"
"time"
"github.com/stretchr/testify/require"
+ "github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/repository"
)
@@ -75,26 +75,10 @@ func TestEdit(t *testing.T) {
}
func TestEditCommentSerialize(t *testing.T) {
- repo := repository.NewMockRepo()
-
- 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)
- require.NoError(t, err)
-
- var after EditCommentOperation
- err = json.Unmarshal(data, &after)
- require.NoError(t, err)
-
- // enforce creating the ID
- before.Id()
-
- // Replace the identity as it's not serialized
- after.Author_ = rene
-
- require.Equal(t, before, &after)
+ dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *EditCommentOperation {
+ return NewEditCommentOp(author, unixTime, "target", "message", nil)
+ })
+ dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *EditCommentOperation {
+ return NewEditCommentOp(author, unixTime, "target", "message", []repository.Hash{"hash1", "hash2"})
+ })
}
diff --git a/bug/op_label_change.go b/bug/op_label_change.go
index 8b0e5ec8..8bcc7853 100644
--- a/bug/op_label_change.go
+++ b/bug/op_label_change.go
@@ -1,13 +1,13 @@
package bug
import (
- "encoding/json"
"fmt"
"sort"
"github.com/pkg/errors"
"github.com/MichaelMure/git-bug/entity"
+ "github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/util/timestamp"
)
@@ -16,18 +16,18 @@ var _ Operation = &LabelChangeOperation{}
// LabelChangeOperation define a Bug operation to add or remove labels
type LabelChangeOperation struct {
- OpBase
+ dag.OpBase
Added []Label `json:"added"`
Removed []Label `json:"removed"`
}
func (op *LabelChangeOperation) Id() entity.Id {
- return idOperation(op, &op.OpBase)
+ return dag.IdOperation(op, &op.OpBase)
}
-// Apply apply the operation
+// Apply applies the operation
func (op *LabelChangeOperation) Apply(snapshot *Snapshot) {
- snapshot.addActor(op.Author_)
+ snapshot.addActor(op.Author())
// Add in the set
AddLoop:
@@ -59,7 +59,7 @@ AddLoop:
item := &LabelChangeTimelineItem{
id: op.Id(),
- Author: op.Author_,
+ Author: op.Author(),
UnixTime: timestamp.Timestamp(op.UnixTime),
Added: op.Added,
Removed: op.Removed,
@@ -92,41 +92,9 @@ func (op *LabelChangeOperation) Validate() error {
return nil
}
-// UnmarshalJSON is a two step JSON unmarshalling
-// This workaround is necessary to avoid the inner OpBase.MarshalJSON
-// overriding the outer op's 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() {}
-
func NewLabelChangeOperation(author identity.Interface, unixTime int64, added, removed []Label) *LabelChangeOperation {
return &LabelChangeOperation{
- OpBase: newOpBase(LabelChangeOp, author, unixTime),
+ OpBase: dag.NewOpBase(LabelChangeOp, author, unixTime),
Added: added,
Removed: removed,
}
@@ -144,11 +112,11 @@ func (l LabelChangeTimelineItem) Id() entity.Id {
return l.id
}
-// Sign post method for gqlgen
-func (l *LabelChangeTimelineItem) IsAuthored() {}
+// IsAuthored is a sign post method for gqlgen
+func (l LabelChangeTimelineItem) IsAuthored() {}
-// ChangeLabels is a convenience function to apply the operation
-func ChangeLabels(b Interface, author identity.Interface, unixTime int64, add, remove []string) ([]LabelChangeResult, *LabelChangeOperation, error) {
+// ChangeLabels is a convenience function to change labels on a bug
+func ChangeLabels(b Interface, author identity.Interface, unixTime int64, add, remove []string, metadata map[string]string) ([]LabelChangeResult, *LabelChangeOperation, error) {
var added, removed []Label
var results []LabelChangeResult
@@ -196,23 +164,25 @@ func ChangeLabels(b Interface, author identity.Interface, unixTime int64, add, r
return results, nil, fmt.Errorf("no label added or removed")
}
- labelOp := NewLabelChangeOperation(author, unixTime, added, removed)
-
- if err := labelOp.Validate(); err != nil {
+ op := NewLabelChangeOperation(author, unixTime, added, removed)
+ for key, val := range metadata {
+ op.SetMetadata(key, val)
+ }
+ if err := op.Validate(); err != nil {
return nil, nil, err
}
- b.Append(labelOp)
+ b.Append(op)
- return results, labelOp, nil
+ return results, op, nil
}
// ForceChangeLabels is a convenience function to apply the operation
// The difference with ChangeLabels is that no checks of deduplications are done. You are entirely
-// responsible of what you are doing. In the general case, you want to use ChangeLabels instead.
+// responsible for what you are doing. In the general case, you want to use ChangeLabels instead.
// The intended use of this function is to allow importers to create legal but unexpected label changes,
// like removing a label with no information of when it was added before.
-func ForceChangeLabels(b Interface, author identity.Interface, unixTime int64, add, remove []string) (*LabelChangeOperation, error) {
+func ForceChangeLabels(b Interface, author identity.Interface, unixTime int64, add, remove []string, metadata map[string]string) (*LabelChangeOperation, error) {
added := make([]Label, len(add))
for i, str := range add {
added[i] = Label(str)
@@ -223,15 +193,18 @@ func ForceChangeLabels(b Interface, author identity.Interface, unixTime int64, a
removed[i] = Label(str)
}
- labelOp := NewLabelChangeOperation(author, unixTime, added, removed)
+ op := NewLabelChangeOperation(author, unixTime, added, removed)
- if err := labelOp.Validate(); err != nil {
+ for key, val := range metadata {
+ op.SetMetadata(key, val)
+ }
+ if err := op.Validate(); err != nil {
return nil, err
}
- b.Append(labelOp)
+ b.Append(op)
- return labelOp, nil
+ return op, nil
}
func labelExist(labels []Label, label Label) bool {
diff --git a/bug/op_label_change_test.go b/bug/op_label_change_test.go
index 1892724e..2c1d8f62 100644
--- a/bug/op_label_change_test.go
+++ b/bug/op_label_change_test.go
@@ -1,37 +1,20 @@
package bug
import (
- "encoding/json"
"testing"
- "time"
-
- "github.com/stretchr/testify/require"
+ "github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity"
- "github.com/MichaelMure/git-bug/repository"
)
func TestLabelChangeSerialize(t *testing.T) {
- repo := repository.NewMockRepo()
-
- 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)
- require.NoError(t, err)
-
- var after LabelChangeOperation
- err = json.Unmarshal(data, &after)
- require.NoError(t, err)
-
- // enforce creating the ID
- before.Id()
-
- // Replace the identity as it's not serialized
- after.Author_ = rene
-
- require.Equal(t, before, &after)
+ dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *LabelChangeOperation {
+ return NewLabelChangeOperation(author, unixTime, []Label{"added"}, []Label{"removed"})
+ })
+ dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *LabelChangeOperation {
+ return NewLabelChangeOperation(author, unixTime, []Label{"added"}, nil)
+ })
+ dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *LabelChangeOperation {
+ return NewLabelChangeOperation(author, unixTime, nil, []Label{"removed"})
+ })
}
diff --git a/bug/op_noop.go b/bug/op_noop.go
deleted file mode 100644
index 1b11e694..00000000
--- a/bug/op_noop.go
+++ /dev/null
@@ -1,77 +0,0 @@
-package bug
-
-import (
- "encoding/json"
-
- "github.com/MichaelMure/git-bug/entity"
- "github.com/MichaelMure/git-bug/identity"
-)
-
-var _ Operation = &NoOpOperation{}
-
-// NoOpOperation is an operation that does not change the bug state. It can
-// however be used to store arbitrary metadata in the bug history, for example
-// to support a bridge feature.
-type NoOpOperation struct {
- OpBase
-}
-
-func (op *NoOpOperation) Id() entity.Id {
- return idOperation(op, &op.OpBase)
-}
-
-func (op *NoOpOperation) Apply(snapshot *Snapshot) {
- // Nothing to do
-}
-
-func (op *NoOpOperation) Validate() error {
- return op.OpBase.Validate(op, NoOpOp)
-}
-
-// UnmarshalJSON is a two step JSON unmarshalling
-// This workaround is necessary to avoid the inner OpBase.MarshalJSON
-// overriding the outer op's 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() {}
-
-func NewNoOpOp(author identity.Interface, unixTime int64) *NoOpOperation {
- return &NoOpOperation{
- OpBase: newOpBase(NoOpOp, author, unixTime),
- }
-}
-
-// Convenience function to apply the operation
-func NoOp(b Interface, author identity.Interface, unixTime int64, metadata map[string]string) (*NoOpOperation, error) {
- op := NewNoOpOp(author, unixTime)
-
- for key, value := range metadata {
- op.SetMetadata(key, value)
- }
-
- if err := op.Validate(); err != nil {
- return nil, err
- }
- b.Append(op)
- return op, nil
-}
diff --git a/bug/op_noop_test.go b/bug/op_noop_test.go
deleted file mode 100644
index 2bbfa219..00000000
--- a/bug/op_noop_test.go
+++ /dev/null
@@ -1,39 +0,0 @@
-package bug
-
-import (
- "encoding/json"
- "testing"
- "time"
-
- "github.com/stretchr/testify/require"
-
- "github.com/MichaelMure/git-bug/identity"
- "github.com/MichaelMure/git-bug/repository"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestNoopSerialize(t *testing.T) {
- repo := repository.NewMockRepo()
-
- rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
- require.NoError(t, err)
-
- 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)
-
- // enforce creating the ID
- before.Id()
-
- // Replace the identity as it's not serialized
- after.Author_ = rene
-
- assert.Equal(t, before, &after)
-}
diff --git a/bug/op_set_metadata.go b/bug/op_set_metadata.go
index 28496fd8..08e1887b 100644
--- a/bug/op_set_metadata.go
+++ b/bug/op_set_metadata.go
@@ -1,108 +1,21 @@
package bug
import (
- "encoding/json"
- "fmt"
-
- "github.com/pkg/errors"
-
"github.com/MichaelMure/git-bug/entity"
+ "github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity"
- "github.com/MichaelMure/git-bug/util/text"
)
-var _ Operation = &SetMetadataOperation{}
-
-type SetMetadataOperation struct {
- OpBase
- Target entity.Id `json:"target"`
- NewMetadata map[string]string `json:"new_metadata"`
-}
-
-func (op *SetMetadataOperation) Id() entity.Id {
- return idOperation(op, &op.OpBase)
-}
-
-func (op *SetMetadataOperation) Apply(snapshot *Snapshot) {
- for _, target := range snapshot.Operations {
- if target.Id() == op.Target {
- // Apply the metadata in an immutable way: if a metadata already
- // exist, it's not possible to override it.
- for key, value := range op.NewMetadata {
- target.setExtraMetadataImmutable(key, value)
- }
- return
- }
- }
-}
-
-func (op *SetMetadataOperation) Validate() error {
- if err := op.OpBase.Validate(op, SetMetadataOp); err != nil {
- return err
- }
-
- if err := op.Target.Validate(); err != nil {
- return errors.Wrap(err, "target invalid")
- }
-
- for key, val := range op.NewMetadata {
- if !text.SafeOneLine(key) {
- return fmt.Errorf("metadata key is unsafe")
- }
- if !text.Safe(val) {
- return fmt.Errorf("metadata value is not fully printable")
- }
- }
-
- return nil
-}
-
-// UnmarshalJSON is a two step JSON unmarshalling
-// This workaround is necessary to avoid the inner OpBase.MarshalJSON
-// overriding the outer op's 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 entity.Id `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() {}
-
-func NewSetMetadataOp(author identity.Interface, unixTime int64, target entity.Id, newMetadata map[string]string) *SetMetadataOperation {
- return &SetMetadataOperation{
- OpBase: newOpBase(SetMetadataOp, author, unixTime),
- Target: target,
- NewMetadata: newMetadata,
- }
+func NewSetMetadataOp(author identity.Interface, unixTime int64, target entity.Id, newMetadata map[string]string) *dag.SetMetadataOperation[*Snapshot] {
+ return dag.NewSetMetadataOp[*Snapshot](SetMetadataOp, author, unixTime, target, newMetadata)
}
-// Convenience function to apply the operation
-func SetMetadata(b Interface, author identity.Interface, unixTime int64, target entity.Id, newMetadata map[string]string) (*SetMetadataOperation, error) {
- SetMetadataOp := NewSetMetadataOp(author, unixTime, target, newMetadata)
- if err := SetMetadataOp.Validate(); err != nil {
+// SetMetadata is a convenience function to add metadata on another operation
+func SetMetadata(b Interface, author identity.Interface, unixTime int64, target entity.Id, newMetadata map[string]string) (*dag.SetMetadataOperation[*Snapshot], error) {
+ op := NewSetMetadataOp(author, unixTime, target, newMetadata)
+ if err := op.Validate(); err != nil {
return nil, err
}
- b.Append(SetMetadataOp)
- return SetMetadataOp, nil
+ b.Append(op)
+ return op, nil
}
diff --git a/bug/op_set_metadata_test.go b/bug/op_set_metadata_test.go
deleted file mode 100644
index 62c1c942..00000000
--- a/bug/op_set_metadata_test.go
+++ /dev/null
@@ -1,126 +0,0 @@
-package bug
-
-import (
- "encoding/json"
- "testing"
- "time"
-
- "github.com/MichaelMure/git-bug/identity"
- "github.com/MichaelMure/git-bug/repository"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestSetMetadata(t *testing.T) {
- snapshot := Snapshot{}
-
- repo := repository.NewMockRepo()
-
- rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
- require.NoError(t, err)
-
- unix := time.Now().Unix()
-
- create := NewCreateOp(rene, unix, "title", "create", nil)
- create.SetMetadata("key", "value")
- create.Apply(&snapshot)
- snapshot.Operations = append(snapshot.Operations, create)
-
- id1 := create.Id()
- require.NoError(t, id1.Validate())
-
- comment := NewAddCommentOp(rene, unix, "comment", nil)
- comment.SetMetadata("key2", "value2")
- comment.Apply(&snapshot)
- snapshot.Operations = append(snapshot.Operations, comment)
-
- id2 := comment.Id()
- require.NoError(t, id2.Validate())
-
- op1 := NewSetMetadataOp(rene, unix, id1, map[string]string{
- "key": "override",
- "key2": "value",
- })
-
- op1.Apply(&snapshot)
- snapshot.Operations = append(snapshot.Operations, op1)
-
- createMetadata := snapshot.Operations[0].AllMetadata()
- require.Len(t, createMetadata, 2)
- // original key is not overrided
- require.Equal(t, createMetadata["key"], "value")
- // new key is set
- require.Equal(t, createMetadata["key2"], "value")
-
- commentMetadata := snapshot.Operations[1].AllMetadata()
- require.Len(t, commentMetadata, 1)
- require.Equal(t, commentMetadata["key2"], "value2")
-
- op2 := NewSetMetadataOp(rene, unix, id2, map[string]string{
- "key2": "value",
- "key3": "value3",
- })
-
- op2.Apply(&snapshot)
- snapshot.Operations = append(snapshot.Operations, op2)
-
- createMetadata = snapshot.Operations[0].AllMetadata()
- require.Len(t, createMetadata, 2)
- require.Equal(t, createMetadata["key"], "value")
- require.Equal(t, createMetadata["key2"], "value")
-
- commentMetadata = snapshot.Operations[1].AllMetadata()
- require.Len(t, commentMetadata, 2)
- // original key is not overrided
- require.Equal(t, commentMetadata["key2"], "value2")
- // new key is set
- require.Equal(t, commentMetadata["key3"], "value3")
-
- op3 := NewSetMetadataOp(rene, unix, id1, map[string]string{
- "key": "override",
- "key2": "override",
- })
-
- op3.Apply(&snapshot)
- snapshot.Operations = append(snapshot.Operations, op3)
-
- createMetadata = snapshot.Operations[0].AllMetadata()
- require.Len(t, createMetadata, 2)
- // original key is not overrided
- require.Equal(t, createMetadata["key"], "value")
- // previously set key is not overrided
- require.Equal(t, createMetadata["key2"], "value")
-
- commentMetadata = snapshot.Operations[1].AllMetadata()
- require.Len(t, commentMetadata, 2)
- require.Equal(t, commentMetadata["key2"], "value2")
- require.Equal(t, commentMetadata["key3"], "value3")
-}
-
-func TestSetMetadataSerialize(t *testing.T) {
- repo := repository.NewMockRepo()
-
- rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
- require.NoError(t, err)
-
- unix := time.Now().Unix()
- before := NewSetMetadataOp(rene, unix, "message", map[string]string{
- "key1": "value1",
- "key2": "value2",
- })
-
- data, err := json.Marshal(before)
- require.NoError(t, err)
-
- var after SetMetadataOperation
- err = json.Unmarshal(data, &after)
- require.NoError(t, err)
-
- // enforce creating the ID
- before.Id()
-
- // Replace the identity as it's not serialized
- after.Author_ = rene
-
- require.Equal(t, before, &after)
-}
diff --git a/bug/op_set_status.go b/bug/op_set_status.go
index e22ded54..ff8df75d 100644
--- a/bug/op_set_status.go
+++ b/bug/op_set_status.go
@@ -1,11 +1,10 @@
package bug
import (
- "encoding/json"
-
"github.com/pkg/errors"
"github.com/MichaelMure/git-bug/entity"
+ "github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/util/timestamp"
)
@@ -14,21 +13,21 @@ var _ Operation = &SetStatusOperation{}
// SetStatusOperation will change the status of a bug
type SetStatusOperation struct {
- OpBase
+ dag.OpBase
Status Status `json:"status"`
}
func (op *SetStatusOperation) Id() entity.Id {
- return idOperation(op, &op.OpBase)
+ return dag.IdOperation(op, &op.OpBase)
}
func (op *SetStatusOperation) Apply(snapshot *Snapshot) {
snapshot.Status = op.Status
- snapshot.addActor(op.Author_)
+ snapshot.addActor(op.Author())
item := &SetStatusTimelineItem{
id: op.Id(),
- Author: op.Author_,
+ Author: op.Author(),
UnixTime: timestamp.Timestamp(op.UnixTime),
Status: op.Status,
}
@@ -48,39 +47,9 @@ func (op *SetStatusOperation) Validate() error {
return nil
}
-// UnmarshalJSON is a two step JSON unmarshalling
-// This workaround is necessary to avoid the inner OpBase.MarshalJSON
-// overriding the outer op's 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() {}
-
func NewSetStatusOp(author identity.Interface, unixTime int64, status Status) *SetStatusOperation {
return &SetStatusOperation{
- OpBase: newOpBase(SetStatusOp, author, unixTime),
+ OpBase: dag.NewOpBase(SetStatusOp, author, unixTime),
Status: status,
}
}
@@ -96,12 +65,15 @@ func (s SetStatusTimelineItem) Id() entity.Id {
return s.id
}
-// Sign post method for gqlgen
-func (s *SetStatusTimelineItem) IsAuthored() {}
+// IsAuthored is a sign post method for gqlgen
+func (s SetStatusTimelineItem) IsAuthored() {}
-// Convenience function to apply the operation
-func Open(b Interface, author identity.Interface, unixTime int64) (*SetStatusOperation, error) {
+// Open is a convenience function to change a bugs state to Open
+func Open(b Interface, author identity.Interface, unixTime int64, metadata map[string]string) (*SetStatusOperation, error) {
op := NewSetStatusOp(author, unixTime, OpenStatus)
+ for key, value := range metadata {
+ op.SetMetadata(key, value)
+ }
if err := op.Validate(); err != nil {
return nil, err
}
@@ -109,9 +81,12 @@ func Open(b Interface, author identity.Interface, unixTime int64) (*SetStatusOpe
return op, nil
}
-// Convenience function to apply the operation
-func Close(b Interface, author identity.Interface, unixTime int64) (*SetStatusOperation, error) {
+// Close is a convenience function to change a bugs state to Close
+func Close(b Interface, author identity.Interface, unixTime int64, metadata map[string]string) (*SetStatusOperation, error) {
op := NewSetStatusOp(author, unixTime, ClosedStatus)
+ for key, value := range metadata {
+ op.SetMetadata(key, value)
+ }
if err := op.Validate(); err != nil {
return nil, err
}
diff --git a/bug/op_set_status_test.go b/bug/op_set_status_test.go
index 75cadae2..385deec1 100644
--- a/bug/op_set_status_test.go
+++ b/bug/op_set_status_test.go
@@ -1,37 +1,14 @@
package bug
import (
- "encoding/json"
"testing"
- "time"
-
- "github.com/stretchr/testify/require"
+ "github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity"
- "github.com/MichaelMure/git-bug/repository"
)
func TestSetStatusSerialize(t *testing.T) {
- repo := repository.NewMockRepo()
-
- 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)
- require.NoError(t, err)
-
- var after SetStatusOperation
- err = json.Unmarshal(data, &after)
- require.NoError(t, err)
-
- // enforce creating the ID
- before.Id()
-
- // Replace the identity as it's not serialized
- after.Author_ = rene
-
- require.Equal(t, before, &after)
+ dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *SetStatusOperation {
+ return NewSetStatusOp(author, unixTime, ClosedStatus)
+ })
}
diff --git a/bug/op_set_title.go b/bug/op_set_title.go
index badd192c..d26a60fa 100644
--- a/bug/op_set_title.go
+++ b/bug/op_set_title.go
@@ -1,10 +1,10 @@
package bug
import (
- "encoding/json"
"fmt"
"github.com/MichaelMure/git-bug/entity"
+ "github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/util/timestamp"
@@ -15,22 +15,22 @@ var _ Operation = &SetTitleOperation{}
// SetTitleOperation will change the title of a bug
type SetTitleOperation struct {
- OpBase
+ dag.OpBase
Title string `json:"title"`
Was string `json:"was"`
}
func (op *SetTitleOperation) Id() entity.Id {
- return idOperation(op, &op.OpBase)
+ return dag.IdOperation(op, &op.OpBase)
}
func (op *SetTitleOperation) Apply(snapshot *Snapshot) {
snapshot.Title = op.Title
- snapshot.addActor(op.Author_)
+ snapshot.addActor(op.Author())
item := &SetTitleTimelineItem{
id: op.Id(),
- Author: op.Author_,
+ Author: op.Author(),
UnixTime: timestamp.Timestamp(op.UnixTime),
Title: op.Title,
Was: op.Was,
@@ -59,41 +59,9 @@ func (op *SetTitleOperation) Validate() error {
return nil
}
-// UnmarshalJSON is a two step JSON unmarshalling
-// This workaround is necessary to avoid the inner OpBase.MarshalJSON
-// overriding the outer op's 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() {}
-
func NewSetTitleOp(author identity.Interface, unixTime int64, title string, was string) *SetTitleOperation {
return &SetTitleOperation{
- OpBase: newOpBase(SetTitleOp, author, unixTime),
+ OpBase: dag.NewOpBase(SetTitleOp, author, unixTime),
Title: title,
Was: was,
}
@@ -111,11 +79,11 @@ func (s SetTitleTimelineItem) Id() entity.Id {
return s.id
}
-// Sign post method for gqlgen
-func (s *SetTitleTimelineItem) IsAuthored() {}
+// IsAuthored is a sign post method for gqlgen
+func (s SetTitleTimelineItem) IsAuthored() {}
-// Convenience function to apply the operation
-func SetTitle(b Interface, author identity.Interface, unixTime int64, title string) (*SetTitleOperation, error) {
+// SetTitle is a convenience function to change a bugs title
+func SetTitle(b Interface, author identity.Interface, unixTime int64, title string, metadata map[string]string) (*SetTitleOperation, error) {
var lastTitleOp *SetTitleOperation
for _, op := range b.Operations() {
switch op := op.(type) {
@@ -131,12 +99,14 @@ func SetTitle(b Interface, author identity.Interface, unixTime int64, title stri
was = b.FirstOp().(*CreateOperation).Title
}
- setTitleOp := NewSetTitleOp(author, unixTime, title, was)
-
- if err := setTitleOp.Validate(); err != nil {
+ op := NewSetTitleOp(author, unixTime, title, was)
+ for key, value := range metadata {
+ op.SetMetadata(key, value)
+ }
+ if err := op.Validate(); err != nil {
return nil, err
}
- b.Append(setTitleOp)
- return setTitleOp, nil
+ b.Append(op)
+ return op, nil
}
diff --git a/bug/op_set_title_test.go b/bug/op_set_title_test.go
index 2a227709..8b45c74e 100644
--- a/bug/op_set_title_test.go
+++ b/bug/op_set_title_test.go
@@ -1,37 +1,14 @@
package bug
import (
- "encoding/json"
"testing"
- "time"
-
- "github.com/stretchr/testify/require"
+ "github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity"
- "github.com/MichaelMure/git-bug/repository"
)
func TestSetTitleSerialize(t *testing.T) {
- repo := repository.NewMockRepo()
-
- 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)
- require.NoError(t, err)
-
- var after SetTitleOperation
- err = json.Unmarshal(data, &after)
- require.NoError(t, err)
-
- // enforce creating the ID
- before.Id()
-
- // Replace the identity as it's not serialized
- after.Author_ = rene
-
- require.Equal(t, before, &after)
+ dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *SetTitleOperation {
+ return NewSetTitleOp(author, unixTime, "title", "was")
+ })
}
diff --git a/bug/operation.go b/bug/operation.go
index b5c6b1de..9c87d8f3 100644
--- a/bug/operation.go
+++ b/bug/operation.go
@@ -1,23 +1,15 @@
package bug
import (
- "crypto/rand"
"encoding/json"
"fmt"
- "time"
- "github.com/pkg/errors"
-
- "github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity"
)
-// OperationType is an operation type identifier
-type OperationType int
-
const (
- _ OperationType = iota
+ _ dag.OperationType = iota
CreateOp
SetTitleOp
AddCommentOp
@@ -32,55 +24,24 @@ const (
type Operation interface {
dag.Operation
- // Type return the type of the operation
- Type() OperationType
-
- // Time return the time when the operation was added
- Time() time.Time
// Apply the operation to a Snapshot to create the final state
Apply(snapshot *Snapshot)
-
- // SetMetadata store arbitrary metadata about the operation
- SetMetadata(key string, value string)
- // GetMetadata retrieve arbitrary metadata about the operation
- GetMetadata(key string) (string, bool)
- // AllMetadata return all metadata for this operation
- AllMetadata() map[string]string
-
- setExtraMetadataImmutable(key string, value string)
}
-func idOperation(op Operation, base *OpBase) entity.Id {
- if base.id == "" {
- // something went really wrong
- panic("op's id not set")
- }
- if base.id == entity.UnsetId {
- // This means we are trying to get the op's Id *before* it has been stored, for instance when
- // adding multiple ops in one go in an OperationPack.
- // As the Id is computed based on the actual bytes written on the disk, we are going to predict
- // those and then get the Id. This is safe as it will be the exact same code writing on disk later.
-
- data, err := json.Marshal(op)
- if err != nil {
- panic(err)
- }
-
- base.id = entity.DeriveId(data)
- }
- return base.id
-}
+// make sure that package external operations do conform to our interface
+var _ Operation = &dag.NoOpOperation[*Snapshot]{}
+var _ Operation = &dag.SetMetadataOperation[*Snapshot]{}
-func operationUnmarshaller(author identity.Interface, raw json.RawMessage, resolver identity.Resolver) (dag.Operation, error) {
+func operationUnmarshaller(raw json.RawMessage, resolver identity.Resolver) (dag.Operation, error) {
var t struct {
- OperationType OperationType `json:"type"`
+ OperationType dag.OperationType `json:"type"`
}
if err := json.Unmarshal(raw, &t); err != nil {
return nil, err
}
- var op Operation
+ var op dag.Operation
switch t.OperationType {
case AddCommentOp:
@@ -92,9 +53,9 @@ func operationUnmarshaller(author identity.Interface, raw json.RawMessage, resol
case LabelChangeOp:
op = &LabelChangeOperation{}
case NoOpOp:
- op = &NoOpOperation{}
+ op = &dag.NoOpOperation[*Snapshot]{}
case SetMetadataOp:
- op = &SetMetadataOperation{}
+ op = &dag.SetMetadataOperation[*Snapshot]{}
case SetStatusOp:
op = &SetStatusOperation{}
case SetTitleOp:
@@ -108,188 +69,5 @@ func operationUnmarshaller(author identity.Interface, raw json.RawMessage, resol
return nil, err
}
- switch op := op.(type) {
- case *AddCommentOperation:
- op.Author_ = author
- case *CreateOperation:
- op.Author_ = author
- case *EditCommentOperation:
- op.Author_ = author
- case *LabelChangeOperation:
- op.Author_ = author
- case *NoOpOperation:
- op.Author_ = author
- case *SetMetadataOperation:
- op.Author_ = author
- case *SetStatusOperation:
- op.Author_ = author
- case *SetTitleOperation:
- op.Author_ = author
- default:
- panic(fmt.Sprintf("unknown operation type %T", op))
- }
-
return op, nil
}
-
-// OpBase implement the common code for all operations
-type OpBase struct {
- OperationType OperationType `json:"type"`
- Author_ identity.Interface `json:"-"` // not serialized
- // TODO: part of the data model upgrade, this should eventually be a timestamp + lamport
- UnixTime int64 `json:"timestamp"`
- Metadata map[string]string `json:"metadata,omitempty"`
-
- // mandatory random bytes to ensure a better randomness of the data used to later generate the ID
- // len(Nonce) should be > 20 and < 64 bytes
- // It has no functional purpose and should be ignored.
- Nonce []byte `json:"nonce"`
-
- // Not serialized. Store the op's id in memory.
- id entity.Id
- // Not serialized. Store the extra metadata in memory,
- // compiled from SetMetadataOperation.
- extraMetadata map[string]string
-}
-
-// newOpBase is the constructor for an OpBase
-func newOpBase(opType OperationType, author identity.Interface, unixTime int64) OpBase {
- return OpBase{
- OperationType: opType,
- Author_: author,
- UnixTime: unixTime,
- Nonce: makeNonce(20),
- id: entity.UnsetId,
- }
-}
-
-func makeNonce(len int) []byte {
- result := make([]byte, len)
- _, err := rand.Read(result)
- if err != nil {
- panic(err)
- }
- return result
-}
-
-func (base *OpBase) UnmarshalJSON(data []byte) error {
- // Compute the Id when loading the op from disk.
- base.id = entity.DeriveId(data)
-
- aux := struct {
- OperationType OperationType `json:"type"`
- UnixTime int64 `json:"timestamp"`
- Metadata map[string]string `json:"metadata,omitempty"`
- Nonce []byte `json:"nonce"`
- }{}
-
- if err := json.Unmarshal(data, &aux); err != nil {
- return err
- }
-
- base.OperationType = aux.OperationType
- base.UnixTime = aux.UnixTime
- base.Metadata = aux.Metadata
- base.Nonce = aux.Nonce
-
- return nil
-}
-
-func (base *OpBase) Type() OperationType {
- return base.OperationType
-}
-
-// Time return the time when the operation was added
-func (base *OpBase) Time() time.Time {
- return time.Unix(base.UnixTime, 0)
-}
-
-// Validate check the OpBase for errors
-func (base *OpBase) Validate(op Operation, opType OperationType) error {
- if base.OperationType != opType {
- return fmt.Errorf("incorrect operation type (expected: %v, actual: %v)", opType, base.OperationType)
- }
-
- if op.Time().Unix() == 0 {
- return fmt.Errorf("time not set")
- }
-
- if base.Author_ == nil {
- return fmt.Errorf("author not set")
- }
-
- if err := op.Author().Validate(); err != nil {
- return errors.Wrap(err, "author")
- }
-
- if op, ok := op.(dag.OperationWithFiles); ok {
- for _, hash := range op.GetFiles() {
- if !hash.IsValid() {
- return fmt.Errorf("file with invalid hash %v", hash)
- }
- }
- }
-
- if len(base.Nonce) > 64 {
- return fmt.Errorf("nonce is too big")
- }
- if len(base.Nonce) < 20 {
- return fmt.Errorf("nonce is too small")
- }
-
- return nil
-}
-
-// SetMetadata store arbitrary metadata about the operation
-func (base *OpBase) SetMetadata(key string, value string) {
- if base.Metadata == nil {
- base.Metadata = make(map[string]string)
- }
-
- base.Metadata[key] = value
- base.id = entity.UnsetId
-}
-
-// GetMetadata retrieve arbitrary metadata about the operation
-func (base *OpBase) GetMetadata(key string) (string, bool) {
- val, ok := base.Metadata[key]
-
- if ok {
- return val, true
- }
-
- // extraMetadata can't replace the original operations value if any
- val, ok = base.extraMetadata[key]
-
- return val, ok
-}
-
-// AllMetadata return all metadata for this operation
-func (base *OpBase) AllMetadata() map[string]string {
- result := make(map[string]string)
-
- for key, val := range base.extraMetadata {
- result[key] = val
- }
-
- // Original metadata take precedence
- for key, val := range base.Metadata {
- result[key] = val
- }
-
- return result
-}
-
-func (base *OpBase) setExtraMetadataImmutable(key string, value string) {
- if base.extraMetadata == nil {
- base.extraMetadata = make(map[string]string)
- }
- if _, exist := base.extraMetadata[key]; !exist {
- base.extraMetadata[key] = value
- }
-}
-
-// Author return author identity
-func (base *OpBase) Author() identity.Interface {
- return base.Author_
-}
diff --git a/bug/operation_test.go b/bug/operation_test.go
index c57a1591..3cfc85c4 100644
--- a/bug/operation_test.go
+++ b/bug/operation_test.go
@@ -6,10 +6,13 @@ import (
"github.com/stretchr/testify/require"
+ "github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity"
"github.com/MichaelMure/git-bug/repository"
)
+// TODO: move to entity/dag?
+
func TestValidate(t *testing.T) {
repo := repository.NewMockRepoClock()
@@ -44,11 +47,7 @@ func TestValidate(t *testing.T) {
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,
- OperationType: CreateOp,
- },
+ &CreateOperation{OpBase: dag.NewOpBase(CreateOp, rene, 0),
Title: "title",
Message: "message",
},
@@ -105,7 +104,7 @@ func TestID(t *testing.T) {
err = rene.Commit(repo)
require.NoError(t, err)
- b, op, err := Create(rene, time.Now().Unix(), "title", "message")
+ b, op, err := Create(rene, time.Now().Unix(), "title", "message", nil, nil)
require.NoError(t, err)
id1 := op.Id()
diff --git a/bug/snapshot.go b/bug/snapshot.go
index d73e4bb6..2efc067e 100644
--- a/bug/snapshot.go
+++ b/bug/snapshot.go
@@ -5,9 +5,12 @@ import (
"time"
"github.com/MichaelMure/git-bug/entity"
+ "github.com/MichaelMure/git-bug/entity/dag"
"github.com/MichaelMure/git-bug/identity"
)
+var _ dag.Snapshot = &Snapshot{}
+
// Snapshot is a compiled form of the Bug data structure used for storage and merge
type Snapshot struct {
id entity.Id
@@ -23,7 +26,7 @@ type Snapshot struct {
Timeline []TimelineItem
- Operations []Operation
+ Operations []dag.Operation
}
// Id returns the Bug identifier
@@ -35,6 +38,10 @@ func (snap *Snapshot) Id() entity.Id {
return snap.id
}
+func (snap *Snapshot) AllOperations() []dag.Operation {
+ return snap.Operations
+}
+
// EditTime returns the last time a bug was modified
func (snap *Snapshot) EditTime() time.Time {
if len(snap.Operations) == 0 {
@@ -133,5 +140,5 @@ func (snap *Snapshot) HasAnyActor(ids ...entity.Id) bool {
return false
}
-// Sign post method for gqlgen
+// IsAuthored is a sign post method for gqlgen
func (snap *Snapshot) IsAuthored() {}
diff --git a/bug/with_snapshot.go b/bug/with_snapshot.go
index 9b706d61..0474cac7 100644
--- a/bug/with_snapshot.go
+++ b/bug/with_snapshot.go
@@ -1,6 +1,8 @@
package bug
-import "github.com/MichaelMure/git-bug/repository"
+import (
+ "github.com/MichaelMure/git-bug/repository"
+)
var _ Interface = &WithSnapshot{}
@@ -10,11 +12,10 @@ type WithSnapshot struct {
snap *Snapshot
}
-// Snapshot return the current snapshot
-func (b *WithSnapshot) Snapshot() *Snapshot {
+func (b *WithSnapshot) Compile() *Snapshot {
if b.snap == nil {
snap := b.Bug.Compile()
- b.snap = &snap
+ b.snap = snap
}
return b.snap
}