bug: have a type for combined ids, fix https://github.com/MichaelMure/git-bug/issues/653

Michael MurĂŠ created

Change summary

api/graphql/gen_graphql.go                |  33 ----
api/graphql/gqlgen.yml                    |   5 
api/graphql/graph/bug.generated.go        | 102 ++++++++++--
api/graphql/graph/identity.generated.go   |  33 +--
api/graphql/graph/mutations.generated.go  |  16 -
api/graphql/graph/operations.generated.go | 193 +++++++-----------------
api/graphql/graph/prelude.generated.go    |  11 +
api/graphql/graph/root_.generated.go      |  97 ++++++-----
api/graphql/graph/timeline.generated.go   |  41 ++--
api/graphql/graph/types.generated.go      |  17 +
api/graphql/handler.go                    |   2 
api/graphql/models/gen_models.go          |   6 
api/graphql/resolvers/bug.go              |   4 
api/graphql/resolvers/comment.go          |   5 
api/graphql/resolvers/mutation.go         |  10 
api/graphql/resolvers/operations.go       |  24 ---
api/graphql/resolvers/timeline.go         |  21 +-
api/graphql/schema/bug.graphql            |   4 
api/graphql/schema/identity.graphql       |   2 
api/graphql/schema/mutations.graphql      |   6 
api/graphql/schema/operations.graphql     |  14 
api/graphql/schema/timeline.graphql       |  12 
api/graphql/schema/types.graphql          |   1 
api/graphql/tools.go                      |   8 +
api/http/git_file_handler.go              |   2 
bridge/github/export_test.go              |   7 
bridge/github/import.go                   |   3 
bridge/gitlab/export_test.go              |   7 
bridge/gitlab/import.go                   |  13 
bridge/jira/import.go                     |   5 
cache/bug_cache.go                        |  11 +
cache/repo_cache_bug.go                   |  14 
commands/comment.go                       |   2 
commands/show.go                          |   6 
entities/bug/comment.go                   |  29 ++-
entities/bug/op_add_comment.go            |  15 +
entities/bug/op_create.go                 |  13 +
entities/bug/op_create_test.go            |  56 ++----
entities/bug/op_edit_comment.go           |  17 +-
entities/bug/op_label_change.go           |  31 ++-
entities/bug/op_set_status.go             |  25 +-
entities/bug/op_set_title.go              |  29 ++-
entities/bug/snapshot.go                  |  23 ++
entities/bug/timeline.go                  |  42 ++--
entity/id.go                              |  18 --
entity/id_interleaved.go                  |  84 ++++++++++
entity/id_interleaved_test.go             |   2 
go.mod                                    |   2 
go.sum                                    |   2 
repository/gogit.go                       |   1 
termui/show_bug.go                        |   8 
termui/termui.go                          |   2 
52 files changed, 583 insertions(+), 553 deletions(-)

Detailed changes

api/graphql/gen_graphql.go 🔗

@@ -1,33 +0,0 @@
-//go:build ignore
-
-package main
-
-import (
-	"fmt"
-	"io/ioutil"
-	"log"
-	"os"
-
-	"github.com/99designs/gqlgen/api"
-	"github.com/99designs/gqlgen/codegen/config"
-	"github.com/pkg/errors"
-)
-
-func main() {
-	fmt.Println("Generating graphql code ...")
-
-	log.SetOutput(ioutil.Discard)
-
-	cfg, err := config.LoadConfigFromDefaultLocations()
-	if os.IsNotExist(errors.Cause(err)) {
-		cfg = config.DefaultConfig()
-	} else if err != nil {
-		_, _ = fmt.Fprintln(os.Stderr, err.Error())
-		os.Exit(2)
-	}
-
-	if err = api.Generate(cfg); err != nil {
-		_, _ = fmt.Fprintln(os.Stderr, err.Error())
-		os.Exit(3)
-	}
-}

api/graphql/gqlgen.yml 🔗

@@ -6,17 +6,18 @@ exec:
 model:
   filename: models/gen_models.go
 
-skip_mod_tidy: true
-
 autobind:
   - "github.com/MichaelMure/git-bug/api/graphql/models"
   - "github.com/MichaelMure/git-bug/repository"
+  - "github.com/MichaelMure/git-bug/entity"
   - "github.com/MichaelMure/git-bug/entity/dag"
   - "github.com/MichaelMure/git-bug/entities/common"
   - "github.com/MichaelMure/git-bug/entities/bug"
   - "github.com/MichaelMure/git-bug/entities/identity"
 
 models:
+  ID:
+    model: github.com/MichaelMure/git-bug/entity.Id
   Bug:
     model: github.com/MichaelMure/git-bug/api/graphql/models.BugWrapper
   Color:

api/graphql/graph/bug.generated.go 🔗

@@ -15,6 +15,7 @@ import (
 	"github.com/MichaelMure/git-bug/api/graphql/models"
 	"github.com/MichaelMure/git-bug/entities/bug"
 	"github.com/MichaelMure/git-bug/entities/common"
+	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/repository"
 	"github.com/vektah/gqlparser/v2/ast"
 )
@@ -22,7 +23,6 @@ import (
 // region    ************************** generated!.gotpl **************************
 
 type BugResolver interface {
-	ID(ctx context.Context, obj models.BugWrapper) (string, error)
 	HumanID(ctx context.Context, obj models.BugWrapper) (string, error)
 
 	Actors(ctx context.Context, obj models.BugWrapper, after *string, before *string, first *int, last *int) (*models.IdentityConnection, error)
@@ -32,6 +32,7 @@ type BugResolver interface {
 	Operations(ctx context.Context, obj models.BugWrapper, after *string, before *string, first *int, last *int) (*models.OperationConnection, error)
 }
 type CommentResolver interface {
+	ID(ctx context.Context, obj *bug.Comment) (entity.CombinedId, error)
 	Author(ctx context.Context, obj *bug.Comment) (models.IdentityWrapper, error)
 }
 
@@ -271,7 +272,7 @@ func (ec *executionContext) _Bug_id(ctx context.Context, field graphql.Collected
 	}()
 	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
 		ctx = rctx // use context from middleware stack in children
-		return ec.resolvers.Bug().ID(rctx, obj)
+		return obj.Id(), nil
 	})
 	if err != nil {
 		ec.Error(ctx, err)
@@ -283,9 +284,9 @@ func (ec *executionContext) _Bug_id(ctx context.Context, field graphql.Collected
 		}
 		return graphql.Null
 	}
-	res := resTmp.(string)
+	res := resTmp.(entity.Id)
 	fc.Result = res
-	return ec.marshalNString2string(ctx, field.Selections, res)
+	return ec.marshalNID2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋentityᚐId(ctx, field.Selections, res)
 }
 
 func (ec *executionContext) fieldContext_Bug_id(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
@@ -293,9 +294,9 @@ func (ec *executionContext) fieldContext_Bug_id(ctx context.Context, field graph
 		Object:     "Bug",
 		Field:      field,
 		IsMethod:   true,
-		IsResolver: true,
+		IsResolver: false,
 		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
-			return nil, errors.New("field of type String does not have child fields")
+			return nil, errors.New("field of type ID does not have child fields")
 		},
 	}
 	return fc, nil
@@ -1294,6 +1295,50 @@ func (ec *executionContext) fieldContext_BugEdge_node(ctx context.Context, field
 	return fc, nil
 }
 
