finish the refactoring for the dedicated identifier type

Michael Muré created

Change summary

bridge/core/export.go                    |  28 +-
bridge/github/export.go                  |  51 ++--
bridge/github/export_test.go             |   4 
bridge/github/import.go                  |  10 
bridge/gitlab/import.go                  |   4 
bridge/launchpad/import.go               |   7 
bug/bug.go                               |  16 
bug/bug_actions.go                       |   9 
bug/op_add_comment_test.go               |   4 
bug/snapshot.go                          |   4 
cache/bug_cache.go                       |  42 +--
cache/bug_excerpt.go                     |  16 
cache/identity_excerpt.go                |   9 
cache/repo_cache.go                      |  74 +++---
cache/repo_cache_test.go                 |   8 
commands/add.go                          |   2 
commands/comment.go                      |   2 
commands/ls-id.go                        |   6 
commands/ls.go                           |   2 
commands/pull.go                         |   6 
commands/select.go                       |   2 
commands/select/select.go                |   9 
commands/select/select_test.go           |   6 
commands/show.go                         |   6 
commands/user.go                         |   2 
commands/user_ls.go                      |   2 
entity/err.go                            |  26 ++
entity/merge.go                          |  12 
graphql/connections/connections.go       |   4 
graphql/connections/gen_lazy_bug.go      |  11 
graphql/connections/gen_lazy_identity.go |  11 
graphql/connections/lazy_bug.go          |   4 
graphql/connections/lazy_identity.go     |   4 
graphql/graph/gen_graph.go               | 305 +++++++++++++++++++------
graphql/resolvers/bug.go                 |   8 
graphql/resolvers/identity.go            |   4 
graphql/resolvers/operations.go          |  45 +++
graphql/resolvers/repo.go                |   9 
graphql/resolvers/root.go                |   2 
graphql/resolvers/timeline.go            |  33 ++
identity/bare.go                         |   3 
identity/common.go                       |  11 
identity/identity_actions.go             |   9 
termui/bug_table.go                      |   6 
termui/show_bug.go                       |  11 
termui/termui.go                         |   3 
46 files changed, 552 insertions(+), 300 deletions(-)

Detailed changes

bridge/core/export.go 🔗

@@ -1,6 +1,10 @@
 package core
 
-import "fmt"
+import (
+	"fmt"
+
+	"github.com/MichaelMure/git-bug/entity"
+)
 
 type ExportEvent int
 
@@ -21,7 +25,7 @@ const (
 type ExportResult struct {
 	Err    error
 	Event  ExportEvent
-	ID     string
+	ID     entity.Id
 	Reason string
 }
 
@@ -46,14 +50,14 @@ func (er ExportResult) String() string {
 	}
 }
 
