bug: use deditated type for all TimelineItem

Michael Muré created

Change summary

bug/op_add_comment.go         |   11 
bug/op_create.go              |    9 
bug/op_create_test.go         |    4 
bug/op_edit_comment.go        |   11 
bug/op_edit_comment_test.go   |    4 
bug/op_label_change.go        |   29 +
bug/op_set_status.go          |   28 
bug/op_set_title.go           |   30 +
bug/snapshot.go               |    7 
bug/timeline.go               |   36 -
graphql/gqlgen.yml            |   10 
graphql/graph/gen_graph.go    | 1023 +++++++++++++++++++++---------------
graphql/resolvers/root.go     |   16 
graphql/resolvers/timeline.go |   29 
graphql/schema.graphql        |   31 
termui/show_bug.go            |   22 
16 files changed, 797 insertions(+), 503 deletions(-)

Detailed changes

bug/op_add_comment.go 🔗

@@ -42,7 +42,11 @@ func (op *AddCommentOperation) Apply(snapshot *Snapshot) {
 		panic(err)
 	}
 
-	snapshot.Timeline = append(snapshot.Timeline, NewCommentTimelineItem(hash, comment))
+	item := &AddCommentTimelineItem{
+		CommentTimelineItem: NewCommentTimelineItem(hash, comment),
+	}
+
+	snapshot.Timeline = append(snapshot.Timeline, item)
 }
 
 func (op *AddCommentOperation) GetFiles() []git.Hash {
@@ -73,6 +77,11 @@ func NewAddCommentOp(author Person, unixTime int64, message string, files []git.
 	}
 }
 
+// CreateTimelineItem replace a AddComment operation in the Timeline and hold its edition history
+type AddCommentTimelineItem struct {
+	CommentTimelineItem
+}
+
 // Convenience function to apply the operation
 func AddComment(b Interface, author Person, unixTime int64, message string) error {
 	return AddCommentWithFiles(b, author, unixTime, message, nil)

bug/op_create.go 🔗

@@ -47,7 +47,9 @@ func (op *CreateOperation) Apply(snapshot *Snapshot) {
 	}
 
 	snapshot.Timeline = []TimelineItem{
-		NewCreateTimelineItem(hash, comment),
+		&CreateTimelineItem{
+			CommentTimelineItem: NewCommentTimelineItem(hash, comment),
+		},
 	}
 }
 
@@ -88,6 +90,11 @@ func NewCreateOp(author Person, unixTime int64, title, message string, files []g
 	}
 }
 
