diff options
author | Michael Muré <batolettre@gmail.com> | 2022-08-01 17:16:45 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-08-01 17:16:45 +0200 |
commit | cd52872475f1b39f3fb6546606c1e78afb6c08e3 (patch) | |
tree | 9f04b1e1ee362e210e74aaa324d950058ed14bf2 /bug | |
parent | 2ade8fb1d570ddcb4aedc9386af46d208b129daa (diff) | |
parent | 56966fec5562c3a0e23340d0fbe754626c3beb64 (diff) | |
download | git-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.go | 16 | ||||
-rw-r--r-- | bug/comment.go | 2 | ||||
-rw-r--r-- | bug/interface.go | 29 | ||||
-rw-r--r-- | bug/op_add_comment.go | 68 | ||||
-rw-r--r-- | bug/op_add_comment_test.go | 33 | ||||
-rw-r--r-- | bug/op_create.go | 103 | ||||
-rw-r--r-- | bug/op_create_test.go | 30 | ||||
-rw-r--r-- | bug/op_edit_comment.go | 70 | ||||
-rw-r--r-- | bug/op_edit_comment_test.go | 30 | ||||
-rw-r--r-- | bug/op_label_change.go | 81 | ||||
-rw-r--r-- | bug/op_label_change_test.go | 37 | ||||
-rw-r--r-- | bug/op_noop.go | 77 | ||||
-rw-r--r-- | bug/op_noop_test.go | 39 | ||||
-rw-r--r-- | bug/op_set_metadata.go | 105 | ||||
-rw-r--r-- | bug/op_set_metadata_test.go | 126 | ||||
-rw-r--r-- | bug/op_set_status.go | 61 | ||||
-rw-r--r-- | bug/op_set_status_test.go | 31 | ||||
-rw-r--r-- | bug/op_set_title.go | 64 | ||||
-rw-r--r-- | bug/op_set_title_test.go | 31 | ||||
-rw-r--r-- | bug/operation.go | 240 | ||||
-rw-r--r-- | bug/operation_test.go | 11 | ||||
-rw-r--r-- | bug/snapshot.go | 11 | ||||
-rw-r--r-- | bug/with_snapshot.go | 9 |
23 files changed, 205 insertions, 1099 deletions
@@ -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 } |