From ad0b39205050622d472c473698ef3d3c0c64015f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Tue, 23 Aug 2022 21:04:54 +0200 Subject: [PATCH] WIP --- entities/board/board.go | 82 ++++++++++++++++++++- entities/board/board_actions.go | 58 +++++++++++++++ entities/board/{draft.go => item_draft.go} | 19 ++++- entities/board/item_entity.go | 21 ++++++ entities/board/op_add_item_draft.go | 85 +++++++++++++++++++-- entities/board/op_add_item_draft_test.go | 19 +++++ entities/board/op_add_item_entity.go | 86 ++++++++++++++++++++-- entities/board/op_add_item_entity_test.go | 25 +++++++ entities/board/op_create.go | 19 ++++- entities/board/op_create_test.go | 20 ++++- entities/board/op_set_description_test.go | 5 +- entities/board/op_set_title_test.go | 5 +- entities/board/operation.go | 14 +++- entities/board/snapshot.go | 17 ++++- entities/bug/comment.go | 2 +- entities/bug/op_add_comment.go | 6 ++ entities/bug/op_create.go | 6 ++ entities/bug/op_edit_comment.go | 6 ++ 18 files changed, 462 insertions(+), 33 deletions(-) create mode 100644 entities/board/board_actions.go rename entities/board/{draft.go => item_draft.go} (60%) create mode 100644 entities/board/item_entity.go create mode 100644 entities/board/op_add_item_draft_test.go create mode 100644 entities/board/op_add_item_entity_test.go diff --git a/entities/board/board.go b/entities/board/board.go index b8331aeb54825b4df58aa6c624eef6d3ee481979..1b35c6b5f65e5b8adf2cf28115ba04d71f8a5fd4 100644 --- a/entities/board/board.go +++ b/entities/board/board.go @@ -1,6 +1,8 @@ package board import ( + "fmt" + "github.com/MichaelMure/git-bug/entities/bug" "github.com/MichaelMure/git-bug/entities/identity" @@ -9,7 +11,7 @@ import ( "github.com/MichaelMure/git-bug/repository" ) -var _ entity.Interface = &Board{} +var _ Interface = &Board{} // 1: original format const formatVersion = 1 @@ -17,12 +19,16 @@ const formatVersion = 1 var def = dag.Definition{ Typename: "board", Namespace: "boards", - OperationUnmarshaler: operationUnmarshaller, + OperationUnmarshaler: operationUnmarshaler, FormatVersion: formatVersion, } var ClockLoader = dag.ClockLoader(def) +type Interface interface { + dag.Interface[*Snapshot, Operation] +} + // Board holds the data of a project board. type Board struct { *dag.Entity @@ -55,3 +61,75 @@ func ReadWithResolver(repo repository.ClockedRepo, resolvers entity.Resolvers, i } return &Board{Entity: e}, nil } + +// Validate check if the Board data is valid +func (board *Board) Validate() error { + if err := board.Entity.Validate(); err != nil { + return err + } + + // The very first Op should be a CreateOp + firstOp := board.FirstOp() + if firstOp == nil || firstOp.Type() != CreateOp { + return fmt.Errorf("first operation should be a Create op") + } + + // Check that there is no more CreateOp op + for i, op := range board.Entity.Operations() { + if i == 0 { + continue + } + if op.Type() == CreateOp { + return fmt.Errorf("only one Create op allowed") + } + } + + return nil +} + +// Append add a new Operation to the Board +func (board *Board) Append(op Operation) { + board.Entity.Append(op) +} + +// Operations return the ordered operations +func (board *Board) Operations() []Operation { + source := board.Entity.Operations() + result := make([]Operation, len(source)) + for i, op := range source { + result[i] = op.(Operation) + } + return result +} + +// Compile a board in an easily usable snapshot +func (board *Board) Compile() *Snapshot { + snap := &Snapshot{ + id: board.Id(), + } + + for _, op := range board.Operations() { + op.Apply(snap) + snap.Operations = append(snap.Operations, op) + } + + return snap +} + +// FirstOp lookup for the very first operation of the board. +// For a valid Board, this operation should be a CreateOp +func (board *Board) FirstOp() Operation { + if fo := board.Entity.FirstOp(); fo != nil { + return fo.(Operation) + } + return nil +} + +// LastOp lookup for the very last operation of the board. +// For a valid Board, should never be nil +func (board *Board) LastOp() Operation { + if lo := board.Entity.LastOp(); lo != nil { + return lo.(Operation) + } + return nil +} diff --git a/entities/board/board_actions.go b/entities/board/board_actions.go new file mode 100644 index 0000000000000000000000000000000000000000..b19a8218b0b82cb80a73ada894207ba1d5b873fa --- /dev/null +++ b/entities/board/board_actions.go @@ -0,0 +1,58 @@ +package board + +import ( + "github.com/MichaelMure/git-bug/entities/identity" + "github.com/MichaelMure/git-bug/entity" + "github.com/MichaelMure/git-bug/entity/dag" + "github.com/MichaelMure/git-bug/repository" +) + +// Fetch retrieve updates from a remote +// This does not change the local board state +func Fetch(repo repository.Repo, remote string) (string, error) { + return dag.Fetch(def, repo, remote) +} + +// Push update a remote with the local changes +func Push(repo repository.Repo, remote string) (string, error) { + return dag.Push(def, repo, remote) +} + +// Pull will do a Fetch + MergeAll +// This function will return an error if a merge fail +// Note: an author is necessary for the case where a merge commit is created, as this commit will +// have an author and may be signed if a signing key is available. +func Pull(repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, mergeAuthor identity.Interface) error { + return dag.Pull(def, repo, resolvers, remote, mergeAuthor) +} + +// MergeAll will merge all the available remote board +// Note: an author is necessary for the case where a merge commit is created, as this commit will +// have an author and may be signed if a signing key is available. +func MergeAll(repo repository.ClockedRepo, resolvers entity.Resolvers, remote string, mergeAuthor identity.Interface) <-chan entity.MergeResult { + out := make(chan entity.MergeResult) + + go func() { + defer close(out) + + results := dag.MergeAll(def, repo, resolvers, remote, mergeAuthor) + + // wrap the dag.Entity into a complete Bug + for result := range results { + result := result + if result.Entity != nil { + result.Entity = &Board{ + Entity: result.Entity.(*dag.Entity), + } + } + out <- result + } + }() + + return out +} + +// Remove will remove a local bug from its entity.Id +func Remove(repo repository.ClockedRepo, id entity.Id) error { + return dag.Remove(def, repo, id) +} diff --git a/entities/board/draft.go b/entities/board/item_draft.go similarity index 60% rename from entities/board/draft.go rename to entities/board/item_draft.go index f77f64357aadec84917aaf321d44db559104b34e..1ebd521e26b6af72364b32d27fd6c83890bb82ec 100644 --- a/entities/board/draft.go +++ b/entities/board/item_draft.go @@ -1,26 +1,39 @@ package board import ( + "github.com/MichaelMure/git-bug/entities/identity" + "github.com/MichaelMure/git-bug/entity" "github.com/dustin/go-humanize" "github.com/MichaelMure/git-bug/entities/common" - "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/timestamp" ) -var _ CardItem = &Draft{} +var _ Item = &Draft{} type Draft struct { + // combinedId should be the result of entity.CombineIds with the Board id and the id + // of the Operation that created the Draft + combinedId entity.CombinedId + + author identity.Interface status common.Status title string message string - files []repository.Hash // Creation time of the comment. // Should be used only for human display, never for ordering as we can't rely on it in a distributed system. unixTime timestamp.Timestamp } +func (d *Draft) CombinedId() entity.CombinedId { + if d.combinedId == "" || d.combinedId == entity.UnsetCombinedId { + // simply panic as it would be a coding error (no id provided at construction) + panic("no combined id") + } + return d.combinedId +} + func (d *Draft) Status() common.Status { // TODO implement me panic("implement me") diff --git a/entities/board/item_entity.go b/entities/board/item_entity.go new file mode 100644 index 0000000000000000000000000000000000000000..d7457027e0f1defb2239259594191c0e7d8ffee9 --- /dev/null +++ b/entities/board/item_entity.go @@ -0,0 +1,21 @@ +package board + +import ( + "github.com/MichaelMure/git-bug/entities/bug" + "github.com/MichaelMure/git-bug/entity" +) + +var _ Item = &BugItem{} + +type BugItem struct { + combinedId entity.CombinedId + bug bug.Interface +} + +func (e *BugItem) CombinedId() entity.CombinedId { + if e.combinedId == "" || e.combinedId == entity.UnsetCombinedId { + // simply panic as it would be a coding error (no id provided at construction) + panic("no combined id") + } + return e.combinedId +} diff --git a/entities/board/op_add_item_draft.go b/entities/board/op_add_item_draft.go index f925602b7772ac71d99cf1a1ba20dd161b651a06..6ce58ad36c749807725a51dc0a4e8eb0875f00b1 100644 --- a/entities/board/op_add_item_draft.go +++ b/entities/board/op_add_item_draft.go @@ -1,28 +1,101 @@ package board import ( + "fmt" + + "github.com/MichaelMure/git-bug/entities/common" + "github.com/MichaelMure/git-bug/entities/identity" "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/entity/dag" + "github.com/MichaelMure/git-bug/repository" + "github.com/MichaelMure/git-bug/util/text" + "github.com/MichaelMure/git-bug/util/timestamp" ) var _ Operation = &AddItemDraftOperation{} type AddItemDraftOperation struct { dag.OpBase - Title string `json:"title"` - Message string `json:"message"` + ColumnId entity.Id `json:"column"` + Title string `json:"title"` + Message string `json:"message"` + Files []repository.Hash `json:"files"` } func (op *AddItemDraftOperation) Id() entity.Id { return dag.IdOperation(op, &op.OpBase) } +func (op *AddItemDraftOperation) GetFiles() []repository.Hash { + return op.Files +} + func (op *AddItemDraftOperation) Validate() error { - // TODO implement me - panic("implement me") + if err := op.OpBase.Validate(op, AddItemDraftOp); err != nil { + return err + } + + if err := op.ColumnId.Validate(); err != nil { + return err + } + + if text.Empty(op.Title) { + return fmt.Errorf("title is empty") + } + if !text.SafeOneLine(op.Title) { + return fmt.Errorf("title has unsafe characters") + } + + if !text.Safe(op.Message) { + return fmt.Errorf("message is not fully printable") + } + + for _, file := range op.Files { + if !file.IsValid() { + return fmt.Errorf("invalid file hash") + } + } + + return nil } func (op *AddItemDraftOperation) Apply(snapshot *Snapshot) { - // TODO implement me - panic("implement me") + snapshot.addActor(op.Author()) + + for _, column := range snapshot.Columns { + if column.Id == op.ColumnId { + column.Items = append(column.Items, &Draft{ + combinedId: entity.CombineIds(snapshot.id, op.Id()), + author: op.Author(), + status: common.OpenStatus, + title: op.Title, + message: op.Message, + unixTime: timestamp.Timestamp(op.UnixTime), + }) + return + } + } +} + +func NewAddItemDraftOp(author identity.Interface, unixTime int64, columnId entity.Id, title, message string, files []repository.Hash) *AddItemDraftOperation { + return &AddItemDraftOperation{ + OpBase: dag.NewOpBase(AddItemDraftOp, author, unixTime), + ColumnId: columnId, + Title: title, + Message: message, + Files: files, + } +} + +// AddItemDraft is a convenience function to add a draft item to a Board +func AddItemDraft(b *Board, author identity.Interface, unixTime int64, columnId entity.Id, title, message string, files []repository.Hash, metadata map[string]string) (*AddItemDraftOperation, error) { + op := NewAddItemDraftOp(author, unixTime, columnId, title, message, files) + for key, val := range metadata { + op.SetMetadata(key, val) + } + if err := op.Validate(); err != nil { + return nil, err + } + b.Append(op) + return op, nil } diff --git a/entities/board/op_add_item_draft_test.go b/entities/board/op_add_item_draft_test.go new file mode 100644 index 0000000000000000000000000000000000000000..3faaea86ef7d51105a65f1abd9b02debf6a076a0 --- /dev/null +++ b/entities/board/op_add_item_draft_test.go @@ -0,0 +1,19 @@ +package board + +import ( + "testing" + + "github.com/MichaelMure/git-bug/entities/identity" + "github.com/MichaelMure/git-bug/entity" + "github.com/MichaelMure/git-bug/entity/dag" + "github.com/MichaelMure/git-bug/repository" +) + +func TestAddItemDraftOpSerialize(t *testing.T) { + dag.SerializeRoundTripTest(t, operationUnmarshaler, func(author identity.Interface, unixTime int64) (*AddItemDraftOperation, entity.Resolvers) { + return NewAddItemDraftOp(author, unixTime, "foo", "title", "message", nil), nil + }) + dag.SerializeRoundTripTest(t, operationUnmarshaler, func(author identity.Interface, unixTime int64) (*AddItemDraftOperation, entity.Resolvers) { + return NewAddItemDraftOp(author, unixTime, "foo", "title", "message", []repository.Hash{"hash1", "hash2"}), nil + }) +} diff --git a/entities/board/op_add_item_entity.go b/entities/board/op_add_item_entity.go index 7df145278da30a3fe497d23f2be25f1c01c3b759..4628c6a08f23c22d8dea30d2e6ed24006bb6360e 100644 --- a/entities/board/op_add_item_entity.go +++ b/entities/board/op_add_item_entity.go @@ -1,16 +1,29 @@ package board import ( + "fmt" + + "github.com/MichaelMure/git-bug/entities/bug" + "github.com/MichaelMure/git-bug/entities/identity" "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/entity/dag" ) +// itemEntityType indicate the type of entity board item +type itemEntityType string + +const ( + entityTypeBug itemEntityType = "bug" +) + var _ Operation = &AddItemEntityOperation{} type AddItemEntityOperation struct { dag.OpBase - // TODO: entity namespace + id ? or solve https://github.com/MichaelMure/git-bug/pull/664 ? - item CardItem + ColumnId entity.Id `json:"column"` + EntityType itemEntityType `json:"entity_type"` + EntityId entity.Id `json:"entity_id"` + entity entity.Interface // not serialized } func (op *AddItemEntityOperation) Id() entity.Id { @@ -18,11 +31,72 @@ func (op *AddItemEntityOperation) Id() entity.Id { } func (op *AddItemEntityOperation) Validate() error { - // TODO implement me - panic("implement me") + if err := op.OpBase.Validate(op, AddItemEntityOp); err != nil { + return err + } + + if err := op.ColumnId.Validate(); err != nil { + return err + } + + switch op.EntityType { + case entityTypeBug: + default: + return fmt.Errorf("unknown entity type") + } + + if err := op.EntityId.Validate(); err != nil { + return err + } + + return nil } func (op *AddItemEntityOperation) Apply(snapshot *Snapshot) { - // TODO implement me - panic("implement me") + if op.entity == nil { + return + } + + snapshot.addActor(op.Author()) + + for _, column := range snapshot.Columns { + if column.Id == op.ColumnId { + switch e := op.entity.(type) { + case bug.Interface: + column.Items = append(column.Items, &BugItem{ + combinedId: entity.CombineIds(snapshot.Id(), e.Id()), + bug: e, + }) + } + return + } + } +} + +func NewAddItemEntityOp(author identity.Interface, unixTime int64, columnId entity.Id, e entity.Interface) *AddItemEntityOperation { + switch e := e.(type) { + case bug.Interface: + return &AddItemEntityOperation{ + OpBase: dag.NewOpBase(AddItemEntityOp, author, unixTime), + ColumnId: columnId, + EntityType: entityTypeBug, + EntityId: e.Id(), + entity: e, + } + default: + panic("invalid entity type") + } +} + +// AddItemEntity is a convenience function to add an entity item to a Board +func AddItemEntity(b *Board, author identity.Interface, unixTime int64, columnId entity.Id, e entity.Interface, metadata map[string]string) (*AddItemEntityOperation, error) { + op := NewAddItemEntityOp(author, unixTime, columnId, e) + for key, val := range metadata { + op.SetMetadata(key, val) + } + if err := op.Validate(); err != nil { + return nil, err + } + b.Append(op) + return op, nil } diff --git a/entities/board/op_add_item_entity_test.go b/entities/board/op_add_item_entity_test.go new file mode 100644 index 0000000000000000000000000000000000000000..307a7b2fe9e6c983996301d2b2331fa0b8ef9e57 --- /dev/null +++ b/entities/board/op_add_item_entity_test.go @@ -0,0 +1,25 @@ +package board + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/MichaelMure/git-bug/entities/bug" + "github.com/MichaelMure/git-bug/entities/identity" + "github.com/MichaelMure/git-bug/entity" + "github.com/MichaelMure/git-bug/entity/dag" +) + +func TestAddItemEntityOpSerialize(t *testing.T) { + dag.SerializeRoundTripTest(t, operationUnmarshaler, func(author identity.Interface, unixTime int64) (*AddItemEntityOperation, entity.Resolvers) { + b, _, err := bug.Create(author, unixTime, "title", "message", nil, nil) + require.NoError(t, err) + + resolvers := entity.Resolvers{ + &bug.Bug{}: entity.MakeResolver(b), + } + + return NewAddItemEntityOp(author, unixTime, "foo", b), resolvers + }) +} diff --git a/entities/board/op_create.go b/entities/board/op_create.go index c956dd7ea4b79651051f3804564821fd89ad548e..a5befbf02a48c6afd8317dc7bc76add87237b5f7 100644 --- a/entities/board/op_create.go +++ b/entities/board/op_create.go @@ -62,6 +62,14 @@ func (op *CreateOperation) Validate() error { } } + set := make(map[string]struct{}) + for _, column := range op.Columns { + set[column] = struct{}{} + } + if len(set) != len(op.Columns) { + return fmt.Errorf("non unique column name") + } + return nil } @@ -75,21 +83,28 @@ func (op *CreateOperation) Apply(snap *Snapshot) { snap.Title = op.Title snap.Description = op.Description + snap.CreateTime = op.Time() for _, name := range op.Columns { - snap.Columns = append(snap.Columns, Column{ + // we derive a unique Id from the original column name + id := entity.DeriveId([]byte(name)) + + snap.Columns = append(snap.Columns, &Column{ + Id: id, Name: name, - Cards: nil, + Items: nil, }) } snap.addActor(op.Author()) } +// CreateDefaultColumns is a convenience function to create a board with the default columns func CreateDefaultColumns(author identity.Interface, unixTime int64, title, description string) (*Board, *CreateOperation, error) { return Create(author, unixTime, title, description, DefaultColumns) } +// Create is a convenience function to create a board func Create(author identity.Interface, unixTime int64, title, description string, columns []string) (*Board, *CreateOperation, error) { b := NewBoard() op := NewCreateOp(author, unixTime, title, description, columns) diff --git a/entities/board/op_create_test.go b/entities/board/op_create_test.go index efce2dd692d316fa294a47123771cd603a344ed2..943fd1fbb491044a1ed36b93d403eaad37b7f6b9 100644 --- a/entities/board/op_create_test.go +++ b/entities/board/op_create_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/require" "github.com/MichaelMure/git-bug/entities/identity" + "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/entity/dag" "github.com/MichaelMure/git-bug/repository" ) @@ -50,8 +51,23 @@ func TestCreate(t *testing.T) { } } +func TestNonUnique(t *testing.T) { + 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", "description", []string{ + "foo", "bar", "foo", + }) + + require.Error(t, create.Validate()) +} + func TestCreateSerialize(t *testing.T) { - dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *CreateOperation { - return NewCreateOp(author, unixTime, "title", "description", DefaultColumns) + dag.SerializeRoundTripTest(t, operationUnmarshaler, func(author identity.Interface, unixTime int64) (*CreateOperation, entity.Resolvers) { + return NewCreateOp(author, unixTime, "title", "description", DefaultColumns), nil }) } diff --git a/entities/board/op_set_description_test.go b/entities/board/op_set_description_test.go index 7bed84fbf0f19553d88fab7090112ac963bf0896..271b1f2962adf1bd392a58e0ddc3b1f9185b212d 100644 --- a/entities/board/op_set_description_test.go +++ b/entities/board/op_set_description_test.go @@ -4,11 +4,12 @@ import ( "testing" "github.com/MichaelMure/git-bug/entities/identity" + "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/entity/dag" ) func TestSetDescriptionSerialize(t *testing.T) { - dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *SetDescriptionOperation { - return NewSetDescriptionOp(author, unixTime, "description", "was") + dag.SerializeRoundTripTest(t, operationUnmarshaler, func(author identity.Interface, unixTime int64) (*SetDescriptionOperation, entity.Resolvers) { + return NewSetDescriptionOp(author, unixTime, "description", "was"), nil }) } diff --git a/entities/board/op_set_title_test.go b/entities/board/op_set_title_test.go index af84c144cd6c01dc106c646ec20c8385d49ce6e7..4cce61d1d9f463602097361261fa743619a0ad02 100644 --- a/entities/board/op_set_title_test.go +++ b/entities/board/op_set_title_test.go @@ -4,11 +4,12 @@ import ( "testing" "github.com/MichaelMure/git-bug/entities/identity" + "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/entity/dag" ) func TestSetTitleSerialize(t *testing.T) { - dag.SerializeRoundTripTest(t, func(author identity.Interface, unixTime int64) *SetTitleOperation { - return NewSetTitleOp(author, unixTime, "title", "was") + dag.SerializeRoundTripTest(t, operationUnmarshaler, func(author identity.Interface, unixTime int64) (*SetTitleOperation, entity.Resolvers) { + return NewSetTitleOp(author, unixTime, "title", "was"), nil }) } diff --git a/entities/board/operation.go b/entities/board/operation.go index a0a26389167c2e74186bdd4fbd97ce031e34f3a1..66cdc3789deb0cb3ba7373a511a338aeb55d57bd 100644 --- a/entities/board/operation.go +++ b/entities/board/operation.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" + "github.com/MichaelMure/git-bug/entities/bug" "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/entity/dag" ) @@ -30,7 +31,7 @@ type Operation interface { Apply(snapshot *Snapshot) } -func operationUnmarshaller(raw json.RawMessage, resolvers entity.Resolvers) (dag.Operation, error) { +func operationUnmarshaler(raw json.RawMessage, resolvers entity.Resolvers) (dag.Operation, error) { var t struct { OperationType dag.OperationType `json:"type"` } @@ -63,8 +64,15 @@ func operationUnmarshaller(raw json.RawMessage, resolvers entity.Resolvers) (dag switch op := op.(type) { case *AddItemEntityOperation: - // TODO: resolve entity - op.item = struct{}{} + switch op.EntityType { + case entityTypeBug: + op.entity, err = entity.Resolve[bug.Interface](resolvers, op.EntityId) + default: + return nil, fmt.Errorf("unknown entity type") + } + if err != nil { + return nil, err + } } return op, nil diff --git a/entities/board/snapshot.go b/entities/board/snapshot.go index 9122a98f7c736b0fc3c02954c22a6c76433a2dc7..ec14843abdaa30639b4f15c94e2a6c5721285e37 100644 --- a/entities/board/snapshot.go +++ b/entities/board/snapshot.go @@ -5,27 +5,36 @@ import ( "github.com/MichaelMure/git-bug/entities/identity" "github.com/MichaelMure/git-bug/entity" + "github.com/MichaelMure/git-bug/entity/dag" ) type Column struct { + Id entity.Id Name string - Cards []CardItem + Items []Item } -type CardItem interface { +type Item interface { + CombinedId() entity.CombinedId // Status() common.Status } +var _ dag.Snapshot = &Snapshot{} + type Snapshot struct { id entity.Id Title string Description string - Columns []Column + Columns []*Column Actors []identity.Interface CreateTime time.Time - Operations []Operation + Operations []dag.Operation +} + +func (snap *Snapshot) AllOperations() []dag.Operation { + return snap.Operations } // Id returns the Board identifier diff --git a/entities/bug/comment.go b/entities/bug/comment.go index 32209fcb0da4f5619973e9aa447ee408d46a242d..52f44f6fa9f570e29b5d08c9f5855c53fa55b4ba 100644 --- a/entities/bug/comment.go +++ b/entities/bug/comment.go @@ -28,7 +28,7 @@ type Comment struct { } func (c Comment) CombinedId() entity.CombinedId { - if c.combinedId == "" { + if c.combinedId == "" || c.combinedId == entity.UnsetCombinedId { // simply panic as it would be a coding error (no id provided at construction) panic("no combined id") } diff --git a/entities/bug/op_add_comment.go b/entities/bug/op_add_comment.go index ea7093972bc2f2887ae8d81cae1fe3b8555020f5..166348a674e15262502207a989ced0f591e67c2f 100644 --- a/entities/bug/op_add_comment.go +++ b/entities/bug/op_add_comment.go @@ -63,6 +63,12 @@ func (op *AddCommentOperation) Validate() error { return fmt.Errorf("message is not fully printable") } + for _, file := range op.Files { + if !file.IsValid() { + return fmt.Errorf("invalid file hash") + } + } + return nil } diff --git a/entities/bug/op_create.go b/entities/bug/op_create.go index c7b6bada38d290110cab186c35bd979d6b59ae69..7004bf5fe9dd15edeb41ab892eeb6ae2d9ce3c79 100644 --- a/entities/bug/op_create.go +++ b/entities/bug/op_create.go @@ -80,6 +80,12 @@ func (op *CreateOperation) Validate() error { return fmt.Errorf("message is not fully printable") } + for _, file := range op.Files { + if !file.IsValid() { + return fmt.Errorf("invalid file hash") + } + } + return nil } diff --git a/entities/bug/op_edit_comment.go b/entities/bug/op_edit_comment.go index 18f130f9829d61a171c3bf4dc80bdf70815ecc58..9b1b61688c928e3a50abf1d8c308ac93ee58ff98 100644 --- a/entities/bug/op_edit_comment.go +++ b/entities/bug/op_edit_comment.go @@ -98,6 +98,12 @@ func (op *EditCommentOperation) Validate() error { return fmt.Errorf("message is not fully printable") } + for _, file := range op.Files { + if !file.IsValid() { + return fmt.Errorf("invalid file hash") + } + } + return nil }