+// CreateTimelineItem replace a Create operation in the Timeline and hold its edition history
+type CreateTimelineItem struct {
+	CommentTimelineItem
+}
+
 // Convenience function to apply the operation
 func Create(author Person, unixTime int64, title, message string) (*Bug, error) {
 	return CreateWithFiles(author, unixTime, title, message, nil)

bug/op_create_test.go 🔗

@@ -36,7 +36,9 @@ func TestCreate(t *testing.T) {
 		Author:    rene,
 		CreatedAt: create.Time(),
 		Timeline: []TimelineItem{
-			NewCreateTimelineItem(hash, comment),
+			&CreateTimelineItem{
+				CommentTimelineItem: NewCommentTimelineItem(hash, comment),
+			},
 		},
 	}
 

bug/op_edit_comment.go 🔗

@@ -33,12 +33,7 @@ func (op *EditCommentOperation) Apply(snapshot *Snapshot) {
 	var commentIndex int
 
 	for i, item := range snapshot.Timeline {
-		h, err := item.Hash()
-
-		if err != nil {
-			// Should never happen, we control what goes into the timeline
-			panic(err)
-		}
+		h := item.Hash()
 
 		if h == op.Target {
 			target = snapshot.Timeline[i]
@@ -68,8 +63,8 @@ func (op *EditCommentOperation) Apply(snapshot *Snapshot) {
 		item := target.(*CreateTimelineItem)
 		item.Append(comment)
 
-	case *CommentTimelineItem:
-		item := target.(*CommentTimelineItem)
+	case *AddCommentTimelineItem:
+		item := target.(*AddCommentTimelineItem)
 		item.Append(comment)
 	}
 

bug/op_edit_comment_test.go 🔗

@@ -38,7 +38,7 @@ func TestEdit(t *testing.T) {
 
 	assert.Equal(t, len(snapshot.Timeline), 2)
 	assert.Equal(t, len(snapshot.Timeline[0].(*CreateTimelineItem).History), 2)
-	assert.Equal(t, len(snapshot.Timeline[1].(*CommentTimelineItem).History), 1)
+	assert.Equal(t, len(snapshot.Timeline[1].(*AddCommentTimelineItem).History), 1)
 	assert.Equal(t, snapshot.Comments[0].Message, "create edited")
 	assert.Equal(t, snapshot.Comments[1].Message, "comment")
 
@@ -47,7 +47,7 @@ func TestEdit(t *testing.T) {
 
 	assert.Equal(t, len(snapshot.Timeline), 2)
 	assert.Equal(t, len(snapshot.Timeline[0].(*CreateTimelineItem).History), 2)
-	assert.Equal(t, len(snapshot.Timeline[1].(*CommentTimelineItem).History), 2)
+	assert.Equal(t, len(snapshot.Timeline[1].(*AddCommentTimelineItem).History), 2)
 	assert.Equal(t, snapshot.Comments[0].Message, "create edited")
 	assert.Equal(t, snapshot.Comments[1].Message, "comment edited")
 }

bug/op_label_change.go 🔗

@@ -55,7 +55,22 @@ AddLoop:
 		return string(snapshot.Labels[i]) < string(snapshot.Labels[j])
 	})
 
-	snapshot.Timeline = append(snapshot.Timeline, op)
+	hash, err := op.Hash()
+	if err != nil {
+		// Should never error unless a programming error happened
+		// (covered in OpBase.Validate())
+		panic(err)
+	}
+
+	item := &LabelChangeTimelineItem{
+		hash:     hash,
+		Author:   op.Author,
+		UnixTime: Timestamp(op.UnixTime),
+		Added:    op.Added,
+		Removed:  op.Removed,
+	}
+
+	snapshot.Timeline = append(snapshot.Timeline, item)
 }
 
 func (op *LabelChangeOperation) Validate() error {
@@ -90,6 +105,18 @@ func NewLabelChangeOperation(author Person, unixTime int64, added, removed []Lab
 	}
 }
 
+type LabelChangeTimelineItem struct {
+	hash     git.Hash
+	Author   Person
+	UnixTime Timestamp
+	Added    []Label
+	Removed  []Label
+}
+
+func (l LabelChangeTimelineItem) Hash() git.Hash {
+	return l.hash
+}
+
 // ChangeLabels is a convenience function to apply the operation
 func ChangeLabels(b Interface, author Person, unixTime int64, add, remove []string) ([]LabelChangeResult, error) {
 	var added, removed []Label

bug/op_set_status.go 🔗

@@ -23,7 +23,22 @@ func (op *SetStatusOperation) Hash() (git.Hash, error) {
 
 func (op *SetStatusOperation) Apply(snapshot *Snapshot) {
 	snapshot.Status = op.Status
-	snapshot.Timeline = append(snapshot.Timeline, op)
+
+	hash, err := op.Hash()
+	if err != nil {
+		// Should never error unless a programming error happened
+		// (covered in OpBase.Validate())
+		panic(err)
+	}
+
+	item := &SetStatusTimelineItem{
+		hash:     hash,
+		Author:   op.Author,
+		UnixTime: Timestamp(op.UnixTime),
+		Status:   op.Status,
+	}
+
+	snapshot.Timeline = append(snapshot.Timeline, item)
 }
 
 func (op *SetStatusOperation) Validate() error {
@@ -45,6 +60,17 @@ func NewSetStatusOp(author Person, unixTime int64, status Status) *SetStatusOper
 	}
 }
 
+type SetStatusTimelineItem struct {
+	hash     git.Hash
+	Author   Person
+	UnixTime Timestamp
+	Status   Status
+}
+
+func (s SetStatusTimelineItem) Hash() git.Hash {
+	return s.hash
+}
+
 // Convenience function to apply the operation
 func Open(b Interface, author Person, unixTime int64) error {
 	op := NewSetStatusOp(author, unixTime, OpenStatus)

bug/op_set_title.go 🔗

@@ -27,7 +27,23 @@ func (op *SetTitleOperation) Hash() (git.Hash, error) {
 
 func (op *SetTitleOperation) Apply(snapshot *Snapshot) {
 	snapshot.Title = op.Title
-	snapshot.Timeline = append(snapshot.Timeline, op)
+
+	hash, err := op.Hash()
+	if err != nil {
+		// Should never error unless a programming error happened
+		// (covered in OpBase.Validate())
+		panic(err)
+	}
+
+	item := &SetTitleTimelineItem{
+		hash:     hash,
+		Author:   op.Author,
+		UnixTime: Timestamp(op.UnixTime),
+		Title:    op.Title,
+		Was:      op.Was,
+	}
+
+	snapshot.Timeline = append(snapshot.Timeline, item)
 }
 
 func (op *SetTitleOperation) Validate() error {
@@ -66,6 +82,18 @@ func NewSetTitleOp(author Person, unixTime int64, title string, was string) *Set
 	}
 }
 
+type SetTitleTimelineItem struct {
+	hash     git.Hash
+	Author   Person
+	UnixTime Timestamp
+	Title    string
+	Was      string
+}
+
+func (s SetTitleTimelineItem) Hash() git.Hash {
+	return s.hash
+}
+
 // Convenience function to apply the operation
 func SetTitle(b Interface, author Person, unixTime int64, title string) error {
 	it := NewOperationIterator(b)

bug/snapshot.go 🔗

@@ -61,12 +61,7 @@ func (snap *Snapshot) LastEditUnix() int64 {
 // SearchTimelineItem will search in the timeline for an item matching the given hash
 func (snap *Snapshot) SearchTimelineItem(hash git.Hash) (TimelineItem, error) {
 	for i := range snap.Timeline {
-		h, err := snap.Timeline[i].Hash()
-		if err != nil {
-			return nil, err
-		}
-
-		if h == hash {
+		if snap.Timeline[i].Hash() == hash {
 			return snap.Timeline[i], nil
 		}
 	}

bug/timeline.go 🔗

@@ -6,7 +6,7 @@ import (
 
 type TimelineItem interface {
 	// Hash return the hash of the item
-	Hash() (git.Hash, error)
+	Hash() git.Hash
 }
 
 type CommentHistoryStep struct {
@@ -14,31 +14,7 @@ type CommentHistoryStep struct {
 	UnixTime Timestamp
 }
 
-// CreateTimelineItem replace a Create operation in the Timeline and hold its edition history
-type CreateTimelineItem struct {
-	CommentTimelineItem
-}
-
-func NewCreateTimelineItem(hash git.Hash, comment Comment) *CreateTimelineItem {
-	return &CreateTimelineItem{
-		CommentTimelineItem: CommentTimelineItem{
-			hash:      hash,
-			Author:    comment.Author,
-			Message:   comment.Message,
-			Files:     comment.Files,
-			CreatedAt: comment.UnixTime,
-			LastEdit:  comment.UnixTime,
-			History: []CommentHistoryStep{
-				{
-					Message:  comment.Message,
-					UnixTime: comment.UnixTime,
-				},
-			},
-		},
-	}
-}
-
-// CommentTimelineItem replace a Comment in the Timeline and hold its edition history
+// CommentTimelineItem is a TimelineItem that holds a Comment and its edition history
 type CommentTimelineItem struct {
 	hash      git.Hash
 	Author    Person
@@ -49,8 +25,8 @@ type CommentTimelineItem struct {
 	History   []CommentHistoryStep
 }
 
-func NewCommentTimelineItem(hash git.Hash, comment Comment) *CommentTimelineItem {
-	return &CommentTimelineItem{
+func NewCommentTimelineItem(hash git.Hash, comment Comment) CommentTimelineItem {
+	return CommentTimelineItem{
 		hash:      hash,
 		Author:    comment.Author,
 		Message:   comment.Message,
@@ -66,8 +42,8 @@ func NewCommentTimelineItem(hash git.Hash, comment Comment) *CommentTimelineItem
 	}
 }
 
-func (c *CommentTimelineItem) Hash() (git.Hash, error) {
-	return c.hash, nil
+func (c *CommentTimelineItem) Hash() git.Hash {
+	return c.hash
 }
 
 // Append will append a new comment in the history and update the other values

graphql/gqlgen.yml 🔗

@@ -37,5 +37,11 @@ models:
     model: github.com/MichaelMure/git-bug/bug.CommentHistoryStep
   CreateTimelineItem:
     model: github.com/MichaelMure/git-bug/bug.CreateTimelineItem
-  CommentTimelineItem:
-    model: github.com/MichaelMure/git-bug/bug.CommentTimelineItem
+  AddCommentTimelineItem:
+    model: github.com/MichaelMure/git-bug/bug.AddCommentTimelineItem
+  LabelChangeTimelineItem:
+    model: github.com/MichaelMure/git-bug/bug.LabelChangeTimelineItem
+  SetStatusTimelineItem:
+    model: github.com/MichaelMure/git-bug/bug.SetStatusTimelineItem
+  SetTitleTimelineItem:
+    model: github.com/MichaelMure/git-bug/bug.SetTitleTimelineItem

graphql/graph/gen_graph.go 🔗

@@ -36,17 +36,20 @@ type Config struct {
 
 type ResolverRoot interface {
 	AddCommentOperation() AddCommentOperationResolver
+	AddCommentTimelineItem() AddCommentTimelineItemResolver
 	Bug() BugResolver
 	CommentHistoryStep() CommentHistoryStepResolver
-	CommentTimelineItem() CommentTimelineItemResolver
 	CreateOperation() CreateOperationResolver
 	CreateTimelineItem() CreateTimelineItemResolver
 	LabelChangeOperation() LabelChangeOperationResolver
+	LabelChangeTimelineItem() LabelChangeTimelineItemResolver
 	Mutation() MutationResolver
 	Query() QueryResolver
 	Repository() RepositoryResolver
 	SetStatusOperation() SetStatusOperationResolver
+	SetStatusTimelineItem() SetStatusTimelineItemResolver
 	SetTitleOperation() SetTitleOperationResolver
+	SetTitleTimelineItem() SetTitleTimelineItemResolver
 }
 
 type DirectiveRoot struct {
@@ -60,6 +63,17 @@ type ComplexityRoot struct {
 		Files   func(childComplexity int) int
 	}
 
+	AddCommentTimelineItem struct {
+		Hash      func(childComplexity int) int
+		Author    func(childComplexity int) int
+		Message   func(childComplexity int) int
+		Files     func(childComplexity int) int
+		CreatedAt func(childComplexity int) int
+		LastEdit  func(childComplexity int) int
+		Edited    func(childComplexity int) int
+		History   func(childComplexity int) int
+	}
+
 	Bug struct {
 		Id         func(childComplexity int) int
 		HumanId    func(childComplexity int) int
@@ -109,17 +123,6 @@ type ComplexityRoot struct {
 		Date    func(childComplexity int) int
 	}
 
-	CommentTimelineItem struct {
-		Hash      func(childComplexity int) int
-		Author    func(childComplexity int) int
-		Message   func(childComplexity int) int
-		Files     func(childComplexity int) int
-		CreatedAt func(childComplexity int) int
-		LastEdit  func(childComplexity int) int
-		Edited    func(childComplexity int) int
-		History   func(childComplexity int) int
-	}
-
 	CreateOperation struct {
 		Author  func(childComplexity int) int
 		Date    func(childComplexity int) int
@@ -147,6 +150,14 @@ type ComplexityRoot struct {
 		Removed func(childComplexity int) int
 	}
 
+	LabelChangeTimelineItem struct {
+		Hash    func(childComplexity int) int
+		Author  func(childComplexity int) int
+		Date    func(childComplexity int) int
+		Added   func(childComplexity int) int
+		Removed func(childComplexity int) int
+	}
+
 	Mutation struct {
 		NewBug       func(childComplexity int, repoRef *string, title string, message string, files []git.Hash) int
 		AddComment   func(childComplexity int, repoRef *string, prefix string, message string, files []git.Hash) int
@@ -199,6 +210,13 @@ type ComplexityRoot struct {
 		Status func(childComplexity int) int
 	}
 
+	SetStatusTimelineItem struct {
+		Hash   func(childComplexity int) int
+		Author func(childComplexity int) int
+		Date   func(childComplexity int) int
+		Status func(childComplexity int) int
+	}
+
 	SetTitleOperation struct {
 		Hash   func(childComplexity int) int
 		Author func(childComplexity int) int
@@ -207,6 +225,14 @@ type ComplexityRoot struct {
 		Was    func(childComplexity int) int
 	}
 
+	SetTitleTimelineItem struct {
+		Hash   func(childComplexity int) int
+		Author func(childComplexity int) int
+		Date   func(childComplexity int) int
+		Title  func(childComplexity int) int
+		Was    func(childComplexity int) int
+	}
+
 	TimelineItemConnection struct {
 		Edges      func(childComplexity int) int
 		Nodes      func(childComplexity int) int
@@ -224,6 +250,10 @@ type AddCommentOperationResolver interface {
 	Author(ctx context.Context, obj *bug.AddCommentOperation) (bug.Person, error)
 	Date(ctx context.Context, obj *bug.AddCommentOperation) (time.Time, error)
 }
+type AddCommentTimelineItemResolver interface {
+	CreatedAt(ctx context.Context, obj *bug.AddCommentTimelineItem) (time.Time, error)
+	LastEdit(ctx context.Context, obj *bug.AddCommentTimelineItem) (time.Time, error)
+}
 type BugResolver interface {
 	Status(ctx context.Context, obj *bug.Snapshot) (models.Status, error)
 
@@ -235,10 +265,6 @@ type BugResolver interface {
 type CommentHistoryStepResolver interface {
 	Date(ctx context.Context, obj *bug.CommentHistoryStep) (time.Time, error)
 }
-type CommentTimelineItemResolver interface {
-	CreatedAt(ctx context.Context, obj *bug.CommentTimelineItem) (time.Time, error)
-	LastEdit(ctx context.Context, obj *bug.CommentTimelineItem) (time.Time, error)
-}
 type CreateOperationResolver interface {
 	Author(ctx context.Context, obj *bug.CreateOperation) (bug.Person, error)
 	Date(ctx context.Context, obj *bug.CreateOperation) (time.Time, error)
@@ -251,6 +277,9 @@ type LabelChangeOperationResolver interface {
 	Author(ctx context.Context, obj *bug.LabelChangeOperation) (bug.Person, error)
 	Date(ctx context.Context, obj *bug.LabelChangeOperation) (time.Time, error)
 }
+type LabelChangeTimelineItemResolver interface {
+	Date(ctx context.Context, obj *bug.LabelChangeTimelineItem) (time.Time, error)
+}
 type MutationResolver interface {
 	NewBug(ctx context.Context, repoRef *string, title string, message string, files []git.Hash) (bug.Snapshot, error)
 	AddComment(ctx context.Context, repoRef *string, prefix string, message string, files []git.Hash) (bug.Snapshot, error)
@@ -273,10 +302,17 @@ type SetStatusOperationResolver interface {
 	Date(ctx context.Context, obj *bug.SetStatusOperation) (time.Time, error)
 	Status(ctx context.Context, obj *bug.SetStatusOperation) (models.Status, error)
 }
+type SetStatusTimelineItemResolver interface {
+	Date(ctx context.Context, obj *bug.SetStatusTimelineItem) (time.Time, error)
+	Status(ctx context.Context, obj *bug.SetStatusTimelineItem) (models.Status, error)
+}
 type SetTitleOperationResolver interface {
 	Author(ctx context.Context, obj *bug.SetTitleOperation) (bug.Person, error)
 	Date(ctx context.Context, obj *bug.SetTitleOperation) (time.Time, error)
 }
+type SetTitleTimelineItemResolver interface {
+	Date(ctx context.Context, obj *bug.SetTitleTimelineItem) (time.Time, error)
+}
 
 func field_Bug_comments_args(rawArgs map[string]interface{}) (map[string]interface{}, error) {
 	args := map[string]interface{}{}
@@ -966,6 +1002,62 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.AddCommentOperation.Files(childComplexity), true
 
+	case "AddCommentTimelineItem.hash":
+		if e.complexity.AddCommentTimelineItem.Hash == nil {
+			break
+		}
+
+		return e.complexity.AddCommentTimelineItem.Hash(childComplexity), true
+
+	case "AddCommentTimelineItem.author":
+		if e.complexity.AddCommentTimelineItem.Author == nil {
+			break
+		}
+
+		return e.complexity.AddCommentTimelineItem.Author(childComplexity), true
+
+	case "AddCommentTimelineItem.message":
+		if e.complexity.AddCommentTimelineItem.Message == nil {
+			break
+		}
+
+		return e.complexity.AddCommentTimelineItem.Message(childComplexity), true
+
+	case "AddCommentTimelineItem.files":
+		if e.complexity.AddCommentTimelineItem.Files == nil {
+			break
+		}
+
+		return e.complexity.AddCommentTimelineItem.Files(childComplexity), true
+
+	case "AddCommentTimelineItem.createdAt":
+		if e.complexity.AddCommentTimelineItem.CreatedAt == nil {
+			break
+		}
+
+		return e.complexity.AddCommentTimelineItem.CreatedAt(childComplexity), true
+
+	case "AddCommentTimelineItem.lastEdit":
+		if e.complexity.AddCommentTimelineItem.LastEdit == nil {
+			break
+		}
+
+		return e.complexity.AddCommentTimelineItem.LastEdit(childComplexity), true
+
+	case "AddCommentTimelineItem.edited":
+		if e.complexity.AddCommentTimelineItem.Edited == nil {
+			break
+		}
+
+		return e.complexity.AddCommentTimelineItem.Edited(childComplexity), true
+
+	case "AddCommentTimelineItem.history":
+		if e.complexity.AddCommentTimelineItem.History == nil {
+			break
+		}
+
+		return e.complexity.AddCommentTimelineItem.History(childComplexity), true
+
 	case "Bug.id":
 		if e.complexity.Bug.Id == nil {
 			break
@@ -1177,62 +1269,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.CommentHistoryStep.Date(childComplexity), true
 
-	case "CommentTimelineItem.hash":
-		if e.complexity.CommentTimelineItem.Hash == nil {
-			break
-		}
-
-		return e.complexity.CommentTimelineItem.Hash(childComplexity), true
-
-	case "CommentTimelineItem.author":
-		if e.complexity.CommentTimelineItem.Author == nil {
-			break
-		}
-
-		return e.complexity.CommentTimelineItem.Author(childComplexity), true
-
-	case "CommentTimelineItem.message":
-		if e.complexity.CommentTimelineItem.Message == nil {
-			break
-		}
-
-		return e.complexity.CommentTimelineItem.Message(childComplexity), true
-
-	case "CommentTimelineItem.files":
-		if e.complexity.CommentTimelineItem.Files == nil {
-			break
-		}
-
-		return e.complexity.CommentTimelineItem.Files(childComplexity), true
-
-	case "CommentTimelineItem.createdAt":
-		if e.complexity.CommentTimelineItem.CreatedAt == nil {
-			break
-		}
-
-		return e.complexity.CommentTimelineItem.CreatedAt(childComplexity), true
-
-	case "CommentTimelineItem.lastEdit":
-		if e.complexity.CommentTimelineItem.LastEdit == nil {
-			break
-		}
-
-		return e.complexity.CommentTimelineItem.LastEdit(childComplexity), true
-
-	case "CommentTimelineItem.edited":
-		if e.complexity.CommentTimelineItem.Edited == nil {
-			break
-		}
-
-		return e.complexity.CommentTimelineItem.Edited(childComplexity), true
-
-	case "CommentTimelineItem.history":
-		if e.complexity.CommentTimelineItem.History == nil {
-			break
-		}
-
-		return e.complexity.CommentTimelineItem.History(childComplexity), true
-
 	case "CreateOperation.author":
 		if e.complexity.CreateOperation.Author == nil {
 			break
@@ -1359,6 +1395,41 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.LabelChangeOperation.Removed(childComplexity), true
 
+	case "LabelChangeTimelineItem.hash":
+		if e.complexity.LabelChangeTimelineItem.Hash == nil {
+			break
+		}
+
+		return e.complexity.LabelChangeTimelineItem.Hash(childComplexity), true
+
+	case "LabelChangeTimelineItem.author":
+		if e.complexity.LabelChangeTimelineItem.Author == nil {
+			break
+		}
+
+		return e.complexity.LabelChangeTimelineItem.Author(childComplexity), true
+
+	case "LabelChangeTimelineItem.date":
+		if e.complexity.LabelChangeTimelineItem.Date == nil {
+			break
+		}
+
+		return e.complexity.LabelChangeTimelineItem.Date(childComplexity), true
+
+	case "LabelChangeTimelineItem.added":
+		if e.complexity.LabelChangeTimelineItem.Added == nil {
+			break
+		}
+
+		return e.complexity.LabelChangeTimelineItem.Added(childComplexity), true
+
+	case "LabelChangeTimelineItem.removed":
+		if e.complexity.LabelChangeTimelineItem.Removed == nil {
+			break
+		}
+
+		return e.complexity.LabelChangeTimelineItem.Removed(childComplexity), true
+
 	case "Mutation.newBug":
 		if e.complexity.Mutation.NewBug == nil {
 			break
@@ -1605,6 +1676,34 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.SetStatusOperation.Status(childComplexity), true
 
+	case "SetStatusTimelineItem.hash":
+		if e.complexity.SetStatusTimelineItem.Hash == nil {
+			break
+		}
+
+		return e.complexity.SetStatusTimelineItem.Hash(childComplexity), true
+
+	case "SetStatusTimelineItem.author":
+		if e.complexity.SetStatusTimelineItem.Author == nil {
+			break
+		}
+
+		return e.complexity.SetStatusTimelineItem.Author(childComplexity), true
+
+	case "SetStatusTimelineItem.date":
+		if e.complexity.SetStatusTimelineItem.Date == nil {
+			break
+		}
+
+		return e.complexity.SetStatusTimelineItem.Date(childComplexity), true
+
+	case "SetStatusTimelineItem.status":
+		if e.complexity.SetStatusTimelineItem.Status == nil {
+			break
+		}
+
+		return e.complexity.SetStatusTimelineItem.Status(childComplexity), true
+
 	case "SetTitleOperation.hash":
 		if e.complexity.SetTitleOperation.Hash == nil {
 			break
@@ -1640,6 +1739,41 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.SetTitleOperation.Was(childComplexity), true
 
+	case "SetTitleTimelineItem.hash":
+		if e.complexity.SetTitleTimelineItem.Hash == nil {
+			break
+		}
+
+		return e.complexity.SetTitleTimelineItem.Hash(childComplexity), true
+
+	case "SetTitleTimelineItem.author":
+		if e.complexity.SetTitleTimelineItem.Author == nil {
+			break
+		}
+
+		return e.complexity.SetTitleTimelineItem.Author(childComplexity), true
+
+	case "SetTitleTimelineItem.date":
+		if e.complexity.SetTitleTimelineItem.Date == nil {
+			break
+		}
+
+		return e.complexity.SetTitleTimelineItem.Date(childComplexity), true
+
+	case "SetTitleTimelineItem.title":
+		if e.complexity.SetTitleTimelineItem.Title == nil {
+			break
+		}
+
+		return e.complexity.SetTitleTimelineItem.Title(childComplexity), true
+
+	case "SetTitleTimelineItem.was":
+		if e.complexity.SetTitleTimelineItem.Was == nil {
+			break
+		}
+
+		return e.complexity.SetTitleTimelineItem.Was(childComplexity), true
+
 	case "TimelineItemConnection.edges":
 		if e.complexity.TimelineItemConnection.Edges == nil {
 			break
@@ -1879,11 +2013,11 @@ func (ec *executionContext) _AddCommentOperation_files(ctx context.Context, fiel
 	return arr1
 }
 
-var bugImplementors = []string{"Bug"}
+var addCommentTimelineItemImplementors = []string{"AddCommentTimelineItem", "TimelineItem"}
 
 // nolint: gocyclo, errcheck, gas, goconst
-func (ec *executionContext) _Bug(ctx context.Context, sel ast.SelectionSet, obj *bug.Snapshot) graphql.Marshaler {
-	fields := graphql.CollectFields(ctx, sel, bugImplementors)
+func (ec *executionContext) _AddCommentTimelineItem(ctx context.Context, sel ast.SelectionSet, obj *bug.AddCommentTimelineItem) graphql.Marshaler {
+	fields := graphql.CollectFields(ctx, sel, addCommentTimelineItemImplementors)
 
 	var wg sync.WaitGroup
 	out := graphql.NewOrderedMap(len(fields))
@@ -1893,82 +2027,55 @@ func (ec *executionContext) _Bug(ctx context.Context, sel ast.SelectionSet, obj
 
 		switch field.Name {
 		case "__typename":
-			out.Values[i] = graphql.MarshalString("Bug")
-		case "id":
-			out.Values[i] = ec._Bug_id(ctx, field, obj)
-			if out.Values[i] == graphql.Null {
-				invalid = true
-			}
-		case "humanId":
-			out.Values[i] = ec._Bug_humanId(ctx, field, obj)
+			out.Values[i] = graphql.MarshalString("AddCommentTimelineItem")
+		case "hash":
+			out.Values[i] = ec._AddCommentTimelineItem_hash(ctx, field, obj)
 			if out.Values[i] == graphql.Null {
 				invalid = true
 			}
-		case "status":
-			wg.Add(1)
-			go func(i int, field graphql.CollectedField) {
-				out.Values[i] = ec._Bug_status(ctx, field, obj)
-				if out.Values[i] == graphql.Null {
-					invalid = true
-				}
-				wg.Done()
-			}(i, field)
-		case "title":
-			out.Values[i] = ec._Bug_title(ctx, field, obj)
+		case "author":
+			out.Values[i] = ec._AddCommentTimelineItem_author(ctx, field, obj)
 			if out.Values[i] == graphql.Null {
 				invalid = true
 			}
-		case "labels":
-			out.Values[i] = ec._Bug_labels(ctx, field, obj)
+		case "message":
+			out.Values[i] = ec._AddCommentTimelineItem_message(ctx, field, obj)
 			if out.Values[i] == graphql.Null {
 				invalid = true
 			}
-		case "author":
-			out.Values[i] = ec._Bug_author(ctx, field, obj)
+		case "files":
+			out.Values[i] = ec._AddCommentTimelineItem_files(ctx, field, obj)
 			if out.Values[i] == graphql.Null {
 				invalid = true
 			}
 		case "createdAt":
-			out.Values[i] = ec._Bug_createdAt(ctx, field, obj)
-			if out.Values[i] == graphql.Null {
-				invalid = true
-			}
-		case "lastEdit":
-			wg.Add(1)
-			go func(i int, field graphql.CollectedField) {
-				out.Values[i] = ec._Bug_lastEdit(ctx, field, obj)
-				if out.Values[i] == graphql.Null {
-					invalid = true
-				}
-				wg.Done()
-			}(i, field)
-		case "comments":
-			wg.Add(1)
-			go func(i int, field graphql.CollectedField) {
-				out.Values[i] = ec._Bug_comments(ctx, field, obj)
-				if out.Values[i] == graphql.Null {
-					invalid = true
-				}
-				wg.Done()
-			}(i, field)
-		case "timeline":
 			wg.Add(1)
 			go func(i int, field graphql.CollectedField) {
-				out.Values[i] = ec._Bug_timeline(ctx, field, obj)
+				out.Values[i] = ec._AddCommentTimelineItem_createdAt(ctx, field, obj)
 				if out.Values[i] == graphql.Null {
 					invalid = true
 				}
 				wg.Done()
 			}(i, field)
-		case "operations":
+		case "lastEdit":
 			wg.Add(1)
 			go func(i int, field graphql.CollectedField) {
-				out.Values[i] = ec._Bug_operations(ctx, field, obj)
+				out.Values[i] = ec._AddCommentTimelineItem_lastEdit(ctx, field, obj)
 				if out.Values[i] == graphql.Null {
 					invalid = true
 				}
 				wg.Done()
 			}(i, field)
+		case "edited":
+			out.Values[i] = ec._AddCommentTimelineItem_edited(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				invalid = true
+			}
+		case "history":
+			out.Values[i] = ec._AddCommentTimelineItem_history(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				invalid = true
+			}
 		default:
 			panic("unknown field " + strconv.Quote(field.Name))
 		}
@@ -1981,15 +2088,15 @@ func (ec *executionContext) _Bug(ctx context.Context, sel ast.SelectionSet, obj
 }
 
 // nolint: vetshadow
-func (ec *executionContext) _Bug_id(ctx context.Context, field graphql.CollectedField, obj *bug.Snapshot) graphql.Marshaler {
+func (ec *executionContext) _AddCommentTimelineItem_hash(ctx context.Context, field graphql.CollectedField, obj *bug.AddCommentTimelineItem) graphql.Marshaler {
 	rctx := &graphql.ResolverContext{
-		Object: "Bug",
+		Object: "AddCommentTimelineItem",
 		Args:   nil,
 		Field:  field,
 	}
 	ctx = graphql.WithResolverContext(ctx, rctx)
 	resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) {
-		return obj.Id(), nil
+		return obj.Hash(), nil
 	})
 	if resTmp == nil {
 		if !ec.HasError(rctx) {
@@ -1997,21 +2104,21 @@ func (ec *executionContext) _Bug_id(ctx context.Context, field graphql.Collected
 		}
 		return graphql.Null
 	}
-	res := resTmp.(string)
+	res := resTmp.(git.Hash)
 	rctx.Result = res
-	return graphql.MarshalString(res)
+	return res
 }
 
 // nolint: vetshadow
-func (ec *executionContext) _Bug_humanId(ctx context.Context, field graphql.CollectedField, obj *bug.Snapshot) graphql.Marshaler {
+func (ec *executionContext) _AddCommentTimelineItem_author(ctx context.Context, field graphql.CollectedField, obj *bug.AddCommentTimelineItem) graphql.Marshaler {
 	rctx := &graphql.ResolverContext{
-		Object: "Bug",
+		Object: "AddCommentTimelineItem",
 		Args:   nil,
 		Field:  field,
 	}
 	ctx = graphql.WithResolverContext(ctx, rctx)
 	resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) {
-		return obj.HumanId(), nil
+		return obj.Author, nil
 	})
 	if resTmp == nil {
 		if !ec.HasError(rctx) {
@@ -2019,21 +2126,22 @@ func (ec *executionContext) _Bug_humanId(ctx context.Context, field graphql.Coll
 		}
 		return graphql.Null
 	}
-	res := resTmp.(string)
+	res := resTmp.(bug.Person)
 	rctx.Result = res
-	return graphql.MarshalString(res)
+
+	return ec._Person(ctx, field.Selections, &res)
 }
 
 // nolint: vetshadow
-func (ec *executionContext) _Bug_status(ctx context.Context, field graphql.CollectedField, obj *bug.Snapshot) graphql.Marshaler {
+func (ec *executionContext) _AddCommentTimelineItem_message(ctx context.Context, field graphql.CollectedField, obj *bug.AddCommentTimelineItem) graphql.Marshaler {
 	rctx := &graphql.ResolverContext{
-		Object: "Bug",
+		Object: "AddCommentTimelineItem",
 		Args:   nil,
 		Field:  field,
 	}
 	ctx = graphql.WithResolverContext(ctx, rctx)
 	resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) {
-		return ec.resolvers.Bug().Status(ctx, obj)
+		return obj.Message, nil
 	})
 	if resTmp == nil {
 		if !ec.HasError(rctx) {
@@ -2041,15 +2149,334 @@ func (ec *executionContext) _Bug_status(ctx context.Context, field graphql.Colle
 		}
 		return graphql.Null
 	}
-	res := resTmp.(models.Status)
+	res := resTmp.(string)
 	rctx.Result = res
-	return res
+	return graphql.MarshalString(res)
 }
 
 // nolint: vetshadow
-func (ec *executionContext) _Bug_title(ctx context.Context, field graphql.CollectedField, obj *bug.Snapshot) graphql.Marshaler {
+func (ec *executionContext) _AddCommentTimelineItem_files(ctx context.Context, field graphql.CollectedField, obj *bug.AddCommentTimelineItem) graphql.Marshaler {
 	rctx := &graphql.ResolverContext{
-		Object: "Bug",
+		Object: "AddCommentTimelineItem",
+		Args:   nil,
+		Field:  field,
+	}
+	ctx = graphql.WithResolverContext(ctx, rctx)
+	resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) {
+		return obj.Files, nil
+	})
+	if resTmp == nil {
+		if !ec.HasError(rctx) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.([]git.Hash)
+	rctx.Result = res
+
+	arr1 := make(graphql.Array, len(res))
+
+	for idx1 := range res {
+		arr1[idx1] = func() graphql.Marshaler {
+			return res[idx1]
+		}()
+	}
+
+	return arr1
+}
+
+// nolint: vetshadow
+func (ec *executionContext) _AddCommentTimelineItem_createdAt(ctx context.Context, field graphql.CollectedField, obj *bug.AddCommentTimelineItem) graphql.Marshaler {
+	rctx := &graphql.ResolverContext{
+		Object: "AddCommentTimelineItem",
+		Args:   nil,
+		Field:  field,
+	}
+	ctx = graphql.WithResolverContext(ctx, rctx)
+	resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) {
+		return ec.resolvers.AddCommentTimelineItem().CreatedAt(ctx, obj)
+	})
+	if resTmp == nil {
+		if !ec.HasError(rctx) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(time.Time)
+	rctx.Result = res
+	return graphql.MarshalTime(res)
+}
+
+// nolint: vetshadow
+func (ec *executionContext) _AddCommentTimelineItem_lastEdit(ctx context.Context, field graphql.CollectedField, obj *bug.AddCommentTimelineItem) graphql.Marshaler {
+	rctx := &graphql.ResolverContext{
+		Object: "AddCommentTimelineItem",
+		Args:   nil,
+		Field:  field,
+	}
+	ctx = graphql.WithResolverContext(ctx, rctx)
+	resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) {
+		return ec.resolvers.AddCommentTimelineItem().LastEdit(ctx, obj)
+	})
+	if resTmp == nil {
+		if !ec.HasError(rctx) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(time.Time)
+	rctx.Result = res
+	return graphql.MarshalTime(res)
+}
+
+// nolint: vetshadow
+func (ec *executionContext) _AddCommentTimelineItem_edited(ctx context.Context, field graphql.CollectedField, obj *bug.AddCommentTimelineItem) graphql.Marshaler {
+	rctx := &graphql.ResolverContext{
+		Object: "AddCommentTimelineItem",
+		Args:   nil,
+		Field:  field,
+	}
+	ctx = graphql.WithResolverContext(ctx, rctx)
+	resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) {
+		return obj.Edited(), nil
+	})
+	if resTmp == nil {
+		if !ec.HasError(rctx) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(bool)
+	rctx.Result = res
+	return graphql.MarshalBoolean(res)
+}
+
+// nolint: vetshadow
+func (ec *executionContext) _AddCommentTimelineItem_history(ctx context.Context, field graphql.CollectedField, obj *bug.AddCommentTimelineItem) graphql.Marshaler {
+	rctx := &graphql.ResolverContext{
+		Object: "AddCommentTimelineItem",
+		Args:   nil,
+		Field:  field,
+	}
+	ctx = graphql.WithResolverContext(ctx, rctx)
+	resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) {
+		return obj.History, nil
+	})
+	if resTmp == nil {
+		if !ec.HasError(rctx) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.([]bug.CommentHistoryStep)
+	rctx.Result = res
+
+	arr1 := make(graphql.Array, len(res))
+	var wg sync.WaitGroup
+
+	isLen1 := len(res) == 1
+	if !isLen1 {
+		wg.Add(len(res))
+	}
+
+	for idx1 := range res {
+		idx1 := idx1
+		rctx := &graphql.ResolverContext{
+			Index:  &idx1,
+			Result: &res[idx1],
+		}
+		ctx := graphql.WithResolverContext(ctx, rctx)
+		f := func(idx1 int) {
+			if !isLen1 {
+				defer wg.Done()
+			}
+			arr1[idx1] = func() graphql.Marshaler {
+
+				return ec._CommentHistoryStep(ctx, field.Selections, &res[idx1])
+			}()
+		}
+		if isLen1 {
+			f(idx1)
+		} else {
+			go f(idx1)
+		}
+
+	}
+	wg.Wait()
+	return arr1
+}
+
+var bugImplementors = []string{"Bug"}
+
+// nolint: gocyclo, errcheck, gas, goconst
+func (ec *executionContext) _Bug(ctx context.Context, sel ast.SelectionSet, obj *bug.Snapshot) graphql.Marshaler {
+	fields := graphql.CollectFields(ctx, sel, bugImplementors)
+
+	var wg sync.WaitGroup
+	out := graphql.NewOrderedMap(len(fields))
+	invalid := false
+	for i, field := range fields {
+		out.Keys[i] = field.Alias
+
+		switch field.Name {
+		case "__typename":
+			out.Values[i] = graphql.MarshalString("Bug")
+		case "id":
+			out.Values[i] = ec._Bug_id(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				invalid = true
+			}
+		case "humanId":
+			out.Values[i] = ec._Bug_humanId(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				invalid = true
+			}
+		case "status":
+			wg.Add(1)
+			go func(i int, field graphql.CollectedField) {
+				out.Values[i] = ec._Bug_status(ctx, field, obj)
+				if out.Values[i] == graphql.Null {
+					invalid = true
+				}
+				wg.Done()
+			}(i, field)
+		case "title":
+			out.Values[i] = ec._Bug_title(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				invalid = true
+			}
+		case "labels":
+			out.Values[i] = ec._Bug_labels(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				invalid = true
+			}
+		case "author":
+			out.Values[i] = ec._Bug_author(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				invalid = true
+			}
+		case "createdAt":
+			out.Values[i] = ec._Bug_createdAt(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				invalid = true
+			}
+		case "lastEdit":
+			wg.Add(1)
+			go func(i int, field graphql.CollectedField) {
+				out.Values[i] = ec._Bug_lastEdit(ctx, field, obj)
+				if out.Values[i] == graphql.Null {
+					invalid = true
+				}
+				wg.Done()
+			}(i, field)
+		case "comments":
+			wg.Add(1)
+			go func(i int, field graphql.CollectedField) {
+				out.Values[i] = ec._Bug_comments(ctx, field, obj)
+				if out.Values[i] == graphql.Null {
+					invalid = true
+				}
+				wg.Done()
+			}(i, field)
+		case "timeline":
+			wg.Add(1)
+			go func(i int, field graphql.CollectedField) {
+				out.Values[i] = ec._Bug_timeline(ctx, field, obj)
+				if out.Values[i] == graphql.Null {
+					invalid = true
+				}
+				wg.Done()
+			}(i, field)
+		case "operations":
+			wg.Add(1)
+			go func(i int, field graphql.CollectedField) {
+				out.Values[i] = ec._Bug_operations(ctx, field, obj)
+				if out.Values[i] == graphql.Null {
+					invalid = true
+				}
+				wg.Done()
+			}(i, field)
+		default:
+			panic("unknown field " + strconv.Quote(field.Name))
+		}
+	}
+	wg.Wait()
+	if invalid {
+		return graphql.Null
+	}
+	return out
+}
+
+// nolint: vetshadow
+func (ec *executionContext) _Bug_id(ctx context.Context, field graphql.CollectedField, obj *bug.Snapshot) graphql.Marshaler {
+	rctx := &graphql.ResolverContext{
+		Object: "Bug",
+		Args:   nil,
+		Field:  field,
+	}
+	ctx = graphql.WithResolverContext(ctx, rctx)
+	resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) {
+		return obj.Id(), nil
+	})
+	if resTmp == nil {
+		if !ec.HasError(rctx) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(string)
+	rctx.Result = res
+	return graphql.MarshalString(res)
+}
+
+// nolint: vetshadow
+func (ec *executionContext) _Bug_humanId(ctx context.Context, field graphql.CollectedField, obj *bug.Snapshot) graphql.Marshaler {
+	rctx := &graphql.ResolverContext{
+		Object: "Bug",
+		Args:   nil,
+		Field:  field,
+	}
+	ctx = graphql.WithResolverContext(ctx, rctx)
+	resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) {
+		return obj.HumanId(), nil
+	})
+	if resTmp == nil {
+		if !ec.HasError(rctx) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(string)
+	rctx.Result = res
+	return graphql.MarshalString(res)
+}
+
+// nolint: vetshadow
+func (ec *executionContext) _Bug_status(ctx context.Context, field graphql.CollectedField, obj *bug.Snapshot) graphql.Marshaler {
+	rctx := &graphql.ResolverContext{
+		Object: "Bug",
+		Args:   nil,
+		Field:  field,
+	}
+	ctx = graphql.WithResolverContext(ctx, rctx)
+	resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) {
+		return ec.resolvers.Bug().Status(ctx, obj)
+	})
+	if resTmp == nil {
+		if !ec.HasError(rctx) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(models.Status)
+	rctx.Result = res
+	return res
+}
+
+// nolint: vetshadow
+func (ec *executionContext) _Bug_title(ctx context.Context, field graphql.CollectedField, obj *bug.Snapshot) graphql.Marshaler {
+	rctx := &graphql.ResolverContext{
+		Object: "Bug",
 		Args:   nil,
 		Field:  field,
 	}
@@ -2939,306 +3366,46 @@ func (ec *executionContext) _CommentHistoryStep(ctx context.Context, sel ast.Sel
 	out := graphql.NewOrderedMap(len(fields))
 	invalid := false
 	for i, field := range fields {
-		out.Keys[i] = field.Alias
-
-		switch field.Name {
-		case "__typename":
-			out.Values[i] = graphql.MarshalString("CommentHistoryStep")
-		case "message":
-			out.Values[i] = ec._CommentHistoryStep_message(ctx, field, obj)
-			if out.Values[i] == graphql.Null {
-				invalid = true
-			}
-		case "date":
-			wg.Add(1)
-			go func(i int, field graphql.CollectedField) {
-				out.Values[i] = ec._CommentHistoryStep_date(ctx, field, obj)
-				if out.Values[i] == graphql.Null {
-					invalid = true
-				}
-				wg.Done()
-			}(i, field)
-		default:
-			panic("unknown field " + strconv.Quote(field.Name))
-		}
-	}
-	wg.Wait()
-	if invalid {
-		return graphql.Null
-	}
-	return out
-}
-
-// nolint: vetshadow
-func (ec *executionContext) _CommentHistoryStep_message(ctx context.Context, field graphql.CollectedField, obj *bug.CommentHistoryStep) graphql.Marshaler {
-	rctx := &graphql.ResolverContext{
-		Object: "CommentHistoryStep",
-		Args:   nil,
-		Field:  field,
-	}
-	ctx = graphql.WithResolverContext(ctx, rctx)
-	resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) {
-		return obj.Message, nil
-	})
-	if resTmp == nil {
-		if !ec.HasError(rctx) {
-			ec.Errorf(ctx, "must not be null")
-		}
-		return graphql.Null
-	}
-	res := resTmp.(string)
-	rctx.Result = res
-	return graphql.MarshalString(res)
-}
-
-// nolint: vetshadow
-func (ec *executionContext) _CommentHistoryStep_date(ctx context.Context, field graphql.CollectedField, obj *bug.CommentHistoryStep) graphql.Marshaler {
-	rctx := &graphql.ResolverContext{
-		Object: "CommentHistoryStep",
-		Args:   nil,
-		Field:  field,
-	}
-	ctx = graphql.WithResolverContext(ctx, rctx)
-	resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) {
-		return ec.resolvers.CommentHistoryStep().Date(ctx, obj)
-	})
-	if resTmp == nil {
-		if !ec.HasError(rctx) {
-			ec.Errorf(ctx, "must not be null")
-		}
-		return graphql.Null
-	}
-	res := resTmp.(time.Time)
-	rctx.Result = res
-	return graphql.MarshalTime(res)
-}
-
-var commentTimelineItemImplementors = []string{"CommentTimelineItem", "TimelineItem"}
-
-// nolint: gocyclo, errcheck, gas, goconst
-func (ec *executionContext) _CommentTimelineItem(ctx context.Context, sel ast.SelectionSet, obj *bug.CommentTimelineItem) graphql.Marshaler {
-	fields := graphql.CollectFields(ctx, sel, commentTimelineItemImplementors)
-
-	var wg sync.WaitGroup
-	out := graphql.NewOrderedMap(len(fields))
-	invalid := false
-	for i, field := range fields {
-		out.Keys[i] = field.Alias
-
-		switch field.Name {
-		case "__typename":
-			out.Values[i] = graphql.MarshalString("CommentTimelineItem")
-		case "hash":
-			out.Values[i] = ec._CommentTimelineItem_hash(ctx, field, obj)
-			if out.Values[i] == graphql.Null {
-				invalid = true
-			}
-		case "author":
-			out.Values[i] = ec._CommentTimelineItem_author(ctx, field, obj)
-			if out.Values[i] == graphql.Null {
-				invalid = true
-			}
-		case "message":
-			out.Values[i] = ec._CommentTimelineItem_message(ctx, field, obj)
-			if out.Values[i] == graphql.Null {
-				invalid = true
-			}
-		case "files":
-			out.Values[i] = ec._CommentTimelineItem_files(ctx, field, obj)
-			if out.Values[i] == graphql.Null {
-				invalid = true
-			}
-		case "createdAt":
-			wg.Add(1)
-			go func(i int, field graphql.CollectedField) {
-				out.Values[i] = ec._CommentTimelineItem_createdAt(ctx, field, obj)
-				if out.Values[i] == graphql.Null {
-					invalid = true
-				}
-				wg.Done()
-			}(i, field)
-		case "lastEdit":
-			wg.Add(1)
-			go func(i int, field graphql.CollectedField) {
-				out.Values[i] = ec._CommentTimelineItem_lastEdit(ctx, field, obj)
-				if out.Values[i] == graphql.Null {
-					invalid = true
-				}
-				wg.Done()
-			}(i, field)
-		case "edited":
-			out.Values[i] = ec._CommentTimelineItem_edited(ctx, field, obj)
-			if out.Values[i] == graphql.Null {
-				invalid = true
-			}
-		case "history":
-			out.Values[i] = ec._CommentTimelineItem_history(ctx, field, obj)
-			if out.Values[i] == graphql.Null {
-				invalid = true
-			}
-		default:
-			panic("unknown field " + strconv.Quote(field.Name))
-		}
-	}
-	wg.Wait()
-	if invalid {
-		return graphql.Null
-	}
-	return out
-}
-
-// nolint: vetshadow
-func (ec *executionContext) _CommentTimelineItem_hash(ctx context.Context, field graphql.CollectedField, obj *bug.CommentTimelineItem) graphql.Marshaler {
-	rctx := &graphql.ResolverContext{
-		Object: "CommentTimelineItem",
-		Args:   nil,
-		Field:  field,
-	}
-	ctx = graphql.WithResolverContext(ctx, rctx)
-	resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) {
-		return obj.Hash()
-	})
-	if resTmp == nil {
-		if !ec.HasError(rctx) {
-			ec.Errorf(ctx, "must not be null")
-		}
-		return graphql.Null
-	}
-	res := resTmp.(git.Hash)
-	rctx.Result = res
-	return res
-}
-
-// nolint: vetshadow
-func (ec *executionContext) _CommentTimelineItem_author(ctx context.Context, field graphql.CollectedField, obj *bug.CommentTimelineItem) graphql.Marshaler {
-	rctx := &graphql.ResolverContext{
-		Object: "CommentTimelineItem",
-		Args:   nil,
-		Field:  field,
-	}
-	ctx = graphql.WithResolverContext(ctx, rctx)
-	resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) {
-		return obj.Author, nil
-	})
-	if resTmp == nil {
-		if !ec.HasError(rctx) {
-			ec.Errorf(ctx, "must not be null")
-		}
-		return graphql.Null
-	}
-	res := resTmp.(bug.Person)
-	rctx.Result = res
-
-	return ec._Person(ctx, field.Selections, &res)
-}
-
-// nolint: vetshadow
-func (ec *executionContext) _CommentTimelineItem_message(ctx context.Context, field graphql.CollectedField, obj *bug.CommentTimelineItem) graphql.Marshaler {
-	rctx := &graphql.ResolverContext{
-		Object: "CommentTimelineItem",
-		Args:   nil,
-		Field:  field,
-	}
-	ctx = graphql.WithResolverContext(ctx, rctx)
-	resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) {
-		return obj.Message, nil
-	})
-	if resTmp == nil {
-		if !ec.HasError(rctx) {
-			ec.Errorf(ctx, "must not be null")
-		}
-		return graphql.Null
-	}
-	res := resTmp.(string)
-	rctx.Result = res
-	return graphql.MarshalString(res)
-}
-
-// nolint: vetshadow
-func (ec *executionContext) _CommentTimelineItem_files(ctx context.Context, field graphql.CollectedField, obj *bug.CommentTimelineItem) graphql.Marshaler {
-	rctx := &graphql.ResolverContext{
-		Object: "CommentTimelineItem",
-		Args:   nil,
-		Field:  field,
-	}
-	ctx = graphql.WithResolverContext(ctx, rctx)
-	resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) {
-		return obj.Files, nil
-	})
-	if resTmp == nil {
-		if !ec.HasError(rctx) {
-			ec.Errorf(ctx, "must not be null")
-		}
-		return graphql.Null
-	}
-	res := resTmp.([]git.Hash)
-	rctx.Result = res
-
-	arr1 := make(graphql.Array, len(res))
-
-	for idx1 := range res {
-		arr1[idx1] = func() graphql.Marshaler {
-			return res[idx1]
-		}()
-	}
-
-	return arr1
-}
-
-// nolint: vetshadow
-func (ec *executionContext) _CommentTimelineItem_createdAt(ctx context.Context, field graphql.CollectedField, obj *bug.CommentTimelineItem) graphql.Marshaler {
-	rctx := &graphql.ResolverContext{
-		Object: "CommentTimelineItem",
-		Args:   nil,
-		Field:  field,
-	}
-	ctx = graphql.WithResolverContext(ctx, rctx)
-	resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) {
-		return ec.resolvers.CommentTimelineItem().CreatedAt(ctx, obj)
-	})
-	if resTmp == nil {
-		if !ec.HasError(rctx) {
-			ec.Errorf(ctx, "must not be null")
-		}
-		return graphql.Null
-	}
-	res := resTmp.(time.Time)
-	rctx.Result = res
-	return graphql.MarshalTime(res)
-}
+		out.Keys[i] = field.Alias
 
-// nolint: vetshadow
-func (ec *executionContext) _CommentTimelineItem_lastEdit(ctx context.Context, field graphql.CollectedField, obj *bug.CommentTimelineItem) graphql.Marshaler {
-	rctx := &graphql.ResolverContext{
-		Object: "CommentTimelineItem",
-		Args:   nil,
-		Field:  field,
-	}
-	ctx = graphql.WithResolverContext(ctx, rctx)
-	resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) {
-		return ec.resolvers.CommentTimelineItem().LastEdit(ctx, obj)
-	})
-	if resTmp == nil {
-		if !ec.HasError(rctx) {
-			ec.Errorf(ctx, "must not be null")
+		switch field.Name {
+		case "__typename":
+			out.Values[i] = graphql.MarshalString("CommentHistoryStep")
+		case "message":
+			out.Values[i] = ec._CommentHistoryStep_message(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				invalid = true
+			}
+		case "date":
+			wg.Add(1)
+			go func(i int, field graphql.CollectedField) {
+				out.Values[i] = ec._CommentHistoryStep_date(ctx, field, obj)
+				if out.Values[i] == graphql.Null {
+					invalid = true
+				}
+				wg.Done()
+			}(i, field)
+		default:
+			panic("unknown field " + strconv.Quote(field.Name))
 		}
+	}
+	wg.Wait()
+	if invalid {
 		return graphql.Null
 	}
-	res := resTmp.(time.Time)
-	rctx.Result = res
-	return graphql.MarshalTime(res)
+	return out
 }
 
 // nolint: vetshadow
-func (ec *executionContext) _CommentTimelineItem_edited(ctx context.Context, field graphql.CollectedField, obj *bug.CommentTimelineItem) graphql.Marshaler {
+func (ec *executionContext) _CommentHistoryStep_message(ctx context.Context, field graphql.CollectedField, obj *bug.CommentHistoryStep) graphql.Marshaler {
 	rctx := &graphql.ResolverContext{
-		Object: "CommentTimelineItem",
+		Object: "CommentHistoryStep",
 		Args:   nil,
 		Field:  field,
 	}
 	ctx = graphql.WithResolverContext(ctx, rctx)
 	resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) {
-		return obj.Edited(), nil
+		return obj.Message, nil
 	})
 	if resTmp == nil {
 		if !ec.HasError(rctx) {

graphql/resolvers/root.go 🔗

@@ -40,14 +40,26 @@ func (RootResolver) CommentHistoryStep() graph.CommentHistoryStepResolver {
 	return &commentHistoryStepResolver{}
 }
 
-func (RootResolver) CommentTimelineItem() graph.CommentTimelineItemResolver {
-	return &commentTimelineItemResolver{}
+func (RootResolver) AddCommentTimelineItem() graph.AddCommentTimelineItemResolver {
+	return &addCommentTimelineItemResolver{}
 }
 
 func (RootResolver) CreateTimelineItem() graph.CreateTimelineItemResolver {
 	return &createTimelineItemResolver{}
 }
 
+func (r RootResolver) LabelChangeTimelineItem() graph.LabelChangeTimelineItemResolver {
+	return &labelChangeTimelineItem{}
+}
+
+func (r RootResolver) SetStatusTimelineItem() graph.SetStatusTimelineItemResolver {
+	return &setStatusTimelineItem{}
+}
+
+func (r RootResolver) SetTitleTimelineItem() graph.SetTitleTimelineItemResolver {
+	return &setTitleTimelineItem{}
+}
+
 func (RootResolver) CreateOperation() graph.CreateOperationResolver {
 	return &createOperationResolver{}
 }

graphql/resolvers/timeline.go 🔗

@@ -5,6 +5,7 @@ import (
 	"time"
 
 	"github.com/MichaelMure/git-bug/bug"
+	"github.com/MichaelMure/git-bug/graphql/models"
 )
 
 type commentHistoryStepResolver struct{}
@@ -13,13 +14,13 @@ func (commentHistoryStepResolver) Date(ctx context.Context, obj *bug.CommentHist
 	return obj.UnixTime.Time(), nil
 }
 
-type commentTimelineItemResolver struct{}
+type addCommentTimelineItemResolver struct{}
 
-func (commentTimelineItemResolver) CreatedAt(ctx context.Context, obj *bug.CommentTimelineItem) (time.Time, error) {
+func (addCommentTimelineItemResolver) CreatedAt(ctx context.Context, obj *bug.AddCommentTimelineItem) (time.Time, error) {
 	return obj.CreatedAt.Time(), nil
 }
 
-func (commentTimelineItemResolver) LastEdit(ctx context.Context, obj *bug.CommentTimelineItem) (time.Time, error) {
+func (addCommentTimelineItemResolver) LastEdit(ctx context.Context, obj *bug.AddCommentTimelineItem) (time.Time, error) {
 	return obj.LastEdit.Time(), nil
 }
 
@@ -27,10 +28,30 @@ type createTimelineItemResolver struct{}
 
 func (createTimelineItemResolver) CreatedAt(ctx context.Context, obj *bug.CreateTimelineItem) (time.Time, error) {
 	return obj.CreatedAt.Time(), nil
-
 }
 
 func (createTimelineItemResolver) LastEdit(ctx context.Context, obj *bug.CreateTimelineItem) (time.Time, error) {
 	return obj.LastEdit.Time(), nil
+}
+
+type labelChangeTimelineItem struct{}
+
+func (labelChangeTimelineItem) Date(ctx context.Context, obj *bug.LabelChangeTimelineItem) (time.Time, error) {
+	return obj.UnixTime.Time(), nil
+}
+
+type setStatusTimelineItem struct{}
 
+func (setStatusTimelineItem) Date(ctx context.Context, obj *bug.SetStatusTimelineItem) (time.Time, error) {
+	return obj.UnixTime.Time(), nil
+}
+
+func (setStatusTimelineItem) Status(ctx context.Context, obj *bug.SetStatusTimelineItem) (models.Status, error) {
+	return convertStatus(obj.Status)
+}
+
+type setTitleTimelineItem struct{}
+
+func (setTitleTimelineItem) Date(ctx context.Context, obj *bug.SetTitleTimelineItem) (time.Time, error) {
+	return obj.UnixTime.Time(), nil
 }

graphql/schema.graphql 🔗

@@ -95,7 +95,7 @@ type CreateOperation implements Operation & Authored {
   files: [Hash!]!
 }
 
-type SetTitleOperation implements Operation & Authored & TimelineItem {
+type SetTitleOperation implements Operation & Authored {
   hash: Hash!
   author: Person!
   date: Time!
@@ -112,7 +112,7 @@ type AddCommentOperation implements Operation & Authored {
   files: [Hash!]!
 }
 
-type SetStatusOperation implements Operation & Authored & TimelineItem {
+type SetStatusOperation implements Operation & Authored {
   hash: Hash!
   author: Person!
   date: Time!
@@ -120,7 +120,7 @@ type SetStatusOperation implements Operation & Authored & TimelineItem {
   status: Status!
 }
 
-type LabelChangeOperation implements Operation & Authored & TimelineItem {
+type LabelChangeOperation implements Operation & Authored {
   hash: Hash!
   author: Person!
   date: Time!
@@ -157,7 +157,7 @@ type CreateTimelineItem implements TimelineItem {
   history: [CommentHistoryStep!]!
 }
 
-type CommentTimelineItem implements TimelineItem {
+type AddCommentTimelineItem implements TimelineItem {
   hash: Hash!
   author: Person!
   message: String!
@@ -168,6 +168,29 @@ type CommentTimelineItem implements TimelineItem {
   history: [CommentHistoryStep!]!
 }
 
+type LabelChangeTimelineItem implements TimelineItem {
+  hash: Hash!
+  author: Person!
+  date: Time!
+  added: [Label!]!
+  removed: [Label!]!
+}
+
+type SetStatusTimelineItem implements TimelineItem {
+  hash: Hash!
+  author: Person!
+  date: Time!
+  status: Status!
+}
+
+type SetTitleTimelineItem implements TimelineItem {
+  hash: Hash!
+  author: Person!
+  date: Time!
+  title: String!
+  was: String!
+}
+
 """The connection type for Bug."""
 type BugConnection {
   """A list of edges."""

termui/show_bug.go 🔗

@@ -253,8 +253,8 @@ func (sb *showBug) renderMain(g *gocui.Gui, mainView *gocui.View) error {
 			fmt.Fprint(v, content)
 			y0 += lines + 2
 
-		case *bug.CommentTimelineItem:
-			comment := op.(*bug.CommentTimelineItem)
+		case *bug.AddCommentTimelineItem:
+			comment := op.(*bug.AddCommentTimelineItem)
 
 			edited := ""
 			if comment.Edited() {
@@ -277,13 +277,13 @@ func (sb *showBug) renderMain(g *gocui.Gui, mainView *gocui.View) error {
 			fmt.Fprint(v, content)
 			y0 += lines + 2
 
-		case *bug.SetTitleOperation:
-			setTitle := op.(*bug.SetTitleOperation)
+		case *bug.SetTitleTimelineItem:
+			setTitle := op.(*bug.SetTitleTimelineItem)
 
 			content := fmt.Sprintf("%s changed the title to %s on %s",
 				colors.Magenta(setTitle.Author.Name),
 				colors.Bold(setTitle.Title),
-				setTitle.Time().Format(timeLayout),
+				setTitle.UnixTime.Time().Format(timeLayout),
 			)
 			content, lines := text.Wrap(content, maxX)
 
@@ -294,13 +294,13 @@ func (sb *showBug) renderMain(g *gocui.Gui, mainView *gocui.View) error {
 			fmt.Fprint(v, content)
 			y0 += lines + 2
 
-		case *bug.SetStatusOperation:
-			setStatus := op.(*bug.SetStatusOperation)
+		case *bug.SetStatusTimelineItem:
+			setStatus := op.(*bug.SetStatusTimelineItem)
 
 			content := fmt.Sprintf("%s %s the bug on %s",
 				colors.Magenta(setStatus.Author.Name),
 				colors.Bold(setStatus.Status.Action()),
-				setStatus.Time().Format(timeLayout),
+				setStatus.UnixTime.Time().Format(timeLayout),
 			)
 			content, lines := text.Wrap(content, maxX)
 
@@ -311,8 +311,8 @@ func (sb *showBug) renderMain(g *gocui.Gui, mainView *gocui.View) error {
 			fmt.Fprint(v, content)
 			y0 += lines + 2
 
-		case *bug.LabelChangeOperation:
-			labelChange := op.(*bug.LabelChangeOperation)
+		case *bug.LabelChangeTimelineItem:
+			labelChange := op.(*bug.LabelChangeTimelineItem)
 
 			var added []string
 			for _, label := range labelChange.Added {
@@ -349,7 +349,7 @@ func (sb *showBug) renderMain(g *gocui.Gui, mainView *gocui.View) error {
 			content := fmt.Sprintf("%s %s on %s",
 				colors.Magenta(labelChange.Author.Name),
 				action.String(),
-				labelChange.Time().Format(timeLayout),
+				labelChange.UnixTime.Time().Format(timeLayout),
 			)
 			content, lines := text.Wrap(content, maxX)