graphql: expose the new Timeline

Michael Muré created

Change summary

graphql/connections/connections.go  |    1 
graphql/connections/gen_timeline.go |  111 +++
graphql/gqlgen.yml                  |    6 
graphql/graph/gen_graph.go          | 1013 +++++++++++++++++++++++-------
graphql/models/edges.go             |    5 
graphql/models/gen_models.go        |   12 
graphql/resolvers/bug.go            |   27 
graphql/schema.graphql              |   49 +
8 files changed, 991 insertions(+), 233 deletions(-)

Detailed changes

graphql/connections/connections.go 🔗

@@ -1,6 +1,7 @@
 //go:generate genny -in=connection_template.go -out=gen_bug.go gen "NodeType=string EdgeType=LazyBugEdge ConnectionType=models.BugConnection"
 //go:generate genny -in=connection_template.go -out=gen_operation.go gen "NodeType=bug.Operation EdgeType=models.OperationEdge ConnectionType=models.OperationConnection"
 //go:generate genny -in=connection_template.go -out=gen_comment.go gen "NodeType=bug.Comment EdgeType=models.CommentEdge ConnectionType=models.CommentConnection"
+//go:generate genny -in=connection_template.go -out=gen_timeline.go gen "NodeType=bug.TimelineItem EdgeType=models.TimelineItemEdge ConnectionType=models.TimelineItemConnection"
 
 // Package connections implement a generic GraphQL relay connection
 package connections

graphql/connections/gen_timeline.go 🔗

@@ -0,0 +1,111 @@
+// This file was automatically generated by genny.
+// Any changes will be lost if this file is regenerated.
+// see https://github.com/cheekybits/genny
+
+package connections
+
+import (
+	"fmt"
+
+	"github.com/MichaelMure/git-bug/bug"
+	"github.com/MichaelMure/git-bug/graphql/models"
+)
+
+// BugTimelineItemEdgeMaker define a function that take a bug.TimelineItem and an offset and
+// create an Edge.
+type BugTimelineItemEdgeMaker func(value bug.TimelineItem, offset int) Edge
+
+// BugTimelineItemConMaker define a function that create a models.TimelineItemConnection
+type BugTimelineItemConMaker func(
+	edges []models.TimelineItemEdge,
+	nodes []bug.TimelineItem,
+	info models.PageInfo,
+	totalCount int) (models.TimelineItemConnection, error)
+
+// BugTimelineItemCon will paginate a source according to the input of a relay connection
+func BugTimelineItemCon(source []bug.TimelineItem, edgeMaker BugTimelineItemEdgeMaker, conMaker BugTimelineItemConMaker, input models.ConnectionInput) (models.TimelineItemConnection, error) {
+	var nodes []bug.TimelineItem
+	var edges []models.TimelineItemEdge
+	var cursors []string
+	var pageInfo models.PageInfo
+	var totalCount = len(source)
+
+	emptyCon, _ := conMaker(edges, nodes, pageInfo, 0)
+
+	offset := 0
+
+	if input.After != nil {
+		for i, value := range source {
+			edge := edgeMaker(value, i)
+			if edge.GetCursor() == *input.After {
+				// remove all previous element including the "after" one
+				source = source[i+1:]
+				offset = i + 1
+				pageInfo.HasPreviousPage = true
+				break
+			}
+		}
+	}
+
+	if input.Before != nil {
+		for i, value := range source {
+			edge := edgeMaker(value, i+offset)
+
+			if edge.GetCursor() == *input.Before {
+				// remove all after element including the "before" one
+				pageInfo.HasNextPage = true
+				break
+			}
+
+			edges = append(edges, edge.(models.TimelineItemEdge))
+			cursors = append(cursors, edge.GetCursor())
+			nodes = append(nodes, value)
+		}
+	} else {
+		edges = make([]models.TimelineItemEdge, len(source))
+		cursors = make([]string, len(source))
+		nodes = source
+
+		for i, value := range source {
+			edge := edgeMaker(value, i+offset)
+			edges[i] = edge.(models.TimelineItemEdge)
+			cursors[i] = edge.GetCursor()
+		}
+	}
+
+	if input.First != nil {
+		if *input.First < 0 {
+			return emptyCon, fmt.Errorf("first less than zero")
+		}
+
+		if len(edges) > *input.First {
+			// Slice result to be of length first by removing edges from the end
+			edges = edges[:*input.First]
+			cursors = cursors[:*input.First]
+			nodes = nodes[:*input.First]
+			pageInfo.HasNextPage = true
+		}
+	}
+
+	if input.Last != nil {
+		if *input.Last < 0 {
+			return emptyCon, fmt.Errorf("last less than zero")
+		}
+
+		if len(edges) > *input.Last {
+			// Slice result to be of length last by removing edges from the start
+			edges = edges[len(edges)-*input.Last:]
+			cursors = cursors[len(cursors)-*input.Last:]
+			nodes = nodes[len(nodes)-*input.Last:]
+			pageInfo.HasPreviousPage = true
+		}
+	}
+
+	// Fill up pageInfo cursors
+	if len(cursors) > 0 {
+		pageInfo.StartCursor = cursors[0]
+		pageInfo.EndCursor = cursors[len(cursors)-1]
+	}
+
+	return conMaker(edges, nodes, pageInfo, totalCount)
+}

graphql/gqlgen.yml 🔗

@@ -31,3 +31,9 @@ models:
     model: github.com/MichaelMure/git-bug/bug.SetStatusOperation
   LabelChangeOperation:
     model: github.com/MichaelMure/git-bug/bug.LabelChangeOperation
+  TimelineItem:
+    model: github.com/MichaelMure/git-bug/bug.TimelineItem
+  CreateTimelineItem:
+    model: github.com/MichaelMure/git-bug/bug.CreateTimelineItem
+  CommentTimelineItem:
+    model: github.com/MichaelMure/git-bug/bug.CommentTimelineItem

graphql/graph/gen_graph.go 🔗

@@ -67,6 +67,7 @@ type ComplexityRoot struct {
 		CreatedAt  func(childComplexity int) int
 		LastEdit   func(childComplexity int) int
 		Comments   func(childComplexity int, after *string, before *string, first *int, last *int) int
+		Timeline   func(childComplexity int, after *string, before *string, first *int, last *int) int
 		Operations func(childComplexity int, after *string, before *string, first *int, last *int) int
 	}
 
@@ -100,6 +101,12 @@ type ComplexityRoot struct {
 		Node   func(childComplexity int) int
 	}
 
