aboutsummaryrefslogtreecommitdiffstats
path: root/bug
diff options
context:
space:
mode:
Diffstat (limited to 'bug')
-rw-r--r--bug/bug.go179
-rw-r--r--bug/bug_actions.go74
-rw-r--r--bug/comment.go45
-rw-r--r--bug/err.go17
-rw-r--r--bug/interface.go44
-rw-r--r--bug/label.go95
-rw-r--r--bug/label_test.go35
-rw-r--r--bug/op_add_comment.go93
-rw-r--r--bug/op_add_comment_test.go18
-rw-r--r--bug/op_create.go112
-rw-r--r--bug/op_create_test.go67
-rw-r--r--bug/op_edit_comment.go129
-rw-r--r--bug/op_edit_comment_test.go84
-rw-r--r--bug/op_label_change.go251
-rw-r--r--bug/op_label_change_test.go20
-rw-r--r--bug/op_set_metadata.go21
-rw-r--r--bug/op_set_status.go95
-rw-r--r--bug/op_set_status_test.go14
-rw-r--r--bug/op_set_title.go112
-rw-r--r--bug/op_set_title_test.go14
-rw-r--r--bug/operation.go73
-rw-r--r--bug/operation_test.go131
-rw-r--r--bug/resolver.go21
-rw-r--r--bug/snapshot.go144
-rw-r--r--bug/sorting.go57
-rw-r--r--bug/status.go57
-rw-r--r--bug/timeline.go80
-rw-r--r--bug/with_snapshot.go53
28 files changed, 0 insertions, 2135 deletions
diff --git a/bug/bug.go b/bug/bug.go
deleted file mode 100644
index 65fb621e..00000000
--- a/bug/bug.go
+++ /dev/null
@@ -1,179 +0,0 @@
-// Package bug contains the bug data model and low-level related functions
-package bug
-
-import (
- "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/repository"
-)
-
-var _ Interface = &Bug{}
-var _ entity.Interface = &Bug{}
-
-// 1: original format
-// 2: no more legacy identities
-// 3: Ids are generated from the create operation serialized data instead of from the first git commit
-// 4: with DAG entity framework
-const formatVersion = 4
-
-var def = dag.Definition{
- Typename: "bug",
- Namespace: "bugs",
- OperationUnmarshaler: operationUnmarshaller,
- FormatVersion: formatVersion,
-}
-
-var ClockLoader = dag.ClockLoader(def)
-
-// Bug holds the data of a bug thread, organized in a way close to
-// how it will be persisted inside Git. This is the data structure
-// used to merge two different version of the same Bug.
-type Bug struct {
- *dag.Entity
-}
-
-// NewBug create a new Bug
-func NewBug() *Bug {
- return &Bug{
- Entity: dag.New(def),
- }
-}
-
-func simpleResolvers(repo repository.ClockedRepo) entity.Resolvers {
- return entity.Resolvers{
- &identity.Identity{}: identity.NewSimpleResolver(repo),
- }
-}
-
-// Read will read a bug from a repository
-func Read(repo repository.ClockedRepo, id entity.Id) (*Bug, error) {
- return ReadWithResolver(repo, simpleResolvers(repo), id)
-}
-
-// ReadWithResolver will read a bug from its Id, with custom resolvers
-func ReadWithResolver(repo repository.ClockedRepo, resolvers entity.Resolvers, id entity.Id) (*Bug, error) {
- e, err := dag.Read(def, repo, resolvers, id)
- if err != nil {
- return nil, err
- }
- return &Bug{Entity: e}, nil
-}
-
-type StreamedBug struct {
- Bug *Bug
- Err error
-}
-
-// ReadAll read and parse all local bugs
-func ReadAll(repo repository.ClockedRepo) <-chan StreamedBug {
- return readAll(repo, simpleResolvers(repo))
-}
-
-// ReadAllWithResolver read and parse all local bugs
-func ReadAllWithResolver(repo repository.ClockedRepo, resolvers entity.Resolvers) <-chan StreamedBug {
- return readAll(repo, resolvers)
-}
-
-// Read and parse all available bug with a given ref prefix
-func readAll(repo repository.ClockedRepo, resolvers entity.Resolvers) <-chan StreamedBug {
- out := make(chan StreamedBug)
-
- go func() {
- defer close(out)
-
- for streamedEntity := range dag.ReadAll(def, repo, resolvers) {
- if streamedEntity.Err != nil {
- out <- StreamedBug{
- Err: streamedEntity.Err,
- }
- } else {
- out <- StreamedBug{
- Bug: &Bug{Entity: streamedEntity.Entity},
- }
- }
- }
- }()
-
- return out
-}
-
-// ListLocalIds list all the available local bug ids
-func ListLocalIds(repo repository.Repo) ([]entity.Id, error) {
- return dag.ListLocalIds(def, repo)
-}
-
-// Validate check if the Bug data is valid
-func (bug *Bug) Validate() error {
- if err := bug.Entity.Validate(); err != nil {
- return err
- }
-
- // The very first Op should be a CreateOp
- firstOp := bug.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 bug.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 Bug
-func (bug *Bug) Append(op Operation) {
- bug.Entity.Append(op)
-}
-
-// Operations return the ordered operations
-func (bug *Bug) Operations() []Operation {
- source := bug.Entity.Operations()
- result := make([]Operation, len(source))
- for i, op := range source {
- result[i] = op.(Operation)
- }
- return result
-}
-
-// Compile a bug in a easily usable snapshot
-func (bug *Bug) Compile() *Snapshot {
- snap := &Snapshot{
- id: bug.Id(),
- Status: OpenStatus,
- }
-
- for _, op := range bug.Operations() {
- op.Apply(snap)
- snap.Operations = append(snap.Operations, op)
- }
-
- return snap
-}
-
-// 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 {
- return fo.(Operation)
- }
- return nil
-}
-
-// 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 {
- return lo.(Operation)
- }
- return nil
-}
diff --git a/bug/bug_actions.go b/bug/bug_actions.go
deleted file mode 100644
index 3a8ec3f0..00000000
--- a/bug/bug_actions.go
+++ /dev/null
@@ -1,74 +0,0 @@
-package bug
-
-import (
- "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/repository"
-)
-
-// Fetch retrieve updates from a remote
-// This does not change the local bugs 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 {
- _, err := Fetch(repo, remote)
- if err != nil {
- return err
- }
-
- for merge := range MergeAll(repo, resolvers, remote, mergeAuthor) {
- if merge.Err != nil {
- return merge.Err
- }
- if merge.Status == entity.MergeStatusInvalid {
- return errors.Errorf("merge failure: %s", merge.Reason)
- }
- }
-
- return nil
-}
-
-// MergeAll will merge all the available remote bug
-// 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 = &Bug{
- Entity: result.Entity.(*dag.Entity),
- }
- }
- out <- result
- }
- }()
-
- return out
-}
-
-// RemoveBug will remove a local bug from its entity.Id
-func RemoveBug(repo repository.ClockedRepo, id entity.Id) error {
- return dag.Remove(def, repo, id)
-}
diff --git a/bug/comment.go b/bug/comment.go
deleted file mode 100644
index 03d58da5..00000000
--- a/bug/comment.go
+++ /dev/null
@@ -1,45 +0,0 @@
-package bug
-
-import (
- "github.com/dustin/go-humanize"
-
- "github.com/MichaelMure/git-bug/entity"
- "github.com/MichaelMure/git-bug/identity"
- "github.com/MichaelMure/git-bug/repository"
- "github.com/MichaelMure/git-bug/util/timestamp"
-)
-
-// Comment represent a comment in a Bug
-type Comment struct {
- // id should be the result of entity.CombineIds with the Bug id and the id
- // of the Operation that created the comment
- id entity.Id
- Author identity.Interface
- 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
-}
-
-// Id return the Comment identifier
-func (c Comment) Id() entity.Id {
- if c.id == "" {
- // simply panic as it would be a coding error (no id provided at construction)
- panic("no id")
- }
- return c.id
-}
-
-// FormatTimeRel format the UnixTime of the comment for human consumption
-func (c Comment) FormatTimeRel() string {
- return humanize.Time(c.UnixTime.Time())
-}
-
-func (c Comment) FormatTime() string {
- return c.UnixTime.Time().Format("Mon Jan 2 15:04:05 2006 +0200")
-}
-
-// IsAuthored is a sign post method for gqlgen
-func (c Comment) IsAuthored() {}
diff --git a/bug/err.go b/bug/err.go
deleted file mode 100644
index 1bd174bb..00000000
--- a/bug/err.go
+++ /dev/null
@@ -1,17 +0,0 @@
-package bug
-
-import (
- "errors"
-
- "github.com/MichaelMure/git-bug/entity"
-)
-
-var ErrBugNotExist = errors.New("bug doesn't exist")
-
-func NewErrMultipleMatchBug(matching []entity.Id) *entity.ErrMultipleMatch {
- return entity.NewErrMultipleMatch("bug", matching)
-}
-
-func NewErrMultipleMatchOp(matching []entity.Id) *entity.ErrMultipleMatch {
- return entity.NewErrMultipleMatch("operation", matching)
-}
diff --git a/bug/interface.go b/bug/interface.go
deleted file mode 100644
index 2ae31fd1..00000000
--- a/bug/interface.go
+++ /dev/null
@@ -1,44 +0,0 @@
-package bug
-
-import (
- "github.com/MichaelMure/git-bug/entity"
- "github.com/MichaelMure/git-bug/repository"
- "github.com/MichaelMure/git-bug/util/lamport"
-)
-
-type Interface interface {
- // Id returns the Bug identifier
- Id() entity.Id
-
- // 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 returns the ordered operations
- Operations() []Operation
-
- // NeedCommit indicates that the in-memory state changed and need to be commit in the repository
- NeedCommit() bool
-
- // Commit writes the staging area in Git and move the operations to the packs
- Commit(repo repository.ClockedRepo) error
-
- // FirstOp lookup for the very first operation of the bug.
- // For a valid Bug, this operation should be a CreateOp
- FirstOp() Operation
-
- // LastOp lookup for the very last operation of the bug.
- // For a valid Bug, should never be nil
- LastOp() Operation
-
- // Compile a bug in an easily usable snapshot
- Compile() *Snapshot
-
- // CreateLamportTime return the Lamport time of creation
- CreateLamportTime() lamport.Time
-
- // EditLamportTime return the Lamport time of the last edit
- EditLamportTime() lamport.Time
-}
diff --git a/bug/label.go b/bug/label.go
deleted file mode 100644
index 79b5f591..00000000
--- a/bug/label.go
+++ /dev/null
@@ -1,95 +0,0 @@
-package bug
-
-import (
- "crypto/sha256"
- "fmt"
- "image/color"
-
- fcolor "github.com/fatih/color"
-
- "github.com/MichaelMure/git-bug/util/text"
-)
-
-type Label string
-
-func (l Label) String() string {
- return string(l)
-}
-
-// RGBA from a Label computed in a deterministic way
-func (l Label) Color() LabelColor {
- // colors from: https://material-ui.com/style/color/
- colors := []LabelColor{
- {R: 244, G: 67, B: 54, A: 255}, // red
- {R: 233, G: 30, B: 99, A: 255}, // pink
- {R: 156, G: 39, B: 176, A: 255}, // purple
- {R: 103, G: 58, B: 183, A: 255}, // deepPurple
- {R: 63, G: 81, B: 181, A: 255}, // indigo
- {R: 33, G: 150, B: 243, A: 255}, // blue
- {R: 3, G: 169, B: 244, A: 255}, // lightBlue
- {R: 0, G: 188, B: 212, A: 255}, // cyan
- {R: 0, G: 150, B: 136, A: 255}, // teal
- {R: 76, G: 175, B: 80, A: 255}, // green
- {R: 139, G: 195, B: 74, A: 255}, // lightGreen
- {R: 205, G: 220, B: 57, A: 255}, // lime
- {R: 255, G: 235, B: 59, A: 255}, // yellow
- {R: 255, G: 193, B: 7, A: 255}, // amber
- {R: 255, G: 152, B: 0, A: 255}, // orange
- {R: 255, G: 87, B: 34, A: 255}, // deepOrange
- {R: 121, G: 85, B: 72, A: 255}, // brown
- {R: 158, G: 158, B: 158, A: 255}, // grey
- {R: 96, G: 125, B: 139, A: 255}, // blueGrey
- }
-
- id := 0
- hash := sha256.Sum256([]byte(l))
- for _, char := range hash {
- id = (id + int(char)) % len(colors)
- }
-
- return colors[id]
-}
-
-func (l Label) Validate() error {
- str := string(l)
-
- if text.Empty(str) {
- return fmt.Errorf("empty")
- }
-
- if !text.SafeOneLine(str) {
- return fmt.Errorf("label has unsafe characters")
- }
-
- return nil
-}
-
-type LabelColor color.RGBA
-
-func (lc LabelColor) RGBA() color.RGBA {
- return color.RGBA(lc)
-}
-
-func (lc LabelColor) Term256() Term256 {
- red := Term256(lc.R) * 6 / 256
- green := Term256(lc.G) * 6 / 256
- blue := Term256(lc.B) * 6 / 256
-
- return red*36 + green*6 + blue + 16
-}
-
-type Term256 int
-
-func (t Term256) Escape() string {
- if fcolor.NoColor {
- return ""
- }
- return fmt.Sprintf("\x1b[38;5;%dm", t)
-}
-
-func (t Term256) Unescape() string {
- if fcolor.NoColor {
- return ""
- }
- return "\x1b[0m"
-}
diff --git a/bug/label_test.go b/bug/label_test.go
deleted file mode 100644
index 49401c49..00000000
--- a/bug/label_test.go
+++ /dev/null
@@ -1,35 +0,0 @@
-package bug
-
-import (
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestLabelRGBA(t *testing.T) {
- rgba := Label("test1").Color()
- expected := LabelColor{R: 0, G: 150, B: 136, A: 255}
-
- require.Equal(t, expected, rgba)
-}
-
-func TestLabelRGBASimilar(t *testing.T) {
- rgba := Label("test2").Color()
- expected := LabelColor{R: 3, G: 169, B: 244, A: 255}
-
- require.Equal(t, expected, rgba)
-}
-
-func TestLabelRGBAReverse(t *testing.T) {
- rgba := Label("tset").Color()
- expected := LabelColor{R: 63, G: 81, B: 181, A: 255}
-
- require.Equal(t, expected, rgba)
-}
-
-func TestLabelRGBAEqual(t *testing.T) {
- color1 := Label("test").Color()
- color2 := Label("test").Color()
-
- require.Equal(t, color1, color2)
-}
diff --git a/bug/op_add_comment.go b/bug/op_add_comment.go
deleted file mode 100644
index eddd585a..00000000
--- a/bug/op_add_comment.go
+++ /dev/null
@@ -1,93 +0,0 @@
-package bug
-
-import (
- "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/repository"
- "github.com/MichaelMure/git-bug/util/text"
- "github.com/MichaelMure/git-bug/util/timestamp"
-)
-
-var _ Operation = &AddCommentOperation{}
-var _ dag.OperationWithFiles = &AddCommentOperation{}
-
-// AddCommentOperation will add a new comment in the bug
-type AddCommentOperation struct {
- 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 dag.IdOperation(op, &op.OpBase)
-}
-
-func (op *AddCommentOperation) Apply(snapshot *Snapshot) {
- snapshot.addActor(op.Author())
- snapshot.addParticipant(op.Author())
-
- comment := Comment{
- id: entity.CombineIds(snapshot.Id(), op.Id()),
- Message: op.Message,
- Author: op.Author(),
- Files: op.Files,
- UnixTime: timestamp.Timestamp(op.UnixTime),
- }
-
- snapshot.Comments = append(snapshot.Comments, comment)
-
- item := &AddCommentTimelineItem{
- CommentTimelineItem: NewCommentTimelineItem(comment),
- }
-
- snapshot.Timeline = append(snapshot.Timeline, item)
-}
-
-func (op *AddCommentOperation) GetFiles() []repository.Hash {
- return op.Files
-}
-
-func (op *AddCommentOperation) Validate() error {
- if err := op.OpBase.Validate(op, AddCommentOp); err != nil {
- return err
- }
-
- if !text.Safe(op.Message) {
- return fmt.Errorf("message is not fully printable")
- }
-
- return nil
-}
-
-func NewAddCommentOp(author identity.Interface, unixTime int64, message string, files []repository.Hash) *AddCommentOperation {
- return &AddCommentOperation{
- OpBase: dag.NewOpBase(AddCommentOp, author, unixTime),
- Message: message,
- Files: files,
- }
-}
-
-// AddCommentTimelineItem hold a comment in the timeline
-type AddCommentTimelineItem struct {
- CommentTimelineItem
-}
-
-// IsAuthored is a sign post method for gqlgen
-func (a *AddCommentTimelineItem) IsAuthored() {}
-
-// 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(op)
- return op, nil
-}
diff --git a/bug/op_add_comment_test.go b/bug/op_add_comment_test.go
deleted file mode 100644
index efdf7601..00000000
--- a/bug/op_add_comment_test.go
+++ /dev/null
@@ -1,18 +0,0 @@
-package bug
-
-import (
- "testing"
-
- "github.com/MichaelMure/git-bug/entity/dag"
- "github.com/MichaelMure/git-bug/identity"
- "github.com/MichaelMure/git-bug/repository"
-)
-
-func TestAddCommentSerialize(t *testing.T) {
- 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
deleted file mode 100644
index ca4f3d8a..00000000
--- a/bug/op_create.go
+++ /dev/null
@@ -1,112 +0,0 @@
-package bug
-
-import (
- "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/repository"
- "github.com/MichaelMure/git-bug/util/text"
- "github.com/MichaelMure/git-bug/util/timestamp"
-)
-
-var _ Operation = &CreateOperation{}
-var _ dag.OperationWithFiles = &CreateOperation{}
-
-// CreateOperation define the initial creation of a bug
-type CreateOperation struct {
- dag.OpBase
- Title string `json:"title"`
- Message string `json:"message"`
- Files []repository.Hash `json:"files"`
-}
-
-func (op *CreateOperation) Id() entity.Id {
- 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() {
- return
- }
-
- snapshot.id = op.Id()
-
- 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(),
- UnixTime: timestamp.Timestamp(op.UnixTime),
- }
-
- snapshot.Comments = []Comment{comment}
- snapshot.Author = op.Author()
- snapshot.CreateTime = op.Time()
-
- snapshot.Timeline = []TimelineItem{
- &CreateTimelineItem{
- CommentTimelineItem: NewCommentTimelineItem(comment),
- },
- }
-}
-
-func (op *CreateOperation) GetFiles() []repository.Hash {
- return op.Files
-}
-
-func (op *CreateOperation) Validate() error {
- if err := op.OpBase.Validate(op, CreateOp); 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")
- }
-
- return nil
-}
-
-func NewCreateOp(author identity.Interface, unixTime int64, title, message string, files []repository.Hash) *CreateOperation {
- return &CreateOperation{
- OpBase: dag.NewOpBase(CreateOp, author, unixTime),
- Title: title,
- Message: message,
- Files: files,
- }
-}
-
-// CreateTimelineItem replace a Create operation in the Timeline and hold its edition history
-type CreateTimelineItem struct {
- CommentTimelineItem
-}
-
-// IsAuthored is a sign post method for gqlgen
-func (c *CreateTimelineItem) IsAuthored() {}
-
-// 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)
- }
- 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
deleted file mode 100644
index 478bc9d4..00000000
--- a/bug/op_create_test.go
+++ /dev/null
@@ -1,67 +0,0 @@
-package bug
-
-import (
- "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"
-)
-
-func TestCreate(t *testing.T) {
- snapshot := Snapshot{}
-
- repo := repository.NewMockRepoClock()
-
- rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
- require.NoError(t, err)
-
- unix := time.Now().Unix()
-
- create := NewCreateOp(rene, unix, "title", "message", nil)
-
- create.Apply(&snapshot)
-
- id := create.Id()
- require.NoError(t, id.Validate())
-
- comment := Comment{
- id: entity.CombineIds(create.Id(), create.Id()),
- Author: rene,
- Message: "message",
- UnixTime: timestamp.Timestamp(create.UnixTime),
- }
-
- expected := Snapshot{
- id: create.Id(),
- Title: "title",
- Comments: []Comment{
- comment,
- },
- Author: rene,
- Participants: []identity.Interface{rene},
- Actors: []identity.Interface{rene},
- CreateTime: create.Time(),
- Timeline: []TimelineItem{
- &CreateTimelineItem{
- CommentTimelineItem: NewCommentTimelineItem(comment),
- },
- },
- }
-
- require.Equal(t, expected, snapshot)
-}
-
-func TestCreateSerialize(t *testing.T) {
- 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
deleted file mode 100644
index 70a362de..00000000
--- a/bug/op_edit_comment.go
+++ /dev/null
@@ -1,129 +0,0 @@
-package bug
-
-import (
- "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/repository"
- "github.com/MichaelMure/git-bug/util/timestamp"
-
- "github.com/MichaelMure/git-bug/util/text"
-)
-
-var _ Operation = &EditCommentOperation{}
-var _ dag.OperationWithFiles = &EditCommentOperation{}
-
-// EditCommentOperation will change a comment in the bug
-type EditCommentOperation struct {
- dag.OpBase
- Target entity.Id `json:"target"`
- Message string `json:"message"`
- Files []repository.Hash `json:"files"`
-}
-
-func (op *EditCommentOperation) Id() entity.Id {
- return dag.IdOperation(op, &op.OpBase)
-}
-
-func (op *EditCommentOperation) Apply(snapshot *Snapshot) {
- // Todo: currently any message can be edited, even by a different author
- // crypto signature are needed.
-
- // Recreate the Comment Id to match on
- commentId := entity.CombineIds(snapshot.Id(), op.Target)
-
- var target TimelineItem
- for i, item := range snapshot.Timeline {
- if item.Id() == commentId {
- target = snapshot.Timeline[i]
- break
- }
- }
-
- if target == nil {
- // Target not found, edit is a no-op
- return
- }
-
- comment := Comment{
- id: commentId,
- Message: op.Message,
- Files: op.Files,
- UnixTime: timestamp.Timestamp(op.UnixTime),
- }
-
- switch target := target.(type) {
- case *CreateTimelineItem:
- target.Append(comment)
- case *AddCommentTimelineItem:
- target.Append(comment)
- default:
- // somehow, the target matched on something that is not a comment
- // we make the op a no-op
- return
- }
-
- snapshot.addActor(op.Author())
-
- // Updating the corresponding comment
-
- for i := range snapshot.Comments {
- if snapshot.Comments[i].Id() == commentId {
- snapshot.Comments[i].Message = op.Message
- snapshot.Comments[i].Files = op.Files
- break
- }
- }
-}
-
-func (op *EditCommentOperation) GetFiles() []repository.Hash {
- return op.Files
-}
-
-func (op *EditCommentOperation) Validate() error {
- if err := op.OpBase.Validate(op, EditCommentOp); err != nil {
- return err
- }
-
- if err := op.Target.Validate(); err != nil {
- return errors.Wrap(err, "target hash is invalid")
- }
-
- if !text.Safe(op.Message) {
- return fmt.Errorf("message is not fully printable")
- }
-
- return nil
-}
-
-func NewEditCommentOp(author identity.Interface, unixTime int64, target entity.Id, message string, files []repository.Hash) *EditCommentOperation {
- return &EditCommentOperation{
- OpBase: dag.NewOpBase(EditCommentOp, author, unixTime),
- Target: target,
- Message: message,
- Files: files,
- }
-}
-
-// EditComment is a convenience function to apply the operation
-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(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, files []repository.Hash, metadata map[string]string) (*EditCommentOperation, error) {
- createOp := b.FirstOp().(*CreateOperation)
- 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
deleted file mode 100644
index 780483e4..00000000
--- a/bug/op_edit_comment_test.go
+++ /dev/null
@@ -1,84 +0,0 @@
-package bug
-
-import (
- "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 TestEdit(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.Apply(&snapshot)
-
- require.NoError(t, create.Id().Validate())
-
- comment1 := NewAddCommentOp(rene, unix, "comment 1", nil)
- comment1.Apply(&snapshot)
-
- require.NoError(t, comment1.Id().Validate())
-
- // add another unrelated op in between
- setTitle := NewSetTitleOp(rene, unix, "edited title", "title")
- setTitle.Apply(&snapshot)
-
- comment2 := NewAddCommentOp(rene, unix, "comment 2", nil)
- comment2.Apply(&snapshot)
-
- require.NoError(t, comment2.Id().Validate())
-
- edit := NewEditCommentOp(rene, unix, create.Id(), "create edited", nil)
- edit.Apply(&snapshot)
-
- require.Len(t, snapshot.Timeline, 4)
- require.Len(t, snapshot.Timeline[0].(*CreateTimelineItem).History, 2)
- require.Len(t, snapshot.Timeline[1].(*AddCommentTimelineItem).History, 1)
- require.Len(t, snapshot.Timeline[3].(*AddCommentTimelineItem).History, 1)
- require.Equal(t, snapshot.Comments[0].Message, "create edited")
- require.Equal(t, snapshot.Comments[1].Message, "comment 1")
- require.Equal(t, snapshot.Comments[2].Message, "comment 2")
-
- edit2 := NewEditCommentOp(rene, unix, comment1.Id(), "comment 1 edited", nil)
- edit2.Apply(&snapshot)
-
- require.Len(t, snapshot.Timeline, 4)
- require.Len(t, snapshot.Timeline[0].(*CreateTimelineItem).History, 2)
- require.Len(t, snapshot.Timeline[1].(*AddCommentTimelineItem).History, 2)
- require.Len(t, snapshot.Timeline[3].(*AddCommentTimelineItem).History, 1)
- require.Equal(t, snapshot.Comments[0].Message, "create edited")
- require.Equal(t, snapshot.Comments[1].Message, "comment 1 edited")
- require.Equal(t, snapshot.Comments[2].Message, "comment 2")
-
- edit3 := NewEditCommentOp(rene, unix, comment2.Id(), "comment 2 edited", nil)
- edit3.Apply(&snapshot)
-
- require.Len(t, snapshot.Timeline, 4)
- require.Len(t, snapshot.Timeline[0].(*CreateTimelineItem).History, 2)
- require.Len(t, snapshot.Timeline[1].(*AddCommentTimelineItem).History, 2)
- require.Len(t, snapshot.Timeline[3].(*AddCommentTimelineItem).History, 2)
- require.Equal(t, snapshot.Comments[0].Message, "create edited")
- require.Equal(t, snapshot.Comments[1].Message, "comment 1 edited")
- require.Equal(t, snapshot.Comments[2].Message, "comment 2 edited")
-}
-
-func TestEditCommentSerialize(t *testing.T) {
- 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
deleted file mode 100644
index 8bcc7853..00000000
--- a/bug/op_label_change.go
+++ /dev/null
@@ -1,251 +0,0 @@
-package bug
-
-import (
- "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"
-)
-
-var _ Operation = &LabelChangeOperation{}
-
-// LabelChangeOperation define a Bug operation to add or remove labels
-type LabelChangeOperation struct {
- dag.OpBase
- Added []Label `json:"added"`
- Removed []Label `json:"removed"`
-}
-
-func (op *LabelChangeOperation) Id() entity.Id {
- return dag.IdOperation(op, &op.OpBase)
-}
-
-// Apply applies the operation
-func (op *LabelChangeOperation) Apply(snapshot *Snapshot) {
- snapshot.addActor(op.Author())
-
- // Add in the set
-AddLoop:
- for _, added := range op.Added {
- for _, label := range snapshot.Labels {
- if label == added {
- // Already exist
- continue AddLoop
- }
- }
-
- snapshot.Labels = append(snapshot.Labels, added)
- }
-
- // Remove in the set
- for _, removed := range op.Removed {
- for i, label := range snapshot.Labels {
- if label == removed {
- snapshot.Labels[i] = snapshot.Labels[len(snapshot.Labels)-1]
- snapshot.Labels = snapshot.Labels[:len(snapshot.Labels)-1]
- }
- }
- }
-
- // Sort
- sort.Slice(snapshot.Labels, func(i, j int) bool {
- return string(snapshot.Labels[i]) < string(snapshot.Labels[j])
- })
-
- item := &LabelChangeTimelineItem{
- id: op.Id(),
- Author: op.Author(),
- UnixTime: timestamp.Timestamp(op.UnixTime),
- Added: op.Added,
- Removed: op.Removed,
- }
-
- snapshot.Timeline = append(snapshot.Timeline, item)
-}
-
-func (op *LabelChangeOperation) Validate() error {
- if err := op.OpBase.Validate(op, LabelChangeOp); err != nil {
- return err
- }
-
- for _, l := range op.Added {
- if err := l.Validate(); err != nil {
- return errors.Wrap(err, "added label")
- }
- }
-
- for _, l := range op.Removed {
- if err := l.Validate(); err != nil {
- return errors.Wrap(err, "removed label")
- }
- }
-
- if len(op.Added)+len(op.Removed) <= 0 {
- return fmt.Errorf("no label change")
- }
-
- return nil
-}
-
-func NewLabelChangeOperation(author identity.Interface, unixTime int64, added, removed []Label) *LabelChangeOperation {
- return &LabelChangeOperation{
- OpBase: dag.NewOpBase(LabelChangeOp, author, unixTime),
- Added: added,
- Removed: removed,
- }
-}
-
-type LabelChangeTimelineItem struct {
- id entity.Id
- Author identity.Interface
- UnixTime timestamp.Timestamp
- Added []Label
- Removed []Label
-}
-
-func (l LabelChangeTimelineItem) Id() entity.Id {
- return l.id
-}
-
-// IsAuthored is a sign post method for gqlgen
-func (l LabelChangeTimelineItem) IsAuthored() {}
-
-// 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
-
- snap := b.Compile()
-
- for _, str := range add {
- label := Label(str)
-
- // check for duplicate
- if labelExist(added, label) {
- results = append(results, LabelChangeResult{Label: label, Status: LabelChangeDuplicateInOp})
- continue
- }
-
- // check that the label doesn't already exist
- if labelExist(snap.Labels, label) {
- results = append(results, LabelChangeResult{Label: label, Status: LabelChangeAlreadySet})
- continue
- }
-
- added = append(added, label)
- results = append(results, LabelChangeResult{Label: label, Status: LabelChangeAdded})
- }
-
- for _, str := range remove {
- label := Label(str)
-
- // check for duplicate
- if labelExist(removed, label) {
- results = append(results, LabelChangeResult{Label: label, Status: LabelChangeDuplicateInOp})
- continue
- }
-
- // check that the label actually exist
- if !labelExist(snap.Labels, label) {
- results = append(results, LabelChangeResult{Label: label, Status: LabelChangeDoesntExist})
- continue
- }
-
- removed = append(removed, label)
- results = append(results, LabelChangeResult{Label: label, Status: LabelChangeRemoved})
- }
-
- if len(added) == 0 && len(removed) == 0 {
- return results, nil, fmt.Errorf("no label added or removed")
- }
-
- 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(op)
-
- 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 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, metadata map[string]string) (*LabelChangeOperation, error) {
- added := make([]Label, len(add))
- for i, str := range add {
- added[i] = Label(str)
- }
-
- removed := make([]Label, len(remove))
- for i, str := range remove {
- removed[i] = Label(str)
- }
-
- op := NewLabelChangeOperation(author, unixTime, added, removed)
-
- for key, val := range metadata {
- op.SetMetadata(key, val)
- }
- if err := op.Validate(); err != nil {
- return nil, err
- }
-
- b.Append(op)
-
- return op, nil
-}
-
-func labelExist(labels []Label, label Label) bool {
- for _, l := range labels {
- if l == label {
- return true
- }
- }
-
- return false
-}
-
-type LabelChangeStatus int
-
-const (
- _ LabelChangeStatus = iota
- LabelChangeAdded
- LabelChangeRemoved
- LabelChangeDuplicateInOp
- LabelChangeAlreadySet
- LabelChangeDoesntExist
-)
-
-type LabelChangeResult struct {
- Label Label
- Status LabelChangeStatus
-}
-
-func (l LabelChangeResult) String() string {
- switch l.Status {
- case LabelChangeAdded:
- return fmt.Sprintf("label %s added", l.Label)
- case LabelChangeRemoved:
- return fmt.Sprintf("label %s removed", l.Label)
- case LabelChangeDuplicateInOp:
- return fmt.Sprintf("label %s is a duplicate", l.Label)
- case LabelChangeAlreadySet:
- return fmt.Sprintf("label %s was already set", l.Label)
- case LabelChangeDoesntExist:
- return fmt.Sprintf("label %s doesn't exist on this bug", l.Label)
- default:
- panic(fmt.Sprintf("unknown label change status %v", l.Status))
- }
-}
diff --git a/bug/op_label_change_test.go b/bug/op_label_change_test.go
deleted file mode 100644
index 2c1d8f62..00000000
--- a/bug/op_label_change_test.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package bug
-
-import (
- "testing"
-
- "github.com/MichaelMure/git-bug/entity/dag"
- "github.com/MichaelMure/git-bug/identity"
-)
-
-func TestLabelChangeSerialize(t *testing.T) {
- 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_set_metadata.go b/bug/op_set_metadata.go
deleted file mode 100644
index 08e1887b..00000000
--- a/bug/op_set_metadata.go
+++ /dev/null
@@ -1,21 +0,0 @@
-package bug
-
-import (
- "github.com/MichaelMure/git-bug/entity"
- "github.com/MichaelMure/git-bug/entity/dag"
- "github.com/MichaelMure/git-bug/identity"
-)
-
-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)
-}
-
-// 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(op)
- return op, nil
-}
diff --git a/bug/op_set_status.go b/bug/op_set_status.go
deleted file mode 100644
index ff8df75d..00000000
--- a/bug/op_set_status.go
+++ /dev/null
@@ -1,95 +0,0 @@
-package bug
-
-import (
- "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"
-)
-
-var _ Operation = &SetStatusOperation{}
-
-// SetStatusOperation will change the status of a bug
-type SetStatusOperation struct {
- dag.OpBase
- Status Status `json:"status"`
-}
-
-func (op *SetStatusOperation) Id() entity.Id {
- return dag.IdOperation(op, &op.OpBase)
-}
-
-func (op *SetStatusOperation) Apply(snapshot *Snapshot) {
- snapshot.Status = op.Status
- snapshot.addActor(op.Author())
-
- item := &SetStatusTimelineItem{
- id: op.Id(),
- Author: op.Author(),
- UnixTime: timestamp.Timestamp(op.UnixTime),
- Status: op.Status,
- }
-
- snapshot.Timeline = append(snapshot.Timeline, item)
-}
-
-func (op *SetStatusOperation) Validate() error {
- if err := op.OpBase.Validate(op, SetStatusOp); err != nil {
- return err
- }
-
- if err := op.Status.Validate(); err != nil {
- return errors.Wrap(err, "status")
- }
-
- return nil
-}
-
-func NewSetStatusOp(author identity.Interface, unixTime int64, status Status) *SetStatusOperation {
- return &SetStatusOperation{
- OpBase: dag.NewOpBase(SetStatusOp, author, unixTime),
- Status: status,
- }
-}
-
-type SetStatusTimelineItem struct {
- id entity.Id
- Author identity.Interface
- UnixTime timestamp.Timestamp
- Status Status
-}
-
-func (s SetStatusTimelineItem) Id() entity.Id {
- return s.id
-}
-
-// IsAuthored is a sign post method for gqlgen
-func (s SetStatusTimelineItem) IsAuthored() {}
-
-// 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
- }
- b.Append(op)
- return op, nil
-}
-
-// 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
- }
- b.Append(op)
- return op, nil
-}
diff --git a/bug/op_set_status_test.go b/bug/op_set_status_test.go
deleted file mode 100644
index 385deec1..00000000
--- a/bug/op_set_status_test.go
+++ /dev/null
@@ -1,14 +0,0 @@
-package bug
-
-import (
- "testing"
-
- "github.com/MichaelMure/git-bug/entity/dag"
- "github.com/MichaelMure/git-bug/identity"
-)
-
-func TestSetStatusSerialize(t *testing.T) {
- 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
deleted file mode 100644
index d26a60fa..00000000
--- a/bug/op_set_title.go
+++ /dev/null
@@ -1,112 +0,0 @@
-package bug
-
-import (
- "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"
-
- "github.com/MichaelMure/git-bug/util/text"
-)
-
-var _ Operation = &SetTitleOperation{}
-
-// SetTitleOperation will change the title of a bug
-type SetTitleOperation struct {
- dag.OpBase
- Title string `json:"title"`
- Was string `json:"was"`
-}
-
-func (op *SetTitleOperation) Id() entity.Id {
- return dag.IdOperation(op, &op.OpBase)
-}
-
-func (op *SetTitleOperation) Apply(snapshot *Snapshot) {
- snapshot.Title = op.Title
- snapshot.addActor(op.Author())
-
- item := &SetTitleTimelineItem{
- id: op.Id(),
- Author: op.Author(),
- UnixTime: timestamp.Timestamp(op.UnixTime),
- Title: op.Title,
- Was: op.Was,
- }
-
- snapshot.Timeline = append(snapshot.Timeline, item)
-}
-
-func (op *SetTitleOperation) Validate() error {
- if err := op.OpBase.Validate(op, SetTitleOp); 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.SafeOneLine(op.Was) {
- return fmt.Errorf("previous title has unsafe characters")
- }
-
- return nil
-}
-
-func NewSetTitleOp(author identity.Interface, unixTime int64, title string, was string) *SetTitleOperation {
- return &SetTitleOperation{
- OpBase: dag.NewOpBase(SetTitleOp, author, unixTime),
- Title: title,
- Was: was,
- }
-}
-
-type SetTitleTimelineItem struct {
- id entity.Id
- Author identity.Interface
- UnixTime timestamp.Timestamp
- Title string
- Was string
-}
-
-func (s SetTitleTimelineItem) Id() entity.Id {
- return s.id
-}
-
-// IsAuthored is a sign post method for gqlgen
-func (s SetTitleTimelineItem) IsAuthored() {}
-
-// 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) {
- case *SetTitleOperation:
- lastTitleOp = op
- }
- }
-
- var was string
- if lastTitleOp != nil {
- was = lastTitleOp.Title
- } else {
- was = b.FirstOp().(*CreateOperation).Title
- }
-
- 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(op)
- return op, nil
-}
diff --git a/bug/op_set_title_test.go b/bug/op_set_title_test.go
deleted file mode 100644
index 8b45c74e..00000000
--- a/bug/op_set_title_test.go
+++ /dev/null
@@ -1,14 +0,0 @@
-package bug
-
-import (
- "testing"
-
- "github.com/MichaelMure/git-bug/entity/dag"
- "github.com/MichaelMure/git-bug/identity"
-)
-
-func TestSetTitleSerialize(t *testing.T) {
- 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
deleted file mode 100644
index a02fc780..00000000
--- a/bug/operation.go
+++ /dev/null
@@ -1,73 +0,0 @@
-package bug
-
-import (
- "encoding/json"
- "fmt"
-
- "github.com/MichaelMure/git-bug/entity"
- "github.com/MichaelMure/git-bug/entity/dag"
-)
-
-const (
- _ dag.OperationType = iota
- CreateOp
- SetTitleOp
- AddCommentOp
- SetStatusOp
- LabelChangeOp
- EditCommentOp
- NoOpOp
- SetMetadataOp
-)
-
-// Operation define the interface to fulfill for an edit operation of a Bug
-type Operation interface {
- dag.Operation
-
- // Apply the operation to a Snapshot to create the final state
- Apply(snapshot *Snapshot)
-}
-
-// make sure that package external operations do conform to our interface
-var _ Operation = &dag.NoOpOperation[*Snapshot]{}
-var _ Operation = &dag.SetMetadataOperation[*Snapshot]{}
-
-func operationUnmarshaller(raw json.RawMessage, resolvers entity.Resolvers) (dag.Operation, error) {
- var t struct {
- OperationType dag.OperationType `json:"type"`
- }
-
- if err := json.Unmarshal(raw, &t); err != nil {
- return nil, err
- }
-
- var op dag.Operation
-
- switch t.OperationType {
- case AddCommentOp:
- op = &AddCommentOperation{}
- case CreateOp:
- op = &CreateOperation{}
- case EditCommentOp:
- op = &EditCommentOperation{}
- case LabelChangeOp:
- op = &LabelChangeOperation{}
- case NoOpOp:
- op = &dag.NoOpOperation[*Snapshot]{}
- case SetMetadataOp:
- op = &dag.SetMetadataOperation[*Snapshot]{}
- case SetStatusOp:
- op = &SetStatusOperation{}
- case SetTitleOp:
- op = &SetTitleOperation{}
- default:
- panic(fmt.Sprintf("unknown operation type %v", t.OperationType))
- }
-
- err := json.Unmarshal(raw, &op)
- if err != nil {
- return nil, err
- }
-
- return op, nil
-}
diff --git a/bug/operation_test.go b/bug/operation_test.go
deleted file mode 100644
index 3cfc85c4..00000000
--- a/bug/operation_test.go
+++ /dev/null
@@ -1,131 +0,0 @@
-package bug
-
-import (
- "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"
-)
-
-// TODO: move to entity/dag?
-
-func TestValidate(t *testing.T) {
- repo := repository.NewMockRepoClock()
-
- makeIdentity := func(t *testing.T, name, email string) *identity.Identity {
- i, err := identity.NewIdentity(repo, name, email)
- require.NoError(t, err)
- return i
- }
-
- rene := makeIdentity(t, "René Descartes", "rene@descartes.fr")
-
- unix := time.Now().Unix()
-
- good := []Operation{
- NewCreateOp(rene, unix, "title", "message", nil),
- NewSetTitleOp(rene, unix, "title2", "title1"),
- NewAddCommentOp(rene, unix, "message2", nil),
- NewSetStatusOp(rene, unix, ClosedStatus),
- NewLabelChangeOperation(rene, unix, []Label{"added"}, []Label{"removed"}),
- }
-
- for _, op := range good {
- if err := op.Validate(); err != nil {
- t.Fatal(err)
- }
- }
-
- bad := []Operation{
- // opbase
- NewSetStatusOp(makeIdentity(t, "", "rene@descartes.fr"), unix, ClosedStatus),
- NewSetStatusOp(makeIdentity(t, "René Descartes\u001b", "rene@descartes.fr"), unix, ClosedStatus),
- 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: dag.NewOpBase(CreateOp, rene, 0),
- Title: "title",
- Message: "message",
- },
-
- NewCreateOp(rene, unix, "multi\nline", "message", nil),
- NewCreateOp(rene, unix, "title", "message", []repository.Hash{repository.Hash("invalid")}),
- NewCreateOp(rene, unix, "title\u001b", "message", nil),
- NewCreateOp(rene, unix, "title", "message\u001b", nil),
- NewSetTitleOp(rene, unix, "multi\nline", "title1"),
- NewSetTitleOp(rene, unix, "title", "multi\nline"),
- NewSetTitleOp(rene, unix, "title\u001b", "title2"),
- NewSetTitleOp(rene, unix, "title", "title2\u001b"),
- NewAddCommentOp(rene, unix, "message\u001b", nil),
- NewAddCommentOp(rene, unix, "message", []repository.Hash{repository.Hash("invalid")}),
- NewSetStatusOp(rene, unix, 1000),
- NewSetStatusOp(rene, unix, 0),
- NewLabelChangeOperation(rene, unix, []Label{}, []Label{}),
- NewLabelChangeOperation(rene, unix, []Label{"multi\nline"}, []Label{}),
- }
-
- for i, op := range bad {
- if err := op.Validate(); err == nil {
- t.Fatal("validation should have failed", i, op)
- }
- }
-}
-
-func TestMetadata(t *testing.T) {
- repo := repository.NewMockRepoClock()
-
- rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
- require.NoError(t, err)
-
- op := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil)
-
- op.SetMetadata("key", "value")
-
- val, ok := op.GetMetadata("key")
- require.True(t, ok)
- require.Equal(t, val, "value")
-}
-
-func TestID(t *testing.T) {
- repo := repository.CreateGoGitTestRepo(t, false)
-
- repos := []repository.ClockedRepo{
- repository.NewMockRepo(),
- repo,
- }
-
- for _, repo := range repos {
- rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr")
- require.NoError(t, err)
- err = rene.Commit(repo)
- require.NoError(t, err)
-
- b, op, err := Create(rene, time.Now().Unix(), "title", "message", nil, nil)
- require.NoError(t, err)
-
- id1 := op.Id()
- require.NoError(t, id1.Validate())
-
- err = b.Commit(repo)
- require.NoError(t, err)
-
- op2 := b.FirstOp()
-
- id2 := op2.Id()
- require.NoError(t, id2.Validate())
- require.Equal(t, id1, id2)
-
- b2, err := Read(repo, b.Id())
- require.NoError(t, err)
-
- op3 := b2.FirstOp()
-
- id3 := op3.Id()
- require.NoError(t, id3.Validate())
- require.Equal(t, id1, id3)
- }
-}
diff --git a/bug/resolver.go b/bug/resolver.go
deleted file mode 100644
index e7beb0e4..00000000
--- a/bug/resolver.go
+++ /dev/null
@@ -1,21 +0,0 @@
-package bug
-
-import (
- "github.com/MichaelMure/git-bug/entity"
- "github.com/MichaelMure/git-bug/repository"
-)
-
-var _ entity.Resolver = &SimpleResolver{}
-
-// SimpleResolver is a Resolver loading Bugs directly from a Repo
-type SimpleResolver struct {
- repo repository.ClockedRepo
-}
-
-func NewSimpleResolver(repo repository.ClockedRepo) *SimpleResolver {
- return &SimpleResolver{repo: repo}
-}
-
-func (r *SimpleResolver) Resolve(id entity.Id) (entity.Interface, error) {
- return Read(r.repo, id)
-}
diff --git a/bug/snapshot.go b/bug/snapshot.go
deleted file mode 100644
index 2efc067e..00000000
--- a/bug/snapshot.go
+++ /dev/null
@@ -1,144 +0,0 @@
-package bug
-
-import (
- "fmt"
- "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
-
- Status Status
- Title string
- Comments []Comment
- Labels []Label
- Author identity.Interface
- Actors []identity.Interface
- Participants []identity.Interface
- CreateTime time.Time
-
- Timeline []TimelineItem
-
- Operations []dag.Operation
-}
-
-// Id returns the Bug identifier
-func (snap *Snapshot) Id() entity.Id {
- if snap.id == "" {
- // simply panic as it would be a coding error (no id provided at construction)
- panic("no 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 {
- return time.Unix(0, 0)
- }
-
- return snap.Operations[len(snap.Operations)-1].Time()
-}
-
-// GetCreateMetadata return the creation metadata
-func (snap *Snapshot) GetCreateMetadata(key string) (string, bool) {
- return snap.Operations[0].GetMetadata(key)
-}
-
-// SearchTimelineItem will search in the timeline for an item matching the given hash
-func (snap *Snapshot) SearchTimelineItem(id entity.Id) (TimelineItem, error) {
- for i := range snap.Timeline {
- if snap.Timeline[i].Id() == id {
- return snap.Timeline[i], nil
- }
- }
-
- return nil, fmt.Errorf("timeline item not found")
-}
-
-// SearchComment will search for a comment matching the given hash
-func (snap *Snapshot) SearchComment(id entity.Id) (*Comment, error) {
- for _, c := range snap.Comments {
- if c.id == id {
- return &c, nil
- }
- }
-
- return nil, fmt.Errorf("comment item not found")
-}
-
-// append the operation author to the actors list
-func (snap *Snapshot) addActor(actor identity.Interface) {
- for _, a := range snap.Actors {
- if actor.Id() == a.Id() {
- return
- }
- }
-
- snap.Actors = append(snap.Actors, actor)
-}
-
-// append the operation author to the participants list
-func (snap *Snapshot) addParticipant(participant identity.Interface) {
- for _, p := range snap.Participants {
- if participant.Id() == p.Id() {
- return
- }
- }
-
- snap.Participants = append(snap.Participants, participant)
-}
-
-// HasParticipant return true if the id is a participant
-func (snap *Snapshot) HasParticipant(id entity.Id) bool {
- for _, p := range snap.Participants {
- if p.Id() == id {
- return true
- }
- }
- return false
-}
-
-// HasAnyParticipant return true if one of the ids is a participant
-func (snap *Snapshot) HasAnyParticipant(ids ...entity.Id) bool {
- for _, id := range ids {
- if snap.HasParticipant(id) {
- return true
- }
- }
- return false
-}
-
-// HasActor return true if the id is a actor
-func (snap *Snapshot) HasActor(id entity.Id) bool {
- for _, p := range snap.Actors {
- if p.Id() == id {
- return true
- }
- }
- return false
-}
-
-// HasAnyActor return true if one of the ids is a actor
-func (snap *Snapshot) HasAnyActor(ids ...entity.Id) bool {
- for _, id := range ids {
- if snap.HasActor(id) {
- return true
- }
- }
- return false
-}
-
-// IsAuthored is a sign post method for gqlgen
-func (snap *Snapshot) IsAuthored() {}
diff --git a/bug/sorting.go b/bug/sorting.go
deleted file mode 100644
index 2e64b92d..00000000
--- a/bug/sorting.go
+++ /dev/null
@@ -1,57 +0,0 @@
-package bug
-
-type BugsByCreationTime []*Bug
-
-func (b BugsByCreationTime) Len() int {
- return len(b)
-}
-
-func (b BugsByCreationTime) Less(i, j int) bool {
- if b[i].CreateLamportTime() < b[j].CreateLamportTime() {
- return true
- }
-
- if b[i].CreateLamportTime() > b[j].CreateLamportTime() {
- return false
- }
-
- // When the logical clocks are identical, that means we had a concurrent
- // edition. In this case we rely on the timestamp. While the timestamp might
- // be incorrect due to a badly set clock, the drift in sorting is bounded
- // by the first sorting using the logical clock. That means that if users
- // synchronize their bugs regularly, the timestamp will rarely be used, and
- // should still provide a kinda accurate sorting when needed.
- return b[i].FirstOp().Time().Before(b[j].FirstOp().Time())
-}
-
-func (b BugsByCreationTime) Swap(i, j int) {
- b[i], b[j] = b[j], b[i]
-}
-
-type BugsByEditTime []*Bug
-
-func (b BugsByEditTime) Len() int {
- return len(b)
-}
-
-func (b BugsByEditTime) Less(i, j int) bool {
- if b[i].EditLamportTime() < b[j].EditLamportTime() {
- return true
- }
-
- if b[i].EditLamportTime() > b[j].EditLamportTime() {
- return false
- }
-
- // When the logical clocks are identical, that means we had a concurrent
- // edition. In this case we rely on the timestamp. While the timestamp might
- // be incorrect due to a badly set clock, the drift in sorting is bounded
- // by the first sorting using the logical clock. That means that if users
- // synchronize their bugs regularly, the timestamp will rarely be used, and
- // should still provide a kinda accurate sorting when needed.
- return b[i].LastOp().Time().Before(b[j].LastOp().Time())
-}
-
-func (b BugsByEditTime) Swap(i, j int) {
- b[i], b[j] = b[j], b[i]
-}
diff --git a/bug/status.go b/bug/status.go
deleted file mode 100644
index 9e998034..00000000
--- a/bug/status.go
+++ /dev/null
@@ -1,57 +0,0 @@
-package bug
-
-import (
- "fmt"
- "strings"
-)
-
-type Status int
-
-const (
- _ Status = iota
- OpenStatus
- ClosedStatus
-)
-
-func (s Status) String() string {
- switch s {
- case OpenStatus:
- return "open"
- case ClosedStatus:
- return "closed"
- default:
- return "unknown status"
- }
-}
-
-func (s Status) Action() string {
- switch s {
- case OpenStatus:
- return "opened"
- case ClosedStatus:
- return "closed"
- default:
- return "unknown status"
- }
-}
-
-func StatusFromString(str string) (Status, error) {
- cleaned := strings.ToLower(strings.TrimSpace(str))
-
- switch cleaned {
- case "open":
- return OpenStatus, nil
- case "closed":
- return ClosedStatus, nil
- default:
- return 0, fmt.Errorf("unknown status")
- }
-}
-
-func (s Status) Validate() error {
- if s != OpenStatus && s != ClosedStatus {
- return fmt.Errorf("invalid")
- }
-
- return nil
-}
diff --git a/bug/timeline.go b/bug/timeline.go
deleted file mode 100644
index 4146db36..00000000
--- a/bug/timeline.go
+++ /dev/null
@@ -1,80 +0,0 @@
-package bug
-
-import (
- "strings"
-
- "github.com/MichaelMure/git-bug/entity"
- "github.com/MichaelMure/git-bug/identity"
- "github.com/MichaelMure/git-bug/repository"
- "github.com/MichaelMure/git-bug/util/timestamp"
-)
-
-type TimelineItem interface {
- // Id return the identifier of the item
- Id() entity.Id
-}
-
-// CommentHistoryStep hold one version of a message in the history
-type CommentHistoryStep struct {
- // The author of the edition, not necessarily the same as the author of the
- // original comment
- Author identity.Interface
- // The new message
- Message string
- UnixTime timestamp.Timestamp
-}
-
-// CommentTimelineItem is a TimelineItem that holds a Comment and its edition history
-type CommentTimelineItem struct {
- // id should be the same as in Comment
- id entity.Id
- Author identity.Interface
- Message string
- Files []repository.Hash
- CreatedAt timestamp.Timestamp
- LastEdit timestamp.Timestamp
- History []CommentHistoryStep
-}
-
-func NewCommentTimelineItem(comment Comment) CommentTimelineItem {
- return CommentTimelineItem{
- id: comment.id,
- Author: comment.Author,
- Message: comment.Message,
- Files: comment.Files,
- CreatedAt: comment.UnixTime,
- LastEdit: comment.UnixTime,
- History: []CommentHistoryStep{
- {
- Message: comment.Message,
- UnixTime: comment.UnixTime,
- },
- },
- }
-}
-
-func (c *CommentTimelineItem) Id() entity.Id {
- return c.id
-}
-
-// Append will append a new comment in the history and update the other values
-func (c *CommentTimelineItem) Append(comment Comment) {
- c.Message = comment.Message
- c.Files = comment.Files
- c.LastEdit = comment.UnixTime
- c.History = append(c.History, CommentHistoryStep{
- Author: comment.Author,
- Message: comment.Message,
- UnixTime: comment.UnixTime,
- })
-}
-
-// Edited say if the comment was edited
-func (c *CommentTimelineItem) Edited() bool {
- return len(c.History) > 1
-}
-
-// MessageIsEmpty return true is the message is empty or only made of spaces
-func (c *CommentTimelineItem) MessageIsEmpty() bool {
- return len(strings.TrimSpace(c.Message)) == 0
-}
diff --git a/bug/with_snapshot.go b/bug/with_snapshot.go
deleted file mode 100644
index 0474cac7..00000000
--- a/bug/with_snapshot.go
+++ /dev/null
@@ -1,53 +0,0 @@
-package bug
-
-import (
- "github.com/MichaelMure/git-bug/repository"
-)
-
-var _ Interface = &WithSnapshot{}
-
-// WithSnapshot encapsulate a Bug and maintain the corresponding Snapshot efficiently
-type WithSnapshot struct {
- *Bug
- snap *Snapshot
-}
-
-func (b *WithSnapshot) Compile() *Snapshot {
- if b.snap == nil {
- snap := b.Bug.Compile()
- b.snap = snap
- }
- return b.snap
-}
-
-// Append intercept Bug.Append() to update the snapshot efficiently
-func (b *WithSnapshot) Append(op Operation) {
- b.Bug.Append(op)
-
- if b.snap == nil {
- return
- }
-
- op.Apply(b.snap)
- b.snap.Operations = append(b.snap.Operations, op)
-}
-
-// Commit intercept Bug.Commit() to update the snapshot efficiently
-func (b *WithSnapshot) Commit(repo repository.ClockedRepo) error {
- err := b.Bug.Commit(repo)
-
- if err != nil {
- b.snap = nil
- return err
- }
-
- // Commit() shouldn't change anything of the bug state apart from the
- // initial ID set
-
- if b.snap == nil {
- return nil
- }
-
- b.snap.id = b.Bug.Id()
- return nil
-}