+func (ec *executionContext) _Comment_id(ctx context.Context, field graphql.CollectedField, obj *bug.Comment) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_Comment_id(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return ec.resolvers.Comment().ID(rctx, obj)
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(entity.CombinedId)
+	fc.Result = res
+	return ec.marshalNCombinedId2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋentityᚐCombinedId(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Comment_id(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "Comment",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: true,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type CombinedId does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
 func (ec *executionContext) _Comment_author(ctx context.Context, field graphql.CollectedField, obj *bug.Comment) (ret graphql.Marshaler) {
 	fc, err := ec.fieldContext_Comment_author(ctx, field)
 	if err != nil {
@@ -1533,6 +1578,8 @@ func (ec *executionContext) fieldContext_CommentConnection_nodes(ctx context.Con
 		IsResolver: false,
 		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
 			switch field.Name {
+			case "id":
+				return ec.fieldContext_Comment_id(ctx, field)
 			case "author":
 				return ec.fieldContext_Comment_author(ctx, field)
 			case "message":
@@ -1727,6 +1774,8 @@ func (ec *executionContext) fieldContext_CommentEdge_node(ctx context.Context, f
 		IsResolver: false,
 		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
 			switch field.Name {
+			case "id":
+				return ec.fieldContext_Comment_id(ctx, field)
 			case "author":
 				return ec.fieldContext_Comment_author(ctx, field)
 			case "message":
@@ -1763,25 +1812,12 @@ func (ec *executionContext) _Bug(ctx context.Context, sel ast.SelectionSet, obj
 		case "__typename":
 			out.Values[i] = graphql.MarshalString("Bug")
 		case "id":
-			field := field
 
-			innerFunc := func(ctx context.Context) (res graphql.Marshaler) {
-				defer func() {
-					if r := recover(); r != nil {
-						ec.Error(ctx, ec.Recover(ctx, r))
-					}
-				}()
-				res = ec._Bug_id(ctx, field, obj)
-				if res == graphql.Null {
-					atomic.AddUint32(&invalids, 1)
-				}
-				return res
-			}
-
-			out.Concurrently(i, func() graphql.Marshaler {
-				return innerFunc(ctx)
+			out.Values[i] = ec._Bug_id(ctx, field, obj)
 
-			})
+			if out.Values[i] == graphql.Null {
+				atomic.AddUint32(&invalids, 1)
+			}
 		case "humanId":
 			field := field
 
@@ -2049,6 +2085,26 @@ func (ec *executionContext) _Comment(ctx context.Context, sel ast.SelectionSet,
 		switch field.Name {
 		case "__typename":
 			out.Values[i] = graphql.MarshalString("Comment")
+		case "id":
+			field := field
+
+			innerFunc := func(ctx context.Context) (res graphql.Marshaler) {
+				defer func() {
+					if r := recover(); r != nil {
+						ec.Error(ctx, ec.Recover(ctx, r))
+					}
+				}()
+				res = ec._Comment_id(ctx, field, obj)
+				if res == graphql.Null {
+					atomic.AddUint32(&invalids, 1)
+				}
+				return res
+			}
+
+			out.Concurrently(i, func() graphql.Marshaler {
+				return innerFunc(ctx)
+
+			})
 		case "author":
 			field := field
 

api/graphql/graph/identity.generated.go 🔗

@@ -12,13 +12,13 @@ import (
 
 	"github.com/99designs/gqlgen/graphql"
 	"github.com/MichaelMure/git-bug/api/graphql/models"
+	"github.com/MichaelMure/git-bug/entity"
 	"github.com/vektah/gqlparser/v2/ast"
 )
 
 // region    ************************** generated!.gotpl **************************
 
 type IdentityResolver interface {
-	ID(ctx context.Context, obj models.IdentityWrapper) (string, error)
 	HumanID(ctx context.Context, obj models.IdentityWrapper) (string, error)
 }
 
@@ -48,7 +48,7 @@ func (ec *executionContext) _Identity_id(ctx context.Context, field graphql.Coll
 	}()
 	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
 		ctx = rctx // use context from middleware stack in children
-		return ec.resolvers.Identity().ID(rctx, obj)
+		return obj.Id(), nil
 	})
 	if err != nil {
 		ec.Error(ctx, err)
@@ -60,9 +60,9 @@ func (ec *executionContext) _Identity_id(ctx context.Context, field graphql.Coll
 		}
 		return graphql.Null
 	}
-	res := resTmp.(string)
+	res := resTmp.(entity.Id)
 	fc.Result = res
-	return ec.marshalNString2string(ctx, field.Selections, res)
+	return ec.marshalNID2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋentityᚐId(ctx, field.Selections, res)
 }
 
 func (ec *executionContext) fieldContext_Identity_id(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
@@ -70,9 +70,9 @@ func (ec *executionContext) fieldContext_Identity_id(ctx context.Context, field
 		Object:     "Identity",
 		Field:      field,
 		IsMethod:   true,
-		IsResolver: true,
+		IsResolver: false,
 		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
-			return nil, errors.New("field of type String does not have child fields")
+			return nil, errors.New("field of type ID does not have child fields")
 		},
 	}
 	return fc, nil
@@ -713,25 +713,12 @@ func (ec *executionContext) _Identity(ctx context.Context, sel ast.SelectionSet,
 		case "__typename":
 			out.Values[i] = graphql.MarshalString("Identity")
 		case "id":
-			field := field
-
-			innerFunc := func(ctx context.Context) (res graphql.Marshaler) {
-				defer func() {
-					if r := recover(); r != nil {
-						ec.Error(ctx, ec.Recover(ctx, r))
-					}
-				}()
-				res = ec._Identity_id(ctx, field, obj)
-				if res == graphql.Null {
-					atomic.AddUint32(&invalids, 1)
-				}
-				return res
-			}
 
-			out.Concurrently(i, func() graphql.Marshaler {
-				return innerFunc(ctx)
+			out.Values[i] = ec._Identity_id(ctx, field, obj)
 
-			})
+			if out.Values[i] == graphql.Null {
+				atomic.AddUint32(&invalids, 1)
+			}
 		case "humanId":
 			field := field
 

api/graphql/graph/mutations.generated.go 🔗

@@ -2097,7 +2097,7 @@ func (ec *executionContext) unmarshalInputEditCommentInput(ctx context.Context,
 		asMap[k] = v
 	}
 
-	fieldsInOrder := [...]string{"clientMutationId", "repoRef", "prefix", "target", "message", "files"}
+	fieldsInOrder := [...]string{"clientMutationId", "repoRef", "targetPrefix", "message", "files"}
 	for _, k := range fieldsInOrder {
 		v, ok := asMap[k]
 		if !ok {
@@ -2120,19 +2120,11 @@ func (ec *executionContext) unmarshalInputEditCommentInput(ctx context.Context,
 			if err != nil {
 				return it, err
 			}
-		case "prefix":
-			var err error
-
-			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("prefix"))
-			it.Prefix, err = ec.unmarshalNString2string(ctx, v)
-			if err != nil {
-				return it, err
-			}
-		case "target":
+		case "targetPrefix":
 			var err error
 
-			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("target"))
-			it.Target, err = ec.unmarshalNString2string(ctx, v)
+			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("targetPrefix"))
+			it.TargetPrefix, err = ec.unmarshalNString2string(ctx, v)
 			if err != nil {
 				return it, err
 			}

api/graphql/graph/operations.generated.go 🔗

@@ -15,6 +15,7 @@ import (
 	"github.com/MichaelMure/git-bug/api/graphql/models"
 	"github.com/MichaelMure/git-bug/entities/bug"
 	"github.com/MichaelMure/git-bug/entities/common"
+	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/entity/dag"
 	"github.com/MichaelMure/git-bug/repository"
 	"github.com/vektah/gqlparser/v2/ast"
@@ -23,33 +24,27 @@ import (
 // region    ************************** generated!.gotpl **************************
 
 type AddCommentOperationResolver interface {
-	ID(ctx context.Context, obj *bug.AddCommentOperation) (string, error)
 	Author(ctx context.Context, obj *bug.AddCommentOperation) (models.IdentityWrapper, error)
 	Date(ctx context.Context, obj *bug.AddCommentOperation) (*time.Time, error)
 }
 type CreateOperationResolver interface {
-	ID(ctx context.Context, obj *bug.CreateOperation) (string, error)
 	Author(ctx context.Context, obj *bug.CreateOperation) (models.IdentityWrapper, error)
 	Date(ctx context.Context, obj *bug.CreateOperation) (*time.Time, error)
 }
 type EditCommentOperationResolver interface {
-	ID(ctx context.Context, obj *bug.EditCommentOperation) (string, error)
 	Author(ctx context.Context, obj *bug.EditCommentOperation) (models.IdentityWrapper, error)
 	Date(ctx context.Context, obj *bug.EditCommentOperation) (*time.Time, error)
 	Target(ctx context.Context, obj *bug.EditCommentOperation) (string, error)
 }
 type LabelChangeOperationResolver interface {
-	ID(ctx context.Context, obj *bug.LabelChangeOperation) (string, error)
 	Author(ctx context.Context, obj *bug.LabelChangeOperation) (models.IdentityWrapper, error)
 	Date(ctx context.Context, obj *bug.LabelChangeOperation) (*time.Time, error)
 }
 type SetStatusOperationResolver interface {
-	ID(ctx context.Context, obj *bug.SetStatusOperation) (string, error)
 	Author(ctx context.Context, obj *bug.SetStatusOperation) (models.IdentityWrapper, error)
 	Date(ctx context.Context, obj *bug.SetStatusOperation) (*time.Time, error)
 }
 type SetTitleOperationResolver interface {
-	ID(ctx context.Context, obj *bug.SetTitleOperation) (string, error)
 	Author(ctx context.Context, obj *bug.SetTitleOperation) (models.IdentityWrapper, error)
 	Date(ctx context.Context, obj *bug.SetTitleOperation) (*time.Time, error)
 }
@@ -80,7 +75,7 @@ func (ec *executionContext) _AddCommentOperation_id(ctx context.Context, field g
 	}()
 	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
 		ctx = rctx // use context from middleware stack in children
-		return ec.resolvers.AddCommentOperation().ID(rctx, obj)
+		return obj.Id(), nil
 	})
 	if err != nil {
 		ec.Error(ctx, err)
@@ -92,9 +87,9 @@ func (ec *executionContext) _AddCommentOperation_id(ctx context.Context, field g
 		}
 		return graphql.Null
 	}
-	res := resTmp.(string)
+	res := resTmp.(entity.Id)
 	fc.Result = res
-	return ec.marshalNString2string(ctx, field.Selections, res)
+	return ec.marshalNID2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋentityᚐId(ctx, field.Selections, res)
 }
 
 func (ec *executionContext) fieldContext_AddCommentOperation_id(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
@@ -102,9 +97,9 @@ func (ec *executionContext) fieldContext_AddCommentOperation_id(ctx context.Cont
 		Object:     "AddCommentOperation",
 		Field:      field,
 		IsMethod:   true,
-		IsResolver: true,
+		IsResolver: false,
 		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
-			return nil, errors.New("field of type String does not have child fields")
+			return nil, errors.New("field of type ID does not have child fields")
 		},
 	}
 	return fc, nil
@@ -318,7 +313,7 @@ func (ec *executionContext) _CreateOperation_id(ctx context.Context, field graph
 	}()
 	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
 		ctx = rctx // use context from middleware stack in children
-		return ec.resolvers.CreateOperation().ID(rctx, obj)
+		return obj.Id(), nil
 	})
 	if err != nil {
 		ec.Error(ctx, err)
@@ -330,9 +325,9 @@ func (ec *executionContext) _CreateOperation_id(ctx context.Context, field graph
 		}
 		return graphql.Null
 	}
-	res := resTmp.(string)
+	res := resTmp.(entity.Id)
 	fc.Result = res
-	return ec.marshalNString2string(ctx, field.Selections, res)
+	return ec.marshalNID2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋentityᚐId(ctx, field.Selections, res)
 }
 
 func (ec *executionContext) fieldContext_CreateOperation_id(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
@@ -340,9 +335,9 @@ func (ec *executionContext) fieldContext_CreateOperation_id(ctx context.Context,
 		Object:     "CreateOperation",
 		Field:      field,
 		IsMethod:   true,
-		IsResolver: true,
+		IsResolver: false,
 		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
-			return nil, errors.New("field of type String does not have child fields")
+			return nil, errors.New("field of type ID does not have child fields")
 		},
 	}
 	return fc, nil
@@ -600,7 +595,7 @@ func (ec *executionContext) _EditCommentOperation_id(ctx context.Context, field
 	}()
 	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
 		ctx = rctx // use context from middleware stack in children
-		return ec.resolvers.EditCommentOperation().ID(rctx, obj)
+		return obj.Id(), nil
 	})
 	if err != nil {
 		ec.Error(ctx, err)
@@ -612,9 +607,9 @@ func (ec *executionContext) _EditCommentOperation_id(ctx context.Context, field
 		}
 		return graphql.Null
 	}
-	res := resTmp.(string)
+	res := resTmp.(entity.Id)
 	fc.Result = res
-	return ec.marshalNString2string(ctx, field.Selections, res)
+	return ec.marshalNID2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋentityᚐId(ctx, field.Selections, res)
 }
 
 func (ec *executionContext) fieldContext_EditCommentOperation_id(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
@@ -622,9 +617,9 @@ func (ec *executionContext) fieldContext_EditCommentOperation_id(ctx context.Con
 		Object:     "EditCommentOperation",
 		Field:      field,
 		IsMethod:   true,
-		IsResolver: true,
+		IsResolver: false,
 		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
-			return nil, errors.New("field of type String does not have child fields")
+			return nil, errors.New("field of type ID does not have child fields")
 		},
 	}
 	return fc, nil
@@ -882,7 +877,7 @@ func (ec *executionContext) _LabelChangeOperation_id(ctx context.Context, field
 	}()
 	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
 		ctx = rctx // use context from middleware stack in children
-		return ec.resolvers.LabelChangeOperation().ID(rctx, obj)
+		return obj.Id(), nil
 	})
 	if err != nil {
 		ec.Error(ctx, err)
@@ -894,9 +889,9 @@ func (ec *executionContext) _LabelChangeOperation_id(ctx context.Context, field
 		}
 		return graphql.Null
 	}
-	res := resTmp.(string)
+	res := resTmp.(entity.Id)
 	fc.Result = res
-	return ec.marshalNString2string(ctx, field.Selections, res)
+	return ec.marshalNID2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋentityᚐId(ctx, field.Selections, res)
 }
 
 func (ec *executionContext) fieldContext_LabelChangeOperation_id(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
@@ -904,9 +899,9 @@ func (ec *executionContext) fieldContext_LabelChangeOperation_id(ctx context.Con
 		Object:     "LabelChangeOperation",
 		Field:      field,
 		IsMethod:   true,
-		IsResolver: true,
+		IsResolver: false,
 		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
-			return nil, errors.New("field of type String does not have child fields")
+			return nil, errors.New("field of type ID does not have child fields")
 		},
 	}
 	return fc, nil
@@ -1412,7 +1407,7 @@ func (ec *executionContext) _SetStatusOperation_id(ctx context.Context, field gr
 	}()
 	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
 		ctx = rctx // use context from middleware stack in children
-		return ec.resolvers.SetStatusOperation().ID(rctx, obj)
+		return obj.Id(), nil
 	})
 	if err != nil {
 		ec.Error(ctx, err)
@@ -1424,9 +1419,9 @@ func (ec *executionContext) _SetStatusOperation_id(ctx context.Context, field gr
 		}
 		return graphql.Null
 	}
-	res := resTmp.(string)
+	res := resTmp.(entity.Id)
 	fc.Result = res
-	return ec.marshalNString2string(ctx, field.Selections, res)
+	return ec.marshalNID2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋentityᚐId(ctx, field.Selections, res)
 }
 
 func (ec *executionContext) fieldContext_SetStatusOperation_id(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
@@ -1434,9 +1429,9 @@ func (ec *executionContext) fieldContext_SetStatusOperation_id(ctx context.Conte
 		Object:     "SetStatusOperation",
 		Field:      field,
 		IsMethod:   true,
-		IsResolver: true,
+		IsResolver: false,
 		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
-			return nil, errors.New("field of type String does not have child fields")
+			return nil, errors.New("field of type ID does not have child fields")
 		},
 	}
 	return fc, nil
@@ -1606,7 +1601,7 @@ func (ec *executionContext) _SetTitleOperation_id(ctx context.Context, field gra
 	}()
 	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
 		ctx = rctx // use context from middleware stack in children
-		return ec.resolvers.SetTitleOperation().ID(rctx, obj)
+		return obj.Id(), nil
 	})
 	if err != nil {
 		ec.Error(ctx, err)
@@ -1618,9 +1613,9 @@ func (ec *executionContext) _SetTitleOperation_id(ctx context.Context, field gra
 		}
 		return graphql.Null
 	}
-	res := resTmp.(string)
+	res := resTmp.(entity.Id)
 	fc.Result = res
-	return ec.marshalNString2string(ctx, field.Selections, res)
+	return ec.marshalNID2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋentityᚐId(ctx, field.Selections, res)
 }
 
 func (ec *executionContext) fieldContext_SetTitleOperation_id(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
@@ -1628,9 +1623,9 @@ func (ec *executionContext) fieldContext_SetTitleOperation_id(ctx context.Contex
 		Object:     "SetTitleOperation",
 		Field:      field,
 		IsMethod:   true,
-		IsResolver: true,
+		IsResolver: false,
 		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
-			return nil, errors.New("field of type String does not have child fields")
+			return nil, errors.New("field of type ID does not have child fields")
 		},
 	}
 	return fc, nil
