timeline: various minor improvements

Michael Muré created

Change summary

bug/comment.go                |   9 
bug/op_add_comment.go         |   2 
bug/op_create.go              |   2 
bug/op_create_test.go         |   2 
bug/op_edit_comment.go        |  14 
bug/time.go                   |   9 
bug/timeline.go               |  81 +++-
graphql/gqlgen.yml            |   2 
graphql/graph/gen_graph.go    | 578 +++++++++++++++++++++++++++++++++++-
graphql/resolvers/root.go     |  16 
graphql/resolvers/timeline.go |  36 ++
graphql/schema.graphql        |  23 +
12 files changed, 695 insertions(+), 79 deletions(-)

Detailed changes

bug/comment.go 🔗

@@ -3,7 +3,6 @@ package bug
 import (
 	"github.com/MichaelMure/git-bug/util/git"
 	"github.com/dustin/go-humanize"
-	"time"
 )
 
 // Comment represent a comment in a Bug
@@ -14,16 +13,14 @@ type Comment struct {
 
 	// 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 int64
+	UnixTime Timestamp
 }
 
 // FormatTimeRel format the UnixTime of the comment for human consumption
 func (c Comment) FormatTimeRel() string {
-	t := time.Unix(c.UnixTime, 0)
-	return humanize.Time(t)
+	return humanize.Time(c.UnixTime.Time())
 }
 
 func (c Comment) FormatTime() string {
-	t := time.Unix(c.UnixTime, 0)
-	return t.Format("Mon Jan 2 15:04:05 2006 +0200")
+	return c.UnixTime.Time().Format("Mon Jan 2 15:04:05 2006 +0200")
 }

bug/op_add_comment.go 🔗

@@ -30,7 +30,7 @@ func (op *AddCommentOperation) Apply(snapshot *Snapshot) {
 		Message:  op.Message,
 		Author:   op.Author,
 		Files:    op.Files,
-		UnixTime: op.UnixTime,
+		UnixTime: Timestamp(op.UnixTime),
 	}
 
 	snapshot.Comments = append(snapshot.Comments, comment)

bug/op_create.go 🔗

@@ -32,7 +32,7 @@ func (op *CreateOperation) Apply(snapshot *Snapshot) {
 	comment := Comment{
 		Message:  op.Message,
 		Author:   op.Author,
-		UnixTime: op.UnixTime,
+		UnixTime: Timestamp(op.UnixTime),
 	}
 
 	snapshot.Comments = []Comment{comment}

bug/op_create_test.go 🔗

@@ -26,7 +26,7 @@ func TestCreate(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	comment := Comment{Author: rene, Message: "message", UnixTime: create.UnixTime}
+	comment := Comment{Author: rene, Message: "message", UnixTime: Timestamp(create.UnixTime)}
 
 	expected := Snapshot{
 		Title: "title",

bug/op_edit_comment.go 🔗

@@ -57,18 +57,20 @@ func (op *EditCommentOperation) Apply(snapshot *Snapshot) {
 		return
 	}
 
+	comment := Comment{
+		Message:  op.Message,
+		Files:    op.Files,
+		UnixTime: Timestamp(op.UnixTime),
+	}
+
 	switch target.(type) {
 	case *CreateTimelineItem:
 		item := target.(*CreateTimelineItem)
-		newComment := item.LastState()
-		newComment.Message = op.Message
-		item.History = append(item.History, newComment)
+		item.Append(comment)
 
 	case *CommentTimelineItem:
 		item := target.(*CommentTimelineItem)
-		newComment := item.LastState()
-		newComment.Message = op.Message
-		item.History = append(item.History, newComment)
+		item.Append(comment)
 	}
 
 	snapshot.Comments[commentIndex].Message = op.Message

bug/time.go 🔗

@@ -0,0 +1,9 @@
+package bug
+
+import "time"
+
+type Timestamp int64
+
+func (t Timestamp) Time() time.Time {
+	return time.Unix(int64(t), 0)
+}

bug/timeline.go 🔗

@@ -1,50 +1,67 @@
 package bug
 
-import "github.com/MichaelMure/git-bug/util/git"
+import (
+	"github.com/MichaelMure/git-bug/util/git"
+)
 
 type TimelineItem interface {
 	// Hash return the hash of the item
 	Hash() (git.Hash, error)
 }
 
+type CommentHistoryStep struct {
+	Message  string
+	UnixTime Timestamp
+}
+
 // CreateTimelineItem replace a Create operation in the Timeline and hold its edition history
 type CreateTimelineItem struct {
-	hash    git.Hash
-	History []Comment
+	CommentTimelineItem
 }
 
 func NewCreateTimelineItem(hash git.Hash, comment Comment) *CreateTimelineItem {
 	return &CreateTimelineItem{
-		hash: hash,
-		History: []Comment{
-			comment,
+		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,
+				},
+			},
 		},
 	}
 }
 
-func (c *CreateTimelineItem) Hash() (git.Hash, error) {
-	return c.hash, nil
-}
-
-func (c *CreateTimelineItem) LastState() Comment {
-	if len(c.History) == 0 {
-		panic("no history yet")
-	}
-
-	return c.History[len(c.History)-1]
-}
-
 // CommentTimelineItem replace a Comment in the Timeline and hold its edition history
 type CommentTimelineItem struct {
-	hash    git.Hash
-	History []Comment
+	hash      git.Hash
+	Author    Person
+	Message   string
+	Files     []git.Hash
+	CreatedAt Timestamp
+	LastEdit  Timestamp
+	History   []CommentHistoryStep
 }
 
 func NewCommentTimelineItem(hash git.Hash, comment Comment) *CommentTimelineItem {
 	return &CommentTimelineItem{
-		hash: hash,
-		History: []Comment{
-			comment,
+		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,
+			},
 		},
 	}
 }