-func NewExportError(err error, reason string) ExportResult {
+func NewExportError(err error, id entity.Id) ExportResult {
 	return ExportResult{
-		Err:    err,
-		Reason: reason,
+		ID:  id,
+		Err: err,
 	}
 }
 
-func NewExportNothing(id string, reason string) ExportResult {
+func NewExportNothing(id entity.Id, reason string) ExportResult {
 	return ExportResult{
 		ID:     id,
 		Reason: reason,
@@ -61,42 +65,42 @@ func NewExportNothing(id string, reason string) ExportResult {
 	}
 }
 
-func NewExportBug(id string) ExportResult {
+func NewExportBug(id entity.Id) ExportResult {
 	return ExportResult{
 		ID:    id,
 		Event: ExportEventBug,
 	}
 }
 
-func NewExportComment(id string) ExportResult {
+func NewExportComment(id entity.Id) ExportResult {
 	return ExportResult{
 		ID:    id,
 		Event: ExportEventComment,
 	}
 }
 
-func NewExportCommentEdition(id string) ExportResult {
+func NewExportCommentEdition(id entity.Id) ExportResult {
 	return ExportResult{
 		ID:    id,
 		Event: ExportEventCommentEdition,
 	}
 }
 
-func NewExportStatusChange(id string) ExportResult {
+func NewExportStatusChange(id entity.Id) ExportResult {
 	return ExportResult{
 		ID:    id,
 		Event: ExportEventStatusChange,
 	}
 }
 
-func NewExportLabelChange(id string) ExportResult {
+func NewExportLabelChange(id entity.Id) ExportResult {
 	return ExportResult{
 		ID:    id,
 		Event: ExportEventLabelChange,
 	}
 }
 
-func NewExportTitleEdition(id string) ExportResult {
+func NewExportTitleEdition(id entity.Id) ExportResult {
 	return ExportResult{
 		ID:    id,
 		Event: ExportEventTitleEdition,

bridge/github/export.go 🔗

@@ -17,6 +17,7 @@ import (
 	"github.com/MichaelMure/git-bug/bridge/core"
 	"github.com/MichaelMure/git-bug/bug"
 	"github.com/MichaelMure/git-bug/cache"
+	"github.com/MichaelMure/git-bug/entity"
 )
 
 var (
@@ -28,17 +29,17 @@ type githubExporter struct {
 	conf core.Configuration
 
 	// cache identities clients
-	identityClient map[string]*githubv4.Client
+	identityClient map[entity.Id]*githubv4.Client
 
 	// map identities with their tokens
-	identityToken map[string]string
+	identityToken map[entity.Id]string
 
 	// github repository ID
 	repositoryID string
 
 	// cache identifiers used to speed up exporting operations
 	// cleared for each bug
-	cachedOperationIDs map[string]string
+	cachedOperationIDs map[entity.Id]string
 
 	// cache labels used to speed up exporting labels events
 	cachedLabels map[string]string
@@ -48,16 +49,16 @@ type githubExporter struct {
 func (ge *githubExporter) Init(conf core.Configuration) error {
 	ge.conf = conf
 	//TODO: initialize with multiple tokens
-	ge.identityToken = make(map[string]string)
-	ge.identityClient = make(map[string]*githubv4.Client)
-	ge.cachedOperationIDs = make(map[string]string)
+	ge.identityToken = make(map[entity.Id]string)
+	ge.identityClient = make(map[entity.Id]*githubv4.Client)
+	ge.cachedOperationIDs = make(map[entity.Id]string)
 	ge.cachedLabels = make(map[string]string)
 	return nil
 }
 
 // getIdentityClient return a githubv4 API client configured with the access token of the given identity.
 // if no client were found it will initialize it from the known tokens map and cache it for next use
-func (ge *githubExporter) getIdentityClient(id string) (*githubv4.Client, error) {
+func (ge *githubExporter) getIdentityClient(id entity.Id) (*githubv4.Client, error) {
 	client, ok := ge.identityClient[id]
 	if ok {
 		return client, nil
@@ -102,7 +103,7 @@ func (ge *githubExporter) ExportAll(repo *cache.RepoCache, since time.Time) (<-c
 	go func() {
 		defer close(out)
 
-		var allIdentitiesIds []string
+		var allIdentitiesIds []entity.Id
 		for id := range ge.identityToken {
 			allIdentitiesIds = append(allIdentitiesIds, id)
 		}
@@ -112,7 +113,7 @@ func (ge *githubExporter) ExportAll(repo *cache.RepoCache, since time.Time) (<-c
 		for _, id := range allBugsIds {
 			b, err := repo.ResolveBug(id)
 			if err != nil {
-				out <- core.NewExportError(err, id)
+				out <- core.NewExportError(errors.Wrap(err, "can't load bug"), id)
 				return
 			}
 
@@ -165,7 +166,7 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time, out chan
 		githubURL, ok := snapshot.GetCreateMetadata(keyGithubUrl)
 		if !ok {
 			// if we find github ID, github URL must be found too
-			err := fmt.Errorf("expected to find github issue URL")
+			err := fmt.Errorf("incomplete Github metadata: expected to find issue URL")
 			out <- core.NewExportError(err, b.Id())
 		}
 
@@ -208,7 +209,7 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time, out chan
 		out <- core.NewExportBug(b.Id())
 
 		// mark bug creation operation as exported
-		if err := markOperationAsExported(b, createOp.ID(), id, url); err != nil {
+		if err := markOperationAsExported(b, createOp.Id(), id, url); err != nil {
 			err := errors.Wrap(err, "marking operation as exported")
 			out <- core.NewExportError(err, b.Id())
 			return
@@ -227,7 +228,7 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time, out chan
 	}
 
 	// cache operation github id
-	ge.cachedOperationIDs[createOp.ID()] = bugGithubID
+	ge.cachedOperationIDs[createOp.Id()] = bugGithubID
 
 	for _, op := range snapshot.Operations[1:] {
 		// ignore SetMetadata operations
@@ -238,15 +239,15 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time, out chan
 		// ignore operations already existing in github (due to import or export)
 		// cache the ID of already exported or imported issues and events from Github
 		if id, ok := op.GetMetadata(keyGithubId); ok {
-			ge.cachedOperationIDs[op.ID()] = id
-			out <- core.NewExportNothing(op.ID(), "already exported operation")
+			ge.cachedOperationIDs[op.Id()] = id
+			out <- core.NewExportNothing(op.Id(), "already exported operation")
 			continue
 		}
 
 		opAuthor := op.GetAuthor()
 		client, err := ge.getIdentityClient(opAuthor.Id())
 		if err != nil {
-			out <- core.NewExportNothing(op.ID(), "missing operation author token")
+			out <- core.NewExportNothing(op.Id(), "missing operation author token")
 			continue
 		}
 
@@ -263,17 +264,17 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time, out chan
 				return
 			}
 
-			out <- core.NewExportComment(op.ID())
+			out <- core.NewExportComment(op.Id())
 
 			// cache comment id
-			ge.cachedOperationIDs[op.ID()] = id
+			ge.cachedOperationIDs[op.Id()] = id
 
 		case *bug.EditCommentOperation:
 
 			opr := op.(*bug.EditCommentOperation)
 
 			// Since github doesn't consider the issue body as a comment
-			if opr.Target == createOp.ID() {
+			if opr.Target == createOp.Id() {
 
 				// case bug creation operation: we need to edit the Github issue
 				if err := updateGithubIssueBody(client, bugGithubID, opr.Message); err != nil {
@@ -282,7 +283,7 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time, out chan
 					return
 				}
 
-				out <- core.NewExportCommentEdition(op.ID())
+				out <- core.NewExportCommentEdition(op.Id())
 
 				id = bugGithubID
 				url = bugGithubURL
@@ -302,7 +303,7 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time, out chan
 					return
 				}
 
-				out <- core.NewExportCommentEdition(op.ID())
+				out <- core.NewExportCommentEdition(op.Id())
 
 				// use comment id/url instead of issue id/url
 				id = eid
@@ -317,7 +318,7 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time, out chan
 				return
 			}
 
-			out <- core.NewExportStatusChange(op.ID())
+			out <- core.NewExportStatusChange(op.Id())
 
 			id = bugGithubID
 			url = bugGithubURL
@@ -330,7 +331,7 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time, out chan
 				return
 			}
 
-			out <- core.NewExportTitleEdition(op.ID())
+			out <- core.NewExportTitleEdition(op.Id())
 
 			id = bugGithubID
 			url = bugGithubURL
@@ -343,7 +344,7 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time, out chan
 				return
 			}
 
-			out <- core.NewExportLabelChange(op.ID())
+			out <- core.NewExportLabelChange(op.Id())
 
 			id = bugGithubID
 			url = bugGithubURL
@@ -353,7 +354,7 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time, out chan
 		}
 
 		// mark operation as exported
-		if err := markOperationAsExported(b, op.ID(), id, url); err != nil {
+		if err := markOperationAsExported(b, op.Id(), id, url); err != nil {
 			err := errors.Wrap(err, "marking operation as exported")
 			out <- core.NewExportError(err, b.Id())
 			return
@@ -411,7 +412,7 @@ func getRepositoryNodeID(owner, project, token string) (string, error) {
 	return aux.NodeID, nil
 }
 
-func markOperationAsExported(b *cache.BugCache, target string, githubID, githubURL string) error {
+func markOperationAsExported(b *cache.BugCache, target entity.Id, githubID, githubURL string) error {
 	_, err := b.SetMetadata(
 		target,
 		map[string]string{

bridge/github/export_test.go 🔗

@@ -58,13 +58,13 @@ func testCases(t *testing.T, repo *cache.RepoCache, identity *cache.IdentityCach
 	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(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(commentOp.Id(), "first comment edited")
 	require.NoError(t, err)
 
 	// bug status changed

bridge/github/import.go 🔗

@@ -10,7 +10,7 @@ import (
 	"github.com/MichaelMure/git-bug/bridge/core"
 	"github.com/MichaelMure/git-bug/bug"
 	"github.com/MichaelMure/git-bug/cache"
-	"github.com/MichaelMure/git-bug/identity"
+	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/util/text"
 )
 
@@ -369,7 +369,7 @@ func (gi *githubImporter) ensureTimelineComment(repo *cache.RepoCache, b *cache.
 				}
 
 				// set target for the nexr edit now that the comment is created
-				targetOpID = op.ID()
+				targetOpID = op.Id()
 
 				continue
 			}
@@ -383,7 +383,7 @@ func (gi *githubImporter) ensureTimelineComment(repo *cache.RepoCache, b *cache.
 	return nil
 }
 
-func (gi *githubImporter) ensureCommentEdit(repo *cache.RepoCache, b *cache.BugCache, target string, edit userContentEdit) error {
+func (gi *githubImporter) ensureCommentEdit(repo *cache.RepoCache, b *cache.BugCache, target entity.Id, edit userContentEdit) error {
 	_, err := b.ResolveOperationWithMetadata(keyGithubId, parseId(edit.Id))
 	if err == nil {
 		// already imported
@@ -445,7 +445,7 @@ func (gi *githubImporter) ensurePerson(repo *cache.RepoCache, actor *actor) (*ca
 	if err == nil {
 		return i, nil
 	}
-	if _, ok := err.(identity.ErrMultipleMatch); ok {
+	if _, ok := err.(entity.ErrMultipleMatch); ok {
 		return nil, err
 	}
 
@@ -488,7 +488,7 @@ func (gi *githubImporter) getGhost(repo *cache.RepoCache) (*cache.IdentityCache,
 	if err == nil {
 		return i, nil
 	}
-	if _, ok := err.(identity.ErrMultipleMatch); ok {
+	if _, ok := err.(entity.ErrMultipleMatch); ok {
 		return nil, err
 	}
 

bridge/gitlab/import.go 🔗

@@ -10,7 +10,7 @@ import (
 	"github.com/MichaelMure/git-bug/bridge/core"
 	"github.com/MichaelMure/git-bug/bug"
 	"github.com/MichaelMure/git-bug/cache"
-	"github.com/MichaelMure/git-bug/identity"
+	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/util/text"
 )
 
@@ -324,7 +324,7 @@ func (gi *gitlabImporter) ensurePerson(repo *cache.RepoCache, id int) (*cache.Id
 	if err == nil {
 		return i, nil
 	}
-	if _, ok := err.(identity.ErrMultipleMatch); ok {
+	if _, ok := err.(entity.ErrMultipleMatch); ok {
 		return nil, err
 	}
 

bridge/launchpad/import.go 🔗

@@ -4,11 +4,12 @@ import (
 	"fmt"
 	"time"
 
+	"github.com/pkg/errors"
+
 	"github.com/MichaelMure/git-bug/bridge/core"
 	"github.com/MichaelMure/git-bug/bug"
 	"github.com/MichaelMure/git-bug/cache"
-	"github.com/MichaelMure/git-bug/identity"
-	"github.com/pkg/errors"
+	"github.com/MichaelMure/git-bug/entity"
 )
 
 type launchpadImporter struct {
@@ -29,7 +30,7 @@ func (li *launchpadImporter) ensurePerson(repo *cache.RepoCache, owner LPPerson)
 	if err == nil {
 		return i, nil
 	}
-	if _, ok := err.(identity.ErrMultipleMatch); ok {
+	if _, ok := err.(entity.ErrMultipleMatch); ok {
 		return nil, err
 	}
 

bug/bug.go 🔗

@@ -29,18 +29,12 @@ const editClockEntryPattern = "edit-clock-%d"
 
 var ErrBugNotExist = errors.New("bug doesn't exist")
 
-type ErrMultipleMatch struct {
-	Matching []entity.Id
+func NewErrMultipleMatchBug(matching []entity.Id) *entity.ErrMultipleMatch {
+	return entity.NewErrMultipleMatch("bug", matching)
 }
 
-func (e ErrMultipleMatch) Error() string {
-	matching := make([]string, len(e.Matching))
-
-	for i, match := range e.Matching {
-		matching[i] = match.String()
-	}
-
-	return fmt.Sprintf("Multiple matching bug found:\n%s", strings.Join(matching, "\n"))
+func NewErrMultipleMatchOp(matching []entity.Id) *entity.ErrMultipleMatch {
+	return entity.NewErrMultipleMatch("operation", matching)
 }
 
 var _ Interface = &Bug{}
@@ -100,7 +94,7 @@ func FindLocalBug(repo repository.ClockedRepo, prefix string) (*Bug, error) {
 	}
 
 	if len(matching) > 1 {
-		return nil, ErrMultipleMatch{Matching: matching}
+		return nil, NewErrMultipleMatchBug(matching)
 	}
 
 	return ReadLocalBug(repo, matching[0])

bug/bug_actions.go 🔗

@@ -65,8 +65,13 @@ func MergeAll(repo repository.ClockedRepo, remote string) <-chan entity.MergeRes
 		}
 
 		for _, remoteRef := range remoteRefs {
-			refSplitted := strings.Split(remoteRef, "/")
-			id := refSplitted[len(refSplitted)-1]
+			refSplit := strings.Split(remoteRef, "/")
+			id := entity.Id(refSplit[len(refSplit)-1])
+
+			if err := id.Validate(); err != nil {
+				out <- entity.NewMergeInvalidStatus(id, errors.Wrap(err, "invalid ref").Error())
+				continue
+			}
 
 			remoteBug, err := readBug(repo, remoteRef)
 

bug/op_add_comment_test.go 🔗

@@ -2,12 +2,12 @@ package bug
 
 import (
 	"encoding/json"
-	"fmt"
 	"testing"
 	"time"
 
-	"github.com/MichaelMure/git-bug/identity"
 	"github.com/stretchr/testify/assert"
+
+	"github.com/MichaelMure/git-bug/identity"
 )
 
 func TestAddCommentSerialize(t *testing.T) {

bug/snapshot.go 🔗

@@ -66,9 +66,9 @@ func (snap *Snapshot) SearchTimelineItem(id entity.Id) (TimelineItem, error) {
 }
 
 // SearchComment will search for a comment matching the given hash
-func (snap *Snapshot) SearchComment(id string) (*Comment, error) {
+func (snap *Snapshot) SearchComment(id entity.Id) (*Comment, error) {
 	for _, c := range snap.Comments {
-		if c.id.String() == id {
+		if c.id == id {
 			return &c, nil
 		}
 	}

cache/bug_cache.go 🔗

@@ -2,13 +2,15 @@ package cache
 
 import (
 	"fmt"
-	"strings"
 	"time"
 
 	"github.com/MichaelMure/git-bug/bug"
+	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/util/git"
 )
 
+var ErrNoMatchingOp = fmt.Errorf("no matching operation found")
+
 // BugCache is a wrapper around a Bug. It provide multiple functions:
 //
 // 1. Provide a higher level API to use than the raw API from Bug.
@@ -29,45 +31,25 @@ func (c *BugCache) Snapshot() *bug.Snapshot {
 	return c.bug.Snapshot()
 }
 
-func (c *BugCache) Id() string {
+func (c *BugCache) Id() entity.Id {
 	return c.bug.Id()
 }
 
-func (c *BugCache) HumanId() string {
-	return c.bug.HumanId()
-}
-
 func (c *BugCache) notifyUpdated() error {
 	return c.repoCache.bugUpdated(c.bug.Id())
 }
 
-var ErrNoMatchingOp = fmt.Errorf("no matching operation found")
-
-type ErrMultipleMatchOp struct {
-	Matching []string
-}
-
-func (e ErrMultipleMatchOp) Error() string {
-	casted := make([]string, len(e.Matching))
-
-	for i := range e.Matching {
-		casted[i] = string(e.Matching[i])
-	}
-
-	return fmt.Sprintf("Multiple matching operation found:\n%s", strings.Join(casted, "\n"))
-}
-
 // ResolveOperationWithMetadata will find an operation that has the matching metadata
-func (c *BugCache) ResolveOperationWithMetadata(key string, value string) (string, error) {
+func (c *BugCache) ResolveOperationWithMetadata(key string, value string) (entity.Id, error) {
 	// preallocate but empty
-	matching := make([]string, 0, 5)
+	matching := make([]entity.Id, 0, 5)
 
 	it := bug.NewOperationIterator(c.bug)
 	for it.Next() {
 		op := it.Value()
 		opValue, ok := op.GetMetadata(key)
 		if ok && value == opValue {
-			matching = append(matching, op.ID())
+			matching = append(matching, op.Id())
 		}
 	}
 
@@ -76,7 +58,7 @@ func (c *BugCache) ResolveOperationWithMetadata(key string, value string) (strin
 	}
 
 	if len(matching) > 1 {
-		return "", ErrMultipleMatchOp{Matching: matching}
+		return "", bug.NewErrMultipleMatchOp(matching)
 	}
 
 	return matching[0], nil
@@ -228,7 +210,7 @@ func (c *BugCache) SetTitleRaw(author *IdentityCache, unixTime int64, title stri
 	return op, c.notifyUpdated()
 }
 
-func (c *BugCache) EditComment(target string, message string) (*bug.EditCommentOperation, error) {
+func (c *BugCache) EditComment(target entity.Id, message string) (*bug.EditCommentOperation, error) {
 	author, err := c.repoCache.GetUserIdentity()
 	if err != nil {
 		return nil, err
@@ -237,7 +219,7 @@ func (c *BugCache) EditComment(target string, message string) (*bug.EditCommentO
 	return c.EditCommentRaw(author, time.Now().Unix(), target, message, nil)
 }
 
-func (c *BugCache) EditCommentRaw(author *IdentityCache, unixTime int64, target string, message string, metadata map[string]string) (*bug.EditCommentOperation, error) {
+func (c *BugCache) EditCommentRaw(author *IdentityCache, unixTime int64, target entity.Id, message string, metadata map[string]string) (*bug.EditCommentOperation, error) {
 	op, err := bug.EditComment(c.bug, author.Identity, unixTime, target, message)
 	if err != nil {
 		return nil, err
@@ -250,7 +232,7 @@ func (c *BugCache) EditCommentRaw(author *IdentityCache, unixTime int64, target
 	return op, c.notifyUpdated()
 }
 
-func (c *BugCache) SetMetadata(target string, newMetadata map[string]string) (*bug.SetMetadataOperation, error) {
+func (c *BugCache) SetMetadata(target entity.Id, newMetadata map[string]string) (*bug.SetMetadataOperation, error) {
 	author, err := c.repoCache.GetUserIdentity()
 	if err != nil {
 		return nil, err
@@ -259,7 +241,7 @@ func (c *BugCache) SetMetadata(target string, newMetadata map[string]string) (*b
 	return c.SetMetadataRaw(author, time.Now().Unix(), target, newMetadata)
 }
 
-func (c *BugCache) SetMetadataRaw(author *IdentityCache, unixTime int64, target string, newMetadata map[string]string) (*bug.SetMetadataOperation, error) {
+func (c *BugCache) SetMetadataRaw(author *IdentityCache, unixTime int64, target entity.Id, newMetadata map[string]string) (*bug.SetMetadataOperation, error) {
 	op, err := bug.SetMetadata(c.bug, author.Identity, unixTime, target, newMetadata)
 	if err != nil {
 		return nil, err

cache/bug_excerpt.go 🔗

@@ -18,7 +18,7 @@ func init() {
 // BugExcerpt hold a subset of the bug values to be able to sort and filter bugs
 // efficiently without having to read and compile each raw bugs.
 type BugExcerpt struct {
-	Id entity.ID
+	Id entity.Id
 
 	CreateLamportTime lamport.Time
 	EditLamportTime   lamport.Time
@@ -29,14 +29,14 @@ type BugExcerpt struct {
 	Labels       []bug.Label
 	Title        string
 	LenComments  int
-	Actors       []entity.ID
-	Participants []entity.ID
+	Actors       []entity.Id
+	Participants []entity.Id
 
 	// If author is identity.Bare, LegacyAuthor is set
 	// If author is identity.Identity, AuthorId is set and data is deported
 	// in a IdentityExcerpt
 	LegacyAuthor LegacyAuthorExcerpt
-	AuthorId     string
+	AuthorId     entity.Id
 
 	CreateMetadata map[string]string
 }
@@ -61,12 +61,12 @@ func (l LegacyAuthorExcerpt) DisplayName() string {
 }
 
 func NewBugExcerpt(b bug.Interface, snap *bug.Snapshot) *BugExcerpt {
-	participantsIds := make([]string, len(snap.Participants))
+	participantsIds := make([]entity.Id, len(snap.Participants))
 	for i, participant := range snap.Participants {
 		participantsIds[i] = participant.Id()
 	}
 
-	actorsIds := make([]string, len(snap.Actors))
+	actorsIds := make([]entity.Id, len(snap.Actors))
 	for i, actor := range snap.Actors {
 		actorsIds[i] = actor.Id()
 	}
@@ -101,10 +101,6 @@ func NewBugExcerpt(b bug.Interface, snap *bug.Snapshot) *BugExcerpt {
 	return e
 }
 
-func (b *BugExcerpt) HumanId() string {
-	return bug.FormatHumanID(b.Id)
-}
-
 /*
  * Sorting
  */

cache/identity_excerpt.go 🔗

@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"strings"
 
+	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/identity"
 )
 
@@ -17,7 +18,7 @@ func init() {
 // filter identities efficiently without having to read and compile each raw
 // identity.
 type IdentityExcerpt struct {
-	Id string
+	Id entity.Id
 
 	Name              string
 	Login             string
@@ -33,10 +34,6 @@ func NewIdentityExcerpt(i *identity.Identity) *IdentityExcerpt {
 	}
 }
 
-func (i *IdentityExcerpt) HumanId() string {
-	return identity.FormatHumanID(i.Id)
-}
-
 // DisplayName return a non-empty string to display, representing the
 // identity, based on the non-empty values.
 func (i *IdentityExcerpt) DisplayName() string {
@@ -54,7 +51,7 @@ func (i *IdentityExcerpt) DisplayName() string {
 
 // Match matches a query with the identity name, login and ID prefixes
 func (i *IdentityExcerpt) Match(query string) bool {
-	return strings.HasPrefix(i.Id, query) ||
+	return i.Id.HasPrefix(query) ||
 		strings.Contains(strings.ToLower(i.Name), query) ||
 		strings.Contains(strings.ToLower(i.Login), query)
 }

cache/repo_cache.go 🔗

@@ -10,16 +10,16 @@ import (
 	"path"
 	"sort"
 	"strconv"
-	"strings"
 	"time"
 
+	"github.com/pkg/errors"
+
 	"github.com/MichaelMure/git-bug/bug"
 	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/repository"
 	"github.com/MichaelMure/git-bug/util/git"
 	"github.com/MichaelMure/git-bug/util/process"
-	"github.com/pkg/errors"
 )
 
 const bugCacheFile = "bug-cache"
@@ -58,24 +58,24 @@ type RepoCache struct {
 	repo repository.ClockedRepo
 
 	// excerpt of bugs data for all bugs
-	bugExcerpts map[string]*BugExcerpt
+	bugExcerpts map[entity.Id]*BugExcerpt
 	// bug loaded in memory
-	bugs map[string]*BugCache
+	bugs map[entity.Id]*BugCache
 
 	// excerpt of identities data for all identities
-	identitiesExcerpts map[string]*IdentityExcerpt
+	identitiesExcerpts map[entity.Id]*IdentityExcerpt
 	// identities loaded in memory
-	identities map[string]*IdentityCache
+	identities map[entity.Id]*IdentityCache
 
 	// the user identity's id, if known
-	userIdentityId string
+	userIdentityId entity.Id
 }
 
 func NewRepoCache(r repository.ClockedRepo) (*RepoCache, error) {
 	c := &RepoCache{
 		repo:       r,
-		bugs:       make(map[string]*BugCache),
-		identities: make(map[string]*IdentityCache),
+		bugs:       make(map[entity.Id]*BugCache),
+		identities: make(map[entity.Id]*IdentityCache),
 	}
 
 	err := c.lock()
@@ -191,7 +191,7 @@ func (c *RepoCache) Close() error {
 
 // bugUpdated is a callback to trigger when the excerpt of a bug changed,
 // that is each time a bug is updated
-func (c *RepoCache) bugUpdated(id string) error {
+func (c *RepoCache) bugUpdated(id entity.Id) error {
 	b, ok := c.bugs[id]
 	if !ok {
 		panic("missing bug in the cache")
@@ -205,7 +205,7 @@ func (c *RepoCache) bugUpdated(id string) error {
 
 // identityUpdated is a callback to trigger when the excerpt of an identity
 // changed, that is each time an identity is updated
-func (c *RepoCache) identityUpdated(id string) error {
+func (c *RepoCache) identityUpdated(id entity.Id) error {
 	i, ok := c.identities[id]
 	if !ok {
 		panic("missing identity in the cache")
@@ -237,7 +237,7 @@ func (c *RepoCache) loadBugCache() error {
 
 	aux := struct {
 		Version  uint
-		Excerpts map[string]*BugExcerpt
+		Excerpts map[entity.Id]*BugExcerpt
 	}{}
 
 	err = decoder.Decode(&aux)
@@ -266,7 +266,7 @@ func (c *RepoCache) loadIdentityCache() error {
 
 	aux := struct {
 		Version  uint
-		Excerpts map[string]*IdentityExcerpt
+		Excerpts map[entity.Id]*IdentityExcerpt
 	}{}
 
 	err = decoder.Decode(&aux)
@@ -299,7 +299,7 @@ func (c *RepoCache) writeBugCache() error {
 
 	aux := struct {
 		Version  uint
-		Excerpts map[string]*BugExcerpt
+		Excerpts map[entity.Id]*BugExcerpt
 	}{
 		Version:  formatVersion,
 		Excerpts: c.bugExcerpts,
@@ -331,7 +331,7 @@ func (c *RepoCache) writeIdentityCache() error {
 
 	aux := struct {
 		Version  uint
-		Excerpts map[string]*IdentityExcerpt
+		Excerpts map[entity.Id]*IdentityExcerpt
 	}{
 		Version:  formatVersion,
 		Excerpts: c.identitiesExcerpts,
@@ -368,7 +368,7 @@ func identityCacheFilePath(repo repository.Repo) string {
 func (c *RepoCache) buildCache() error {
 	_, _ = fmt.Fprintf(os.Stderr, "Building identity cache... ")
 
-	c.identitiesExcerpts = make(map[string]*IdentityExcerpt)
+	c.identitiesExcerpts = make(map[entity.Id]*IdentityExcerpt)
 
 	allIdentities := identity.ReadAllLocalIdentities(c.repo)
 
@@ -384,7 +384,7 @@ func (c *RepoCache) buildCache() error {
 
 	_, _ = fmt.Fprintf(os.Stderr, "Building bug cache... ")
 
-	c.bugExcerpts = make(map[string]*BugExcerpt)
+	c.bugExcerpts = make(map[entity.Id]*BugExcerpt)
 
 	allBugs := bug.ReadAllLocalBugs(c.repo)
 
@@ -402,7 +402,7 @@ func (c *RepoCache) buildCache() error {
 }
 
 // ResolveBug retrieve a bug matching the exact given id
-func (c *RepoCache) ResolveBug(id string) (*BugCache, error) {
+func (c *RepoCache) ResolveBug(id entity.Id) (*BugCache, error) {
 	cached, ok := c.bugs[id]
 	if ok {
 		return cached, nil
@@ -420,7 +420,7 @@ func (c *RepoCache) ResolveBug(id string) (*BugCache, error) {
 }
 
 // ResolveBugExcerpt retrieve a BugExcerpt matching the exact given id
-func (c *RepoCache) ResolveBugExcerpt(id string) (*BugExcerpt, error) {
+func (c *RepoCache) ResolveBugExcerpt(id entity.Id) (*BugExcerpt, error) {
 	e, ok := c.bugExcerpts[id]
 	if !ok {
 		return nil, bug.ErrBugNotExist
@@ -433,16 +433,16 @@ func (c *RepoCache) ResolveBugExcerpt(id string) (*BugExcerpt, error) {
 // bugs match.
 func (c *RepoCache) ResolveBugPrefix(prefix string) (*BugCache, error) {
 	// preallocate but empty
-	matching := make([]string, 0, 5)
+	matching := make([]entity.Id, 0, 5)
 
 	for id := range c.bugExcerpts {
-		if strings.HasPrefix(id, prefix) {
+		if id.HasPrefix(prefix) {
 			matching = append(matching, id)
 		}
 	}
 
 	if len(matching) > 1 {
-		return nil, bug.ErrMultipleMatch{Matching: matching}
+		return nil, bug.NewErrMultipleMatchBug(matching)
 	}
 
 	if len(matching) == 0 {
@@ -457,7 +457,7 @@ func (c *RepoCache) ResolveBugPrefix(prefix string) (*BugCache, error) {
 // match.
 func (c *RepoCache) ResolveBugCreateMetadata(key string, value string) (*BugCache, error) {
 	// preallocate but empty
-	matching := make([]string, 0, 5)
+	matching := make([]entity.Id, 0, 5)
 
 	for id, excerpt := range c.bugExcerpts {
 		if excerpt.CreateMetadata[key] == value {
@@ -466,7 +466,7 @@ func (c *RepoCache) ResolveBugCreateMetadata(key string, value string) (*BugCach
 	}
 
 	if len(matching) > 1 {
-		return nil, bug.ErrMultipleMatch{Matching: matching}
+		return nil, bug.NewErrMultipleMatchBug(matching)
 	}
 
 	if len(matching) == 0 {
@@ -477,7 +477,7 @@ func (c *RepoCache) ResolveBugCreateMetadata(key string, value string) (*BugCach
 }
 
 // QueryBugs return the id of all Bug matching the given Query
-func (c *RepoCache) QueryBugs(query *Query) []string {
+func (c *RepoCache) QueryBugs(query *Query) []entity.Id {
 	if query == nil {
 		return c.AllBugsIds()
 	}
@@ -509,7 +509,7 @@ func (c *RepoCache) QueryBugs(query *Query) []string {
 
 	sort.Sort(sorter)
 
-	result := make([]string, len(filtered))
+	result := make([]entity.Id, len(filtered))
 
 	for i, val := range filtered {
 		result[i] = val.Id
@@ -519,8 +519,8 @@ func (c *RepoCache) QueryBugs(query *Query) []string {
 }
 
 // AllBugsIds return all known bug ids
-func (c *RepoCache) AllBugsIds() []string {
-	result := make([]string, len(c.bugExcerpts))
+func (c *RepoCache) AllBugsIds() []entity.Id {
+	result := make([]entity.Id, len(c.bugExcerpts))
 
 	i := 0
 	for _, excerpt := range c.bugExcerpts {
@@ -778,7 +778,7 @@ func repoIsAvailable(repo repository.Repo) error {
 }
 
 // ResolveIdentity retrieve an identity matching the exact given id
-func (c *RepoCache) ResolveIdentity(id string) (*IdentityCache, error) {
+func (c *RepoCache) ResolveIdentity(id entity.Id) (*IdentityCache, error) {
 	cached, ok := c.identities[id]
 	if ok {
 		return cached, nil
@@ -796,7 +796,7 @@ func (c *RepoCache) ResolveIdentity(id string) (*IdentityCache, error) {
 }
 
 // ResolveIdentityExcerpt retrieve a IdentityExcerpt matching the exact given id
-func (c *RepoCache) ResolveIdentityExcerpt(id string) (*IdentityExcerpt, error) {
+func (c *RepoCache) ResolveIdentityExcerpt(id entity.Id) (*IdentityExcerpt, error) {
 	e, ok := c.identitiesExcerpts[id]
 	if !ok {
 		return nil, identity.ErrIdentityNotExist
@@ -809,16 +809,16 @@ func (c *RepoCache) ResolveIdentityExcerpt(id string) (*IdentityExcerpt, error)
 // It fails if multiple identities match.
 func (c *RepoCache) ResolveIdentityPrefix(prefix string) (*IdentityCache, error) {
 	// preallocate but empty
-	matching := make([]string, 0, 5)
+	matching := make([]entity.Id, 0, 5)
 
 	for id := range c.identitiesExcerpts {
-		if strings.HasPrefix(id, prefix) {
+		if id.HasPrefix(prefix) {
 			matching = append(matching, id)
 		}
 	}
 
 	if len(matching) > 1 {
-		return nil, identity.ErrMultipleMatch{Matching: matching}
+		return nil, identity.NewErrMultipleMatch(matching)
 	}
 
 	if len(matching) == 0 {
@@ -832,7 +832,7 @@ func (c *RepoCache) ResolveIdentityPrefix(prefix string) (*IdentityCache, error)
 // one of it's version. If multiple version have the same key, the first defined take precedence.
 func (c *RepoCache) ResolveIdentityImmutableMetadata(key string, value string) (*IdentityCache, error) {
 	// preallocate but empty
-	matching := make([]string, 0, 5)
+	matching := make([]entity.Id, 0, 5)
 
 	for id, i := range c.identitiesExcerpts {
 		if i.ImmutableMetadata[key] == value {
@@ -841,7 +841,7 @@ func (c *RepoCache) ResolveIdentityImmutableMetadata(key string, value string) (
 	}
 
 	if len(matching) > 1 {
-		return nil, identity.ErrMultipleMatch{Matching: matching}
+		return nil, identity.NewErrMultipleMatch(matching)
 	}
 
 	if len(matching) == 0 {
@@ -852,8 +852,8 @@ func (c *RepoCache) ResolveIdentityImmutableMetadata(key string, value string) (
 }
 
 // AllIdentityIds return all known identity ids
-func (c *RepoCache) AllIdentityIds() []string {
-	result := make([]string, len(c.identitiesExcerpts))
+func (c *RepoCache) AllIdentityIds() []entity.Id {
+	result := make([]entity.Id, len(c.identitiesExcerpts))
 
 	i := 0
 	for _, excerpt := range c.identitiesExcerpts {

cache/repo_cache_test.go 🔗

@@ -57,14 +57,14 @@ func TestCache(t *testing.T) {
 	require.NoError(t, err)
 	_, err = cache.ResolveIdentityExcerpt(iden1.Id())
 	require.NoError(t, err)
-	_, err = cache.ResolveIdentityPrefix(iden1.Id()[:10])
+	_, err = cache.ResolveIdentityPrefix(iden1.Id().String()[:10])
 	require.NoError(t, err)
 
 	_, err = cache.ResolveBug(bug1.Id())
 	require.NoError(t, err)
 	_, err = cache.ResolveBugExcerpt(bug1.Id())
 	require.NoError(t, err)
-	_, err = cache.ResolveBugPrefix(bug1.Id()[:10])
+	_, err = cache.ResolveBugPrefix(bug1.Id().String()[:10])
 	require.NoError(t, err)
 
 	// Querying
@@ -91,14 +91,14 @@ func TestCache(t *testing.T) {
 	require.NoError(t, err)
 	_, err = cache.ResolveIdentityExcerpt(iden1.Id())
 	require.NoError(t, err)
-	_, err = cache.ResolveIdentityPrefix(iden1.Id()[:10])
+	_, err = cache.ResolveIdentityPrefix(iden1.Id().String()[:10])
 	require.NoError(t, err)
 
 	_, err = cache.ResolveBug(bug1.Id())
 	require.NoError(t, err)
 	_, err = cache.ResolveBugExcerpt(bug1.Id())
 	require.NoError(t, err)
-	_, err = cache.ResolveBugPrefix(bug1.Id()[:10])
+	_, err = cache.ResolveBugPrefix(bug1.Id().String()[:10])
 	require.NoError(t, err)
 }
 

commands/add.go 🔗

@@ -49,7 +49,7 @@ func runAddBug(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	fmt.Printf("%s created\n", b.HumanId())
+	fmt.Printf("%s created\n", b.Id().Human())
 
 	return nil
 }

commands/comment.go 🔗

@@ -39,7 +39,7 @@ func commentsTextOutput(comments []bug.Comment) {
 		}
 
 		fmt.Printf("Author: %s\n", colors.Magenta(comment.Author.DisplayName()))
-		fmt.Printf("Id: %s\n", colors.Cyan(comment.HumanId()))
+		fmt.Printf("Id: %s\n", colors.Cyan(comment.Id().Human()))
 		fmt.Printf("Date: %s\n\n", comment.FormatTime())
 		fmt.Println(text.LeftPad(comment.Message, 4))
 	}

commands/ls-id.go 🔗

@@ -2,11 +2,11 @@ package commands
 
 import (
 	"fmt"
-	"strings"
+
+	"github.com/spf13/cobra"
 
 	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/util/interrupt"
-	"github.com/spf13/cobra"
 )
 
 func runLsID(cmd *cobra.Command, args []string) error {
@@ -24,7 +24,7 @@ func runLsID(cmd *cobra.Command, args []string) error {
 	}
 
 	for _, id := range backend.AllBugsIds() {
-		if prefix == "" || strings.HasPrefix(id, prefix) {
+		if prefix == "" || id.HasPrefix(prefix) {
 			fmt.Println(id)
 		}
 	}

commands/ls.go 🔗

@@ -70,7 +70,7 @@ func runLsBug(cmd *cobra.Command, args []string) error {
 		authorFmt := text.LeftPadMaxLine(name, 15, 0)
 
 		fmt.Printf("%s %s\t%s\t%s\tC:%d L:%d\n",
-			colors.Cyan(b.HumanId()),
+			colors.Cyan(b.Id.Human()),
 			colors.Yellow(b.Status),
 			titleFmt,
 			colors.Magenta(authorFmt),

commands/pull.go 🔗

@@ -4,11 +4,11 @@ import (
 	"errors"
 	"fmt"
 
-	"github.com/MichaelMure/git-bug/bug"
+	"github.com/spf13/cobra"
+
 	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/util/interrupt"
-	"github.com/spf13/cobra"
 )
 
 func runPull(cmd *cobra.Command, args []string) error {
@@ -45,7 +45,7 @@ func runPull(cmd *cobra.Command, args []string) error {
 		}
 
 		if result.Status != entity.MergeStatusNothing {
-			fmt.Printf("%s: %s\n", bug.FormatHumanID(result.Id), result)
+			fmt.Printf("%s: %s\n", result.Id.Human(), result)
 		}
 	}
 

commands/select.go 🔗

@@ -34,7 +34,7 @@ func runSelect(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	fmt.Printf("selected bug %s: %s\n", b.HumanId(), b.Snapshot().Title)
+	fmt.Printf("selected bug %s: %s\n", b.Id().Human(), b.Snapshot().Title)
 
 	return nil
 }

commands/select/select.go 🔗

@@ -11,6 +11,7 @@ import (
 
 	"github.com/MichaelMure/git-bug/bug"
 	"github.com/MichaelMure/git-bug/cache"
+	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/repository"
 )
 
@@ -69,7 +70,7 @@ func ResolveBug(repo *cache.RepoCache, args []string) (*cache.BugCache, []string
 }
 
 // Select will select a bug for future use
-func Select(repo *cache.RepoCache, id string) error {
+func Select(repo *cache.RepoCache, id entity.Id) error {
 	selectPath := selectFilePath(repo)
 
 	f, err := os.OpenFile(selectPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
@@ -77,7 +78,7 @@ func Select(repo *cache.RepoCache, id string) error {
 		return err
 	}
 
-	_, err = f.WriteString(id)
+	_, err = f.WriteString(id.String())
 	if err != nil {
 		return err
 	}
@@ -112,8 +113,8 @@ func selected(repo *cache.RepoCache) (*cache.BugCache, error) {
 		return nil, fmt.Errorf("the select file should be < 100 bytes")
 	}
 
-	id := string(buf)
-	if !bug.IDIsValid(id) {
+	id := entity.Id(buf)
+	if err := id.Validate(); err != nil {
 		err = os.Remove(selectPath)
 		if err != nil {
 			return nil, errors.Wrap(err, "error while removing invalid select file")

commands/select/select_test.go 🔗

@@ -52,12 +52,12 @@ func TestSelect(t *testing.T) {
 	require.Equal(t, b1.Id(), b3.Id())
 
 	// override selection with same id
-	b4, _, err := ResolveBug(repoCache, []string{b1.Id()})
+	b4, _, err := ResolveBug(repoCache, []string{b1.Id().String()})
 	require.NoError(t, err)
 	require.Equal(t, b1.Id(), b4.Id())
 
 	// override selection with a prefix
-	b5, _, err := ResolveBug(repoCache, []string{b1.HumanId()})
+	b5, _, err := ResolveBug(repoCache, []string{b1.Id().Human()})
 	require.NoError(t, err)
 	require.Equal(t, b1.Id(), b5.Id())
 
@@ -67,7 +67,7 @@ func TestSelect(t *testing.T) {
 	require.Equal(t, b1.Id(), b6.Id())
 
 	// override with a different id
-	b7, _, err := ResolveBug(repoCache, []string{b2.Id()})
+	b7, _, err := ResolveBug(repoCache, []string{b2.Id().String()})
 	require.NoError(t, err)
 	require.Equal(t, b2.Id(), b7.Id())
 

commands/show.go 🔗

@@ -46,7 +46,7 @@ func runShowBug(cmd *cobra.Command, args []string) error {
 		case "createTime":
 			fmt.Printf("%s\n", firstComment.FormatTime())
 		case "humanId":
-			fmt.Printf("%s\n", snapshot.HumanId())
+			fmt.Printf("%s\n", snapshot.Id().Human())
 		case "id":
 			fmt.Printf("%s\n", snapshot.Id())
 		case "labels":
@@ -62,7 +62,7 @@ func runShowBug(cmd *cobra.Command, args []string) error {
 				fmt.Printf("%s\n", p.DisplayName())
 			}
 		case "shortId":
-			fmt.Printf("%s\n", snapshot.HumanId())
+			fmt.Printf("%s\n", snapshot.Id().Human())
 		case "status":
 			fmt.Printf("%s\n", snapshot.Status)
 		case "title":
@@ -77,7 +77,7 @@ func runShowBug(cmd *cobra.Command, args []string) error {
 	// Header
 	fmt.Printf("[%s] %s %s\n\n",
 		colors.Yellow(snapshot.Status),
-		colors.Cyan(snapshot.HumanId()),
+		colors.Cyan(snapshot.Id().Human()),
 		snapshot.Title,
 	)
 

commands/user.go 🔗

@@ -41,7 +41,7 @@ func runUser(cmd *cobra.Command, args []string) error {
 		case "email":
 			fmt.Printf("%s\n", id.Email())
 		case "humanId":
-			fmt.Printf("%s\n", id.HumanId())
+			fmt.Printf("%s\n", id.Id().Human())
 		case "id":
 			fmt.Printf("%s\n", id.Id())
 		case "lastModification":

commands/user_ls.go 🔗

@@ -24,7 +24,7 @@ func runUserLs(cmd *cobra.Command, args []string) error {
 		}
 
 		fmt.Printf("%s %s\n",
-			colors.Cyan(i.HumanId()),
+			colors.Cyan(i.Id.Human()),
 			i.DisplayName(),
 		)
 	}

entity/err.go 🔗

@@ -1 +1,27 @@
 package entity
+
+import (
+	"fmt"
+	"strings"
+)
+
+type ErrMultipleMatch struct {
+	entityType string
+	Matching   []Id
+}
+
+func NewErrMultipleMatch(entityType string, matching []Id) *ErrMultipleMatch {
+	return &ErrMultipleMatch{entityType: entityType, Matching: matching}
+}
+
+func (e ErrMultipleMatch) Error() string {
+	matching := make([]string, len(e.Matching))
+
+	for i, match := range e.Matching {
+		matching[i] = match.String()
+	}
+
+	return fmt.Sprintf("Multiple matching %s found:\n%s",
+		e.entityType,
+		strings.Join(matching, "\n"))
+}

entity/merge.go 🔗

@@ -1,6 +1,8 @@
 package entity
 
-import "fmt"
+import (
+	"fmt"
+)
 
 // MergeStatus represent the result of a merge operation of an entity
 type MergeStatus int
@@ -17,7 +19,7 @@ type MergeResult struct {
 	// Err is set when a terminal error occur in the process
 	Err error
 
-	Id     string
+	Id     Id
 	Status MergeStatus
 
 	// Only set for invalid status
@@ -42,14 +44,14 @@ func (mr MergeResult) String() string {
 	}
 }
 
-func NewMergeError(err error, id string) MergeResult {
+func NewMergeError(err error, id Id) MergeResult {
 	return MergeResult{
 		Err: err,
 		Id:  id,
 	}
 }
 
-func NewMergeStatus(status MergeStatus, id string, entity Interface) MergeResult {
+func NewMergeStatus(status MergeStatus, id Id, entity Interface) MergeResult {
 	return MergeResult{
 		Id:     id,
 		Status: status,
@@ -59,7 +61,7 @@ func NewMergeStatus(status MergeStatus, id string, entity Interface) MergeResult
 	}
 }
 
-func NewMergeInvalidStatus(id string, reason string) MergeResult {
+func NewMergeInvalidStatus(id Id, reason string) MergeResult {
 	return MergeResult{
 		Id:     id,
 		Status: MergeStatusInvalid,

graphql/connections/connections.go 🔗

@@ -1,5 +1,5 @@
-//go:generate genny -in=connection_template.go -out=gen_lazy_bug.go gen "Name=LazyBug NodeType=string EdgeType=LazyBugEdge ConnectionType=models.BugConnection"
-//go:generate genny -in=connection_template.go -out=gen_lazy_identity.go gen "Name=LazyIdentity NodeType=string EdgeType=LazyIdentityEdge ConnectionType=models.IdentityConnection"
+//go:generate genny -in=connection_template.go -out=gen_lazy_bug.go gen "Name=LazyBug NodeType=entity.Id EdgeType=LazyBugEdge ConnectionType=models.BugConnection"
+//go:generate genny -in=connection_template.go -out=gen_lazy_identity.go gen "Name=LazyIdentity NodeType=entity.Id EdgeType=LazyIdentityEdge ConnectionType=models.IdentityConnection"
 //go:generate genny -in=connection_template.go -out=gen_identity.go gen "Name=Identity NodeType=identity.Interface EdgeType=models.IdentityEdge ConnectionType=models.IdentityConnection"
 //go:generate genny -in=connection_template.go -out=gen_operation.go gen "Name=Operation NodeType=bug.Operation EdgeType=models.OperationEdge ConnectionType=models.OperationConnection"
 //go:generate genny -in=connection_template.go -out=gen_comment.go gen "Name=Comment NodeType=bug.Comment EdgeType=models.CommentEdge ConnectionType=models.CommentConnection"

graphql/connections/gen_lazy_bug.go 🔗

@@ -7,23 +7,24 @@ package connections
 import (
 	"fmt"
 
+	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/graphql/models"
 )
 
-// StringEdgeMaker define a function that take a string and an offset and
+// EntityIdEdgeMaker define a function that take a entity.Id and an offset and
 // create an Edge.
-type LazyBugEdgeMaker func(value string, offset int) Edge
+type LazyBugEdgeMaker func(value entity.Id, offset int) Edge
 
 // LazyBugConMaker define a function that create a models.BugConnection
 type LazyBugConMaker func(
 	edges []*LazyBugEdge,
-	nodes []string,
+	nodes []entity.Id,
 	info *models.PageInfo,
 	totalCount int) (*models.BugConnection, error)
 
 // LazyBugCon will paginate a source according to the input of a relay connection
-func LazyBugCon(source []string, edgeMaker LazyBugEdgeMaker, conMaker LazyBugConMaker, input models.ConnectionInput) (*models.BugConnection, error) {
-	var nodes []string
+func LazyBugCon(source []entity.Id, edgeMaker LazyBugEdgeMaker, conMaker LazyBugConMaker, input models.ConnectionInput) (*models.BugConnection, error) {
+	var nodes []entity.Id
 	var edges []*LazyBugEdge
 	var cursors []string
 	var pageInfo = &models.PageInfo{}

graphql/connections/gen_lazy_identity.go 🔗

@@ -7,23 +7,24 @@ package connections
 import (
 	"fmt"
 
+	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/graphql/models"
 )
 
-// StringEdgeMaker define a function that take a string and an offset and
+// EntityIdEdgeMaker define a function that take a entity.Id and an offset and
 // create an Edge.
-type LazyIdentityEdgeMaker func(value string, offset int) Edge
+type LazyIdentityEdgeMaker func(value entity.Id, offset int) Edge
 
 // LazyIdentityConMaker define a function that create a models.IdentityConnection
 type LazyIdentityConMaker func(
 	edges []*LazyIdentityEdge,
-	nodes []string,
+	nodes []entity.Id,
 	info *models.PageInfo,
 	totalCount int) (*models.IdentityConnection, error)
 
 // LazyIdentityCon will paginate a source according to the input of a relay connection
-func LazyIdentityCon(source []string, edgeMaker LazyIdentityEdgeMaker, conMaker LazyIdentityConMaker, input models.ConnectionInput) (*models.IdentityConnection, error) {
-	var nodes []string
+func LazyIdentityCon(source []entity.Id, edgeMaker LazyIdentityEdgeMaker, conMaker LazyIdentityConMaker, input models.ConnectionInput) (*models.IdentityConnection, error) {
+	var nodes []entity.Id
 	var edges []*LazyIdentityEdge
 	var cursors []string
 	var pageInfo = &models.PageInfo{}

graphql/connections/lazy_bug.go 🔗

@@ -1,8 +1,10 @@
 package connections
 
+import "github.com/MichaelMure/git-bug/entity"
+
 // LazyBugEdge is a special relay edge used to implement a lazy loading connection
 type LazyBugEdge struct {
-	Id     string
+	Id     entity.Id
 	Cursor string
 }
 

graphql/connections/lazy_identity.go 🔗

@@ -1,8 +1,10 @@
 package connections
 
+import "github.com/MichaelMure/git-bug/entity"
+
 // LazyIdentityEdge is a special relay edge used to implement a lazy loading connection
 type LazyIdentityEdge struct {
-	Id     string
+	Id     entity.Id
 	Cursor string
 }
 

graphql/graph/gen_graph.go 🔗

@@ -98,8 +98,8 @@ type ComplexityRoot struct {
 		Author       func(childComplexity int) int
 		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
+		HumanID      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
@@ -358,13 +358,19 @@ type ComplexityRoot struct {
 }
 
 type AddCommentOperationResolver interface {
+	ID(ctx context.Context, obj *bug.AddCommentOperation) (string, error)
+
 	Date(ctx context.Context, obj *bug.AddCommentOperation) (*time.Time, error)
 }
 type AddCommentTimelineItemResolver interface {
+	ID(ctx context.Context, obj *bug.AddCommentTimelineItem) (string, error)
+
 	CreatedAt(ctx context.Context, obj *bug.AddCommentTimelineItem) (*time.Time, error)
 	LastEdit(ctx context.Context, obj *bug.AddCommentTimelineItem) (*time.Time, error)
 }
 type BugResolver interface {
+	ID(ctx context.Context, obj *bug.Snapshot) (string, error)
+	HumanID(ctx context.Context, obj *bug.Snapshot) (string, error)
 	Status(ctx context.Context, obj *bug.Snapshot) (models.Status, error)
 
 	LastEdit(ctx context.Context, obj *bug.Snapshot) (*time.Time, error)
@@ -383,14 +389,21 @@ type CommentHistoryStepResolver interface {
 	Date(ctx context.Context, obj *bug.CommentHistoryStep) (*time.Time, error)
 }
 type CreateOperationResolver interface {
+	ID(ctx context.Context, obj *bug.CreateOperation) (string, error)
+
 	Date(ctx context.Context, obj *bug.CreateOperation) (*time.Time, error)
 }
 type CreateTimelineItemResolver interface {
+	ID(ctx context.Context, obj *bug.CreateTimelineItem) (string, error)
+
 	CreatedAt(ctx context.Context, obj *bug.CreateTimelineItem) (*time.Time, error)
 	LastEdit(ctx context.Context, obj *bug.CreateTimelineItem) (*time.Time, error)
 }
 type EditCommentOperationResolver interface {
+	ID(ctx context.Context, obj *bug.EditCommentOperation) (string, error)
+
 	Date(ctx context.Context, obj *bug.EditCommentOperation) (*time.Time, error)
+	Target(ctx context.Context, obj *bug.EditCommentOperation) (string, error)
 }
 type IdentityResolver interface {
 	ID(ctx context.Context, obj *identity.Interface) (string, error)
@@ -407,12 +420,16 @@ type LabelResolver interface {
 	Color(ctx context.Context, obj *bug.Label) (*color.RGBA, error)
 }
 type LabelChangeOperationResolver interface {
+	ID(ctx context.Context, obj *bug.LabelChangeOperation) (string, error)
+
 	Date(ctx context.Context, obj *bug.LabelChangeOperation) (*time.Time, error)
 }
 type LabelChangeResultResolver interface {
 	Status(ctx context.Context, obj *bug.LabelChangeResult) (models.LabelChangeStatus, error)
 }
 type LabelChangeTimelineItemResolver interface {
+	ID(ctx context.Context, obj *bug.LabelChangeTimelineItem) (string, error)
+
 	Date(ctx context.Context, obj *bug.LabelChangeTimelineItem) (*time.Time, error)
 }
 type MutationResolver interface {
@@ -438,17 +455,25 @@ type RepositoryResolver interface {
 	ValidLabels(ctx context.Context, obj *models.Repository) ([]bug.Label, error)
 }
 type SetStatusOperationResolver interface {
+	ID(ctx context.Context, obj *bug.SetStatusOperation) (string, error)
+
 	Date(ctx context.Context, obj *bug.SetStatusOperation) (*time.Time, error)
 	Status(ctx context.Context, obj *bug.SetStatusOperation) (models.Status, error)
 }
 type SetStatusTimelineItemResolver interface {
+	ID(ctx context.Context, obj *bug.SetStatusTimelineItem) (string, error)
+
 	Date(ctx context.Context, obj *bug.SetStatusTimelineItem) (*time.Time, error)
 	Status(ctx context.Context, obj *bug.SetStatusTimelineItem) (models.Status, error)
 }
 type SetTitleOperationResolver interface {
+	ID(ctx context.Context, obj *bug.SetTitleOperation) (string, error)
+
 	Date(ctx context.Context, obj *bug.SetTitleOperation) (*time.Time, error)
 }
 type SetTitleTimelineItemResolver interface {
+	ID(ctx context.Context, obj *bug.SetTitleTimelineItem) (string, error)
+
 	Date(ctx context.Context, obj *bug.SetTitleTimelineItem) (*time.Time, error)
 }
 
@@ -625,18 +650,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 		return e.complexity.Bug.CreatedAt(childComplexity), true
 
 	case "Bug.humanId":
-		if e.complexity.Bug.HumanId == nil {
+		if e.complexity.Bug.HumanID == nil {
 			break
 		}
 
-		return e.complexity.Bug.HumanId(childComplexity), true
+		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 {
@@ -2918,7 +2943,7 @@ func (ec *executionContext) _AddCommentOperation_id(ctx context.Context, field g
 	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
 	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
 		ctx = rctx // use context from middleware stack in children
-		return obj.ID(), nil
+		return ec.resolvers.AddCommentOperation().ID(rctx, obj)
 	})
 	if err != nil {
 		ec.Error(ctx, err)
@@ -3211,7 +3236,7 @@ func (ec *executionContext) _AddCommentTimelineItem_id(ctx context.Context, fiel
 	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
 	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
 		ctx = rctx // use context from middleware stack in children
-		return obj.ID(), nil
+		return ec.resolvers.AddCommentTimelineItem().ID(rctx, obj)
 	})
 	if err != nil {
 		ec.Error(ctx, err)
@@ -3544,7 +3569,7 @@ func (ec *executionContext) _Bug_id(ctx context.Context, field graphql.Collected
 	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
 	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
 		ctx = rctx // use context from middleware stack in children
-		return obj.Id(), nil
+		return ec.resolvers.Bug().ID(rctx, obj)
 	})
 	if err != nil {
 		ec.Error(ctx, err)
@@ -3581,7 +3606,7 @@ func (ec *executionContext) _Bug_humanId(ctx context.Context, field graphql.Coll
 	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
 	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
 		ctx = rctx // use context from middleware stack in children
-		return obj.HumanId(), nil
+		return ec.resolvers.Bug().HumanID(rctx, obj)
 	})
 	if err != nil {
 		ec.Error(ctx, err)
@@ -5195,7 +5220,7 @@ func (ec *executionContext) _CreateOperation_id(ctx context.Context, field graph
 	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
 	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
 		ctx = rctx // use context from middleware stack in children
-		return obj.ID(), nil
+		return ec.resolvers.CreateOperation().ID(rctx, obj)
 	})
 	if err != nil {
 		ec.Error(ctx, err)
@@ -5417,7 +5442,7 @@ func (ec *executionContext) _CreateTimelineItem_id(ctx context.Context, field gr
 	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
 	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
 		ctx = rctx // use context from middleware stack in children
-		return obj.ID(), nil
+		return ec.resolvers.CreateTimelineItem().ID(rctx, obj)
 	})
 	if err != nil {
 		ec.Error(ctx, err)
@@ -5750,7 +5775,7 @@ func (ec *executionContext) _EditCommentOperation_id(ctx context.Context, field
 	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
 	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
 		ctx = rctx // use context from middleware stack in children
-		return obj.ID(), nil
+		return ec.resolvers.EditCommentOperation().ID(rctx, obj)
 	})
 	if err != nil {
 		ec.Error(ctx, err)
@@ -5855,13 +5880,13 @@ func (ec *executionContext) _EditCommentOperation_target(ctx context.Context, fi
 		Object:   "EditCommentOperation",
 		Field:    field,
 		Args:     nil,
-		IsMethod: false,
+		IsMethod: true,
 	}
 	ctx = graphql.WithResolverContext(ctx, rctx)
 	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
 	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
 		ctx = rctx // use context from middleware stack in children
-		return obj.Target, nil
+		return ec.resolvers.EditCommentOperation().Target(rctx, obj)
 	})
 	if err != nil {
 		ec.Error(ctx, err)
@@ -6552,7 +6577,7 @@ func (ec *executionContext) _LabelChangeOperation_id(ctx context.Context, field
 	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
 	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
 		ctx = rctx // use context from middleware stack in children
-		return obj.ID(), nil
+		return ec.resolvers.LabelChangeOperation().ID(rctx, obj)
 	})
 	if err != nil {
 		ec.Error(ctx, err)
@@ -6811,7 +6836,7 @@ func (ec *executionContext) _LabelChangeTimelineItem_id(ctx context.Context, fie
 	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
 	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
 		ctx = rctx // use context from middleware stack in children
-		return obj.ID(), nil
+		return ec.resolvers.LabelChangeTimelineItem().ID(rctx, obj)
 	})
 	if err != nil {
 		ec.Error(ctx, err)
@@ -8325,7 +8350,7 @@ func (ec *executionContext) _SetStatusOperation_id(ctx context.Context, field gr
 	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
 	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
 		ctx = rctx // use context from middleware stack in children
-		return obj.ID(), nil
+		return ec.resolvers.SetStatusOperation().ID(rctx, obj)
 	})
 	if err != nil {
 		ec.Error(ctx, err)
@@ -8473,7 +8498,7 @@ func (ec *executionContext) _SetStatusTimelineItem_id(ctx context.Context, field
 	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
 	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
 		ctx = rctx // use context from middleware stack in children
-		return obj.ID(), nil
+		return ec.resolvers.SetStatusTimelineItem().ID(rctx, obj)
 	})
 	if err != nil {
 		ec.Error(ctx, err)
@@ -8621,7 +8646,7 @@ func (ec *executionContext) _SetTitleOperation_id(ctx context.Context, field gra
 	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
 	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
 		ctx = rctx // use context from middleware stack in children
-		return obj.ID(), nil
+		return ec.resolvers.SetTitleOperation().ID(rctx, obj)
 	})
 	if err != nil {
 		ec.Error(ctx, err)
@@ -8914,7 +8939,7 @@ func (ec *executionContext) _SetTitleTimelineItem_id(ctx context.Context, field
 	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
 	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
 		ctx = rctx // use context from middleware stack in children
-		return obj.ID(), nil
+		return ec.resolvers.SetTitleTimelineItem().ID(rctx, obj)
 	})
 	if err != nil {
 		ec.Error(ctx, err)
@@ -10838,10 +10863,19 @@ func (ec *executionContext) _AddCommentOperation(ctx context.Context, sel ast.Se
 		case "__typename":
 			out.Values[i] = graphql.MarshalString("AddCommentOperation")
 		case "id":
-			out.Values[i] = ec._AddCommentOperation_id(ctx, field, obj)
-			if out.Values[i] == graphql.Null {
-				atomic.AddUint32(&invalids, 1)
-			}
+			field := field
+			out.Concurrently(i, func() (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
+			})
 		case "author":
 			out.Values[i] = ec._AddCommentOperation_author(ctx, field, obj)
 			if out.Values[i] == graphql.Null {
@@ -10928,10 +10962,19 @@ func (ec *executionContext) _AddCommentTimelineItem(ctx context.Context, sel ast
 		case "__typename":
 			out.Values[i] = graphql.MarshalString("AddCommentTimelineItem")
 		case "id":
-			out.Values[i] = ec._AddCommentTimelineItem_id(ctx, field, obj)
-			if out.Values[i] == graphql.Null {
-				atomic.AddUint32(&invalids, 1)
-			}
+			field := field
+			out.Concurrently(i, func() (res graphql.Marshaler) {
+				defer func() {
+					if r := recover(); r != nil {
+						ec.Error(ctx, ec.Recover(ctx, r))
+					}
+				}()
+				res = ec._AddCommentTimelineItem_id(ctx, field, obj)
+				if res == graphql.Null {
+					atomic.AddUint32(&invalids, 1)
+				}
+				return res
+			})
 		case "author":
 			out.Values[i] = ec._AddCommentTimelineItem_author(ctx, field, obj)
 			if out.Values[i] == graphql.Null {
@@ -11013,15 +11056,33 @@ func (ec *executionContext) _Bug(ctx context.Context, sel ast.SelectionSet, obj
 		case "__typename":
 			out.Values[i] = graphql.MarshalString("Bug")
 		case "id":
-			out.Values[i] = ec._Bug_id(ctx, field, obj)
-			if out.Values[i] == graphql.Null {
-				atomic.AddUint32(&invalids, 1)
-			}
+			field := field
+			out.Concurrently(i, func() (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
+			})
 		case "humanId":
-			out.Values[i] = ec._Bug_humanId(ctx, field, obj)
-			if out.Values[i] == graphql.Null {
-				atomic.AddUint32(&invalids, 1)
-			}
+			field := field
+			out.Concurrently(i, func() (res graphql.Marshaler) {
+				defer func() {
+					if r := recover(); r != nil {
+						ec.Error(ctx, ec.Recover(ctx, r))
+					}
+				}()
+				res = ec._Bug_humanId(ctx, field, obj)
+				if res == graphql.Null {
+					atomic.AddUint32(&invalids, 1)
+				}
+				return res
+			})
 		case "status":
 			field := field
 			out.Concurrently(i, func() (res graphql.Marshaler) {
@@ -11584,10 +11645,19 @@ func (ec *executionContext) _CreateOperation(ctx context.Context, sel ast.Select
 		case "__typename":
 			out.Values[i] = graphql.MarshalString("CreateOperation")
 		case "id":
-			out.Values[i] = ec._CreateOperation_id(ctx, field, obj)
-			if out.Values[i] == graphql.Null {
-				atomic.AddUint32(&invalids, 1)
-			}
+			field := field
+			out.Concurrently(i, func() (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
+			})
 		case "author":
 			out.Values[i] = ec._CreateOperation_author(ctx, field, obj)
 			if out.Values[i] == graphql.Null {
@@ -11645,10 +11715,19 @@ func (ec *executionContext) _CreateTimelineItem(ctx context.Context, sel ast.Sel
 		case "__typename":
 			out.Values[i] = graphql.MarshalString("CreateTimelineItem")
 		case "id":
-			out.Values[i] = ec._CreateTimelineItem_id(ctx, field, obj)
-			if out.Values[i] == graphql.Null {
-				atomic.AddUint32(&invalids, 1)
-			}
+			field := field
+			out.Concurrently(i, func() (res graphql.Marshaler) {
+				defer func() {
+					if r := recover(); r != nil {
+						ec.Error(ctx, ec.Recover(ctx, r))
+					}
+				}()
+				res = ec._CreateTimelineItem_id(ctx, field, obj)
+				if res == graphql.Null {
+					atomic.AddUint32(&invalids, 1)
+				}
+				return res
+			})
 		case "author":
 			out.Values[i] = ec._CreateTimelineItem_author(ctx, field, obj)
 			if out.Values[i] == graphql.Null {
@@ -11730,10 +11809,19 @@ func (ec *executionContext) _EditCommentOperation(ctx context.Context, sel ast.S
 		case "__typename":
 			out.Values[i] = graphql.MarshalString("EditCommentOperation")
 		case "id":
-			out.Values[i] = ec._EditCommentOperation_id(ctx, field, obj)
-			if out.Values[i] == graphql.Null {
-				atomic.AddUint32(&invalids, 1)
-			}
+			field := field
+			out.Concurrently(i, func() (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
+			})
 		case "author":
 			out.Values[i] = ec._EditCommentOperation_author(ctx, field, obj)
 			if out.Values[i] == graphql.Null {
@@ -11754,10 +11842,19 @@ func (ec *executionContext) _EditCommentOperation(ctx context.Context, sel ast.S
 				return res
 			})
 		case "target":
-			out.Values[i] = ec._EditCommentOperation_target(ctx, field, obj)
-			if out.Values[i] == graphql.Null {
-				atomic.AddUint32(&invalids, 1)
-			}
+			field := field
+			out.Concurrently(i, func() (res graphql.Marshaler) {
+				defer func() {
+					if r := recover(); r != nil {
+						ec.Error(ctx, ec.Recover(ctx, r))
+					}
+				}()
+				res = ec._EditCommentOperation_target(ctx, field, obj)
+				if res == graphql.Null {
+					atomic.AddUint32(&invalids, 1)
+				}
+				return res
+			})
 		case "message":
 			out.Values[i] = ec._EditCommentOperation_message(ctx, field, obj)
 			if out.Values[i] == graphql.Null {
@@ -12037,10 +12134,19 @@ func (ec *executionContext) _LabelChangeOperation(ctx context.Context, sel ast.S
 		case "__typename":
 			out.Values[i] = graphql.MarshalString("LabelChangeOperation")
 		case "id":
-			out.Values[i] = ec._LabelChangeOperation_id(ctx, field, obj)
-			if out.Values[i] == graphql.Null {
-				atomic.AddUint32(&invalids, 1)
-			}
+			field := field
+			out.Concurrently(i, func() (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
+			})
 		case "author":
 			out.Values[i] = ec._LabelChangeOperation_author(ctx, field, obj)
 			if out.Values[i] == graphql.Null {
@@ -12134,10 +12240,19 @@ func (ec *executionContext) _LabelChangeTimelineItem(ctx context.Context, sel as
 		case "__typename":
 			out.Values[i] = graphql.MarshalString("LabelChangeTimelineItem")
 		case "id":
-			out.Values[i] = ec._LabelChangeTimelineItem_id(ctx, field, obj)
-			if out.Values[i] == graphql.Null {
-				atomic.AddUint32(&invalids, 1)
-			}
+			field := field
+			out.Concurrently(i, func() (res graphql.Marshaler) {
+				defer func() {
+					if r := recover(); r != nil {
+						ec.Error(ctx, ec.Recover(ctx, r))
+					}
+				}()
+				res = ec._LabelChangeTimelineItem_id(ctx, field, obj)
+				if res == graphql.Null {
+					atomic.AddUint32(&invalids, 1)
+				}
+				return res
+			})
 		case "author":
 			out.Values[i] = ec._LabelChangeTimelineItem_author(ctx, field, obj)
 			if out.Values[i] == graphql.Null {
@@ -12589,10 +12704,19 @@ func (ec *executionContext) _SetStatusOperation(ctx context.Context, sel ast.Sel
 		case "__typename":
 			out.Values[i] = graphql.MarshalString("SetStatusOperation")
 		case "id":
-			out.Values[i] = ec._SetStatusOperation_id(ctx, field, obj)
-			if out.Values[i] == graphql.Null {
-				atomic.AddUint32(&invalids, 1)
-			}
+			field := field
+			out.Concurrently(i, func() (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
+			})
 		case "author":
 			out.Values[i] = ec._SetStatusOperation_author(ctx, field, obj)
 			if out.Values[i] == graphql.Null {
@@ -12649,10 +12773,19 @@ func (ec *executionContext) _SetStatusTimelineItem(ctx context.Context, sel ast.
 		case "__typename":
 			out.Values[i] = graphql.MarshalString("SetStatusTimelineItem")
 		case "id":
-			out.Values[i] = ec._SetStatusTimelineItem_id(ctx, field, obj)
-			if out.Values[i] == graphql.Null {
-				atomic.AddUint32(&invalids, 1)
-			}
+			field := field
+			out.Concurrently(i, func() (res graphql.Marshaler) {
+				defer func() {
+					if r := recover(); r != nil {
+						ec.Error(ctx, ec.Recover(ctx, r))
+					}
+				}()
+				res = ec._SetStatusTimelineItem_id(ctx, field, obj)
+				if res == graphql.Null {
+					atomic.AddUint32(&invalids, 1)
+				}
+				return res
+			})
 		case "author":
 			out.Values[i] = ec._SetStatusTimelineItem_author(ctx, field, obj)
 			if out.Values[i] == graphql.Null {
@@ -12709,10 +12842,19 @@ func (ec *executionContext) _SetTitleOperation(ctx context.Context, sel ast.Sele
 		case "__typename":
 			out.Values[i] = graphql.MarshalString("SetTitleOperation")
 		case "id":
-			out.Values[i] = ec._SetTitleOperation_id(ctx, field, obj)
-			if out.Values[i] == graphql.Null {
-				atomic.AddUint32(&invalids, 1)
-			}
+			field := field
+			out.Concurrently(i, func() (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
+			})
 		case "author":
 			out.Values[i] = ec._SetTitleOperation_author(ctx, field, obj)
 			if out.Values[i] == graphql.Null {
@@ -12799,10 +12941,19 @@ func (ec *executionContext) _SetTitleTimelineItem(ctx context.Context, sel ast.S
 		case "__typename":
 			out.Values[i] = graphql.MarshalString("SetTitleTimelineItem")
 		case "id":
-			out.Values[i] = ec._SetTitleTimelineItem_id(ctx, field, obj)
-			if out.Values[i] == graphql.Null {
-				atomic.AddUint32(&invalids, 1)
-			}
+			field := field
+			out.Concurrently(i, func() (res graphql.Marshaler) {
+				defer func() {
+					if r := recover(); r != nil {
+						ec.Error(ctx, ec.Recover(ctx, r))
+					}
+				}()
+				res = ec._SetTitleTimelineItem_id(ctx, field, obj)
+				if res == graphql.Null {
+					atomic.AddUint32(&invalids, 1)
+				}
+				return res
+			})
 		case "author":
 			out.Values[i] = ec._SetTitleTimelineItem_author(ctx, field, obj)
 			if out.Values[i] == graphql.Null {

graphql/resolvers/bug.go 🔗

@@ -15,6 +15,14 @@ var _ graph.BugResolver = &bugResolver{}
 
 type bugResolver struct{}
 
+func (bugResolver) ID(ctx context.Context, obj *bug.Snapshot) (string, error) {
+	return obj.Id().String(), nil
+}
+
+func (bugResolver) HumanID(ctx context.Context, obj *bug.Snapshot) (string, error) {
+	return obj.Id().Human(), nil
+}
+
 func (bugResolver) Status(ctx context.Context, obj *bug.Snapshot) (models.Status, error) {
 	return convertStatus(obj.Status)
 }

graphql/resolvers/identity.go 🔗

@@ -12,11 +12,11 @@ var _ graph.IdentityResolver = &identityResolver{}
 type identityResolver struct{}
 
 func (identityResolver) ID(ctx context.Context, obj *identity.Interface) (string, error) {
-	return (*obj).Id(), nil
+	return (*obj).Id().String(), nil
 }
 
 func (identityResolver) HumanID(ctx context.Context, obj *identity.Interface) (string, error) {
-	return (*obj).HumanId(), nil
+	return (*obj).Id().Human(), nil
 }
 
 func (identityResolver) Name(ctx context.Context, obj *identity.Interface) (*string, error) {

graphql/resolvers/operations.go 🔗

@@ -6,39 +6,74 @@ import (
 	"time"
 
 	"github.com/MichaelMure/git-bug/bug"
+	"github.com/MichaelMure/git-bug/graphql/graph"
 	"github.com/MichaelMure/git-bug/graphql/models"
 )
 
+var _ graph.CreateOperationResolver = createOperationResolver{}
+
 type createOperationResolver struct{}
 
+func (createOperationResolver) ID(ctx context.Context, obj *bug.CreateOperation) (string, error) {
+	return obj.Id().String(), nil
+}
+
 func (createOperationResolver) Date(ctx context.Context, obj *bug.CreateOperation) (*time.Time, error) {
 	t := obj.Time()
 	return &t, nil
 }
 
+var _ graph.AddCommentOperationResolver = addCommentOperationResolver{}
+
 type addCommentOperationResolver struct{}
 
+func (addCommentOperationResolver) ID(ctx context.Context, obj *bug.AddCommentOperation) (string, error) {
+	return obj.Id().String(), nil
+}
+
 func (addCommentOperationResolver) Date(ctx context.Context, obj *bug.AddCommentOperation) (*time.Time, error) {
 	t := obj.Time()
 	return &t, nil
 }
 
+var _ graph.EditCommentOperationResolver = editCommentOperationResolver{}
+
 type editCommentOperationResolver struct{}
 
+func (editCommentOperationResolver) ID(ctx context.Context, obj *bug.EditCommentOperation) (string, error) {
+	return obj.Id().String(), nil
+}
+
+func (editCommentOperationResolver) Target(ctx context.Context, obj *bug.EditCommentOperation) (string, error) {
+	panic("implement me")
+}
+
 func (editCommentOperationResolver) Date(ctx context.Context, obj *bug.EditCommentOperation) (*time.Time, error) {
 	t := obj.Time()
 	return &t, nil
 }
 
-type labelChangeOperation struct{}
+var _ graph.LabelChangeOperationResolver = labelChangeOperationResolver{}
+
+type labelChangeOperationResolver struct{}
 
-func (labelChangeOperation) Date(ctx context.Context, obj *bug.LabelChangeOperation) (*time.Time, error) {
+func (labelChangeOperationResolver) ID(ctx context.Context, obj *bug.LabelChangeOperation) (string, error) {
+	return obj.Id().String(), nil
+}
+
+func (labelChangeOperationResolver) Date(ctx context.Context, obj *bug.LabelChangeOperation) (*time.Time, error) {
 	t := obj.Time()
 	return &t, nil
 }
 
+var _ graph.SetStatusOperationResolver = setStatusOperationResolver{}
+
 type setStatusOperationResolver struct{}
 
+func (setStatusOperationResolver) ID(ctx context.Context, obj *bug.SetStatusOperation) (string, error) {
+	return obj.Id().String(), nil
+}
+
 func (setStatusOperationResolver) Date(ctx context.Context, obj *bug.SetStatusOperation) (*time.Time, error) {
 	t := obj.Time()
 	return &t, nil
@@ -48,8 +83,14 @@ func (setStatusOperationResolver) Status(ctx context.Context, obj *bug.SetStatus
 	return convertStatus(obj.Status)
 }
 
+var _ graph.SetTitleOperationResolver = setTitleOperationResolver{}
+
 type setTitleOperationResolver struct{}
 
+func (setTitleOperationResolver) ID(ctx context.Context, obj *bug.SetTitleOperation) (string, error) {
+	return obj.Id().String(), nil
+}
+
 func (setTitleOperationResolver) Date(ctx context.Context, obj *bug.SetTitleOperation) (*time.Time, error) {
 	t := obj.Time()
 	return &t, nil

graphql/resolvers/repo.go 🔗

@@ -5,6 +5,7 @@ import (
 
 	"github.com/MichaelMure/git-bug/bug"
 	"github.com/MichaelMure/git-bug/cache"
+	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/graphql/connections"
 	"github.com/MichaelMure/git-bug/graphql/graph"
 	"github.com/MichaelMure/git-bug/graphql/models"
@@ -38,7 +39,7 @@ func (repoResolver) AllBugs(ctx context.Context, obj *models.Repository, after *
 	source := obj.Repo.QueryBugs(query)
 
 	// The edger create a custom edge holding just the id
-	edger := func(id string, offset int) connections.Edge {
+	edger := func(id entity.Id, offset int) connections.Edge {
 		return connections.LazyBugEdge{
 			Id:     id,
 			Cursor: connections.OffsetToCursor(offset),
@@ -46,7 +47,7 @@ func (repoResolver) AllBugs(ctx context.Context, obj *models.Repository, after *
 	}
 
 	// The conMaker will finally load and compile bugs from git to replace the selected edges
-	conMaker := func(lazyBugEdges []*connections.LazyBugEdge, lazyNode []string, info *models.PageInfo, totalCount int) (*models.BugConnection, error) {
+	conMaker := func(lazyBugEdges []*connections.LazyBugEdge, lazyNode []entity.Id, info *models.PageInfo, totalCount int) (*models.BugConnection, error) {
 		edges := make([]*models.BugEdge, len(lazyBugEdges))
 		nodes := make([]*bug.Snapshot, len(lazyBugEdges))
 
@@ -99,7 +100,7 @@ func (repoResolver) AllIdentities(ctx context.Context, obj *models.Repository, a
 	source := obj.Repo.AllIdentityIds()
 
 	// The edger create a custom edge holding just the id
-	edger := func(id string, offset int) connections.Edge {
+	edger := func(id entity.Id, offset int) connections.Edge {
 		return connections.LazyIdentityEdge{
 			Id:     id,
 			Cursor: connections.OffsetToCursor(offset),
@@ -107,7 +108,7 @@ func (repoResolver) AllIdentities(ctx context.Context, obj *models.Repository, a
 	}
 
 	// The conMaker will finally load and compile identities from git to replace the selected edges
-	conMaker := func(lazyIdentityEdges []*connections.LazyIdentityEdge, lazyNode []string, info *models.PageInfo, totalCount int) (*models.IdentityConnection, error) {
+	conMaker := func(lazyIdentityEdges []*connections.LazyIdentityEdge, lazyNode []entity.Id, info *models.PageInfo, totalCount int) (*models.IdentityConnection, error) {
 		edges := make([]*models.IdentityEdge, len(lazyIdentityEdges))
 		nodes := make([]identity.Interface, len(lazyIdentityEdges))
 

graphql/resolvers/root.go 🔗

@@ -87,7 +87,7 @@ func (r RootResolver) EditCommentOperation() graph.EditCommentOperationResolver
 }
 
 func (RootResolver) LabelChangeOperation() graph.LabelChangeOperationResolver {
-	return &labelChangeOperation{}
+	return &labelChangeOperationResolver{}
 }
 
 func (RootResolver) SetStatusOperation() graph.SetStatusOperationResolver {

graphql/resolvers/timeline.go 🔗

@@ -5,9 +5,12 @@ import (
 	"time"
 
 	"github.com/MichaelMure/git-bug/bug"
+	"github.com/MichaelMure/git-bug/graphql/graph"
 	"github.com/MichaelMure/git-bug/graphql/models"
 )
 
+var _ graph.CommentHistoryStepResolver = commentHistoryStepResolver{}
+
 type commentHistoryStepResolver struct{}
 
 func (commentHistoryStepResolver) Date(ctx context.Context, obj *bug.CommentHistoryStep) (*time.Time, error) {
@@ -15,8 +18,14 @@ func (commentHistoryStepResolver) Date(ctx context.Context, obj *bug.CommentHist
 	return &t, nil
 }
 
+var _ graph.AddCommentTimelineItemResolver = addCommentTimelineItemResolver{}
+
 type addCommentTimelineItemResolver struct{}
 
+func (addCommentTimelineItemResolver) ID(ctx context.Context, obj *bug.AddCommentTimelineItem) (string, error) {
+	return obj.Id().String(), nil
+}
+
 func (addCommentTimelineItemResolver) CreatedAt(ctx context.Context, obj *bug.AddCommentTimelineItem) (*time.Time, error) {
 	t := obj.CreatedAt.Time()
 	return &t, nil
@@ -27,8 +36,14 @@ func (addCommentTimelineItemResolver) LastEdit(ctx context.Context, obj *bug.Add
 	return &t, nil
 }
 
+var _ graph.CreateTimelineItemResolver = createTimelineItemResolver{}
+
 type createTimelineItemResolver struct{}
 
+func (createTimelineItemResolver) ID(ctx context.Context, obj *bug.CreateTimelineItem) (string, error) {
+	return obj.Id().String(), nil
+}
+
 func (createTimelineItemResolver) CreatedAt(ctx context.Context, obj *bug.CreateTimelineItem) (*time.Time, error) {
 	t := obj.CreatedAt.Time()
 	return &t, nil
@@ -39,15 +54,27 @@ func (createTimelineItemResolver) LastEdit(ctx context.Context, obj *bug.CreateT
 	return &t, nil
 }
 
+var _ graph.LabelChangeTimelineItemResolver = labelChangeTimelineItem{}
+
 type labelChangeTimelineItem struct{}
 
+func (labelChangeTimelineItem) ID(ctx context.Context, obj *bug.LabelChangeTimelineItem) (string, error) {
+	return obj.Id().String(), nil
+}
+
 func (labelChangeTimelineItem) Date(ctx context.Context, obj *bug.LabelChangeTimelineItem) (*time.Time, error) {
 	t := obj.UnixTime.Time()
 	return &t, nil
 }
 
+var _ graph.SetStatusTimelineItemResolver = setStatusTimelineItem{}
+
 type setStatusTimelineItem struct{}
 
+func (setStatusTimelineItem) ID(ctx context.Context, obj *bug.SetStatusTimelineItem) (string, error) {
+	return obj.Id().String(), nil
+}
+
 func (setStatusTimelineItem) Date(ctx context.Context, obj *bug.SetStatusTimelineItem) (*time.Time, error) {
 	t := obj.UnixTime.Time()
 	return &t, nil
@@ -57,8 +84,14 @@ func (setStatusTimelineItem) Status(ctx context.Context, obj *bug.SetStatusTimel
 	return convertStatus(obj.Status)
 }
 
+var _ graph.SetTitleTimelineItemResolver = setTitleTimelineItem{}
+
 type setTitleTimelineItem struct{}
 
+func (setTitleTimelineItem) ID(ctx context.Context, obj *bug.SetTitleTimelineItem) (string, error) {
+	return obj.Id().String(), nil
+}
+
 func (setTitleTimelineItem) Date(ctx context.Context, obj *bug.SetTitleTimelineItem) (*time.Time, error) {
 	t := obj.UnixTime.Time()
 	return &t, nil

identity/bare.go 🔗

@@ -50,13 +50,12 @@ type bareIdentityJSON struct {
 }
 
 func (i *Bare) MarshalJSON() ([]byte, error) {
-	data, err := json.Marshal(bareIdentityJSON{
+	return json.Marshal(bareIdentityJSON{
 		Name:      i.name,
 		Email:     i.email,
 		Login:     i.login,
 		AvatarUrl: i.avatarUrl,
 	})
-	return data, err
 }
 
 func (i *Bare) UnmarshalJSON(data []byte) error {

identity/common.go 🔗

@@ -4,17 +4,14 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
-	"strings"
+
+	"github.com/MichaelMure/git-bug/entity"
 )
 
 var ErrIdentityNotExist = errors.New("identity doesn't exist")
 
-type ErrMultipleMatch struct {
-	Matching []string
-}
-
-func (e ErrMultipleMatch) Error() string {
-	return fmt.Sprintf("Multiple matching identities found:\n%s", strings.Join(e.Matching, "\n"))
+func NewErrMultipleMatch(matching []entity.Id) *entity.ErrMultipleMatch {
+	return entity.NewErrMultipleMatch("identity", matching)
 }
 
 // Custom unmarshaling function to allow package user to delegate

identity/identity_actions.go 🔗

@@ -59,8 +59,13 @@ func MergeAll(repo repository.ClockedRepo, remote string) <-chan entity.MergeRes
 		}
 
 		for _, remoteRef := range remoteRefs {
-			refSplitted := strings.Split(remoteRef, "/")
-			id := refSplitted[len(refSplitted)-1]
+			refSplit := strings.Split(remoteRef, "/")
+			id := entity.Id(refSplit[len(refSplit)-1])
+
+			if err := id.Validate(); err != nil {
+				out <- entity.NewMergeInvalidStatus(id, errors.Wrap(err, "invalid ref").Error())
+				continue
+			}
 
 			remoteIdentity, err := read(repo, remoteRef)
 

termui/bug_table.go 🔗

@@ -25,7 +25,7 @@ type bugTable struct {
 	repo         *cache.RepoCache
 	queryStr     string
 	query        *cache.Query
-	allIds       []string
+	allIds       []entity.Id
 	excerpts     []*cache.BugExcerpt
 	pageCursor   int
 	selectCursor int
@@ -308,7 +308,7 @@ func (bt *bugTable) render(v *gocui.View, maxX int) {
 
 		lastEditTime := time.Unix(excerpt.EditUnixTime, 0)
 
-		id := text.LeftPadMaxLine(excerpt.HumanId(), columnWidths["id"], 1)
+		id := text.LeftPadMaxLine(excerpt.Id.Human(), columnWidths["id"], 1)
 		status := text.LeftPadMaxLine(excerpt.Status.String(), columnWidths["status"], 1)
 		title := text.LeftPadMaxLine(excerpt.Title, columnWidths["title"], 1)
 		author := text.LeftPadMaxLine(authorDisplayName, columnWidths["author"], 1)
@@ -482,7 +482,7 @@ func (bt *bugTable) pull(g *gocui.Gui, v *gocui.View) error {
 				})
 			} else {
 				_, _ = fmt.Fprintf(&buffer, "%s%s: %s",
-					beginLine, colors.Cyan(result.Entity.HumanId()), result,
+					beginLine, colors.Cyan(result.Entity.Id().Human()), result,
 				)
 
 				beginLine = "\n"

termui/show_bug.go 🔗

@@ -7,6 +7,7 @@ import (
 
 	"github.com/MichaelMure/git-bug/bug"
 	"github.com/MichaelMure/git-bug/cache"
+	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/util/colors"
 	"github.com/MichaelMure/git-bug/util/text"
 	"github.com/MichaelMure/gocui"
@@ -213,7 +214,7 @@ func (sb *showBug) renderMain(g *gocui.Gui, mainView *gocui.View) error {
 	}
 
 	bugHeader := fmt.Sprintf("[%s] %s\n\n[%s] %s opened this bug on %s%s",
-		colors.Cyan(snap.HumanId()),
+		colors.Cyan(snap.Id().Human()),
 		colors.Bold(snap.Title),
 		colors.Yellow(snap.Status),
 		colors.Magenta(snap.Author.DisplayName()),
@@ -231,7 +232,7 @@ func (sb *showBug) renderMain(g *gocui.Gui, mainView *gocui.View) error {
 	y0 += lines + 1
 
 	for _, op := range snap.Timeline {
-		viewName := op.ID()
+		viewName := op.Id().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
@@ -642,7 +643,7 @@ func (sb *showBug) edit(g *gocui.Gui, v *gocui.View) error {
 		return nil
 	}
 
-	op, err := snap.SearchTimelineItem(sb.selected)
+	op, err := snap.SearchTimelineItem(entity.Id(sb.selected))
 	if err != nil {
 		return err
 	}
@@ -650,10 +651,10 @@ func (sb *showBug) edit(g *gocui.Gui, v *gocui.View) error {
 	switch op.(type) {
 	case *bug.AddCommentTimelineItem:
 		message := op.(*bug.AddCommentTimelineItem).Message
-		return editCommentWithEditor(sb.bug, op.ID(), message)
+		return editCommentWithEditor(sb.bug, op.Id(), message)
 	case *bug.CreateTimelineItem:
 		preMessage := op.(*bug.CreateTimelineItem).Message
-		return editCommentWithEditor(sb.bug, op.ID(), preMessage)
+		return editCommentWithEditor(sb.bug, op.Id(), preMessage)
 	case *bug.LabelChangeTimelineItem:
 		return sb.editLabels(g, snap)
 	}

termui/termui.go 🔗

@@ -6,6 +6,7 @@ import (
 	"github.com/pkg/errors"
 
 	"github.com/MichaelMure/git-bug/cache"
+	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/input"
 )
 
@@ -237,7 +238,7 @@ func addCommentWithEditor(bug *cache.BugCache) error {
 	return errTerminateMainloop
 }
 
-func editCommentWithEditor(bug *cache.BugCache, target string, preMessage string) error {
+func editCommentWithEditor(bug *cache.BugCache, target entity.Id, 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.