+	CommentTimelineItem struct {
+		Hash      func(childComplexity int) int
+		LastState func(childComplexity int) int
+		History   func(childComplexity int) int
+	}
+
 	CreateOperation struct {
 		Author  func(childComplexity int) int
 		Date    func(childComplexity int) int
@@ -108,7 +115,14 @@ type ComplexityRoot struct {
 		Files   func(childComplexity int) int
 	}
 
+	CreateTimelineItem struct {
+		Hash      func(childComplexity int) int
+		LastState func(childComplexity int) int
+		History   func(childComplexity int) int
+	}
+
 	LabelChangeOperation struct {
+		Hash    func(childComplexity int) int
 		Author  func(childComplexity int) int
 		Date    func(childComplexity int) int
 		Added   func(childComplexity int) int
@@ -161,17 +175,31 @@ type ComplexityRoot struct {
 	}
 
 	SetStatusOperation 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
 		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
+		PageInfo   func(childComplexity int) int
+		TotalCount func(childComplexity int) int
+	}
+
+	TimelineItemEdge struct {
+		Cursor func(childComplexity int) int
+		Node   func(childComplexity int) int
+	}
 }
 
 type AddCommentOperationResolver interface {
@@ -183,6 +211,7 @@ type BugResolver interface {
 
 	LastEdit(ctx context.Context, obj *bug.Snapshot) (time.Time, error)
 	Comments(ctx context.Context, obj *bug.Snapshot, after *string, before *string, first *int, last *int) (models.CommentConnection, error)
+	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 CreateOperationResolver interface {
@@ -282,6 +311,68 @@ func field_Bug_comments_args(rawArgs map[string]interface{}) (map[string]interfa
 
 }
 
+func field_Bug_timeline_args(rawArgs map[string]interface{}) (map[string]interface{}, error) {
+	args := map[string]interface{}{}
+	var arg0 *string
+	if tmp, ok := rawArgs["after"]; ok {
+		var err error
+		var ptr1 string
+		if tmp != nil {
+			ptr1, err = graphql.UnmarshalString(tmp)
+			arg0 = &ptr1
+		}
+
+		if err != nil {
+			return nil, err
+		}
+	}
+	args["after"] = arg0
+	var arg1 *string
+	if tmp, ok := rawArgs["before"]; ok {
+		var err error
+		var ptr1 string
+		if tmp != nil {
+			ptr1, err = graphql.UnmarshalString(tmp)
+			arg1 = &ptr1
+		}
+
+		if err != nil {
+			return nil, err
+		}
+	}
+	args["before"] = arg1
+	var arg2 *int
+	if tmp, ok := rawArgs["first"]; ok {
+		var err error
+		var ptr1 int
+		if tmp != nil {
+			ptr1, err = graphql.UnmarshalInt(tmp)
+			arg2 = &ptr1
+		}
+
+		if err != nil {
+			return nil, err
+		}
+	}
+	args["first"] = arg2
+	var arg3 *int
+	if tmp, ok := rawArgs["last"]; ok {
+		var err error
+		var ptr1 int
+		if tmp != nil {
+			ptr1, err = graphql.UnmarshalInt(tmp)
+			arg3 = &ptr1
+		}
+
+		if err != nil {
+			return nil, err
+		}
+	}
+	args["last"] = arg3
+	return args, nil
+
+}
+
 func field_Bug_operations_args(rawArgs map[string]interface{}) (map[string]interface{}, error) {
 	args := map[string]interface{}{}
 	var arg0 *string
@@ -914,6 +1005,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.Bug.Comments(childComplexity, args["after"].(*string), args["before"].(*string), args["first"].(*int), args["last"].(*int)), true
 
+	case "Bug.timeline":
+		if e.complexity.Bug.Timeline == nil {
+			break
+		}
+
+		args, err := field_Bug_timeline_args(rawArgs)
+		if err != nil {
+			return 0, false
+		}
+
+		return e.complexity.Bug.Timeline(childComplexity, args["after"].(*string), args["before"].(*string), args["first"].(*int), args["last"].(*int)), true
+
 	case "Bug.operations":
 		if e.complexity.Bug.Operations == nil {
 			break
@@ -1031,6 +1134,27 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.CommentEdge.Node(childComplexity), true
 
+	case "CommentTimelineItem.hash":
+		if e.complexity.CommentTimelineItem.Hash == nil {
+			break
+		}
+
+		return e.complexity.CommentTimelineItem.Hash(childComplexity), true
+
+	case "CommentTimelineItem.lastState":
+		if e.complexity.CommentTimelineItem.LastState == nil {
+			break
+		}
+
+		return e.complexity.CommentTimelineItem.LastState(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
@@ -1066,6 +1190,34 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.CreateOperation.Files(childComplexity), true
 
+	case "CreateTimelineItem.hash":
+		if e.complexity.CreateTimelineItem.Hash == nil {
+			break
+		}
+
+		return e.complexity.CreateTimelineItem.Hash(childComplexity), true
+
+	case "CreateTimelineItem.lastState":
+		if e.complexity.CreateTimelineItem.LastState == nil {
+			break
+		}
+
+		return e.complexity.CreateTimelineItem.LastState(childComplexity), true
+
+	case "CreateTimelineItem.history":
+		if e.complexity.CreateTimelineItem.History == nil {
+			break
+		}
+
+		return e.complexity.CreateTimelineItem.History(childComplexity), true
+
+	case "LabelChangeOperation.hash":
+		if e.complexity.LabelChangeOperation.Hash == nil {
+			break
+		}
+
+		return e.complexity.LabelChangeOperation.Hash(childComplexity), true
+
 	case "LabelChangeOperation.author":
 		if e.complexity.LabelChangeOperation.Author == nil {
 			break
@@ -1312,6 +1464,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.Repository.Bug(childComplexity, args["prefix"].(string)), true
 
+	case "SetStatusOperation.hash":
+		if e.complexity.SetStatusOperation.Hash == nil {
+			break
+		}
+
+		return e.complexity.SetStatusOperation.Hash(childComplexity), true
+
 	case "SetStatusOperation.author":
 		if e.complexity.SetStatusOperation.Author == nil {
 			break
@@ -1333,6 +1492,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.SetStatusOperation.Status(childComplexity), true
 
+	case "SetTitleOperation.hash":
+		if e.complexity.SetTitleOperation.Hash == nil {
+			break
+		}
+
+		return e.complexity.SetTitleOperation.Hash(childComplexity), true
+
 	case "SetTitleOperation.author":
 		if e.complexity.SetTitleOperation.Author == nil {
 			break
@@ -1361,6 +1527,48 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.SetTitleOperation.Was(childComplexity), true
 
+	case "TimelineItemConnection.edges":
+		if e.complexity.TimelineItemConnection.Edges == nil {
+			break
+		}
+
+		return e.complexity.TimelineItemConnection.Edges(childComplexity), true
+
+	case "TimelineItemConnection.nodes":
+		if e.complexity.TimelineItemConnection.Nodes == nil {
+			break
+		}
+
+		return e.complexity.TimelineItemConnection.Nodes(childComplexity), true
+
+	case "TimelineItemConnection.pageInfo":
+		if e.complexity.TimelineItemConnection.PageInfo == nil {
+			break
+		}
+
+		return e.complexity.TimelineItemConnection.PageInfo(childComplexity), true
+
+	case "TimelineItemConnection.totalCount":
+		if e.complexity.TimelineItemConnection.TotalCount == nil {
+			break
+		}
+
+		return e.complexity.TimelineItemConnection.TotalCount(childComplexity), true
+
+	case "TimelineItemEdge.cursor":
+		if e.complexity.TimelineItemEdge.Cursor == nil {
+			break
+		}
+
+		return e.complexity.TimelineItemEdge.Cursor(childComplexity), true
+
+	case "TimelineItemEdge.node":
+		if e.complexity.TimelineItemEdge.Node == nil {
+			break
+		}
+
+		return e.complexity.TimelineItemEdge.Node(childComplexity), true
+
 	}
 	return 0, false
 }
@@ -1630,6 +1838,15 @@ func (ec *executionContext) _Bug(ctx context.Context, sel ast.SelectionSet, obj
 				}
 				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) {
@@ -1865,6 +2082,35 @@ func (ec *executionContext) _Bug_comments(ctx context.Context, field graphql.Col
 	return ec._CommentConnection(ctx, field.Selections, &res)
 }
 
+// nolint: vetshadow
+func (ec *executionContext) _Bug_timeline(ctx context.Context, field graphql.CollectedField, obj *bug.Snapshot) graphql.Marshaler {
+	rawArgs := field.ArgumentMap(ec.Variables)
+	args, err := field_Bug_timeline_args(rawArgs)
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	rctx := &graphql.ResolverContext{
+		Object: "Bug",
+		Args:   args,
+		Field:  field,
+	}
+	ctx = graphql.WithResolverContext(ctx, rctx)
+	resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) {
+		return ec.resolvers.Bug().Timeline(ctx, obj, args["after"].(*string), args["before"].(*string), args["first"].(*int), args["last"].(*int))
+	})
+	if resTmp == nil {
+		if !ec.HasError(rctx) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(models.TimelineItemConnection)
+	rctx.Result = res
+
+	return ec._TimelineItemConnection(ctx, field.Selections, &res)
+}
+
 // nolint: vetshadow
 func (ec *executionContext) _Bug_operations(ctx context.Context, field graphql.CollectedField, obj *bug.Snapshot) graphql.Marshaler {
 	rawArgs := field.ArgumentMap(ec.Variables)
@@ -2570,13 +2816,12 @@ func (ec *executionContext) _CommentEdge_node(ctx context.Context, field graphql
 	return ec._Comment(ctx, field.Selections, &res)
 }
 
-var createOperationImplementors = []string{"CreateOperation", "Operation", "Authored"}
+var commentTimelineItemImplementors = []string{"CommentTimelineItem", "TimelineItem"}
 
 // nolint: gocyclo, errcheck, gas, goconst
-func (ec *executionContext) _CreateOperation(ctx context.Context, sel ast.SelectionSet, obj *bug.CreateOperation) graphql.Marshaler {
-	fields := graphql.CollectFields(ctx, sel, createOperationImplementors)
+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 {
@@ -2584,37 +2829,19 @@ func (ec *executionContext) _CreateOperation(ctx context.Context, sel ast.Select
 
 		switch field.Name {
 		case "__typename":
-			out.Values[i] = graphql.MarshalString("CreateOperation")
-		case "author":
-			wg.Add(1)
-			go func(i int, field graphql.CollectedField) {
-				out.Values[i] = ec._CreateOperation_author(ctx, field, obj)
-				if out.Values[i] == graphql.Null {
-					invalid = true
-				}
-				wg.Done()
-			}(i, field)
-		case "date":
-			wg.Add(1)
-			go func(i int, field graphql.CollectedField) {
-				out.Values[i] = ec._CreateOperation_date(ctx, field, obj)
-				if out.Values[i] == graphql.Null {
-					invalid = true
-				}
-				wg.Done()
-			}(i, field)
-		case "title":
-			out.Values[i] = ec._CreateOperation_title(ctx, field, obj)
+			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 "message":
-			out.Values[i] = ec._CreateOperation_message(ctx, field, obj)
+		case "lastState":
+			out.Values[i] = ec._CommentTimelineItem_lastState(ctx, field, obj)
 			if out.Values[i] == graphql.Null {
 				invalid = true
 			}
-		case "files":
-			out.Values[i] = ec._CreateOperation_files(ctx, field, obj)
+		case "history":
+			out.Values[i] = ec._CommentTimelineItem_history(ctx, field, obj)
 			if out.Values[i] == graphql.Null {
 				invalid = true
 			}
@@ -2622,7 +2849,7 @@ func (ec *executionContext) _CreateOperation(ctx context.Context, sel ast.Select
 			panic("unknown field " + strconv.Quote(field.Name))
 		}
 	}
-	wg.Wait()
+
 	if invalid {
 		return graphql.Null
 	}
@@ -2630,15 +2857,15 @@ func (ec *executionContext) _CreateOperation(ctx context.Context, sel ast.Select
 }
 
 // nolint: vetshadow
-func (ec *executionContext) _CreateOperation_author(ctx context.Context, field graphql.CollectedField, obj *bug.CreateOperation) graphql.Marshaler {
+func (ec *executionContext) _CommentTimelineItem_hash(ctx context.Context, field graphql.CollectedField, obj *bug.CommentTimelineItem) graphql.Marshaler {
 	rctx := &graphql.ResolverContext{
-		Object: "CreateOperation",
+		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.CreateOperation().Author(ctx, obj)
+		return obj.Hash()
 	})
 	if resTmp == nil {
 		if !ec.HasError(rctx) {
@@ -2646,22 +2873,21 @@ func (ec *executionContext) _CreateOperation_author(ctx context.Context, field g
 		}
 		return graphql.Null
 	}
-	res := resTmp.(bug.Person)
+	res := resTmp.(git.Hash)
 	rctx.Result = res
-
-	return ec._Person(ctx, field.Selections, &res)
+	return res
 }
 
 // nolint: vetshadow
-func (ec *executionContext) _CreateOperation_date(ctx context.Context, field graphql.CollectedField, obj *bug.CreateOperation) graphql.Marshaler {
+func (ec *executionContext) _CommentTimelineItem_lastState(ctx context.Context, field graphql.CollectedField, obj *bug.CommentTimelineItem) graphql.Marshaler {
 	rctx := &graphql.ResolverContext{
-		Object: "CreateOperation",
+		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.CreateOperation().Date(ctx, obj)
+		return obj.LastState(), nil
 	})
 	if resTmp == nil {
 		if !ec.HasError(rctx) {
@@ -2669,21 +2895,22 @@ func (ec *executionContext) _CreateOperation_date(ctx context.Context, field gra
 		}
 		return graphql.Null
 	}
-	res := resTmp.(time.Time)
+	res := resTmp.(bug.Comment)
 	rctx.Result = res
-	return graphql.MarshalTime(res)
+
+	return ec._Comment(ctx, field.Selections, &res)
 }
 
 // nolint: vetshadow
-func (ec *executionContext) _CreateOperation_title(ctx context.Context, field graphql.CollectedField, obj *bug.CreateOperation) graphql.Marshaler {
+func (ec *executionContext) _CommentTimelineItem_history(ctx context.Context, field graphql.CollectedField, obj *bug.CommentTimelineItem) graphql.Marshaler {
 	rctx := &graphql.ResolverContext{
-		Object: "CreateOperation",
+		Object: "CommentTimelineItem",
 		Args:   nil,
 		Field:  field,
 	}
 	ctx = graphql.WithResolverContext(ctx, rctx)
 	resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) {
-		return obj.Title, nil
+		return obj.History, nil
 	})
 	if resTmp == nil {
 		if !ec.HasError(rctx) {
@@ -2691,69 +2918,49 @@ func (ec *executionContext) _CreateOperation_title(ctx context.Context, field gr
 		}
 		return graphql.Null
 	}
-	res := resTmp.(string)
+	res := resTmp.([]bug.Comment)
 	rctx.Result = res
-	return graphql.MarshalString(res)
-}
 
-// nolint: vetshadow
-func (ec *executionContext) _CreateOperation_message(ctx context.Context, field graphql.CollectedField, obj *bug.CreateOperation) graphql.Marshaler {
-	rctx := &graphql.ResolverContext{
-		Object: "CreateOperation",
-		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
+	arr1 := make(graphql.Array, len(res))
+	var wg sync.WaitGroup
+
+	isLen1 := len(res) == 1
+	if !isLen1 {
+		wg.Add(len(res))
 	}
-	res := resTmp.(string)
-	rctx.Result = res
-	return graphql.MarshalString(res)
-}
 
-// nolint: vetshadow
-func (ec *executionContext) _CreateOperation_files(ctx context.Context, field graphql.CollectedField, obj *bug.CreateOperation) graphql.Marshaler {
-	rctx := &graphql.ResolverContext{
-		Object: "CreateOperation",
-		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")
+	for idx1 := range res {
+		idx1 := idx1
+		rctx := &graphql.ResolverContext{
+			Index:  &idx1,
+			Result: &res[idx1],
 		}
-		return graphql.Null
-	}
-	res := resTmp.([]git.Hash)
-	rctx.Result = res
+		ctx := graphql.WithResolverContext(ctx, rctx)
+		f := func(idx1 int) {
+			if !isLen1 {
+				defer wg.Done()
+			}
+			arr1[idx1] = func() graphql.Marshaler {
 
-	arr1 := make(graphql.Array, len(res))
+				return ec._Comment(ctx, field.Selections, &res[idx1])
+			}()
+		}
+		if isLen1 {
+			f(idx1)
+		} else {
+			go f(idx1)
+		}
 
-	for idx1 := range res {
-		arr1[idx1] = func() graphql.Marshaler {
-			return res[idx1]
-		}()
 	}
-
+	wg.Wait()
 	return arr1
 }
 
-var labelChangeOperationImplementors = []string{"LabelChangeOperation", "Operation", "Authored"}
+var createOperationImplementors = []string{"CreateOperation", "Operation", "Authored"}
 
 // nolint: gocyclo, errcheck, gas, goconst
-func (ec *executionContext) _LabelChangeOperation(ctx context.Context, sel ast.SelectionSet, obj *bug.LabelChangeOperation) graphql.Marshaler {
-	fields := graphql.CollectFields(ctx, sel, labelChangeOperationImplementors)
+func (ec *executionContext) _CreateOperation(ctx context.Context, sel ast.SelectionSet, obj *bug.CreateOperation) graphql.Marshaler {
+	fields := graphql.CollectFields(ctx, sel, createOperationImplementors)
 
 	var wg sync.WaitGroup
 	out := graphql.NewOrderedMap(len(fields))
@@ -2763,11 +2970,11 @@ func (ec *executionContext) _LabelChangeOperation(ctx context.Context, sel ast.S
 
 		switch field.Name {
 		case "__typename":
-			out.Values[i] = graphql.MarshalString("LabelChangeOperation")
+			out.Values[i] = graphql.MarshalString("CreateOperation")
 		case "author":
 			wg.Add(1)
 			go func(i int, field graphql.CollectedField) {
-				out.Values[i] = ec._LabelChangeOperation_author(ctx, field, obj)
+				out.Values[i] = ec._CreateOperation_author(ctx, field, obj)
 				if out.Values[i] == graphql.Null {
 					invalid = true
 				}
@@ -2776,19 +2983,24 @@ func (ec *executionContext) _LabelChangeOperation(ctx context.Context, sel ast.S
 		case "date":
 			wg.Add(1)
 			go func(i int, field graphql.CollectedField) {
-				out.Values[i] = ec._LabelChangeOperation_date(ctx, field, obj)
+				out.Values[i] = ec._CreateOperation_date(ctx, field, obj)
 				if out.Values[i] == graphql.Null {
 					invalid = true
 				}
 				wg.Done()
 			}(i, field)
-		case "added":
-			out.Values[i] = ec._LabelChangeOperation_added(ctx, field, obj)
+		case "title":
+			out.Values[i] = ec._CreateOperation_title(ctx, field, obj)
 			if out.Values[i] == graphql.Null {
 				invalid = true
 			}
-		case "removed":
-			out.Values[i] = ec._LabelChangeOperation_removed(ctx, field, obj)
+		case "message":
+			out.Values[i] = ec._CreateOperation_message(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				invalid = true
+			}
+		case "files":
+			out.Values[i] = ec._CreateOperation_files(ctx, field, obj)
 			if out.Values[i] == graphql.Null {
 				invalid = true
 			}
@@ -2804,15 +3016,15 @@ func (ec *executionContext) _LabelChangeOperation(ctx context.Context, sel ast.S
 }
 
 // nolint: vetshadow
-func (ec *executionContext) _LabelChangeOperation_author(ctx context.Context, field graphql.CollectedField, obj *bug.LabelChangeOperation) graphql.Marshaler {
+func (ec *executionContext) _CreateOperation_author(ctx context.Context, field graphql.CollectedField, obj *bug.CreateOperation) graphql.Marshaler {
 	rctx := &graphql.ResolverContext{
-		Object: "LabelChangeOperation",
+		Object: "CreateOperation",
 		Args:   nil,
 		Field:  field,
 	}
 	ctx = graphql.WithResolverContext(ctx, rctx)
 	resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) {
-		return ec.resolvers.LabelChangeOperation().Author(ctx, obj)
+		return ec.resolvers.CreateOperation().Author(ctx, obj)
 	})
 	if resTmp == nil {
 		if !ec.HasError(rctx) {
@@ -2827,15 +3039,15 @@ func (ec *executionContext) _LabelChangeOperation_author(ctx context.Context, fi
 }
 
 // nolint: vetshadow
-func (ec *executionContext) _LabelChangeOperation_date(ctx context.Context, field graphql.CollectedField, obj *bug.LabelChangeOperation) graphql.Marshaler {
+func (ec *executionContext) _CreateOperation_date(ctx context.Context, field graphql.CollectedField, obj *bug.CreateOperation) graphql.Marshaler {
 	rctx := &graphql.ResolverContext{
-		Object: "LabelChangeOperation",
+		Object: "CreateOperation",
 		Args:   nil,
 		Field:  field,
 	}
 	ctx = graphql.WithResolverContext(ctx, rctx)
 	resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) {
-		return ec.resolvers.LabelChangeOperation().Date(ctx, obj)
+		return ec.resolvers.CreateOperation().Date(ctx, obj)
 	})
 	if resTmp == nil {
 		if !ec.HasError(rctx) {
@@ -2849,15 +3061,15 @@ func (ec *executionContext) _LabelChangeOperation_date(ctx context.Context, fiel
 }
 
 // nolint: vetshadow
-func (ec *executionContext) _LabelChangeOperation_added(ctx context.Context, field graphql.CollectedField, obj *bug.LabelChangeOperation) graphql.Marshaler {
+func (ec *executionContext) _CreateOperation_title(ctx context.Context, field graphql.CollectedField, obj *bug.CreateOperation) graphql.Marshaler {
 	rctx := &graphql.ResolverContext{
-		Object: "LabelChangeOperation",
+		Object: "CreateOperation",
 		Args:   nil,
 		Field:  field,
 	}
 	ctx = graphql.WithResolverContext(ctx, rctx)
 	resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) {
-		return obj.Added, nil
+		return obj.Title, nil
 	})
 	if resTmp == nil {
 		if !ec.HasError(rctx) {
@@ -2865,30 +3077,43 @@ func (ec *executionContext) _LabelChangeOperation_added(ctx context.Context, fie
 		}
 		return graphql.Null
 	}
-	res := resTmp.([]bug.Label)
+	res := resTmp.(string)
 	rctx.Result = res
+	return graphql.MarshalString(res)
+}
 
-	arr1 := make(graphql.Array, len(res))
-
-	for idx1 := range res {
-		arr1[idx1] = func() graphql.Marshaler {
-			return res[idx1]
-		}()
+// nolint: vetshadow
+func (ec *executionContext) _CreateOperation_message(ctx context.Context, field graphql.CollectedField, obj *bug.CreateOperation) graphql.Marshaler {
+	rctx := &graphql.ResolverContext{
+		Object: "CreateOperation",
+		Args:   nil,
+		Field:  field,
 	}
-
-	return arr1
+	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) _LabelChangeOperation_removed(ctx context.Context, field graphql.CollectedField, obj *bug.LabelChangeOperation) graphql.Marshaler {
+func (ec *executionContext) _CreateOperation_files(ctx context.Context, field graphql.CollectedField, obj *bug.CreateOperation) graphql.Marshaler {
 	rctx := &graphql.ResolverContext{
-		Object: "LabelChangeOperation",
+		Object: "CreateOperation",
 		Args:   nil,
 		Field:  field,
 	}
 	ctx = graphql.WithResolverContext(ctx, rctx)
 	resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) {
-		return obj.Removed, nil
+		return obj.Files, nil
 	})
 	if resTmp == nil {
 		if !ec.HasError(rctx) {
@@ -2896,7 +3121,7 @@ func (ec *executionContext) _LabelChangeOperation_removed(ctx context.Context, f
 		}
 		return graphql.Null
 	}
-	res := resTmp.([]bug.Label)
+	res := resTmp.([]git.Hash)
 	rctx.Result = res
 
 	arr1 := make(graphql.Array, len(res))
@@ -2910,15 +3135,11 @@ func (ec *executionContext) _LabelChangeOperation_removed(ctx context.Context, f
 	return arr1
 }
 
-var mutationImplementors = []string{"Mutation"}
+var createTimelineItemImplementors = []string{"CreateTimelineItem", "TimelineItem"}
 
 // nolint: gocyclo, errcheck, gas, goconst
-func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler {
-	fields := graphql.CollectFields(ctx, sel, mutationImplementors)
-
-	ctx = graphql.WithResolverContext(ctx, &graphql.ResolverContext{
-		Object: "Mutation",
-	})
+func (ec *executionContext) _CreateTimelineItem(ctx context.Context, sel ast.SelectionSet, obj *bug.CreateTimelineItem) graphql.Marshaler {
+	fields := graphql.CollectFields(ctx, sel, createTimelineItemImplementors)
 
 	out := graphql.NewOrderedMap(len(fields))
 	invalid := false
@@ -2927,39 +3148,19 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
 
 		switch field.Name {
 		case "__typename":
-			out.Values[i] = graphql.MarshalString("Mutation")
-		case "newBug":
-			out.Values[i] = ec._Mutation_newBug(ctx, field)
-			if out.Values[i] == graphql.Null {
-				invalid = true
-			}
-		case "addComment":
-			out.Values[i] = ec._Mutation_addComment(ctx, field)
-			if out.Values[i] == graphql.Null {
-				invalid = true
-			}
-		case "changeLabels":
-			out.Values[i] = ec._Mutation_changeLabels(ctx, field)
-			if out.Values[i] == graphql.Null {
-				invalid = true
-			}
-		case "open":
-			out.Values[i] = ec._Mutation_open(ctx, field)
-			if out.Values[i] == graphql.Null {
-				invalid = true
-			}
-		case "close":
-			out.Values[i] = ec._Mutation_close(ctx, field)
+			out.Values[i] = graphql.MarshalString("CreateTimelineItem")
+		case "hash":
+			out.Values[i] = ec._CreateTimelineItem_hash(ctx, field, obj)
 			if out.Values[i] == graphql.Null {
 				invalid = true
 			}
-		case "setTitle":
-			out.Values[i] = ec._Mutation_setTitle(ctx, field)
+		case "lastState":
+			out.Values[i] = ec._CreateTimelineItem_lastState(ctx, field, obj)
 			if out.Values[i] == graphql.Null {
 				invalid = true
 			}
-		case "commit":
-			out.Values[i] = ec._Mutation_commit(ctx, field)
+		case "history":
+			out.Values[i] = ec._CreateTimelineItem_history(ctx, field, obj)
 			if out.Values[i] == graphql.Null {
 				invalid = true
 			}
@@ -2975,21 +3176,15 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
 }
 
 // nolint: vetshadow
-func (ec *executionContext) _Mutation_newBug(ctx context.Context, field graphql.CollectedField) graphql.Marshaler {
-	rawArgs := field.ArgumentMap(ec.Variables)
-	args, err := field_Mutation_newBug_args(rawArgs)
-	if err != nil {
-		ec.Error(ctx, err)
-		return graphql.Null
-	}
+func (ec *executionContext) _CreateTimelineItem_hash(ctx context.Context, field graphql.CollectedField, obj *bug.CreateTimelineItem) graphql.Marshaler {
 	rctx := &graphql.ResolverContext{
-		Object: "Mutation",
-		Args:   args,
+		Object: "CreateTimelineItem",
+		Args:   nil,
 		Field:  field,
 	}
 	ctx = graphql.WithResolverContext(ctx, rctx)
-	resTmp := ec.FieldMiddleware(ctx, nil, func(ctx context.Context) (interface{}, error) {
-		return ec.resolvers.Mutation().NewBug(ctx, args["repoRef"].(*string), args["title"].(string), args["message"].(string), args["files"].([]git.Hash))
+	resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) {
+		return obj.Hash()
 	})
 	if resTmp == nil {
 		if !ec.HasError(rctx) {
@@ -2997,28 +3192,21 @@ func (ec *executionContext) _Mutation_newBug(ctx context.Context, field graphql.
 		}
 		return graphql.Null
 	}
-	res := resTmp.(bug.Snapshot)
+	res := resTmp.(git.Hash)
 	rctx.Result = res
-
-	return ec._Bug(ctx, field.Selections, &res)
+	return res
 }
 
 // nolint: vetshadow
-func (ec *executionContext) _Mutation_addComment(ctx context.Context, field graphql.CollectedField) graphql.Marshaler {
-	rawArgs := field.ArgumentMap(ec.Variables)
-	args, err := field_Mutation_addComment_args(rawArgs)
-	if err != nil {
-		ec.Error(ctx, err)
-		return graphql.Null
-	}
+func (ec *executionContext) _CreateTimelineItem_lastState(ctx context.Context, field graphql.CollectedField, obj *bug.CreateTimelineItem) graphql.Marshaler {
 	rctx := &graphql.ResolverContext{
-		Object: "Mutation",
-		Args:   args,
+		Object: "CreateTimelineItem",
+		Args:   nil,
 		Field:  field,
 	}
 	ctx = graphql.WithResolverContext(ctx, rctx)
-	resTmp := ec.FieldMiddleware(ctx, nil, func(ctx context.Context) (interface{}, error) {
-		return ec.resolvers.Mutation().AddComment(ctx, args["repoRef"].(*string), args["prefix"].(string), args["message"].(string), args["files"].([]git.Hash))
+	resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) {
+		return obj.LastState(), nil
 	})
 	if resTmp == nil {
 		if !ec.HasError(rctx) {
@@ -3026,28 +3214,22 @@ func (ec *executionContext) _Mutation_addComment(ctx context.Context, field grap
 		}
 		return graphql.Null
 	}
-	res := resTmp.(bug.Snapshot)
+	res := resTmp.(bug.Comment)
 	rctx.Result = res
 
-	return ec._Bug(ctx, field.Selections, &res)
+	return ec._Comment(ctx, field.Selections, &res)
 }
 
 // nolint: vetshadow
-func (ec *executionContext) _Mutation_changeLabels(ctx context.Context, field graphql.CollectedField) graphql.Marshaler {
-	rawArgs := field.ArgumentMap(ec.Variables)
-	args, err := field_Mutation_changeLabels_args(rawArgs)
-	if err != nil {
-		ec.Error(ctx, err)
-		return graphql.Null
-	}
+func (ec *executionContext) _CreateTimelineItem_history(ctx context.Context, field graphql.CollectedField, obj *bug.CreateTimelineItem) graphql.Marshaler {
 	rctx := &graphql.ResolverContext{
-		Object: "Mutation",
-		Args:   args,
+		Object: "CreateTimelineItem",
+		Args:   nil,
 		Field:  field,
 	}
 	ctx = graphql.WithResolverContext(ctx, rctx)
-	resTmp := ec.FieldMiddleware(ctx, nil, func(ctx context.Context) (interface{}, error) {
-		return ec.resolvers.Mutation().ChangeLabels(ctx, args["repoRef"].(*string), args["prefix"].(string), args["added"].([]string), args["removed"].([]string))
+	resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) {
+		return obj.History, nil
 	})
 	if resTmp == nil {
 		if !ec.HasError(rctx) {
@@ -3055,47 +3237,418 @@ func (ec *executionContext) _Mutation_changeLabels(ctx context.Context, field gr
 		}
 		return graphql.Null
 	}
-	res := resTmp.(bug.Snapshot)
+	res := resTmp.([]bug.Comment)
 	rctx.Result = res
 
-	return ec._Bug(ctx, field.Selections, &res)
-}
+	arr1 := make(graphql.Array, len(res))
+	var wg sync.WaitGroup
 
-// nolint: vetshadow
-func (ec *executionContext) _Mutation_open(ctx context.Context, field graphql.CollectedField) graphql.Marshaler {
-	rawArgs := field.ArgumentMap(ec.Variables)
-	args, err := field_Mutation_open_args(rawArgs)
-	if err != nil {
-		ec.Error(ctx, err)
-		return graphql.Null
-	}
-	rctx := &graphql.ResolverContext{
-		Object: "Mutation",
-		Args:   args,
-		Field:  field,
-	}
-	ctx = graphql.WithResolverContext(ctx, rctx)
-	resTmp := ec.FieldMiddleware(ctx, nil, func(ctx context.Context) (interface{}, error) {
-		return ec.resolvers.Mutation().Open(ctx, args["repoRef"].(*string), args["prefix"].(string))
-	})
-	if resTmp == nil {
-		if !ec.HasError(rctx) {
-			ec.Errorf(ctx, "must not be null")
-		}
-		return graphql.Null
+	isLen1 := len(res) == 1
+	if !isLen1 {
+		wg.Add(len(res))
 	}
-	res := resTmp.(bug.Snapshot)
-	rctx.Result = res
 
-	return ec._Bug(ctx, field.Selections, &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 {
 
-// nolint: vetshadow
-func (ec *executionContext) _Mutation_close(ctx context.Context, field graphql.CollectedField) graphql.Marshaler {
-	rawArgs := field.ArgumentMap(ec.Variables)
-	args, err := field_Mutation_close_args(rawArgs)
-	if err != nil {
-		ec.Error(ctx, err)
+				return ec._Comment(ctx, field.Selections, &res[idx1])
+			}()
+		}
+		if isLen1 {
+			f(idx1)
+		} else {
+			go f(idx1)
+		}
+
+	}
+	wg.Wait()
+	return arr1
+}
+
+var labelChangeOperationImplementors = []string{"LabelChangeOperation", "Operation", "Authored", "TimelineItem"}
+
+// nolint: gocyclo, errcheck, gas, goconst
+func (ec *executionContext) _LabelChangeOperation(ctx context.Context, sel ast.SelectionSet, obj *bug.LabelChangeOperation) graphql.Marshaler {
+	fields := graphql.CollectFields(ctx, sel, labelChangeOperationImplementors)
+
+	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("LabelChangeOperation")
+		case "hash":
+			out.Values[i] = ec._LabelChangeOperation_hash(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				invalid = true
+			}
+		case "author":
+			wg.Add(1)
+			go func(i int, field graphql.CollectedField) {
+				out.Values[i] = ec._LabelChangeOperation_author(ctx, field, obj)
+				if out.Values[i] == graphql.Null {
+					invalid = true
+				}
+				wg.Done()
+			}(i, field)
+		case "date":
+			wg.Add(1)
+			go func(i int, field graphql.CollectedField) {
+				out.Values[i] = ec._LabelChangeOperation_date(ctx, field, obj)
+				if out.Values[i] == graphql.Null {
+					invalid = true
+				}
+				wg.Done()
+			}(i, field)
+		case "added":
+			out.Values[i] = ec._LabelChangeOperation_added(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				invalid = true
+			}
+		case "removed":
+			out.Values[i] = ec._LabelChangeOperation_removed(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) _LabelChangeOperation_hash(ctx context.Context, field graphql.CollectedField, obj *bug.LabelChangeOperation) graphql.Marshaler {
+	rctx := &graphql.ResolverContext{
+		Object: "LabelChangeOperation",
+		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) _LabelChangeOperation_author(ctx context.Context, field graphql.CollectedField, obj *bug.LabelChangeOperation) graphql.Marshaler {
+	rctx := &graphql.ResolverContext{
+		Object: "LabelChangeOperation",
+		Args:   nil,
+		Field:  field,
+	}
+	ctx = graphql.WithResolverContext(ctx, rctx)
+	resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) {
+		return ec.resolvers.LabelChangeOperation().Author(ctx, obj)
+	})
+	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) _LabelChangeOperation_date(ctx context.Context, field graphql.CollectedField, obj *bug.LabelChangeOperation) graphql.Marshaler {
+	rctx := &graphql.ResolverContext{
+		Object: "LabelChangeOperation",
+		Args:   nil,
+		Field:  field,
+	}
+	ctx = graphql.WithResolverContext(ctx, rctx)
+	resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) {
+		return ec.resolvers.LabelChangeOperation().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)
+}
+
+// nolint: vetshadow
+func (ec *executionContext) _LabelChangeOperation_added(ctx context.Context, field graphql.CollectedField, obj *bug.LabelChangeOperation) graphql.Marshaler {
+	rctx := &graphql.ResolverContext{
+		Object: "LabelChangeOperation",
+		Args:   nil,
+		Field:  field,
+	}
+	ctx = graphql.WithResolverContext(ctx, rctx)
+	resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) {
+		return obj.Added, nil
+	})
+	if resTmp == nil {
+		if !ec.HasError(rctx) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.([]bug.Label)
+	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) _LabelChangeOperation_removed(ctx context.Context, field graphql.CollectedField, obj *bug.LabelChangeOperation) graphql.Marshaler {
+	rctx := &graphql.ResolverContext{
+		Object: "LabelChangeOperation",
+		Args:   nil,
+		Field:  field,
+	}
+	ctx = graphql.WithResolverContext(ctx, rctx)
+	resTmp := ec.FieldMiddleware(ctx, obj, func(ctx context.Context) (interface{}, error) {
+		return obj.Removed, nil
+	})
+	if resTmp == nil {
+		if !ec.HasError(rctx) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.([]bug.Label)
+	rctx.Result = res
+
+	arr1 := make(graphql.Array, len(res))
+
+	for idx1 := range res {
+		arr1[idx1] = func() graphql.Marshaler {
+			return res[idx1]
+		}()
+	}
+
+	return arr1
+}
+
+var mutationImplementors = []string{"Mutation"}
+
+// nolint: gocyclo, errcheck, gas, goconst
+func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler {
+	fields := graphql.CollectFields(ctx, sel, mutationImplementors)
+
+	ctx = graphql.WithResolverContext(ctx, &graphql.ResolverContext{
+		Object: "Mutation",
+	})
+
+	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("Mutation")
+		case "newBug":
+			out.Values[i] = ec._Mutation_newBug(ctx, field)
+			if out.Values[i] == graphql.Null {
+				invalid = true
+			}
+		case "addComment":
+			out.Values[i] = ec._Mutation_addComment(ctx, field)
+			if out.Values[i] == graphql.Null {
+				invalid = true
+			}
+		case "changeLabels":
+			out.Values[i] = ec._Mutation_changeLabels(ctx, field)
+			if out.Values[i] == graphql.Null {
+				invalid = true
+			}
+		case "open":
+			out.Values[i] = ec._Mutation_open(ctx, field)
+			if out.Values[i] == graphql.Null {
+				invalid = true
+			}
+		case "close":
+			out.Values[i] = ec._Mutation_close(ctx, field)
+			if out.Values[i] == graphql.Null {
+				invalid = true
+			}
+		case "setTitle":
+			out.Values[i] = ec._Mutation_setTitle(ctx, field)
+			if out.Values[i] == graphql.Null {
+				invalid = true
+			}
+		case "commit":
+			out.Values[i] = ec._Mutation_commit(ctx, field)
+			if out.Values[i] == graphql.Null {
+				invalid = true
+			}
+		default:
+			panic("unknown field " + strconv.Quote(field.Name))
+		}
+	}
+
+	if invalid {
+		return graphql.Null
+	}
+	return out
+}
+
+// nolint: vetshadow
+func (ec *executionContext) _Mutation_newBug(ctx context.Context, field graphql.CollectedField) graphql.Marshaler {
+	rawArgs := field.ArgumentMap(ec.Variables)
+	args, err := field_Mutation_newBug_args(rawArgs)
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	rctx := &graphql.ResolverContext{
+		Object: "Mutation",
+		Args:   args,
+		Field:  field,
+	}
+	ctx = graphql.WithResolverContext(ctx, rctx)
+	resTmp := ec.FieldMiddleware(ctx, nil, func(ctx context.Context) (interface{}, error) {
+		return ec.resolvers.Mutation().NewBug(ctx, args["repoRef"].(*string), args["title"].(string), args["message"].(string), args["files"].([]git.Hash))
+	})
+	if resTmp == nil {
+		if !ec.HasError(rctx) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(bug.Snapshot)
+	rctx.Result = res
+
+	return ec._Bug(ctx, field.Selections, &res)
+}
+
+// nolint: vetshadow
+func (ec *executionContext) _Mutation_addComment(ctx context.Context, field graphql.CollectedField) graphql.Marshaler {
+	rawArgs := field.ArgumentMap(ec.Variables)
+	args, err := field_Mutation_addComment_args(rawArgs)
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	rctx := &graphql.ResolverContext{
+		Object: "Mutation",
+		Args:   args,
+		Field:  field,
+	}
+	ctx = graphql.WithResolverContext(ctx, rctx)
+	resTmp := ec.FieldMiddleware(ctx, nil, func(ctx context.Context) (interface{}, error) {
+		return ec.resolvers.Mutation().AddComment(ctx, args["repoRef"].(*string), args["prefix"].(string), args["message"].(string), args["files"].([]git.Hash))
+	})
+	if resTmp == nil {
+		if !ec.HasError(rctx) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(bug.Snapshot)
+	rctx.Result = res
+
+	return ec._Bug(ctx, field.Selections, &res)
+}
+
+// nolint: vetshadow
+func (ec *executionContext) _Mutation_changeLabels(ctx context.Context, field graphql.CollectedField) graphql.Marshaler {
+	rawArgs := field.ArgumentMap(ec.Variables)
+	args, err := field_Mutation_changeLabels_args(rawArgs)
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	rctx := &graphql.ResolverContext{
+		Object: "Mutation",
+		Args:   args,
+		Field:  field,
+	}
+	ctx = graphql.WithResolverContext(ctx, rctx)
+	resTmp := ec.FieldMiddleware(ctx, nil, func(ctx context.Context) (interface{}, error) {
+		return ec.resolvers.Mutation().ChangeLabels(ctx, args["repoRef"].(*string), args["prefix"].(string), args["added"].([]string), args["removed"].([]string))
+	})
+	if resTmp == nil {
+		if !ec.HasError(rctx) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(bug.Snapshot)
+	rctx.Result = res
+
+	return ec._Bug(ctx, field.Selections, &res)
+}
+
+// nolint: vetshadow
+func (ec *executionContext) _Mutation_open(ctx context.Context, field graphql.CollectedField) graphql.Marshaler {
+	rawArgs := field.ArgumentMap(ec.Variables)
+	args, err := field_Mutation_open_args(rawArgs)
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	rctx := &graphql.ResolverContext{
+		Object: "Mutation",
+		Args:   args,
+		Field:  field,
+	}
+	ctx = graphql.WithResolverContext(ctx, rctx)
+	resTmp := ec.FieldMiddleware(ctx, nil, func(ctx context.Context) (interface{}, error) {
+		return ec.resolvers.Mutation().Open(ctx, args["repoRef"].(*string), args["prefix"].(string))
+	})
+	if resTmp == nil {
+		if !ec.HasError(rctx) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(bug.Snapshot)
+	rctx.Result = res
+
+	return ec._Bug(ctx, field.Selections, &res)
+}
+
+// nolint: vetshadow
+func (ec *executionContext) _Mutation_close(ctx context.Context, field graphql.CollectedField) graphql.Marshaler {
+	rawArgs := field.ArgumentMap(ec.Variables)
+	args, err := field_Mutation_close_args(rawArgs)
+	if err != nil {
+		ec.Error(ctx, err)
 		return graphql.Null
 	}
 	rctx := &graphql.ResolverContext{

graphql/models/edges.go 🔗

@@ -14,3 +14,8 @@ func (e BugEdge) GetCursor() string {
 func (e CommentEdge) GetCursor() string {
 	return e.Cursor
 }
+
+// GetCursor return the cursor entry of an edge
+func (e TimelineItemEdge) GetCursor() string {
+	return e.Cursor
+}

graphql/models/gen_models.go 🔗

@@ -59,6 +59,18 @@ type PageInfo struct {
 	EndCursor       string `json:"endCursor"`
 }
 
+type TimelineItemConnection struct {
+	Edges      []TimelineItemEdge `json:"edges"`
+	Nodes      []bug.TimelineItem `json:"nodes"`
+	PageInfo   PageInfo           `json:"pageInfo"`
+	TotalCount int                `json:"totalCount"`
+}
+
+type TimelineItemEdge struct {
+	Cursor string           `json:"cursor"`
+	Node   bug.TimelineItem `json:"node"`
+}
+
 type Status string
 
 const (

graphql/resolvers/bug.go 🔗

@@ -69,6 +69,33 @@ func (bugResolver) Operations(ctx context.Context, obj *bug.Snapshot, after *str
 	return connections.BugOperationCon(obj.Operations, edger, conMaker, input)
 }
 
+func (bugResolver) Timeline(ctx context.Context, obj *bug.Snapshot, after *string, before *string, first *int, last *int) (models.TimelineItemConnection, error) {
+	input := models.ConnectionInput{
+		Before: before,
+		After:  after,
+		First:  first,
+		Last:   last,
+	}
+
+	edger := func(op bug.TimelineItem, offset int) connections.Edge {
+		return models.TimelineItemEdge{
+			Node:   op,
+			Cursor: connections.OffsetToCursor(offset),
+		}
+	}
+
+	conMaker := func(edges []models.TimelineItemEdge, nodes []bug.TimelineItem, info models.PageInfo, totalCount int) (models.TimelineItemConnection, error) {
+		return models.TimelineItemConnection{
+			Edges:      edges,
+			Nodes:      nodes,
+			PageInfo:   info,
+			TotalCount: totalCount,
+		}, nil
+	}
+
+	return connections.BugTimelineItemCon(obj.Timeline, edger, conMaker, input)
+}
+
 func (bugResolver) LastEdit(ctx context.Context, obj *bug.Snapshot) (time.Time, error) {
 	return obj.LastEditTime(), nil
 }

graphql/schema.graphql 🔗

@@ -73,6 +73,11 @@ type OperationEdge {
   node: Operation!
 }
 
+"""An item in the timeline of events"""
+interface TimelineItem {
+  hash: Hash!
+}
+
 """An operation applied to a bug."""
 interface Operation {
   """The operations author."""
@@ -90,7 +95,8 @@ type CreateOperation implements Operation & Authored {
   files: [Hash!]!
 }
 
-type SetTitleOperation implements Operation & Authored {
+type SetTitleOperation implements Operation & Authored & TimelineItem {
+  hash: Hash!
   author: Person!
   date: Time!
 
@@ -106,14 +112,16 @@ type AddCommentOperation implements Operation & Authored {
   files: [Hash!]!
 }
 
-type SetStatusOperation implements Operation & Authored {
+type SetStatusOperation implements Operation & Authored & TimelineItem {
+  hash: Hash!
   author: Person!
   date: Time!
 
   status: Status!
 }
 
-type LabelChangeOperation implements Operation & Authored {
+type LabelChangeOperation implements Operation & Authored & TimelineItem {
+  hash: Hash!
   author: Person!
   date: Time!
 
@@ -121,6 +129,30 @@ type LabelChangeOperation implements Operation & Authored {
   removed: [Label!]!
 }
 
+type TimelineItemConnection {
+  edges: [TimelineItemEdge!]!
+  nodes: [TimelineItem!]!
+  pageInfo: PageInfo!
+  totalCount: Int!
+}
+
+type TimelineItemEdge {
+  cursor: String!
+  node: TimelineItem!
+}
+
+type CreateTimelineItem implements TimelineItem {
+  hash: Hash!
+  lastState: Comment!
+  history: [Comment!]!
+}
+
+type CommentTimelineItem implements TimelineItem {
+  hash: Hash!
+  lastState: Comment!
+  history: [Comment!]!
+}
+
 """The connection type for Bug."""
 type BugConnection {
   """A list of edges."""
@@ -161,6 +193,17 @@ type Bug {
     last: Int
   ): CommentConnection!
 
+  timeline(
+    """Returns the elements in the list that come after the specified cursor."""
+    after: String
+    """Returns the elements in the list that come before the specified cursor."""
+    before: String
+    """Returns the first _n_ elements from the list."""
+    first: Int
+    """Returns the last _n_ elements from the list."""
+    last: Int
+  ): TimelineItemConnection!
+
   operations(
     """Returns the elements in the list that come after the specified cursor."""
     after: String