@@ -53,10 +70,18 @@ func (c *CommentTimelineItem) Hash() (git.Hash, error) {
 	return c.hash, nil
 }
 
-func (c *CommentTimelineItem) LastState() Comment {
-	if len(c.History) == 0 {
-		panic("no history yet")
-	}
+// 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{
+		Message:  comment.Message,
+		UnixTime: comment.UnixTime,
+	})
+}
 
-	return c.History[len(c.History)-1]
+// Edited say if the comment was edited
+func (c *CommentTimelineItem) Edited() bool {
+	return len(c.History) > 1
 }

graphql/gqlgen.yml 🔗

@@ -33,6 +33,8 @@ models:
     model: github.com/MichaelMure/git-bug/bug.LabelChangeOperation
   TimelineItem:
     model: github.com/MichaelMure/git-bug/bug.TimelineItem
+  CommentHistoryStep:
+    model: github.com/MichaelMure/git-bug/bug.CommentHistoryStep
   CreateTimelineItem:
     model: github.com/MichaelMure/git-bug/bug.CreateTimelineItem
   CommentTimelineItem:

graphql/graph/gen_graph.go 🔗

@@ -37,7 +37,10 @@ type Config struct {
 type ResolverRoot interface {
 	AddCommentOperation() AddCommentOperationResolver
 	Bug() BugResolver
+	CommentHistoryStep() CommentHistoryStepResolver
+	CommentTimelineItem() CommentTimelineItemResolver
 	CreateOperation() CreateOperationResolver
+	CreateTimelineItem() CreateTimelineItemResolver
 	LabelChangeOperation() LabelChangeOperationResolver
 	Mutation() MutationResolver
 	Query() QueryResolver
@@ -101,9 +104,19 @@ type ComplexityRoot struct {
 		Node   func(childComplexity int) int
 	}
 
+	CommentHistoryStep struct {
+		Message func(childComplexity int) int
+		Date    func(childComplexity int) int
+	}
+
 	CommentTimelineItem struct {
 		Hash      func(childComplexity int) int
-		LastState 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
 	}
 
@@ -117,7 +130,12 @@ type ComplexityRoot struct {
 
 	CreateTimelineItem struct {
 		Hash      func(childComplexity int) int
-		LastState 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
 	}
 
@@ -214,10 +232,21 @@ type BugResolver interface {
 	Timeline(ctx context.Context, obj *bug.Snapshot, after *string, before *string, first *int, last *int) (models.TimelineItemConnection, error)
 	Operations(ctx context.Context, obj *bug.Snapshot, after *string, before *string, first *int, last *int) (models.OperationConnection, error)
 }
+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)
 }
+type CreateTimelineItemResolver interface {
+	CreatedAt(ctx context.Context, obj *bug.CreateTimelineItem) (time.Time, error)
+	LastEdit(ctx context.Context, obj *bug.CreateTimelineItem) (time.Time, error)
+}
 type LabelChangeOperationResolver interface {
 	Author(ctx context.Context, obj *bug.LabelChangeOperation) (bug.Person, error)
 	Date(ctx context.Context, obj *bug.LabelChangeOperation) (time.Time, error)
@@ -1134,6 +1163,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.CommentEdge.Node(childComplexity), true
 
+	case "CommentHistoryStep.message":
+		if e.complexity.CommentHistoryStep.Message == nil {
+			break
+		}
+
+		return e.complexity.CommentHistoryStep.Message(childComplexity), true
+
+	case "CommentHistoryStep.date":
+		if e.complexity.CommentHistoryStep.Date == nil {
+			break
+		}
+
+		return e.complexity.CommentHistoryStep.Date(childComplexity), true
+
 	case "CommentTimelineItem.hash":
 		if e.complexity.CommentTimelineItem.Hash == nil {
 			break
@@ -1141,12 +1184,47 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.CommentTimelineItem.Hash(childComplexity), true
 
-	case "CommentTimelineItem.lastState":
-		if e.complexity.CommentTimelineItem.LastState == nil {
+	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.LastState(childComplexity), true
+		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 {
@@ -1197,12 +1275,47 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.CreateTimelineItem.Hash(childComplexity), true
 
-	case "CreateTimelineItem.lastState":
-		if e.complexity.CreateTimelineItem.LastState == nil {
+	case "CreateTimelineItem.author":
+		if e.complexity.CreateTimelineItem.Author == nil {
+			break
+		}
+
+		return e.complexity.CreateTimelineItem.Author(childComplexity), true
+
+	case "CreateTimelineItem.message":
+		if e.complexity.CreateTimelineItem.Message == nil {
+			break
+		}
+
+		return e.complexity.CreateTimelineItem.Message(childComplexity), true
+
+	case "CreateTimelineItem.files":
+		if e.complexity.CreateTimelineItem.Files == nil {
+			break
+		}
+
+		return e.complexity.CreateTimelineItem.Files(childComplexity), true
+
+	case "CreateTimelineItem.createdAt":
+		if e.complexity.CreateTimelineItem.CreatedAt == nil {
+			break
+		}
+
+		return e.complexity.CreateTimelineItem.CreatedAt(childComplexity), true
+
+	case "CreateTimelineItem.lastEdit":
+		if e.complexity.CreateTimelineItem.LastEdit == nil {
+			break
+		}
+
+		return e.complexity.CreateTimelineItem.LastEdit(childComplexity), true
+
+	case "CreateTimelineItem.edited":
+		if e.complexity.CreateTimelineItem.Edited == nil {
 			break
 		}
 
-		return e.complexity.CreateTimelineItem.LastState(childComplexity), true
+		return e.complexity.CreateTimelineItem.Edited(childComplexity), true
 
 	case "CreateTimelineItem.history":
 		if e.complexity.CreateTimelineItem.History == nil {
@@ -2816,12 +2929,97 @@ func (ec *executionContext) _CommentEdge_node(ctx context.Context, field graphql
 	return ec._Comment(ctx, field.Selections, &res)
 }
 
+var commentHistoryStepImplementors = []string{"CommentHistoryStep"}
+
+// nolint: gocyclo, errcheck, gas, goconst
+func (ec *executionContext) _CommentHistoryStep(ctx context.Context, sel ast.SelectionSet, obj *bug.CommentHistoryStep) graphql.Marshaler {
+	fields := graphql.CollectFields(ctx, sel, commentHistoryStepImplementors)
+
+	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("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 {
@@ -2835,8 +3033,41 @@ func (ec *executionContext) _CommentTimelineItem(ctx context.Context, sel ast.Se
 			if out.Values[i] == graphql.Null {
 				invalid = true
 			}
-		case "lastState":
-			out.Values[i] = ec._CommentTimelineItem_lastState(ctx, field, obj)
+		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
 			}
@@ -2849,7 +3080,7 @@ func (ec *executionContext) _CommentTimelineItem(ctx context.Context, sel ast.Se
 			panic("unknown field " + strconv.Quote(field.Name))
 		}
 	}
-
+	wg.Wait()
 	if invalid {
 		return graphql.Null
 	}
@@ -2879,7 +3110,7 @@ func (ec *executionContext) _CommentTimelineItem_hash(ctx context.Context, field
 }
 
 // nolint: vetshadow
-func (ec *executionContext) _CommentTimelineItem_lastState(ctx context.Context, field graphql.CollectedField, obj *bug.CommentTimelineItem) graphql.Marshaler {
+func (ec *executionContext) _CommentTimelineItem_author(ctx context.Context, field graphql.CollectedField, obj *bug.CommentTimelineItem) graphql.Marshaler {
 	rctx := &graphql.ResolverContext{
 		Object: "CommentTimelineItem",
 		Args:   nil,
@@ -2887,7 +3118,7 @@ func (ec *executionContext) _CommentTimelineItem_lastState(ctx context.Context,
 	}
 	ctx = graphql.WithResolverContext(ctx, rctx)
 	resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) {
-		return obj.LastState(), nil
+		return obj.Author, nil
 	})
 	if resTmp == nil {
 		if !ec.HasError(rctx) {
@@ -2895,10 +3126,129 @@ func (ec *executionContext) _CommentTimelineItem_lastState(ctx context.Context,
 		}
 		return graphql.Null
 	}
-	res := resTmp.(bug.Comment)
+	res := resTmp.(bug.Person)
 	rctx.Result = res
 
-	return ec._Comment(ctx, field.Selections, &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)
+}
+
+// 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")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(time.Time)
+	rctx.Result = res
+	return graphql.MarshalTime(res)
+}
+
+// nolint: vetshadow
+func (ec *executionContext) _CommentTimelineItem_edited(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.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
@@ -2918,7 +3268,7 @@ func (ec *executionContext) _CommentTimelineItem_history(ctx context.Context, fi
 		}
 		return graphql.Null
 	}
-	res := resTmp.([]bug.Comment)
+	res := resTmp.([]bug.CommentHistoryStep)
 	rctx.Result = res
 
 	arr1 := make(graphql.Array, len(res))
@@ -2942,7 +3292,7 @@ func (ec *executionContext) _CommentTimelineItem_history(ctx context.Context, fi
 			}
 			arr1[idx1] = func() graphql.Marshaler {
 
-				return ec._Comment(ctx, field.Selections, &res[idx1])
+				return ec._CommentHistoryStep(ctx, field.Selections, &res[idx1])
 			}()
 		}
 		if isLen1 {
@@ -3141,6 +3491,7 @@ var createTimelineItemImplementors = []string{"CreateTimelineItem", "TimelineIte
 func (ec *executionContext) _CreateTimelineItem(ctx context.Context, sel ast.SelectionSet, obj *bug.CreateTimelineItem) graphql.Marshaler {
 	fields := graphql.CollectFields(ctx, sel, createTimelineItemImplementors)
 
+	var wg sync.WaitGroup
 	out := graphql.NewOrderedMap(len(fields))
 	invalid := false
 	for i, field := range fields {
@@ -3154,8 +3505,41 @@ func (ec *executionContext) _CreateTimelineItem(ctx context.Context, sel ast.Sel
 			if out.Values[i] == graphql.Null {
 				invalid = true
 			}
-		case "lastState":
-			out.Values[i] = ec._CreateTimelineItem_lastState(ctx, field, obj)
+		case "author":
+			out.Values[i] = ec._CreateTimelineItem_author(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				invalid = true
+			}
+		case "message":
+			out.Values[i] = ec._CreateTimelineItem_message(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				invalid = true
+			}
+		case "files":
+			out.Values[i] = ec._CreateTimelineItem_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._CreateTimelineItem_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._CreateTimelineItem_lastEdit(ctx, field, obj)
+				if out.Values[i] == graphql.Null {
+					invalid = true
+				}
+				wg.Done()
+			}(i, field)
+		case "edited":
+			out.Values[i] = ec._CreateTimelineItem_edited(ctx, field, obj)
 			if out.Values[i] == graphql.Null {
 				invalid = true
 			}
@@ -3168,7 +3552,7 @@ func (ec *executionContext) _CreateTimelineItem(ctx context.Context, sel ast.Sel
 			panic("unknown field " + strconv.Quote(field.Name))
 		}
 	}
-
+	wg.Wait()
 	if invalid {
 		return graphql.Null
 	}
@@ -3198,7 +3582,7 @@ func (ec *executionContext) _CreateTimelineItem_hash(ctx context.Context, field
 }
 
 // nolint: vetshadow
-func (ec *executionContext) _CreateTimelineItem_lastState(ctx context.Context, field graphql.CollectedField, obj *bug.CreateTimelineItem) graphql.Marshaler {
+func (ec *executionContext) _CreateTimelineItem_author(ctx context.Context, field graphql.CollectedField, obj *bug.CreateTimelineItem) graphql.Marshaler {
 	rctx := &graphql.ResolverContext{
 		Object: "CreateTimelineItem",
 		Args:   nil,
@@ -3206,7 +3590,7 @@ func (ec *executionContext) _CreateTimelineItem_lastState(ctx context.Context, f
 	}
 	ctx = graphql.WithResolverContext(ctx, rctx)
 	resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) {
-		return obj.LastState(), nil
+		return obj.Author, nil
 	})
 	if resTmp == nil {
 		if !ec.HasError(rctx) {
@@ -3214,10 +3598,129 @@ func (ec *executionContext) _CreateTimelineItem_lastState(ctx context.Context, f
 		}
 		return graphql.Null
 	}
-	res := resTmp.(bug.Comment)
+	res := resTmp.(bug.Person)
 	rctx.Result = res
 
-	return ec._Comment(ctx, field.Selections, &res)
+	return ec._Person(ctx, field.Selections, &res)
+}
+
+// nolint: vetshadow
+func (ec *executionContext) _CreateTimelineItem_message(ctx context.Context, field graphql.CollectedField, obj *bug.CreateTimelineItem) graphql.Marshaler {
+	rctx := &graphql.ResolverContext{
+		Object: "CreateTimelineItem",
+		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) _CreateTimelineItem_files(ctx context.Context, field graphql.CollectedField, obj *bug.CreateTimelineItem) graphql.Marshaler {
+	rctx := &graphql.ResolverContext{
+		Object: "CreateTimelineItem",
+		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) _CreateTimelineItem_createdAt(ctx context.Context, field graphql.CollectedField, obj *bug.CreateTimelineItem) graphql.Marshaler {
+	rctx := &graphql.ResolverContext{
+		Object: "CreateTimelineItem",
+		Args:   nil,
+		Field:  field,
+	}
+	ctx = graphql.WithResolverContext(ctx, rctx)
+	resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) {
+		return ec.resolvers.CreateTimelineItem().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) _CreateTimelineItem_lastEdit(ctx context.Context, field graphql.CollectedField, obj *bug.CreateTimelineItem) graphql.Marshaler {
+	rctx := &graphql.ResolverContext{
+		Object: "CreateTimelineItem",
+		Args:   nil,
+		Field:  field,
+	}
+	ctx = graphql.WithResolverContext(ctx, rctx)
+	resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) {
+		return ec.resolvers.CreateTimelineItem().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) _CreateTimelineItem_edited(ctx context.Context, field graphql.CollectedField, obj *bug.CreateTimelineItem) graphql.Marshaler {
+	rctx := &graphql.ResolverContext{
+		Object: "CreateTimelineItem",
+		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
@@ -3237,7 +3740,7 @@ func (ec *executionContext) _CreateTimelineItem_history(ctx context.Context, fie
 		}
 		return graphql.Null
 	}
-	res := resTmp.([]bug.Comment)
+	res := resTmp.([]bug.CommentHistoryStep)
 	rctx.Result = res
 
 	arr1 := make(graphql.Array, len(res))
@@ -3261,7 +3764,7 @@ func (ec *executionContext) _CreateTimelineItem_history(ctx context.Context, fie
 			}
 			arr1[idx1] = func() graphql.Marshaler {
 
-				return ec._Comment(ctx, field.Selections, &res[idx1])
+				return ec._CommentHistoryStep(ctx, field.Selections, &res[idx1])
 			}()
 		}
 		if isLen1 {
@@ -6603,16 +7106,31 @@ type TimelineItemEdge {
   node: TimelineItem!
 }
 
+type CommentHistoryStep {
+  message: String!
+  date: Time!
+}
+
 type CreateTimelineItem implements TimelineItem {
   hash: Hash!
-  lastState: Comment!
-  history: [Comment!]!
+  author: Person!
+  message: String!
+  files: [Hash!]!
+  createdAt: Time!
+  lastEdit: Time!
+  edited: Boolean!
+  history: [CommentHistoryStep!]!
 }
 
 type CommentTimelineItem implements TimelineItem {
   hash: Hash!
-  lastState: Comment!
-  history: [Comment!]!
+  author: Person!
+  message: String!
+  files: [Hash!]!
+  createdAt: Time!
+  lastEdit: Time!
+  edited: Boolean!
+  history: [CommentHistoryStep!]!
 }
 
 """The connection type for Bug."""

graphql/resolvers/root.go 🔗

@@ -32,10 +32,22 @@ func (RootResolver) AddCommentOperation() graph.AddCommentOperationResolver {
 	return &addCommentOperationResolver{}
 }
 
-func (r RootResolver) Bug() graph.BugResolver {
+func (RootResolver) Bug() graph.BugResolver {
 	return &bugResolver{}
 }
 
+func (RootResolver) CommentHistoryStep() graph.CommentHistoryStepResolver {
+	return &commentHistoryStepResolver{}
+}
+
+func (RootResolver) CommentTimelineItem() graph.CommentTimelineItemResolver {
+	return &commentTimelineItemResolver{}
+}
+
+func (RootResolver) CreateTimelineItem() graph.CreateTimelineItemResolver {
+	return &createTimelineItemResolver{}
+}
+
 func (RootResolver) CreateOperation() graph.CreateOperationResolver {
 	return &createOperationResolver{}
 }
@@ -44,7 +56,7 @@ func (RootResolver) LabelChangeOperation() graph.LabelChangeOperationResolver {
 	return &labelChangeOperation{}
 }
 
-func (r RootResolver) Repository() graph.RepositoryResolver {
+func (RootResolver) Repository() graph.RepositoryResolver {
 	return &repoResolver{}
 }
 

graphql/resolvers/timeline.go 🔗

@@ -0,0 +1,36 @@
+package resolvers
+
+import (
+	"context"
+	"time"
+
+	"github.com/MichaelMure/git-bug/bug"
+)
+
+type commentHistoryStepResolver struct{}
+
+func (commentHistoryStepResolver) Date(ctx context.Context, obj *bug.CommentHistoryStep) (time.Time, error) {
+	return obj.UnixTime.Time(), nil
+}
+
+type commentTimelineItemResolver struct{}
+
+func (commentTimelineItemResolver) CreatedAt(ctx context.Context, obj *bug.CommentTimelineItem) (time.Time, error) {
+	return obj.CreatedAt.Time(), nil
+}
+
+func (commentTimelineItemResolver) LastEdit(ctx context.Context, obj *bug.CommentTimelineItem) (time.Time, error) {
+	return obj.LastEdit.Time(), nil
+}
+
+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
+
+}

graphql/schema.graphql 🔗

@@ -141,16 +141,31 @@ type TimelineItemEdge {
   node: TimelineItem!
 }
 
+type CommentHistoryStep {
+  message: String!
+  date: Time!
+}
+
 type CreateTimelineItem implements TimelineItem {
   hash: Hash!
-  lastState: Comment!
-  history: [Comment!]!
+  author: Person!
+  message: String!
+  files: [Hash!]!
+  createdAt: Time!
+  lastEdit: Time!
+  edited: Boolean!
+  history: [CommentHistoryStep!]!
 }
 
 type CommentTimelineItem implements TimelineItem {
   hash: Hash!
-  lastState: Comment!
-  history: [Comment!]!
+  author: Person!
+  message: String!
+  files: [Hash!]!
+  createdAt: Time!
+  lastEdit: Time!
+  edited: Boolean!
+  history: [CommentHistoryStep!]!
 }
 
 """The connection type for Bug."""