@@ -1892,25 +1887,12 @@ func (ec *executionContext) _AddCommentOperation(ctx context.Context, sel ast.Se
 		case "__typename":
 			out.Values[i] = graphql.MarshalString("AddCommentOperation")
 		case "id":
-			field := field
-
-			innerFunc := func(ctx context.Context) (res graphql.Marshaler) {
-				defer func() {
-					if r := recover(); r != nil {
-						ec.Error(ctx, ec.Recover(ctx, r))
-					}
-				}()
-				res = ec._AddCommentOperation_id(ctx, field, obj)
-				if res == graphql.Null {
-					atomic.AddUint32(&invalids, 1)
-				}
-				return res
-			}
 
-			out.Concurrently(i, func() graphql.Marshaler {
-				return innerFunc(ctx)
+			out.Values[i] = ec._AddCommentOperation_id(ctx, field, obj)
 
-			})
+			if out.Values[i] == graphql.Null {
+				atomic.AddUint32(&invalids, 1)
+			}
 		case "author":
 			field := field
 
@@ -1987,25 +1969,12 @@ func (ec *executionContext) _CreateOperation(ctx context.Context, sel ast.Select
 		case "__typename":
 			out.Values[i] = graphql.MarshalString("CreateOperation")
 		case "id":
-			field := field
-
-			innerFunc := func(ctx context.Context) (res graphql.Marshaler) {
-				defer func() {
-					if r := recover(); r != nil {
-						ec.Error(ctx, ec.Recover(ctx, r))
-					}
-				}()
-				res = ec._CreateOperation_id(ctx, field, obj)
-				if res == graphql.Null {
-					atomic.AddUint32(&invalids, 1)
-				}
-				return res
-			}
 
-			out.Concurrently(i, func() graphql.Marshaler {
-				return innerFunc(ctx)
+			out.Values[i] = ec._CreateOperation_id(ctx, field, obj)
 
-			})
+			if out.Values[i] == graphql.Null {
+				atomic.AddUint32(&invalids, 1)
+			}
 		case "author":
 			field := field
 
@@ -2089,25 +2058,12 @@ func (ec *executionContext) _EditCommentOperation(ctx context.Context, sel ast.S
 		case "__typename":
 			out.Values[i] = graphql.MarshalString("EditCommentOperation")
 		case "id":
-			field := field
-
-			innerFunc := func(ctx context.Context) (res graphql.Marshaler) {
-				defer func() {
-					if r := recover(); r != nil {
-						ec.Error(ctx, ec.Recover(ctx, r))
-					}
-				}()
-				res = ec._EditCommentOperation_id(ctx, field, obj)
-				if res == graphql.Null {
-					atomic.AddUint32(&invalids, 1)
-				}
-				return res
-			}
 
-			out.Concurrently(i, func() graphql.Marshaler {
-				return innerFunc(ctx)
+			out.Values[i] = ec._EditCommentOperation_id(ctx, field, obj)
 
-			})
+			if out.Values[i] == graphql.Null {
+				atomic.AddUint32(&invalids, 1)
+			}
 		case "author":
 			field := field
 
@@ -2204,25 +2160,12 @@ func (ec *executionContext) _LabelChangeOperation(ctx context.Context, sel ast.S
 		case "__typename":
 			out.Values[i] = graphql.MarshalString("LabelChangeOperation")
 		case "id":
-			field := field
-
-			innerFunc := func(ctx context.Context) (res graphql.Marshaler) {
-				defer func() {
-					if r := recover(); r != nil {
-						ec.Error(ctx, ec.Recover(ctx, r))
-					}
-				}()
-				res = ec._LabelChangeOperation_id(ctx, field, obj)
-				if res == graphql.Null {
-					atomic.AddUint32(&invalids, 1)
-				}
-				return res
-			}
 
-			out.Concurrently(i, func() graphql.Marshaler {
-				return innerFunc(ctx)
+			out.Values[i] = ec._LabelChangeOperation_id(ctx, field, obj)
 
-			})
+			if out.Values[i] == graphql.Null {
+				atomic.AddUint32(&invalids, 1)
+			}
 		case "author":
 			field := field
 
@@ -2383,25 +2326,12 @@ func (ec *executionContext) _SetStatusOperation(ctx context.Context, sel ast.Sel
 		case "__typename":
 			out.Values[i] = graphql.MarshalString("SetStatusOperation")
 		case "id":
-			field := field
-
-			innerFunc := func(ctx context.Context) (res graphql.Marshaler) {
-				defer func() {
-					if r := recover(); r != nil {
-						ec.Error(ctx, ec.Recover(ctx, r))
-					}
-				}()
-				res = ec._SetStatusOperation_id(ctx, field, obj)
-				if res == graphql.Null {
-					atomic.AddUint32(&invalids, 1)
-				}
-				return res
-			}
 
-			out.Concurrently(i, func() graphql.Marshaler {
-				return innerFunc(ctx)
+			out.Values[i] = ec._SetStatusOperation_id(ctx, field, obj)
 
-			})
+			if out.Values[i] == graphql.Null {
+				atomic.AddUint32(&invalids, 1)
+			}
 		case "author":
 			field := field
 
@@ -2471,25 +2401,12 @@ func (ec *executionContext) _SetTitleOperation(ctx context.Context, sel ast.Sele
 		case "__typename":
 			out.Values[i] = graphql.MarshalString("SetTitleOperation")
 		case "id":
-			field := field
 
-			innerFunc := func(ctx context.Context) (res graphql.Marshaler) {
-				defer func() {
-					if r := recover(); r != nil {
-						ec.Error(ctx, ec.Recover(ctx, r))
-					}
-				}()
-				res = ec._SetTitleOperation_id(ctx, field, obj)
-				if res == graphql.Null {
-					atomic.AddUint32(&invalids, 1)
-				}
-				return res
-			}
-
-			out.Concurrently(i, func() graphql.Marshaler {
-				return innerFunc(ctx)
+			out.Values[i] = ec._SetTitleOperation_id(ctx, field, obj)
 
-			})
+			if out.Values[i] == graphql.Null {
+				atomic.AddUint32(&invalids, 1)
+			}
 		case "author":
 			field := field
 

api/graphql/graph/prelude.generated.go 🔗

@@ -11,6 +11,7 @@ import (
 
 	"github.com/99designs/gqlgen/graphql"
 	"github.com/99designs/gqlgen/graphql/introspection"
+	"github.com/MichaelMure/git-bug/entity"
 	"github.com/vektah/gqlparser/v2/ast"
 )
 
@@ -2172,6 +2173,16 @@ func (ec *executionContext) marshalNBoolean2bool(ctx context.Context, sel ast.Se
 	return res
 }
 
+func (ec *executionContext) unmarshalNID2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋentityᚐId(ctx context.Context, v interface{}) (entity.Id, error) {
+	var res entity.Id
+	err := res.UnmarshalGQL(v)
+	return res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalNID2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋentityᚐId(ctx context.Context, sel ast.SelectionSet, v entity.Id) graphql.Marshaler {
+	return v
+}
+
 func (ec *executionContext) unmarshalNInt2int(ctx context.Context, v interface{}) (int, error) {
 	res, err := graphql.UnmarshalInt(v)
 	return res, graphql.ErrorOnPath(ctx, err)

api/graphql/graph/root_.generated.go 🔗

@@ -74,7 +74,7 @@ type ComplexityRoot struct {
 		Author  func(childComplexity int) int
 		Date    func(childComplexity int) int
 		Files   func(childComplexity int) int
-		ID      func(childComplexity int) int
+		Id      func(childComplexity int) int
 		Message func(childComplexity int) int
 	}
 
@@ -102,7 +102,7 @@ type ComplexityRoot struct {
 		Comments     func(childComplexity int, after *string, before *string, first *int, last *int) int
 		CreatedAt    func(childComplexity int) int
 		HumanID      func(childComplexity int) int
-		ID           func(childComplexity int) int
+		Id           func(childComplexity int) int
 		Labels       func(childComplexity int) int
 		LastEdit     func(childComplexity int) int
 		Operations   func(childComplexity int, after *string, before *string, first *int, last *int) int
@@ -146,6 +146,7 @@ type ComplexityRoot struct {
 	Comment struct {
 		Author  func(childComplexity int) int
 		Files   func(childComplexity int) int
+		ID      func(childComplexity int) int
 		Message func(childComplexity int) int
 	}
 
@@ -170,7 +171,7 @@ type ComplexityRoot struct {
 		Author  func(childComplexity int) int
 		Date    func(childComplexity int) int
 		Files   func(childComplexity int) int
-		ID      func(childComplexity int) int
+		Id      func(childComplexity int) int
 		Message func(childComplexity int) int
 		Title   func(childComplexity int) int
 	}
@@ -191,7 +192,7 @@ type ComplexityRoot struct {
 		Author  func(childComplexity int) int
 		Date    func(childComplexity int) int
 		Files   func(childComplexity int) int
-		ID      func(childComplexity int) int
+		Id      func(childComplexity int) int
 		Message func(childComplexity int) int
 		Target  func(childComplexity int) int
 	}
@@ -207,7 +208,7 @@ type ComplexityRoot struct {
 		DisplayName func(childComplexity int) int
 		Email       func(childComplexity int) int
 		HumanID     func(childComplexity int) int
-		ID          func(childComplexity int) int
+		Id          func(childComplexity int) int
 		IsProtected func(childComplexity int) int
 		Login       func(childComplexity int) int
 		Name        func(childComplexity int) int
@@ -234,7 +235,7 @@ type ComplexityRoot struct {
 		Added   func(childComplexity int) int
 		Author  func(childComplexity int) int
 		Date    func(childComplexity int) int
-		ID      func(childComplexity int) int
+		Id      func(childComplexity int) int
 		Removed func(childComplexity int) int
 	}
 
@@ -323,7 +324,7 @@ type ComplexityRoot struct {
 	SetStatusOperation struct {
 		Author func(childComplexity int) int
 		Date   func(childComplexity int) int
-		ID     func(childComplexity int) int
+		Id     func(childComplexity int) int
 		Status func(childComplexity int) int
 	}
 
@@ -337,7 +338,7 @@ type ComplexityRoot struct {
 	SetTitleOperation struct {
 		Author func(childComplexity int) int
 		Date   func(childComplexity int) int
-		ID     func(childComplexity int) int
+		Id     func(childComplexity int) int
 		Title  func(childComplexity int) int
 		Was    func(childComplexity int) int
 	}
@@ -462,11 +463,11 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 		return e.complexity.AddCommentOperation.Files(childComplexity), true
 
 	case "AddCommentOperation.id":
-		if e.complexity.AddCommentOperation.ID == nil {
+		if e.complexity.AddCommentOperation.Id == nil {
 			break
 		}
 
-		return e.complexity.AddCommentOperation.ID(childComplexity), true
+		return e.complexity.AddCommentOperation.Id(childComplexity), true
 
 	case "AddCommentOperation.message":
 		if e.complexity.AddCommentOperation.Message == nil {
@@ -605,11 +606,11 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 		return e.complexity.Bug.HumanID(childComplexity), true
 
 	case "Bug.id":
-		if e.complexity.Bug.ID == nil {
+		if e.complexity.Bug.Id == nil {
 			break
 		}
 
-		return e.complexity.Bug.ID(childComplexity), true
+		return e.complexity.Bug.Id(childComplexity), true
 
 	case "Bug.labels":
 		if e.complexity.Bug.Labels == nil {
@@ -801,6 +802,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.Comment.Files(childComplexity), true
 
+	case "Comment.id":
+		if e.complexity.Comment.ID == nil {
+			break
+		}
+
+		return e.complexity.Comment.ID(childComplexity), true
+
 	case "Comment.message":
 		if e.complexity.Comment.Message == nil {
 			break
@@ -886,11 +894,11 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 		return e.complexity.CreateOperation.Files(childComplexity), true
 
 	case "CreateOperation.id":
-		if e.complexity.CreateOperation.ID == nil {
+		if e.complexity.CreateOperation.Id == nil {
 			break
 		}
 
-		return e.complexity.CreateOperation.ID(childComplexity), true
+		return e.complexity.CreateOperation.Id(childComplexity), true
 
 	case "CreateOperation.message":
 		if e.complexity.CreateOperation.Message == nil {
@@ -991,11 +999,11 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 		return e.complexity.EditCommentOperation.Files(childComplexity), true
 
 	case "EditCommentOperation.id":
-		if e.complexity.EditCommentOperation.ID == nil {
+		if e.complexity.EditCommentOperation.Id == nil {
 			break
 		}
 
-		return e.complexity.EditCommentOperation.ID(childComplexity), true
+		return e.complexity.EditCommentOperation.Id(childComplexity), true
 
 	case "EditCommentOperation.message":
 		if e.complexity.EditCommentOperation.Message == nil {
@@ -1061,11 +1069,11 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 		return e.complexity.Identity.HumanID(childComplexity), true
 
 	case "Identity.id":
-		if e.complexity.Identity.ID == nil {
+		if e.complexity.Identity.Id == nil {
 			break
 		}
 
-		return e.complexity.Identity.ID(childComplexity), true
+		return e.complexity.Identity.Id(childComplexity), true
 
 	case "Identity.isProtected":
 		if e.complexity.Identity.IsProtected == nil {
@@ -1166,11 +1174,11 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 		return e.complexity.LabelChangeOperation.Date(childComplexity), true
 
 	case "LabelChangeOperation.id":
-		if e.complexity.LabelChangeOperation.ID == nil {
+		if e.complexity.LabelChangeOperation.Id == nil {
 			break
 		}
 
-		return e.complexity.LabelChangeOperation.ID(childComplexity), true
+		return e.complexity.LabelChangeOperation.Id(childComplexity), true
 
 	case "LabelChangeOperation.removed":
 		if e.complexity.LabelChangeOperation.Removed == nil {
@@ -1591,11 +1599,11 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 		return e.complexity.SetStatusOperation.Date(childComplexity), true
 
 	case "SetStatusOperation.id":
-		if e.complexity.SetStatusOperation.ID == nil {
+		if e.complexity.SetStatusOperation.Id == nil {
 			break
 		}
 
-		return e.complexity.SetStatusOperation.ID(childComplexity), true
+		return e.complexity.SetStatusOperation.Id(childComplexity), true
 
 	case "SetStatusOperation.status":
 		if e.complexity.SetStatusOperation.Status == nil {
@@ -1647,11 +1655,11 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 		return e.complexity.SetTitleOperation.Date(childComplexity), true
 
 	case "SetTitleOperation.id":
-		if e.complexity.SetTitleOperation.ID == nil {
+		if e.complexity.SetTitleOperation.Id == nil {
 			break
 		}
 
-		return e.complexity.SetTitleOperation.ID(childComplexity), true
+		return e.complexity.SetTitleOperation.Id(childComplexity), true
 
 	case "SetTitleOperation.title":
 		if e.complexity.SetTitleOperation.Title == nil {
@@ -1844,6 +1852,8 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er
 var sources = []*ast.Source{
 	{Name: "../schema/bug.graphql", Input: `"""Represents a comment on a bug."""
 type Comment implements Authored {
+  id: CombinedId!
+
   """The author of this comment."""
   author: Identity!
 
@@ -1873,7 +1883,7 @@ enum Status {
 
 type Bug implements Authored {
   """The identifier for this bug"""
-  id: String!
+  id: ID!
   """The human version (truncated) identifier for this bug"""
   humanId: String!
   status: Status!
@@ -1964,7 +1974,7 @@ type BugEdge {
 	{Name: "../schema/identity.graphql", Input: `"""Represents an identity"""
 type Identity {
     """The identifier for this identity"""
-    id: String!
+    id: ID!
     """The human version (truncated) identifier for this identity"""
     humanId: String!
     """The name of the person, if known."""
@@ -2109,10 +2119,8 @@ input EditCommentInput {
     clientMutationId: String
     """The name of the repository. If not set, the default repository is used."""
     repoRef: String
-    """The bug ID's prefix."""
-    prefix: String!
-    """The ID of the comment to be changed."""
-    target: String!
+    """A prefix of the CombinedId of the comment to be changed."""
+    targetPrefix: String!
     """The new message to be set."""
     message: String!
     """The collection of file's hash required for the first message."""
@@ -2226,7 +2234,7 @@ type SetTitlePayload {
 	{Name: "../schema/operations.graphql", Input: `"""An operation applied to a bug."""
 interface Operation {
     """The identifier of the operation"""
-    id: String!
+    id: ID!
     """The operations author."""
     author: Identity!
     """The datetime when this operation was issued."""
@@ -2253,7 +2261,7 @@ type OperationEdge {
 
 type CreateOperation implements Operation & Authored {
     """The identifier of the operation"""
-    id: String!
+    id: ID!
     """The author of this object."""
     author: Identity!
     """The datetime when this operation was issued."""
@@ -2266,7 +2274,7 @@ type CreateOperation implements Operation & Authored {
 
 type SetTitleOperation implements Operation & Authored {
     """The identifier of the operation"""
-    id: String!
+    id: ID!
     """The author of this object."""
     author: Identity!
     """The datetime when this operation was issued."""
@@ -2278,7 +2286,7 @@ type SetTitleOperation implements Operation & Authored {
 
 type AddCommentOperation implements Operation & Authored {
     """The identifier of the operation"""
-    id: String!
+    id: ID!
     """The author of this object."""
     author: Identity!
     """The datetime when this operation was issued."""
@@ -2290,7 +2298,7 @@ type AddCommentOperation implements Operation & Authored {
 
 type EditCommentOperation implements Operation & Authored {
     """The identifier of the operation"""
-    id: String!
+    id: ID!
     """The author of this object."""
     author: Identity!
     """The datetime when this operation was issued."""
@@ -2303,7 +2311,7 @@ type EditCommentOperation implements Operation & Authored {
 
 type SetStatusOperation implements Operation & Authored {
     """The identifier of the operation"""
-    id: String!
+    id: ID!
     """The author of this object."""
     author: Identity!
     """The datetime when this operation was issued."""
@@ -2314,7 +2322,7 @@ type SetStatusOperation implements Operation & Authored {
 
 type LabelChangeOperation implements Operation & Authored {
     """The identifier of the operation"""
-    id: String!
+    id: ID!
     """The author of this object."""
     author: Identity!
     """The datetime when this operation was issued."""
@@ -2404,7 +2412,7 @@ type Mutation {
 	{Name: "../schema/timeline.graphql", Input: `"""An item in the timeline of events"""
 interface TimelineItem {
     """The identifier of the source operation"""
-    id: String!
+    id: CombinedId!
 }
 
 """CommentHistoryStep hold one version of a message in the history"""
@@ -2434,7 +2442,7 @@ type TimelineItemEdge {
 """CreateTimelineItem is a TimelineItem that represent the creation of a bug and its message edition history"""
 type CreateTimelineItem implements TimelineItem & Authored {
     """The identifier of the source operation"""
-    id: String!
+    id: CombinedId!
     author: Identity!
     message: String!
     messageIsEmpty: Boolean!
@@ -2448,7 +2456,7 @@ type CreateTimelineItem implements TimelineItem & Authored {
 """AddCommentTimelineItem is a TimelineItem that represent a Comment and its edition history"""
 type AddCommentTimelineItem implements TimelineItem & Authored {
     """The identifier of the source operation"""
-    id: String!
+    id: CombinedId!
     author: Identity!
     message: String!
     messageIsEmpty: Boolean!
@@ -2462,7 +2470,7 @@ type AddCommentTimelineItem implements TimelineItem & Authored {
 """LabelChangeTimelineItem is a TimelineItem that represent a change in the labels of a bug"""
 type LabelChangeTimelineItem implements TimelineItem & Authored {
     """The identifier of the source operation"""
-    id: String!
+    id: CombinedId!
     author: Identity!
     date: Time!
     added: [Label!]!
@@ -2472,7 +2480,7 @@ type LabelChangeTimelineItem implements TimelineItem & Authored {
 """SetStatusTimelineItem is a TimelineItem that represent a change in the status of a bug"""
 type SetStatusTimelineItem implements TimelineItem & Authored {
     """The identifier of the source operation"""
-    id: String!
+    id: CombinedId!
     author: Identity!
     date: Time!
     status: Status!
@@ -2481,14 +2489,15 @@ type SetStatusTimelineItem implements TimelineItem & Authored {
 """LabelChangeTimelineItem is a TimelineItem that represent a change in the title of a bug"""
 type SetTitleTimelineItem implements TimelineItem & Authored {
     """The identifier of the source operation"""
-    id: String!
+    id: CombinedId!
     author: Identity!
     date: Time!
     title: String!
     was: String!
 }
 `, BuiltIn: false},
-	{Name: "../schema/types.graphql", Input: `scalar Time
+	{Name: "../schema/types.graphql", Input: `scalar CombinedId
+scalar Time
 scalar Hash
 
 """Defines a color by red, green and blue components."""

api/graphql/graph/timeline.generated.go 🔗

@@ -15,6 +15,7 @@ import (
 	"github.com/MichaelMure/git-bug/api/graphql/models"
 	"github.com/MichaelMure/git-bug/entities/bug"
 	"github.com/MichaelMure/git-bug/entities/common"
+	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/repository"
 	"github.com/vektah/gqlparser/v2/ast"
 )
@@ -22,7 +23,7 @@ import (
 // region    ************************** generated!.gotpl **************************
 
 type AddCommentTimelineItemResolver interface {
-	ID(ctx context.Context, obj *bug.AddCommentTimelineItem) (string, error)
+	ID(ctx context.Context, obj *bug.AddCommentTimelineItem) (entity.CombinedId, error)
 	Author(ctx context.Context, obj *bug.AddCommentTimelineItem) (models.IdentityWrapper, error)
 
 	CreatedAt(ctx context.Context, obj *bug.AddCommentTimelineItem) (*time.Time, error)
@@ -32,24 +33,24 @@ type CommentHistoryStepResolver interface {
 	Date(ctx context.Context, obj *bug.CommentHistoryStep) (*time.Time, error)
 }
 type CreateTimelineItemResolver interface {
-	ID(ctx context.Context, obj *bug.CreateTimelineItem) (string, error)
+	ID(ctx context.Context, obj *bug.CreateTimelineItem) (entity.CombinedId, error)
 	Author(ctx context.Context, obj *bug.CreateTimelineItem) (models.IdentityWrapper, error)
 
 	CreatedAt(ctx context.Context, obj *bug.CreateTimelineItem) (*time.Time, error)
 	LastEdit(ctx context.Context, obj *bug.CreateTimelineItem) (*time.Time, error)
 }
 type LabelChangeTimelineItemResolver interface {
-	ID(ctx context.Context, obj *bug.LabelChangeTimelineItem) (string, error)
+	ID(ctx context.Context, obj *bug.LabelChangeTimelineItem) (entity.CombinedId, error)
 	Author(ctx context.Context, obj *bug.LabelChangeTimelineItem) (models.IdentityWrapper, error)
 	Date(ctx context.Context, obj *bug.LabelChangeTimelineItem) (*time.Time, error)
 }
 type SetStatusTimelineItemResolver interface {
-	ID(ctx context.Context, obj *bug.SetStatusTimelineItem) (string, error)
+	ID(ctx context.Context, obj *bug.SetStatusTimelineItem) (entity.CombinedId, error)
 	Author(ctx context.Context, obj *bug.SetStatusTimelineItem) (models.IdentityWrapper, error)
 	Date(ctx context.Context, obj *bug.SetStatusTimelineItem) (*time.Time, error)
 }
 type SetTitleTimelineItemResolver interface {
-	ID(ctx context.Context, obj *bug.SetTitleTimelineItem) (string, error)
+	ID(ctx context.Context, obj *bug.SetTitleTimelineItem) (entity.CombinedId, error)
 	Author(ctx context.Context, obj *bug.SetTitleTimelineItem) (models.IdentityWrapper, error)
 	Date(ctx context.Context, obj *bug.SetTitleTimelineItem) (*time.Time, error)
 }
@@ -92,9 +93,9 @@ func (ec *executionContext) _AddCommentTimelineItem_id(ctx context.Context, fiel
 		}
 		return graphql.Null
 	}
-	res := resTmp.(string)
+	res := resTmp.(entity.CombinedId)
 	fc.Result = res
-	return ec.marshalNString2string(ctx, field.Selections, res)
+	return ec.marshalNCombinedId2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋentityᚐCombinedId(ctx, field.Selections, res)
 }
 
 func (ec *executionContext) fieldContext_AddCommentTimelineItem_id(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
@@ -104,7 +105,7 @@ func (ec *executionContext) fieldContext_AddCommentTimelineItem_id(ctx context.C
 		IsMethod:   true,
 		IsResolver: true,
 		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
-			return nil, errors.New("field of type String does not have child fields")
+			return nil, errors.New("field of type CombinedId does not have child fields")
 		},
 	}
 	return fc, nil
@@ -600,9 +601,9 @@ func (ec *executionContext) _CreateTimelineItem_id(ctx context.Context, field gr
 		}
 		return graphql.Null
 	}
-	res := resTmp.(string)
+	res := resTmp.(entity.CombinedId)
 	fc.Result = res
-	return ec.marshalNString2string(ctx, field.Selections, res)
+	return ec.marshalNCombinedId2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋentityᚐCombinedId(ctx, field.Selections, res)
 }
 
 func (ec *executionContext) fieldContext_CreateTimelineItem_id(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
@@ -612,7 +613,7 @@ func (ec *executionContext) fieldContext_CreateTimelineItem_id(ctx context.Conte
 		IsMethod:   true,
 		IsResolver: true,
 		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
-			return nil, errors.New("field of type String does not have child fields")
+			return nil, errors.New("field of type CombinedId does not have child fields")
 		},
 	}
 	return fc, nil
@@ -1020,9 +1021,9 @@ func (ec *executionContext) _LabelChangeTimelineItem_id(ctx context.Context, fie
 		}
 		return graphql.Null
 	}
-	res := resTmp.(string)
+	res := resTmp.(entity.CombinedId)
 	fc.Result = res
-	return ec.marshalNString2string(ctx, field.Selections, res)
+	return ec.marshalNCombinedId2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋentityᚐCombinedId(ctx, field.Selections, res)
 }
 
 func (ec *executionContext) fieldContext_LabelChangeTimelineItem_id(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
@@ -1032,7 +1033,7 @@ func (ec *executionContext) fieldContext_LabelChangeTimelineItem_id(ctx context.
 		IsMethod:   true,
 		IsResolver: true,
 		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
-			return nil, errors.New("field of type String does not have child fields")
+			return nil, errors.New("field of type CombinedId does not have child fields")
 		},
 	}
 	return fc, nil
@@ -1270,9 +1271,9 @@ func (ec *executionContext) _SetStatusTimelineItem_id(ctx context.Context, field
 		}
 		return graphql.Null
 	}
-	res := resTmp.(string)
+	res := resTmp.(entity.CombinedId)
 	fc.Result = res
-	return ec.marshalNString2string(ctx, field.Selections, res)
+	return ec.marshalNCombinedId2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋentityᚐCombinedId(ctx, field.Selections, res)
 }
 
 func (ec *executionContext) fieldContext_SetStatusTimelineItem_id(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
@@ -1282,7 +1283,7 @@ func (ec *executionContext) fieldContext_SetStatusTimelineItem_id(ctx context.Co
 		IsMethod:   true,
 		IsResolver: true,
 		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
-			return nil, errors.New("field of type String does not have child fields")
+			return nil, errors.New("field of type CombinedId does not have child fields")
 		},
 	}
 	return fc, nil
@@ -1464,9 +1465,9 @@ func (ec *executionContext) _SetTitleTimelineItem_id(ctx context.Context, field
 		}
 		return graphql.Null
 	}
-	res := resTmp.(string)
+	res := resTmp.(entity.CombinedId)
 	fc.Result = res
-	return ec.marshalNString2string(ctx, field.Selections, res)
+	return ec.marshalNCombinedId2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋentityᚐCombinedId(ctx, field.Selections, res)
 }
 
 func (ec *executionContext) fieldContext_SetTitleTimelineItem_id(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
@@ -1476,7 +1477,7 @@ func (ec *executionContext) fieldContext_SetTitleTimelineItem_id(ctx context.Con
 		IsMethod:   true,
 		IsResolver: true,
 		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
-			return nil, errors.New("field of type String does not have child fields")
+			return nil, errors.New("field of type CombinedId does not have child fields")
 		},
 	}
 	return fc, nil

api/graphql/graph/types.generated.go 🔗

@@ -14,6 +14,7 @@ import (
 	"github.com/99designs/gqlgen/graphql"
 	"github.com/MichaelMure/git-bug/api/graphql/models"
 	"github.com/MichaelMure/git-bug/entities/bug"
+	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/repository"
 	"github.com/vektah/gqlparser/v2/ast"
 )
@@ -410,22 +411,16 @@ func (ec *executionContext) _Authored(ctx context.Context, sel ast.SelectionSet,
 			return graphql.Null
 		}
 		return ec._AddCommentTimelineItem(ctx, sel, obj)
-	case bug.LabelChangeTimelineItem:
-		return ec._LabelChangeTimelineItem(ctx, sel, &obj)
 	case *bug.LabelChangeTimelineItem:
 		if obj == nil {
 			return graphql.Null
 		}
 		return ec._LabelChangeTimelineItem(ctx, sel, obj)
-	case bug.SetStatusTimelineItem:
-		return ec._SetStatusTimelineItem(ctx, sel, &obj)
 	case *bug.SetStatusTimelineItem:
 		if obj == nil {
 			return graphql.Null
 		}
 		return ec._SetStatusTimelineItem(ctx, sel, obj)
-	case bug.SetTitleTimelineItem:
-		return ec._SetTitleTimelineItem(ctx, sel, &obj)
 	case *bug.SetTitleTimelineItem:
 		if obj == nil {
 			return graphql.Null
@@ -588,6 +583,16 @@ func (ec *executionContext) marshalNColor2ᚖimageᚋcolorᚐRGBA(ctx context.Co
 	return ec._Color(ctx, sel, v)
 }
 
+func (ec *executionContext) unmarshalNCombinedId2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋentityᚐCombinedId(ctx context.Context, v interface{}) (entity.CombinedId, error) {
+	var res entity.CombinedId
+	err := res.UnmarshalGQL(v)
+	return res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalNCombinedId2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋentityᚐCombinedId(ctx context.Context, sel ast.SelectionSet, v entity.CombinedId) graphql.Marshaler {
+	return v
+}
+
 func (ec *executionContext) unmarshalNHash2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋrepositoryᚐHash(ctx context.Context, v interface{}) (repository.Hash, error) {
 	var res repository.Hash
 	err := res.UnmarshalGQL(v)

api/graphql/handler.go 🔗

@@ -1,4 +1,4 @@
-//go:generate go run gen_graphql.go
+//go:generate go run github.com/99designs/gqlgen generate
 
 // Package graphql contains the root GraphQL http handler
 package graphql

api/graphql/models/gen_models.go 🔗

@@ -161,10 +161,8 @@ type EditCommentInput struct {
 	ClientMutationID *string `json:"clientMutationId"`
 	// The name of the repository. If not set, the default repository is used.
 	RepoRef *string `json:"repoRef"`
-	// The bug ID's prefix.
-	Prefix string `json:"prefix"`
-	// The ID of the comment to be changed.
-	Target string `json:"target"`
+	// A prefix of the CombinedId of the comment to be changed.
+	TargetPrefix string `json:"targetPrefix"`
 	// The new message to be set.
 	Message string `json:"message"`
 	// The collection of file's hash required for the first message.

api/graphql/resolvers/bug.go 🔗

@@ -14,10 +14,6 @@ var _ graph.BugResolver = &bugResolver{}
 
 type bugResolver struct{}
 
-func (bugResolver) ID(_ context.Context, obj models.BugWrapper) (string, error) {
-	return obj.Id().String(), nil
-}
-
 func (bugResolver) HumanID(_ context.Context, obj models.BugWrapper) (string, error) {
 	return obj.Id().Human(), nil
 }

api/graphql/resolvers/comment.go 🔗

@@ -6,12 +6,17 @@ import (
 	"github.com/MichaelMure/git-bug/api/graphql/graph"
 	"github.com/MichaelMure/git-bug/api/graphql/models"
 	"github.com/MichaelMure/git-bug/entities/bug"
+	"github.com/MichaelMure/git-bug/entity"
 )
 
 var _ graph.CommentResolver = &commentResolver{}
 
 type commentResolver struct{}
 
+func (c commentResolver) ID(ctx context.Context, obj *bug.Comment) (entity.CombinedId, error) {
+	return obj.CombinedId(), nil
+}
+
 func (c commentResolver) Author(_ context.Context, obj *bug.Comment) (models.IdentityWrapper, error) {
 	return models.NewLoadedIdentity(obj.Author), nil
 }

api/graphql/resolvers/mutation.go 🔗

@@ -9,7 +9,6 @@ import (
 	"github.com/MichaelMure/git-bug/api/graphql/models"
 	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/entities/bug"
-	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/util/text"
 )
 
@@ -177,7 +176,12 @@ func (r mutationResolver) AddCommentAndReopen(ctx context.Context, input models.
 }
 
 func (r mutationResolver) EditComment(ctx context.Context, input models.EditCommentInput) (*models.EditCommentPayload, error) {
-	repo, b, err := r.getBug(input.RepoRef, input.Prefix)
+	repo, err := r.getRepo(input.RepoRef)
+	if err != nil {
+		return nil, err
+	}
+
+	b, target, err := repo.ResolveComment(input.TargetPrefix)
 	if err != nil {
 		return nil, err
 	}
@@ -190,7 +194,7 @@ func (r mutationResolver) EditComment(ctx context.Context, input models.EditComm
 	op, err := b.EditCommentRaw(
 		author,
 		time.Now().Unix(),
-		entity.Id(input.Target),
+		target,
 		text.Cleanup(input.Message),
 		nil,
 	)

api/graphql/resolvers/operations.go 🔗

@@ -13,10 +13,6 @@ var _ graph.CreateOperationResolver = createOperationResolver{}
 
 type createOperationResolver struct{}
 
-func (createOperationResolver) ID(_ context.Context, obj *bug.CreateOperation) (string, error) {
-	return obj.Id().String(), nil
-}
-
 func (createOperationResolver) Author(_ context.Context, obj *bug.CreateOperation) (models.IdentityWrapper, error) {
 	return models.NewLoadedIdentity(obj.Author()), nil
 }
@@ -30,10 +26,6 @@ var _ graph.AddCommentOperationResolver = addCommentOperationResolver{}
 
 type addCommentOperationResolver struct{}
 
-func (addCommentOperationResolver) ID(_ context.Context, obj *bug.AddCommentOperation) (string, error) {
-	return obj.Id().String(), nil
-}
-
 func (addCommentOperationResolver) Author(_ context.Context, obj *bug.AddCommentOperation) (models.IdentityWrapper, error) {
 	return models.NewLoadedIdentity(obj.Author()), nil
 }
@@ -47,10 +39,6 @@ var _ graph.EditCommentOperationResolver = editCommentOperationResolver{}
 
 type editCommentOperationResolver struct{}
 
-func (editCommentOperationResolver) ID(_ context.Context, obj *bug.EditCommentOperation) (string, error) {
-	return obj.Id().String(), nil
-}
-
 func (editCommentOperationResolver) Target(_ context.Context, obj *bug.EditCommentOperation) (string, error) {
 	return obj.Target.String(), nil
 }
@@ -68,10 +56,6 @@ var _ graph.LabelChangeOperationResolver = labelChangeOperationResolver{}
 
 type labelChangeOperationResolver struct{}
 
-func (labelChangeOperationResolver) ID(_ context.Context, obj *bug.LabelChangeOperation) (string, error) {
-	return obj.Id().String(), nil
-}
-
 func (labelChangeOperationResolver) Author(_ context.Context, obj *bug.LabelChangeOperation) (models.IdentityWrapper, error) {
 	return models.NewLoadedIdentity(obj.Author()), nil
 }
@@ -85,10 +69,6 @@ var _ graph.SetStatusOperationResolver = setStatusOperationResolver{}
 
 type setStatusOperationResolver struct{}
 
-func (setStatusOperationResolver) ID(_ context.Context, obj *bug.SetStatusOperation) (string, error) {
-	return obj.Id().String(), nil
-}
-
 func (setStatusOperationResolver) Author(_ context.Context, obj *bug.SetStatusOperation) (models.IdentityWrapper, error) {
 	return models.NewLoadedIdentity(obj.Author()), nil
 }
@@ -102,10 +82,6 @@ var _ graph.SetTitleOperationResolver = setTitleOperationResolver{}
 
 type setTitleOperationResolver struct{}
 
-func (setTitleOperationResolver) ID(_ context.Context, obj *bug.SetTitleOperation) (string, error) {
-	return obj.Id().String(), nil
-}
-
 func (setTitleOperationResolver) Author(_ context.Context, obj *bug.SetTitleOperation) (models.IdentityWrapper, error) {
 	return models.NewLoadedIdentity(obj.Author()), nil
 }

api/graphql/resolvers/timeline.go 🔗

@@ -7,6 +7,7 @@ import (
 	"github.com/MichaelMure/git-bug/api/graphql/graph"
 	"github.com/MichaelMure/git-bug/api/graphql/models"
 	"github.com/MichaelMure/git-bug/entities/bug"
+	"github.com/MichaelMure/git-bug/entity"
 )
 
 var _ graph.CommentHistoryStepResolver = commentHistoryStepResolver{}
@@ -22,8 +23,8 @@ var _ graph.AddCommentTimelineItemResolver = addCommentTimelineItemResolver{}
 
 type addCommentTimelineItemResolver struct{}
 
-func (addCommentTimelineItemResolver) ID(_ context.Context, obj *bug.AddCommentTimelineItem) (string, error) {
-	return obj.Id().String(), nil
+func (addCommentTimelineItemResolver) ID(_ context.Context, obj *bug.AddCommentTimelineItem) (entity.CombinedId, error) {
+	return obj.CombinedId(), nil
 }
 
 func (addCommentTimelineItemResolver) Author(_ context.Context, obj *bug.AddCommentTimelineItem) (models.IdentityWrapper, error) {
@@ -44,8 +45,8 @@ var _ graph.CreateTimelineItemResolver = createTimelineItemResolver{}
 
 type createTimelineItemResolver struct{}
 
-func (createTimelineItemResolver) ID(_ context.Context, obj *bug.CreateTimelineItem) (string, error) {
-	return obj.Id().String(), nil
+func (createTimelineItemResolver) ID(_ context.Context, obj *bug.CreateTimelineItem) (entity.CombinedId, error) {
+	return obj.CombinedId(), nil
 }
 
 func (r createTimelineItemResolver) Author(_ context.Context, obj *bug.CreateTimelineItem) (models.IdentityWrapper, error) {
@@ -66,8 +67,8 @@ var _ graph.LabelChangeTimelineItemResolver = labelChangeTimelineItem{}
 
 type labelChangeTimelineItem struct{}
 
-func (labelChangeTimelineItem) ID(_ context.Context, obj *bug.LabelChangeTimelineItem) (string, error) {
-	return obj.Id().String(), nil
+func (labelChangeTimelineItem) ID(_ context.Context, obj *bug.LabelChangeTimelineItem) (entity.CombinedId, error) {
+	return obj.CombinedId(), nil
 }
 
 func (i labelChangeTimelineItem) Author(_ context.Context, obj *bug.LabelChangeTimelineItem) (models.IdentityWrapper, error) {
@@ -83,8 +84,8 @@ var _ graph.SetStatusTimelineItemResolver = setStatusTimelineItem{}
 
 type setStatusTimelineItem struct{}
 
-func (setStatusTimelineItem) ID(_ context.Context, obj *bug.SetStatusTimelineItem) (string, error) {
-	return obj.Id().String(), nil
+func (setStatusTimelineItem) ID(_ context.Context, obj *bug.SetStatusTimelineItem) (entity.CombinedId, error) {
+	return obj.CombinedId(), nil
 }
 
 func (i setStatusTimelineItem) Author(_ context.Context, obj *bug.SetStatusTimelineItem) (models.IdentityWrapper, error) {
@@ -100,8 +101,8 @@ var _ graph.SetTitleTimelineItemResolver = setTitleTimelineItem{}
 
 type setTitleTimelineItem struct{}
 
-func (setTitleTimelineItem) ID(_ context.Context, obj *bug.SetTitleTimelineItem) (string, error) {
-	return obj.Id().String(), nil
+func (setTitleTimelineItem) ID(_ context.Context, obj *bug.SetTitleTimelineItem) (entity.CombinedId, error) {
+	return obj.CombinedId(), nil
 }
 
 func (i setTitleTimelineItem) Author(_ context.Context, obj *bug.SetTitleTimelineItem) (models.IdentityWrapper, error) {

api/graphql/schema/bug.graphql 🔗

@@ -1,5 +1,7 @@
 """Represents a comment on a bug."""
 type Comment implements Authored {
+  id: CombinedId!
+
   """The author of this comment."""
   author: Identity!
 
@@ -29,7 +31,7 @@ enum Status {
 
 type Bug implements Authored {
   """The identifier for this bug"""
-  id: String!
+  id: ID!
   """The human version (truncated) identifier for this bug"""
   humanId: String!
   status: Status!

api/graphql/schema/identity.graphql 🔗

@@ -1,7 +1,7 @@
 """Represents an identity"""
 type Identity {
     """The identifier for this identity"""
-    id: String!
+    id: ID!
     """The human version (truncated) identifier for this identity"""
     humanId: String!
     """The name of the person, if known."""

api/graphql/schema/mutations.graphql 🔗

@@ -95,10 +95,8 @@ input EditCommentInput {
     clientMutationId: String
     """The name of the repository. If not set, the default repository is used."""
     repoRef: String
-    """The bug ID's prefix."""
-    prefix: String!
-    """The ID of the comment to be changed."""
-    target: String!
+    """A prefix of the CombinedId of the comment to be changed."""
+    targetPrefix: String!
     """The new message to be set."""
     message: String!
     """The collection of file's hash required for the first message."""

api/graphql/schema/operations.graphql 🔗

@@ -1,7 +1,7 @@
 """An operation applied to a bug."""
 interface Operation {
     """The identifier of the operation"""
-    id: String!
+    id: ID!
     """The operations author."""
     author: Identity!
     """The datetime when this operation was issued."""
@@ -28,7 +28,7 @@ type OperationEdge {
 
 type CreateOperation implements Operation & Authored {
     """The identifier of the operation"""
-    id: String!
+    id: ID!
     """The author of this object."""
     author: Identity!
     """The datetime when this operation was issued."""
@@ -41,7 +41,7 @@ type CreateOperation implements Operation & Authored {
 
 type SetTitleOperation implements Operation & Authored {
     """The identifier of the operation"""
-    id: String!
+    id: ID!
     """The author of this object."""
     author: Identity!
     """The datetime when this operation was issued."""
@@ -53,7 +53,7 @@ type SetTitleOperation implements Operation & Authored {
 
 type AddCommentOperation implements Operation & Authored {
     """The identifier of the operation"""
-    id: String!
+    id: ID!
     """The author of this object."""
     author: Identity!
     """The datetime when this operation was issued."""
@@ -65,7 +65,7 @@ type AddCommentOperation implements Operation & Authored {
 
 type EditCommentOperation implements Operation & Authored {
     """The identifier of the operation"""
-    id: String!
+    id: ID!
     """The author of this object."""
     author: Identity!
     """The datetime when this operation was issued."""
@@ -78,7 +78,7 @@ type EditCommentOperation implements Operation & Authored {
 
 type SetStatusOperation implements Operation & Authored {
     """The identifier of the operation"""
-    id: String!
+    id: ID!
     """The author of this object."""
     author: Identity!
     """The datetime when this operation was issued."""
@@ -89,7 +89,7 @@ type SetStatusOperation implements Operation & Authored {
 
 type LabelChangeOperation implements Operation & Authored {
     """The identifier of the operation"""
-    id: String!
+    id: ID!
     """The author of this object."""
     author: Identity!
     """The datetime when this operation was issued."""

api/graphql/schema/timeline.graphql 🔗

@@ -1,7 +1,7 @@
 """An item in the timeline of events"""
 interface TimelineItem {
     """The identifier of the source operation"""
-    id: String!
+    id: CombinedId!
 }
 
 """CommentHistoryStep hold one version of a message in the history"""
@@ -31,7 +31,7 @@ type TimelineItemEdge {
 """CreateTimelineItem is a TimelineItem that represent the creation of a bug and its message edition history"""
 type CreateTimelineItem implements TimelineItem & Authored {
     """The identifier of the source operation"""
-    id: String!
+    id: CombinedId!
     author: Identity!
     message: String!
     messageIsEmpty: Boolean!
@@ -45,7 +45,7 @@ type CreateTimelineItem implements TimelineItem & Authored {
 """AddCommentTimelineItem is a TimelineItem that represent a Comment and its edition history"""
 type AddCommentTimelineItem implements TimelineItem & Authored {
     """The identifier of the source operation"""
-    id: String!
+    id: CombinedId!
     author: Identity!
     message: String!
     messageIsEmpty: Boolean!
@@ -59,7 +59,7 @@ type AddCommentTimelineItem implements TimelineItem & Authored {
 """LabelChangeTimelineItem is a TimelineItem that represent a change in the labels of a bug"""
 type LabelChangeTimelineItem implements TimelineItem & Authored {
     """The identifier of the source operation"""
-    id: String!
+    id: CombinedId!
     author: Identity!
     date: Time!
     added: [Label!]!
@@ -69,7 +69,7 @@ type LabelChangeTimelineItem implements TimelineItem & Authored {
 """SetStatusTimelineItem is a TimelineItem that represent a change in the status of a bug"""
 type SetStatusTimelineItem implements TimelineItem & Authored {
     """The identifier of the source operation"""
-    id: String!
+    id: CombinedId!
     author: Identity!
     date: Time!
     status: Status!
@@ -78,7 +78,7 @@ type SetStatusTimelineItem implements TimelineItem & Authored {
 """LabelChangeTimelineItem is a TimelineItem that represent a change in the title of a bug"""
 type SetTitleTimelineItem implements TimelineItem & Authored {
     """The identifier of the source operation"""
-    id: String!
+    id: CombinedId!
     author: Identity!
     date: Time!
     title: String!

api/graphql/tools.go 🔗

@@ -0,0 +1,8 @@
+//go:build tools
+// +build tools
+
+package graphql
+
+import (
+	_ "github.com/99designs/gqlgen"
+)

api/http/git_file_handler.go 🔗

@@ -47,7 +47,7 @@ func (gfh *gitFileHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	// TODO: this mean that the whole file will he buffered in memory
+	// TODO: this mean that the whole file will be buffered in memory
 	// This can be a problem for big files. There might be a way around
 	// that by implementing a io.ReadSeeker that would read and discard
 	// data when a seek is called.

bridge/github/export_test.go 🔗

@@ -16,6 +16,7 @@ import (
 	"github.com/MichaelMure/git-bug/bridge/core"
 	"github.com/MichaelMure/git-bug/bridge/core/auth"
 	"github.com/MichaelMure/git-bug/cache"
+	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/entity/dag"
 	"github.com/MichaelMure/git-bug/repository"
 	"github.com/MichaelMure/git-bug/util/interrupt"
@@ -66,13 +67,15 @@ func testCases(t *testing.T, repo *cache.RepoCache) []*testCase {
 	bugWithCommentEditions, createOp, err := repo.NewBug("bug with comments editions", "new bug")
 	require.NoError(t, err)
 
-	_, err = bugWithCommentEditions.EditComment(createOp.Id(), "first comment edited")
+	_, err = bugWithCommentEditions.EditComment(
+		entity.CombineIds(bugWithCommentEditions.Id(), createOp.Id()), "first comment edited")
 	require.NoError(t, err)
 
 	commentOp, err := bugWithCommentEditions.AddComment("first comment")
 	require.NoError(t, err)
 
-	_, err = bugWithCommentEditions.EditComment(commentOp.Id(), "first comment edited")
+	_, err = bugWithCommentEditions.EditComment(
+		entity.CombineIds(bugWithCommentEditions.Id(), commentOp.Id()), "first comment edited")
 	require.NoError(t, err)
 
 	// bug status changed

bridge/github/import.go 🔗

@@ -405,6 +405,7 @@ func (gi *githubImporter) ensureCommentEdit(ctx context.Context, repo *cache.Rep
 	if err != nil {
 		return err
 	}
+	// check if the comment edition already exist
 	_, err = b.ResolveOperationWithMetadata(metaKeyGithubId, parseId(edit.Id))
 	if err == nil {
 		return nil
@@ -428,7 +429,7 @@ func (gi *githubImporter) ensureCommentEdit(ctx context.Context, repo *cache.Rep
 	op, err := b.EditCommentRaw(
 		editor,
 		edit.CreatedAt.Unix(),
-		target,
+		entity.CombineIds(b.Id(), target),
 		text.Cleanup(string(*edit.Diff)),
 		map[string]string{
 			metaKeyGithubId: parseId(edit.Id),

bridge/gitlab/export_test.go 🔗

@@ -11,6 +11,7 @@ import (
 
 	"github.com/xanzy/go-gitlab"
 
+	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/entity/dag"
 
 	"github.com/stretchr/testify/require"
@@ -63,13 +64,15 @@ func testCases(t *testing.T, repo *cache.RepoCache) []*testCase {
 	bugWithCommentEditions, createOp, err := repo.NewBug("bug with comments editions", "new bug")
 	require.NoError(t, err)
 
-	_, err = bugWithCommentEditions.EditComment(createOp.Id(), "first comment edited")
+	_, err = bugWithCommentEditions.EditComment(
+		entity.CombineIds(bugWithCommentEditions.Id(), createOp.Id()), "first comment edited")
 	require.NoError(t, err)
 
 	commentOp, err := bugWithCommentEditions.AddComment("first comment")
 	require.NoError(t, err)
 
-	_, err = bugWithCommentEditions.EditComment(commentOp.Id(), "first comment edited")
+	_, err = bugWithCommentEditions.EditComment(
+		entity.CombineIds(bugWithCommentEditions.Id(), commentOp.Id()), "first comment edited")
 	require.NoError(t, err)
 
 	// bug status changed

bridge/gitlab/import.go 🔗

@@ -55,7 +55,6 @@ func (gi *gitlabImporter) Init(_ context.Context, repo *cache.RepoCache, conf co
 // ImportAll iterate over all the configured repository issues (notes) and ensure the creation
 // of the missing issues / comments / label events / title changes ...
 func (gi *gitlabImporter) ImportAll(ctx context.Context, repo *cache.RepoCache, since time.Time) (<-chan core.ImportResult, error) {
-
 	out := make(chan core.ImportResult)
 	gi.out = out
 
@@ -150,7 +149,6 @@ func (gi *gitlabImporter) ensureIssue(repo *cache.RepoCache, issue *gitlab.Issue
 }
 
 func (gi *gitlabImporter) ensureIssueEvent(repo *cache.RepoCache, b *cache.BugCache, issue *gitlab.Issue, event Event) error {
-
 	id, errResolve := b.ResolveOperationWithMetadata(metaKeyGitlabId, event.ID())
 	if errResolve != nil && errResolve != cache.ErrNoMatchingOp {
 		return errResolve
@@ -205,13 +203,14 @@ func (gi *gitlabImporter) ensureIssueEvent(repo *cache.RepoCache, b *cache.BugCa
 		// since gitlab doesn't provide the issue history
 		// we should check for "changed the description" notes and compare issue texts
 		// TODO: Check only one time and ignore next 'description change' within one issue
-		if errResolve == cache.ErrNoMatchingOp && issue.Description != firstComment.Message {
+		cleanedDesc := text.Cleanup(issue.Description)
+		if errResolve == cache.ErrNoMatchingOp && cleanedDesc != firstComment.Message {
 			// comment edition
 			op, err := b.EditCommentRaw(
 				author,
 				event.(NoteEvent).UpdatedAt.Unix(),
-				firstComment.Id(),
-				text.Cleanup(issue.Description),
+				firstComment.CombinedId(),
+				cleanedDesc,
 				map[string]string{
 					metaKeyGitlabId: event.ID(),
 				},
@@ -249,7 +248,7 @@ func (gi *gitlabImporter) ensureIssueEvent(repo *cache.RepoCache, b *cache.BugCa
 		// if comment was already exported
 
 		// search for last comment update
-		comment, err := b.Snapshot().SearchComment(id)
+		comment, err := b.Snapshot().SearchCommentByOpId(id)
 		if err != nil {
 			return err
 		}
@@ -260,7 +259,7 @@ func (gi *gitlabImporter) ensureIssueEvent(repo *cache.RepoCache, b *cache.BugCa
 			op, err := b.EditCommentRaw(
 				author,
 				event.(NoteEvent).UpdatedAt.Unix(),
-				comment.Id(),
+				comment.CombinedId(),
 				cleanText,
 				nil,
 			)

bridge/jira/import.go 🔗

@@ -270,8 +270,7 @@ func (ji *jiraImporter) ensureComment(repo *cache.RepoCache, b *cache.BugCache,
 		return err
 	}
 
-	targetOpID, err := b.ResolveOperationWithMetadata(
-		metaKeyJiraId, item.ID)
+	targetOpID, err := b.ResolveOperationWithMetadata(metaKeyJiraId, item.ID)
 	if err != nil && err != cache.ErrNoMatchingOp {
 		return err
 	}
@@ -334,7 +333,7 @@ func (ji *jiraImporter) ensureComment(repo *cache.RepoCache, b *cache.BugCache,
 	op, err := b.EditCommentRaw(
 		editor,
 		item.Updated.Unix(),
-		targetOpID,
+		entity.CombineIds(b.Id(), targetOpID),
 		text.Cleanup(item.Body),
 		map[string]string{
 			metaKeyJiraId: derivedID,

cache/bug_cache.go 🔗

@@ -209,7 +209,7 @@ func (c *BugCache) EditCreateCommentRaw(author *IdentityCache, unixTime int64, b
 	return op, c.notifyUpdated()
 }
 
-func (c *BugCache) EditComment(target entity.Id, message string) (*bug.EditCommentOperation, error) {
+func (c *BugCache) EditComment(target entity.CombinedId, message string) (*bug.EditCommentOperation, error) {
 	author, err := c.repoCache.GetUserIdentity()
 	if err != nil {
 		return nil, err
@@ -218,9 +218,14 @@ func (c *BugCache) EditComment(target entity.Id, message string) (*bug.EditComme
 	return c.EditCommentRaw(author, time.Now().Unix(), target, message, nil)
 }
 
-func (c *BugCache) EditCommentRaw(author *IdentityCache, unixTime int64, target entity.Id, message string, metadata map[string]string) (*bug.EditCommentOperation, error) {
+func (c *BugCache) EditCommentRaw(author *IdentityCache, unixTime int64, target entity.CombinedId, message string, metadata map[string]string) (*bug.EditCommentOperation, error) {
+	comment, err := c.Snapshot().SearchComment(target)
+	if err != nil {
+		return nil, err
+	}
+
 	c.mu.Lock()
-	op, err := bug.EditComment(c.bug, author.Identity, unixTime, target, message, nil, metadata)
+	op, err := bug.EditComment(c.bug, author.Identity, unixTime, comment.TargetId(), message, nil, metadata)
 	c.mu.Unlock()
 	if err != nil {
 		return nil, err

cache/repo_cache_bug.go 🔗

@@ -263,7 +263,7 @@ func (c *RepoCache) resolveBugMatcher(f func(*BugExcerpt) bool) (entity.Id, erro
 // ResolveComment search for a Bug/Comment combination matching the merged
 // bug/comment Id prefix. Returns the Bug containing the Comment and the Comment's
 // Id.
-func (c *RepoCache) ResolveComment(prefix string) (*BugCache, entity.Id, error) {
+func (c *RepoCache) ResolveComment(prefix string) (*BugCache, entity.CombinedId, error) {
 	bugPrefix, _ := entity.SeparateIds(prefix)
 	bugCandidate := make([]entity.Id, 0, 5)
 
@@ -277,7 +277,7 @@ func (c *RepoCache) ResolveComment(prefix string) (*BugCache, entity.Id, error)
 	c.muBug.RUnlock()
 
 	matchingBugIds := make([]entity.Id, 0, 5)
-	matchingCommentId := entity.UnsetId
+	matchingCommentId := entity.UnsetCombinedId
 	var matchingBug *BugCache
 
 	// search for matching comments
@@ -286,22 +286,22 @@ func (c *RepoCache) ResolveComment(prefix string) (*BugCache, entity.Id, error)
 	for _, bugId := range bugCandidate {
 		b, err := c.ResolveBug(bugId)
 		if err != nil {
-			return nil, entity.UnsetId, err
+			return nil, entity.UnsetCombinedId, err
 		}
 
 		for _, comment := range b.Snapshot().Comments {
-			if comment.Id().HasPrefix(prefix) {
+			if comment.TargetId().HasPrefix(prefix) {
 				matchingBugIds = append(matchingBugIds, bugId)
 				matchingBug = b
-				matchingCommentId = comment.Id()
+				matchingCommentId = comment.CombinedId()
 			}
 		}
 	}
 
 	if len(matchingBugIds) > 1 {
-		return nil, entity.UnsetId, entity.NewErrMultipleMatch("bug/comment", matchingBugIds)
+		return nil, entity.UnsetCombinedId, entity.NewErrMultipleMatch("bug/comment", matchingBugIds)
 	} else if len(matchingBugIds) == 0 {
-		return nil, entity.UnsetId, errors.New("comment doesn't exist")
+		return nil, entity.UnsetCombinedId, errors.New("comment doesn't exist")
 	}
 
 	return matchingBug, matchingCommentId, nil

commands/comment.go 🔗

@@ -41,7 +41,7 @@ func runComment(env *Env, args []string) error {
 		}
 
 		env.out.Printf("Author: %s\n", colors.Magenta(comment.Author.DisplayName()))
-		env.out.Printf("Id: %s\n", colors.Cyan(comment.Id().Human()))
+		env.out.Printf("Id: %s\n", colors.Cyan(comment.CombinedId().Human()))
 		env.out.Printf("Date: %s\n\n", comment.FormatTime())
 		env.out.Println(text.LeftPadLines(comment.Message, 4))
 	}

commands/show.go 🔗

@@ -163,7 +163,7 @@ func showDefaultFormatter(env *Env, snapshot *bug.Snapshot) error {
 		var message string
 		env.out.Printf("%s%s #%d %s <%s>\n\n",
 			indent,
-			comment.Id().Human(),
+			comment.CombinedId().Human(),
 			i,
 			comment.Author.DisplayName(),
 			comment.Author.Email(),
@@ -207,8 +207,8 @@ type JSONComment struct {
 
 func NewJSONComment(comment bug.Comment) JSONComment {
 	return JSONComment{
-		Id:      comment.Id().String(),
-		HumanId: comment.Id().Human(),
+		Id:      comment.CombinedId().String(),
+		HumanId: comment.CombinedId().Human(),
 		Author:  NewJSONIdentity(comment.Author),
 		Message: comment.Message,
 	}

entities/bug/comment.go 🔗

@@ -11,34 +11,41 @@ import (
 
 // Comment represent a comment in a Bug
 type Comment struct {
-	// id should be the result of entity.CombineIds with the Bug id and the id
+	// combinedId should be the result of entity.CombineIds with the Bug id and the id
 	// of the Operation that created the comment
-	id      entity.Id
+	combinedId entity.CombinedId
+
+	// targetId is the Id of the Operation that originally created that Comment
+	targetId entity.Id
+
 	Author  identity.Interface
 	Message string
 	Files   []repository.Hash
 
 	// Creation time of the comment.
 	// Should be used only for human display, never for ordering as we can't rely on it in a distributed system.
-	UnixTime timestamp.Timestamp
+	unixTime timestamp.Timestamp
 }
 
-// Id return the Comment identifier
-func (c Comment) Id() entity.Id {
-	if c.id == "" {
+func (c Comment) CombinedId() entity.CombinedId {
+	if c.combinedId == "" {
 		// simply panic as it would be a coding error (no id provided at construction)
-		panic("no id")
+		panic("no combined id")
 	}
-	return c.id
+	return c.combinedId
+}
+
+func (c Comment) TargetId() entity.Id {
+	return c.targetId
 }
 
-// FormatTimeRel format the UnixTime of the comment for human consumption
+// FormatTimeRel format the unixTime of the comment for human consumption
 func (c Comment) FormatTimeRel() string {
-	return humanize.Time(c.UnixTime.Time())
+	return humanize.Time(c.unixTime.Time())
 }
 
 func (c Comment) FormatTime() string {
-	return c.UnixTime.Time().Format("Mon Jan 2 15:04:05 2006 +0200")
+	return c.unixTime.Time().Format("Mon Jan 2 15:04:05 2006 +0200")
 }
 
 // IsAuthored is a sign post method for gqlgen

entities/bug/op_add_comment.go 🔗

@@ -30,12 +30,15 @@ func (op *AddCommentOperation) Apply(snapshot *Snapshot) {
 	snapshot.addActor(op.Author())
 	snapshot.addParticipant(op.Author())
 
+	opId := op.Id()
+
 	comment := Comment{
-		id:       entity.CombineIds(snapshot.Id(), op.Id()),
-		Message:  op.Message,
-		Author:   op.Author(),
-		Files:    op.Files,
-		UnixTime: timestamp.Timestamp(op.UnixTime),
+		combinedId: entity.CombineIds(snapshot.Id(), opId),
+		targetId:   opId,
+		Message:    op.Message,
+		Author:     op.Author(),
+		Files:      op.Files,
+		unixTime:   timestamp.Timestamp(op.UnixTime),
 	}
 
 	snapshot.Comments = append(snapshot.Comments, comment)
@@ -71,7 +74,7 @@ func NewAddCommentOp(author identity.Interface, unixTime int64, message string,
 	}
 }
 
-// AddCommentTimelineItem hold a comment in the timeline
+// AddCommentTimelineItem replace a AddComment operation in the Timeline and hold its edition history
 type AddCommentTimelineItem struct {
 	CommentTimelineItem
 }

entities/bug/op_create.go 🔗

@@ -32,7 +32,9 @@ func (op *CreateOperation) Apply(snapshot *Snapshot) {
 		return
 	}
 
-	snapshot.id = op.Id()
+	// the Id of the Bug/Snapshot is the Id of the first Operation: CreateOperation
+	opId := op.Id()
+	snapshot.id = opId
 
 	snapshot.addActor(op.Author())
 	snapshot.addParticipant(op.Author())
@@ -40,10 +42,11 @@ func (op *CreateOperation) Apply(snapshot *Snapshot) {
 	snapshot.Title = op.Title
 
 	comment := Comment{
-		id:       entity.CombineIds(snapshot.Id(), op.Id()),
-		Message:  op.Message,
-		Author:   op.Author(),
-		UnixTime: timestamp.Timestamp(op.UnixTime),
+		combinedId: entity.CombineIds(snapshot.id, opId),
+		targetId:   opId,
+		Message:    op.Message,
+		Author:     op.Author(),
+		unixTime:   timestamp.Timestamp(op.UnixTime),
 	}
 
 	snapshot.Comments = []Comment{comment}

entities/bug/op_create_test.go 🔗

@@ -6,55 +6,37 @@ import (
 
 	"github.com/stretchr/testify/require"
 
+	"github.com/MichaelMure/git-bug/entities/common"
 	"github.com/MichaelMure/git-bug/entities/identity"
 	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/entity/dag"
 	"github.com/MichaelMure/git-bug/repository"
-	"github.com/MichaelMure/git-bug/util/timestamp"
 )
 
 func TestCreate(t *testing.T) {
-	snapshot := Snapshot{}
-
-	repo := repository.NewMockRepoClock()
+	repo := repository.NewMockRepo()
 
 	rene, err := identity.NewIdentity(repo, "RenĂŠ Descartes", "rene@descartes.fr")
 	require.NoError(t, err)
 
-	unix := time.Now().Unix()
-
-	create := NewCreateOp(rene, unix, "title", "message", nil)
-
-	create.Apply(&snapshot)
-
-	id := create.Id()
-	require.NoError(t, id.Validate())
-
-	comment := Comment{
-		id:       entity.CombineIds(create.Id(), create.Id()),
-		Author:   rene,
-		Message:  "message",
-		UnixTime: timestamp.Timestamp(create.UnixTime),
-	}
-
-	expected := Snapshot{
-		id:    create.Id(),
-		Title: "title",
-		Comments: []Comment{
-			comment,
-		},
-		Author:       rene,
-		Participants: []identity.Interface{rene},
-		Actors:       []identity.Interface{rene},
-		CreateTime:   create.Time(),
-		Timeline: []TimelineItem{
-			&CreateTimelineItem{
-				CommentTimelineItem: NewCommentTimelineItem(comment),
-			},
-		},
-	}
+	b, op, err := Create(rene, time.Now().Unix(), "title", "message", nil, nil)
+	require.NoError(t, err)
 
-	require.Equal(t, expected, snapshot)
+	require.Equal(t, "title", op.Title)
+	require.Equal(t, "message", op.Message)
+
+	// Create generate the initial operation and create a new timeline item
+	snap := b.Compile()
+	require.Equal(t, common.OpenStatus, snap.Status)
+	require.Equal(t, rene, snap.Author)
+	require.Equal(t, "title", snap.Title)
+	require.Len(t, snap.Operations, 1)
+	require.Equal(t, op, snap.Operations[0])
+
+	require.Len(t, snap.Timeline, 1)
+	require.Equal(t, entity.CombineIds(b.Id(), op.Id()), snap.Timeline[0].CombinedId())
+	require.Equal(t, rene, snap.Timeline[0].(*CreateTimelineItem).Author)
+	require.Equal(t, "message", snap.Timeline[0].(*CreateTimelineItem).Message)
 }
 
 func TestCreateSerialize(t *testing.T) {

entities/bug/op_edit_comment.go 🔗

@@ -33,12 +33,12 @@ func (op *EditCommentOperation) Apply(snapshot *Snapshot) {
 	// Todo: currently any message can be edited, even by a different author
 	// crypto signature are needed.
 
-	// Recreate the Comment Id to match on
-	commentId := entity.CombineIds(snapshot.Id(), op.Target)
+	// Recreate the combined Id to match on
+	combinedId := entity.CombineIds(snapshot.Id(), op.Target)
 
 	var target TimelineItem
 	for i, item := range snapshot.Timeline {
-		if item.Id() == commentId {
+		if item.CombinedId() == combinedId {
 			target = snapshot.Timeline[i]
 			break
 		}
@@ -50,10 +50,11 @@ func (op *EditCommentOperation) Apply(snapshot *Snapshot) {
 	}
 
 	comment := Comment{
-		id:       commentId,
-		Message:  op.Message,
-		Files:    op.Files,
-		UnixTime: timestamp.Timestamp(op.UnixTime),
+		combinedId: combinedId,
+		targetId:   op.Target,
+		Message:    op.Message,
+		Files:      op.Files,
+		unixTime:   timestamp.Timestamp(op.UnixTime),
 	}
 
 	switch target := target.(type) {
@@ -72,7 +73,7 @@ func (op *EditCommentOperation) Apply(snapshot *Snapshot) {
 	// Updating the corresponding comment
 
 	for i := range snapshot.Comments {
-		if snapshot.Comments[i].Id() == commentId {
+		if snapshot.Comments[i].CombinedId() == combinedId {
 			snapshot.Comments[i].Message = op.Message
 			snapshot.Comments[i].Files = op.Files
 			break

entities/bug/op_label_change.go 🔗

@@ -59,12 +59,14 @@ AddLoop:
 		return string(snapshot.Labels[i]) < string(snapshot.Labels[j])
 	})
 
+	id := op.Id()
 	item := &LabelChangeTimelineItem{
-		id:       op.Id(),
-		Author:   op.Author(),
-		UnixTime: timestamp.Timestamp(op.UnixTime),
-		Added:    op.Added,
-		Removed:  op.Removed,
+		// id:         id,
+		combinedId: entity.CombineIds(snapshot.Id(), id),
+		Author:     op.Author(),
+		UnixTime:   timestamp.Timestamp(op.UnixTime),
+		Added:      op.Added,
+		Removed:    op.Removed,
 	}
 
 	snapshot.Timeline = append(snapshot.Timeline, item)
@@ -103,19 +105,20 @@ func NewLabelChangeOperation(author identity.Interface, unixTime int64, added, r
 }
 
 type LabelChangeTimelineItem struct {
-	id       entity.Id
-	Author   identity.Interface
-	UnixTime timestamp.Timestamp
-	Added    []Label
-	Removed  []Label
+	// id         entity.Id
+	combinedId entity.CombinedId
+	Author     identity.Interface
+	UnixTime   timestamp.Timestamp
+	Added      []Label
+	Removed    []Label
 }
 
-func (l LabelChangeTimelineItem) Id() entity.Id {
-	return l.id
+func (l LabelChangeTimelineItem) CombinedId() entity.CombinedId {
+	return l.combinedId
 }
 
 // IsAuthored is a sign post method for gqlgen
-func (l LabelChangeTimelineItem) IsAuthored() {}
+func (l *LabelChangeTimelineItem) IsAuthored() {}
 
 // ChangeLabels is a convenience function to change labels on a bug
 func ChangeLabels(b Interface, author identity.Interface, unixTime int64, add, remove []string, metadata map[string]string) ([]LabelChangeResult, *LabelChangeOperation, error) {
@@ -180,7 +183,7 @@ func ChangeLabels(b Interface, author identity.Interface, unixTime int64, add, r
 }
 
 // ForceChangeLabels is a convenience function to apply the operation
-// The difference with ChangeLabels is that no checks of deduplications are done. You are entirely
+// The difference with ChangeLabels is that no checks for deduplication are done. You are entirely
 // responsible for what you are doing. In the general case, you want to use ChangeLabels instead.
 // The intended use of this function is to allow importers to create legal but unexpected label changes,
 // like removing a label with no information of when it was added before.

entities/bug/op_set_status.go 🔗

@@ -26,11 +26,13 @@ func (op *SetStatusOperation) Apply(snapshot *Snapshot) {
 	snapshot.Status = op.Status
 	snapshot.addActor(op.Author())
 
+	id := op.Id()
 	item := &SetStatusTimelineItem{
-		id:       op.Id(),
-		Author:   op.Author(),
-		UnixTime: timestamp.Timestamp(op.UnixTime),
-		Status:   op.Status,
+		// id:         id,
+		combinedId: entity.CombineIds(snapshot.Id(), id),
+		Author:     op.Author(),
+		UnixTime:   timestamp.Timestamp(op.UnixTime),
+		Status:     op.Status,
 	}
 
 	snapshot.Timeline = append(snapshot.Timeline, item)
@@ -56,18 +58,19 @@ func NewSetStatusOp(author identity.Interface, unixTime int64, status common.Sta
 }
 
 type SetStatusTimelineItem struct {
-	id       entity.Id
-	Author   identity.Interface
-	UnixTime timestamp.Timestamp
-	Status   common.Status
+	// id         entity.Id
+	combinedId entity.CombinedId
+	Author     identity.Interface
+	UnixTime   timestamp.Timestamp
+	Status     common.Status
 }
 
-func (s SetStatusTimelineItem) Id() entity.Id {
-	return s.id
+func (s SetStatusTimelineItem) CombinedId() entity.CombinedId {
+	return s.combinedId
 }
 
 // IsAuthored is a sign post method for gqlgen
-func (s SetStatusTimelineItem) IsAuthored() {}
+func (s *SetStatusTimelineItem) IsAuthored() {}
 
 // Open is a convenience function to change a bugs state to Open
 func Open(b Interface, author identity.Interface, unixTime int64, metadata map[string]string) (*SetStatusOperation, error) {

entities/bug/op_set_title.go 🔗

@@ -28,12 +28,14 @@ func (op *SetTitleOperation) Apply(snapshot *Snapshot) {
 	snapshot.Title = op.Title
 	snapshot.addActor(op.Author())
 
+	id := op.Id()
 	item := &SetTitleTimelineItem{
-		id:       op.Id(),
-		Author:   op.Author(),
-		UnixTime: timestamp.Timestamp(op.UnixTime),
-		Title:    op.Title,
-		Was:      op.Was,
+		id:         id,
+		combinedId: entity.CombineIds(snapshot.Id(), id),
+		Author:     op.Author(),
+		UnixTime:   timestamp.Timestamp(op.UnixTime),
+		Title:      op.Title,
+		Was:        op.Was,
 	}
 
 	snapshot.Timeline = append(snapshot.Timeline, item)
@@ -68,19 +70,24 @@ func NewSetTitleOp(author identity.Interface, unixTime int64, title string, was
 }
 
 type SetTitleTimelineItem struct {
-	id       entity.Id
-	Author   identity.Interface
-	UnixTime timestamp.Timestamp
-	Title    string
-	Was      string
+	id         entity.Id
+	combinedId entity.CombinedId
+	Author     identity.Interface
+	UnixTime   timestamp.Timestamp
+	Title      string
+	Was        string
 }
 
 func (s SetTitleTimelineItem) Id() entity.Id {
 	return s.id
 }
 
+func (s SetTitleTimelineItem) CombinedId() entity.CombinedId {
+	return s.combinedId
+}
+
 // IsAuthored is a sign post method for gqlgen
-func (s SetTitleTimelineItem) IsAuthored() {}
+func (s *SetTitleTimelineItem) IsAuthored() {}
 
 // SetTitle is a convenience function to change a bugs title
 func SetTitle(b Interface, author identity.Interface, unixTime int64, title string, metadata map[string]string) (*SetTitleOperation, error) {

entities/bug/snapshot.go 🔗

@@ -58,9 +58,9 @@ func (snap *Snapshot) GetCreateMetadata(key string) (string, bool) {
 }
 
 // SearchTimelineItem will search in the timeline for an item matching the given hash
-func (snap *Snapshot) SearchTimelineItem(id entity.Id) (TimelineItem, error) {
+func (snap *Snapshot) SearchTimelineItem(id entity.CombinedId) (TimelineItem, error) {
 	for i := range snap.Timeline {
-		if snap.Timeline[i].Id() == id {
+		if snap.Timeline[i].CombinedId() == id {
 			return snap.Timeline[i], nil
 		}
 	}
@@ -68,15 +68,26 @@ func (snap *Snapshot) SearchTimelineItem(id entity.Id) (TimelineItem, error) {
 	return nil, fmt.Errorf("timeline item not found")
 }
 
-// SearchComment will search for a comment matching the given hash
-func (snap *Snapshot) SearchComment(id entity.Id) (*Comment, error) {
+// SearchComment will search for a comment matching the given id
+func (snap *Snapshot) SearchComment(id entity.CombinedId) (*Comment, error) {
 	for _, c := range snap.Comments {
-		if c.id == id {
+		if c.combinedId == id {
 			return &c, nil
 		}
 	}
 
-	return nil, fmt.Errorf("comment item not found")
+	return nil, fmt.Errorf("comment not found")
+}
+
+// SearchCommentByOpId will search for a comment generated by the given operation Id
+func (snap *Snapshot) SearchCommentByOpId(id entity.Id) (*Comment, error) {
+	for _, c := range snap.Comments {
+		if c.targetId == id {
+			return &c, nil
+		}
+	}
+
+	return nil, fmt.Errorf("comment not found")
 }
 
 // append the operation author to the actors list

entities/bug/timeline.go 🔗

@@ -10,8 +10,8 @@ import (
 )
 
 type TimelineItem interface {
-	// Id return the identifier of the item
-	Id() entity.Id
+	// CombinedId returns the global identifier of the item
+	CombinedId() entity.CombinedId
 }
 
 // CommentHistoryStep hold one version of a message in the history
@@ -26,46 +26,46 @@ type CommentHistoryStep struct {
 
 // CommentTimelineItem is a TimelineItem that holds a Comment and its edition history
 type CommentTimelineItem struct {
-	// id should be the same as in Comment
-	id        entity.Id
-	Author    identity.Interface
-	Message   string
-	Files     []repository.Hash
-	CreatedAt timestamp.Timestamp
-	LastEdit  timestamp.Timestamp
-	History   []CommentHistoryStep
+	combinedId entity.CombinedId
+	Author     identity.Interface
+	Message    string
+	Files      []repository.Hash
+	CreatedAt  timestamp.Timestamp
+	LastEdit   timestamp.Timestamp
+	History    []CommentHistoryStep
 }
 
 func NewCommentTimelineItem(comment Comment) CommentTimelineItem {
 	return CommentTimelineItem{
-		id:        comment.id,
-		Author:    comment.Author,
-		Message:   comment.Message,
-		Files:     comment.Files,
-		CreatedAt: comment.UnixTime,
-		LastEdit:  comment.UnixTime,
+		// id: comment.id,
+		combinedId: comment.combinedId,
+		Author:     comment.Author,
+		Message:    comment.Message,
+		Files:      comment.Files,
+		CreatedAt:  comment.unixTime,
+		LastEdit:   comment.unixTime,
 		History: []CommentHistoryStep{
 			{
 				Message:  comment.Message,
-				UnixTime: comment.UnixTime,
+				UnixTime: comment.unixTime,
 			},
 		},
 	}
 }
 
-func (c *CommentTimelineItem) Id() entity.Id {
-	return c.id
+func (c *CommentTimelineItem) CombinedId() entity.CombinedId {
+	return c.combinedId
 }
 
 // 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.LastEdit = comment.unixTime
 	c.History = append(c.History, CommentHistoryStep{
 		Author:   comment.Author,
 		Message:  comment.Message,
-		UnixTime: comment.UnixTime,
+		UnixTime: comment.unixTime,
 	})
 }
 

entity/id.go 🔗

@@ -79,21 +79,3 @@ func (i Id) Validate() error {
 	}
 	return nil
 }
-
-/*
- * Sorting
- */
-
-type Alphabetical []Id
-
-func (a Alphabetical) Len() int {
-	return len(a)
-}
-
-func (a Alphabetical) Less(i, j int) bool {
-	return a[i] < a[j]
-}
-
-func (a Alphabetical) Swap(i, j int) {
-	a[i], a[j] = a[j], a[i]
-}

entity/id_interleaved.go 🔗

@@ -1,13 +1,91 @@
 package entity
 
 import (
+	"fmt"
+	"io"
 	"strings"
+
+	"github.com/pkg/errors"
 )
 
+const UnsetCombinedId = CombinedId("unset")
+
+// CombinedId is an Id holding information from both a primary Id and a secondary Id.
+// While it looks like a regular Id, do not just cast from one to another.
+// Instead, use CombineIds and SeparateIds to create it and split it.
+type CombinedId string
+
+// String return the identifier as a string
+func (ci CombinedId) String() string {
+	return string(ci)
+}
+
+// Human return the identifier, shortened for human consumption
+func (ci CombinedId) Human() string {
+	format := fmt.Sprintf("%%.%ds", humanIdLength)
+	return fmt.Sprintf(format, ci)
+}
+
+func (ci CombinedId) HasPrefix(prefix string) bool {
+	return strings.HasPrefix(string(ci), prefix)
+}
+
+// UnmarshalGQL implement the Unmarshaler interface for gqlgen
+func (ci *CombinedId) UnmarshalGQL(v interface{}) error {
+	_, ok := v.(string)
+	if !ok {
+		return fmt.Errorf("CombinedIds must be strings")
+	}
+
+	*ci = v.(CombinedId)
+
+	if err := ci.Validate(); err != nil {
+		return errors.Wrap(err, "invalid CombinedId")
+	}
+
+	return nil
+}
+
+// MarshalGQL implement the Marshaler interface for gqlgen
+func (ci CombinedId) MarshalGQL(w io.Writer) {
+	_, _ = w.Write([]byte(`"` + ci.String() + `"`))
+}
+
+// Validate tell if the Id is valid
+func (ci CombinedId) Validate() error {
+	// Special case to detect outdated repo
+	if len(ci) == 40 {
+		return fmt.Errorf("outdated repository format, please use https://github.com/MichaelMure/git-bug-migration to upgrade")
+	}
+	if len(ci) != idLength {
+		return fmt.Errorf("invalid length")
+	}
+	for _, r := range ci {
+		if (r < 'a' || r > 'z') && (r < '0' || r > '9') {
+			return fmt.Errorf("invalid character")
+		}
+	}
+	return nil
+}
+
+// PrimaryPrefix is a helper to extract the primary prefix.
+// If practical, use SeparateIds instead.
+func (ci CombinedId) PrimaryPrefix() string {
+	primaryPrefix, _ := SeparateIds(string(ci))
+	return primaryPrefix
+}
+
+// SecondaryPrefix is a helper to extract the secondary prefix.
+// If practical, use SeparateIds instead.
+func (ci CombinedId) SecondaryPrefix() string {
+	_, secondaryPrefix := SeparateIds(string(ci))
+	return secondaryPrefix
+}
+
 // CombineIds compute a merged Id holding information from both the primary Id
 // and the secondary Id.
 //
-// This allow to later find efficiently a secondary element because we can access
+// This allows to later find efficiently a secondary element because we can access
 // the primary one directly instead of searching for a primary that has a
 // secondary matching the Id.
 //
@@ -32,7 +110,7 @@ import (
 // 7:    4P, 3S
 // 10:   6P, 4S
 // 16:  11P, 5S
-func CombineIds(primary Id, secondary Id) Id {
+func CombineIds(primary Id, secondary Id) CombinedId {
 	var id strings.Builder
 
 	for i := 0; i < idLength; i++ {
@@ -46,7 +124,7 @@ func CombineIds(primary Id, secondary Id) Id {
 		}
 	}
 
-	return Id(id.String())
+	return CombinedId(id.String())
 }
 
 // SeparateIds extract primary and secondary prefix from an arbitrary length prefix

entity/id_interleaved_test.go 🔗

@@ -9,7 +9,7 @@ import (
 func TestInterleaved(t *testing.T) {
 	primary := Id("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWX______________")
 	secondary := Id("YZ0123456789+/________________________________________________")
-	expectedId := Id("aYbZc0def1ghij2klmn3opqr4stuv5wxyz6ABCD7EFGH8IJKL9MNOP+QRST/UVWX")
+	expectedId := CombinedId("aYbZc0def1ghij2klmn3opqr4stuv5wxyz6ABCD7EFGH8IJKL9MNOP+QRST/UVWX")
 
 	interleaved := CombineIds(primary, secondary)
 	require.Equal(t, expectedId, interleaved)

go.mod 🔗

@@ -89,8 +89,10 @@ require (
 	github.com/steveyen/gtreap v0.1.0 // indirect
 	github.com/stretchr/objx v0.4.0 // indirect
 	github.com/tinylib/msgp v1.1.0 // indirect
+	github.com/urfave/cli/v2 v2.8.1 // indirect
 	github.com/willf/bitset v1.1.10 // indirect
 	github.com/xanzy/ssh-agent v0.3.0 // indirect
+	github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
 	go.etcd.io/bbolt v1.3.5 // indirect
 	golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
 	golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 // indirect

go.sum 🔗

@@ -365,6 +365,7 @@ github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c/go.mod h1:ahpPrc7
 github.com/tinylib/msgp v1.1.0 h1:9fQd+ICuRIu/ue4vxJZu6/LzxN0HwMds2nq/0cFvxHU=
 github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
 github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
+github.com/urfave/cli/v2 v2.8.1 h1:CGuYNZF9IKZY/rfBe3lJpccSoIY1ytfvmgQT90cNOl4=
 github.com/urfave/cli/v2 v2.8.1/go.mod h1:Z41J9TPoffeoqP0Iza0YbAhGvymRdZAd2uPmZ5JxRdY=
 github.com/vektah/gqlparser/v2 v2.4.6 h1:Yjzp66g6oVq93Jihbi0qhGnf/6zIWjcm8H6gA27zstE=
 github.com/vektah/gqlparser/v2 v2.4.6/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0=
@@ -375,6 +376,7 @@ github.com/xanzy/go-gitlab v0.68.0/go.mod h1:o4yExCtdaqlM8YGdDJWuZoBmfxBsmA9TPEj
 github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI=
 github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
 github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=

repository/gogit.go 🔗

@@ -476,6 +476,7 @@ func (repo *GoGitRepo) ReadData(hash Hash) ([]byte, error) {
 		return nil, err
 	}
 
+	// TODO: return a io.Reader instead
 	return ioutil.ReadAll(r)
 }
 

termui/show_bug.go 🔗

@@ -244,7 +244,7 @@ func (sb *showBug) renderMain(g *gocui.Gui, mainView *gocui.View) error {
 	y0 += lines + 1
 
 	for _, op := range snap.Timeline {
-		viewName := op.Id().String()
+		viewName := op.CombinedId().String()
 
 		// TODO: me might skip the rendering of blocks that are outside of the view
 		// but to do that we need to rework how sb.mainSelectableView is maintained
@@ -647,16 +647,16 @@ func (sb *showBug) edit(g *gocui.Gui, v *gocui.View) error {
 		return nil
 	}
 
-	op, err := snap.SearchTimelineItem(entity.Id(sb.selected))
+	op, err := snap.SearchTimelineItem(entity.CombinedId(sb.selected))
 	if err != nil {
 		return err
 	}
 
 	switch op := op.(type) {
 	case *bug.AddCommentTimelineItem:
-		return editCommentWithEditor(sb.bug, op.Id(), op.Message)
+		return editCommentWithEditor(sb.bug, op.CombinedId(), op.Message)
 	case *bug.CreateTimelineItem:
-		return editCommentWithEditor(sb.bug, op.Id(), op.Message)
+		return editCommentWithEditor(sb.bug, op.CombinedId(), op.Message)
 	case *bug.LabelChangeTimelineItem:
 		return sb.editLabels(g, snap)
 	}

termui/termui.go 🔗

@@ -250,7 +250,7 @@ func addCommentWithEditor(bug *cache.BugCache) error {
 	return errTerminateMainloop
 }
 
-func editCommentWithEditor(bug *cache.BugCache, target entity.Id, preMessage string) error {
+func editCommentWithEditor(bug *cache.BugCache, target entity.CombinedId, preMessage string) error {
 	// This is somewhat hacky.
 	// As there is no way to pause gocui, run the editor and restart gocui,
 	// we have to stop it entirely and start a new one later.