diff options
Diffstat (limited to 'bug')
-rw-r--r-- | bug/bug.go | 179 | ||||
-rw-r--r-- | bug/bug_actions.go | 74 | ||||
-rw-r--r-- | bug/comment.go | 45 | ||||
-rw-r--r-- | bug/err.go | 17 | ||||
-rw-r--r-- | bug/interface.go | 44 | ||||
-rw-r--r-- | bug/label.go | 95 | ||||
-rw-r--r-- | bug/label_test.go | 35 | ||||
-rw-r--r-- | bug/op_add_comment.go | 93 | ||||
-rw-r--r-- | bug/op_add_comment_test.go | 18 | ||||
-rw-r--r-- | bug/op_create.go | 112 | ||||
-rw-r--r-- | bug/op_create_test.go | 67 | ||||
-rw-r--r-- | bug/op_edit_comment.go | 129 | ||||
-rw-r--r-- | bug/op_edit_comment_test.go | 84 | ||||
-rw-r--r-- | bug/op_label_change.go | 251 | ||||
-rw-r--r-- | bug/op_label_change_test.go | 20 | ||||
-rw-r--r-- | bug/op_set_metadata.go | 21 | ||||
-rw-r--r-- | bug/op_set_status.go | 95 | ||||
-rw-r--r-- | bug/op_set_status_test.go | 14 | ||||
-rw-r--r-- | bug/op_set_title.go | 112 | ||||
-rw-r--r-- | bug/op_set_title_test.go | 14 | ||||
-rw-r--r-- | bug/operation.go | 73 | ||||
-rw-r--r-- | bug/operation_test.go | 131 | ||||
-rw-r--r-- | bug/resolver.go | 21 | ||||
-rw-r--r-- | bug/snapshot.go | 144 | ||||
-rw-r--r-- | bug/sorting.go | 57 | ||||
-rw-r--r-- | bug/status.go | 57 | ||||
-rw-r--r-- | bug/timeline.go | 80 | ||||
-rw-r--r-- | bug/with_snapshot.go | 53 |
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 -} |