identity: more progress and fixes

Michael Muré created

Change summary

bridge/github/import.go       |  23 ++-
bridge/launchpad/launchpad.go |   2 
cache/bug_excerpt.go          |   2 
cache/repo_cache.go           |  70 ++++++++++
commands/id.go                |  18 ++
graphql/gqlgen.yml            |   2 
graphql/graph/gen_graph.go    | 244 +++++++++++++++---------------------
repository/repo.go            |   2 
termui/bug_table.go           |  10 -
9 files changed, 206 insertions(+), 167 deletions(-)

Detailed changes

bridge/github/import.go 🔗

@@ -8,18 +8,20 @@ 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/util/git"
 	"github.com/shurcooL/githubv4"
 )
 
 const keyGithubId = "github-id"
 const keyGithubUrl = "github-url"
+const keyGithubLogin = "github-login"
 
 // githubImporter implement the Importer interface
 type githubImporter struct {
 	client *githubv4.Client
 	conf   core.Configuration
-	ghost  bug.Person
+	ghost  identity.Interface
 }
 
 func (gi *githubImporter) Init(conf core.Configuration) error {
@@ -69,7 +71,10 @@ func (gi *githubImporter) ImportAll(repo *cache.RepoCache) error {
 		}
 
 		for _, itemEdge := range q.Repository.Issues.Nodes[0].Timeline.Edges {
-			gi.ensureTimelineItem(b, itemEdge.Cursor, itemEdge.Node, variables)
+			err = gi.ensureTimelineItem(b, itemEdge.Cursor, itemEdge.Node, variables)
+			if err != nil {
+				return err
+			}
 		}
 
 		if !issue.Timeline.PageInfo.HasNextPage {
@@ -561,7 +566,7 @@ func (gi *githubImporter) ensureCommentEdit(b *cache.BugCache, target git.Hash,
 }
 
 // makePerson create a bug.Person from the Github data
-func (gi *githubImporter) makePerson(actor *actor) bug.Person {
+func (gi *githubImporter) makePerson(actor *actor) identity.Interface {
 	if actor == nil {
 		return gi.ghost
 	}
@@ -609,12 +614,12 @@ func (gi *githubImporter) fetchGhost() error {
 		name = string(*q.User.Name)
 	}
 
-	gi.ghost = bug.Person{
-		Name:      name,
-		Login:     string(q.User.Login),
-		AvatarUrl: string(q.User.AvatarUrl),
-		Email:     string(q.User.Email),
-	}
+	gi.ghost = identity.NewIdentityFull(
+		name,
+		string(q.User.Login),
+		string(q.User.AvatarUrl),
+		string(q.User.Email),
+	)
 
 	return nil
 }

bridge/launchpad/launchpad.go 🔗

@@ -1,4 +1,4 @@
-// Package launchad contains the Launchpad bridge implementation
+// Package launchpad contains the Launchpad bridge implementation
 package launchpad
 
 import (

cache/bug_excerpt.go 🔗

@@ -20,7 +20,7 @@ type BugExcerpt struct {
 	EditUnixTime      int64
 
 	Status bug.Status
-	Author *identity.Identity
+	Author identity.Interface
 	Labels []bug.Label
 
 	CreateMetadata map[string]string

cache/repo_cache.go 🔗

@@ -30,6 +30,8 @@ type RepoCache struct {
 	excerpts map[string]*BugExcerpt
 	// bug loaded in memory
 	bugs map[string]*BugCache
+	// identities loaded in memory
+	identities map[string]*identity.Identity
 }
 
 func NewRepoCache(r repository.ClockedRepo) (*RepoCache, error) {
@@ -279,6 +281,7 @@ func (c *RepoCache) ResolveBugCreateMetadata(key string, value string) (*BugCach
 	return c.ResolveBug(matching[0])
 }
 
+// QueryBugs return the id of all Bug matching the given Query
 func (c *RepoCache) QueryBugs(query *Query) []string {
 	if query == nil {
 		return c.AllBugsIds()
@@ -525,3 +528,70 @@ func repoIsAvailable(repo repository.Repo) error {
 
 	return nil
 }
+
+// ResolveIdentity retrieve an identity matching the exact given id
+func (c *RepoCache) ResolveIdentity(id string) (*identity.Identity, error) {
+	cached, ok := c.identities[id]
+	if ok {
+		return cached, nil
+	}
+
+	i, err := identity.Read(c.repo, id)
+	if err != nil {
+		return nil, err
+	}
+
+	c.identities[id] = i
+
+	return i, nil
+}
+
+// ResolveIdentityPrefix retrieve an Identity matching an id prefix. It fails if multiple
+// bugs match.
+// func (c *RepoCache) ResolveIdentityPrefix(prefix string) (*BugCache, error) {
+// 	// preallocate but empty
+// 	matching := make([]string, 0, 5)
+//
+// 	for id := range c.excerpts {
+// 		if strings.HasPrefix(id, prefix) {
+// 			matching = append(matching, id)
+// 		}
+// 	}
+//
+// 	if len(matching) > 1 {
+// 		return nil, bug.ErrMultipleMatch{Matching: matching}
+// 	}
+//
+// 	if len(matching) == 0 {
+// 		return nil, bug.ErrBugNotExist
+// 	}
+//
+// 	return c.ResolveBug(matching[0])
+// }
+
+// ResolveIdentityImmutableMetadata retrieve an Identity that has the exact given metadata on
+// 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) (*BugCache, error) {
+	// // preallocate but empty
+	// matching := make([]string, 0, 5)
+	//
+	// for id, excerpt := range c.excerpts {
+	// 	if excerpt.CreateMetadata[key] == value {
+	// 		matching = append(matching, id)
+	// 	}
+	// }
+	//
+	// if len(matching) > 1 {
+	// 	return nil, bug.ErrMultipleMatch{Matching: matching}
+	// }
+	//
+	// if len(matching) == 0 {
+	// 	return nil, bug.ErrBugNotExist
+	// }
+	//
+	// return c.ResolveBug(matching[0])
+
+	// TODO
+
+	return nil, nil
+}

commands/id.go 🔗

@@ -1,6 +1,7 @@
 package commands
 
 import (
+	"errors"
 	"fmt"
 
 	"github.com/MichaelMure/git-bug/identity"
@@ -8,7 +9,19 @@ import (
 )
 
 func runId(cmd *cobra.Command, args []string) error {
-	id, err := identity.GetIdentity(repo)
+	if len(args) > 1 {
+		return errors.New("only one identity can be displayed at a time")
+	}
+
+	var id *identity.Identity
+	var err error
+
+	if len(args) == 1 {
+		id, err = identity.Read(repo, args[0])
+	} else {
+		id, err = identity.GetIdentity(repo)
+	}
+
 	if err != nil {
 		return err
 	}
@@ -24,7 +37,7 @@ func runId(cmd *cobra.Command, args []string) error {
 }
 
 var idCmd = &cobra.Command{
-	Use:     "id",
+	Use:     "id [<id>]",
 	Short:   "Display or change the user identity",
 	PreRunE: loadRepo,
 	RunE:    runId,
@@ -32,4 +45,5 @@ var idCmd = &cobra.Command{
 
 func init() {
 	RootCmd.AddCommand(idCmd)
+	selectCmd.Flags().SortFlags = false
 }

graphql/gqlgen.yml 🔗

@@ -14,7 +14,7 @@ models:
   Comment:
     model: github.com/MichaelMure/git-bug/bug.Comment
   Identity:
-    model: github.com/MichaelMure/git-bug/identity.Identity
+    model: github.com/MichaelMure/git-bug/identity.Interface
   Label:
     model: github.com/MichaelMure/git-bug/bug.Label
   Hash:

graphql/graph/gen_graph.go 🔗

@@ -44,6 +44,7 @@ type ResolverRoot interface {
 	CreateOperation() CreateOperationResolver
 	CreateTimelineItem() CreateTimelineItemResolver
 	EditCommentOperation() EditCommentOperationResolver
+	Identity() IdentityResolver
 	LabelChangeOperation() LabelChangeOperationResolver
 	LabelChangeTimelineItem() LabelChangeTimelineItemResolver
 	Mutation() MutationResolver
@@ -292,6 +293,13 @@ type CreateTimelineItemResolver interface {
 type EditCommentOperationResolver interface {
 	Date(ctx context.Context, obj *bug.EditCommentOperation) (time.Time, error)
 }
+type IdentityResolver interface {
+	Name(ctx context.Context, obj *identity.Interface) (*string, error)
+	Email(ctx context.Context, obj *identity.Interface) (*string, error)
+	Login(ctx context.Context, obj *identity.Interface) (*string, error)
+	DisplayName(ctx context.Context, obj *identity.Interface) (string, error)
+	AvatarURL(ctx context.Context, obj *identity.Interface) (*string, error)
+}
 type LabelChangeOperationResolver interface {
 	Date(ctx context.Context, obj *bug.LabelChangeOperation) (time.Time, error)
 }
@@ -2065,18 +2073,11 @@ func (ec *executionContext) _AddCommentOperation_author(ctx context.Context, fie
 		}
 		return graphql.Null
 	}
-	res := resTmp.(*identity.Identity)
+	res := resTmp.(identity.Interface)
 	rctx.Result = res
 	ctx = ec.Tracer.StartFieldChildExecution(ctx)
 
-	if res == nil {
-		if !ec.HasError(rctx) {
-			ec.Errorf(ctx, "must not be null")
-		}
-		return graphql.Null
-	}
-
-	return ec._Identity(ctx, field.Selections, res)
+	return ec._Identity(ctx, field.Selections, &res)
 }
 
 // nolint: vetshadow
@@ -2296,18 +2297,11 @@ func (ec *executionContext) _AddCommentTimelineItem_author(ctx context.Context,
 		}
 		return graphql.Null
 	}
-	res := resTmp.(*identity.Identity)
+	res := resTmp.(identity.Interface)
 	rctx.Result = res
 	ctx = ec.Tracer.StartFieldChildExecution(ctx)
 
-	if res == nil {
-		if !ec.HasError(rctx) {
-			ec.Errorf(ctx, "must not be null")
-		}
-		return graphql.Null
-	}
-
-	return ec._Identity(ctx, field.Selections, res)
+	return ec._Identity(ctx, field.Selections, &res)
 }
 
 // nolint: vetshadow
@@ -2807,18 +2801,11 @@ func (ec *executionContext) _Bug_author(ctx context.Context, field graphql.Colle
 		}
 		return graphql.Null
 	}
-	res := resTmp.(*identity.Identity)
+	res := resTmp.(identity.Interface)
 	rctx.Result = res
 	ctx = ec.Tracer.StartFieldChildExecution(ctx)
 
-	if res == nil {
-		if !ec.HasError(rctx) {
-			ec.Errorf(ctx, "must not be null")
-		}
-		return graphql.Null
-	}
-
-	return ec._Identity(ctx, field.Selections, res)
+	return ec._Identity(ctx, field.Selections, &res)
 }
 
 // nolint: vetshadow
@@ -3348,18 +3335,11 @@ func (ec *executionContext) _Comment_author(ctx context.Context, field graphql.C
 		}
 		return graphql.Null
 	}
-	res := resTmp.(*identity.Identity)
+	res := resTmp.(identity.Interface)
 	rctx.Result = res
 	ctx = ec.Tracer.StartFieldChildExecution(ctx)
 
-	if res == nil {
-		if !ec.HasError(rctx) {
-			ec.Errorf(ctx, "must not be null")
-		}
-		return graphql.Null
-	}
-
-	return ec._Identity(ctx, field.Selections, res)
+	return ec._Identity(ctx, field.Selections, &res)
 }
 
 // nolint: vetshadow
@@ -3937,18 +3917,11 @@ func (ec *executionContext) _CreateOperation_author(ctx context.Context, field g
 		}
 		return graphql.Null
 	}
-	res := resTmp.(*identity.Identity)
+	res := resTmp.(identity.Interface)
 	rctx.Result = res
 	ctx = ec.Tracer.StartFieldChildExecution(ctx)
 
-	if res == nil {
-		if !ec.HasError(rctx) {
-			ec.Errorf(ctx, "must not be null")
-		}
-		return graphql.Null
-	}
-
-	return ec._Identity(ctx, field.Selections, res)
+	return ec._Identity(ctx, field.Selections, &res)
 }
 
 // nolint: vetshadow
@@ -4195,18 +4168,11 @@ func (ec *executionContext) _CreateTimelineItem_author(ctx context.Context, fiel
 		}
 		return graphql.Null
 	}
-	res := resTmp.(*identity.Identity)
+	res := resTmp.(identity.Interface)
 	rctx.Result = res
 	ctx = ec.Tracer.StartFieldChildExecution(ctx)
 
-	if res == nil {
-		if !ec.HasError(rctx) {
-			ec.Errorf(ctx, "must not be null")
-		}
-		return graphql.Null
-	}
-
-	return ec._Identity(ctx, field.Selections, res)
+	return ec._Identity(ctx, field.Selections, &res)
 }
 
 // nolint: vetshadow
@@ -4548,18 +4514,11 @@ func (ec *executionContext) _EditCommentOperation_author(ctx context.Context, fi
 		}
 		return graphql.Null
 	}
-	res := resTmp.(*identity.Identity)
+	res := resTmp.(identity.Interface)
 	rctx.Result = res
 	ctx = ec.Tracer.StartFieldChildExecution(ctx)
 
-	if res == nil {
-		if !ec.HasError(rctx) {
-			ec.Errorf(ctx, "must not be null")
-		}
-		return graphql.Null
-	}
-
-	return ec._Identity(ctx, field.Selections, res)
+	return ec._Identity(ctx, field.Selections, &res)
 }
 
 // nolint: vetshadow
@@ -4682,9 +4641,10 @@ func (ec *executionContext) _EditCommentOperation_files(ctx context.Context, fie
 var identityImplementors = []string{"Identity"}
 
 // nolint: gocyclo, errcheck, gas, goconst
-func (ec *executionContext) _Identity(ctx context.Context, sel ast.SelectionSet, obj *identity.Identity) graphql.Marshaler {
+func (ec *executionContext) _Identity(ctx context.Context, sel ast.SelectionSet, obj *identity.Interface) graphql.Marshaler {
 	fields := graphql.CollectFields(ctx, sel, identityImplementors)
 
+	var wg sync.WaitGroup
 	out := graphql.NewOrderedMap(len(fields))
 	invalid := false
 	for i, field := range fields {
@@ -4694,23 +4654,43 @@ func (ec *executionContext) _Identity(ctx context.Context, sel ast.SelectionSet,
 		case "__typename":
 			out.Values[i] = graphql.MarshalString("Identity")
 		case "name":
-			out.Values[i] = ec._Identity_name(ctx, field, obj)
+			wg.Add(1)
+			go func(i int, field graphql.CollectedField) {
+				out.Values[i] = ec._Identity_name(ctx, field, obj)
+				wg.Done()
+			}(i, field)
 		case "email":
-			out.Values[i] = ec._Identity_email(ctx, field, obj)
+			wg.Add(1)
+			go func(i int, field graphql.CollectedField) {
+				out.Values[i] = ec._Identity_email(ctx, field, obj)
+				wg.Done()
+			}(i, field)
 		case "login":
-			out.Values[i] = ec._Identity_login(ctx, field, obj)
+			wg.Add(1)
+			go func(i int, field graphql.CollectedField) {
+				out.Values[i] = ec._Identity_login(ctx, field, obj)
+				wg.Done()
+			}(i, field)
 		case "displayName":
-			out.Values[i] = ec._Identity_displayName(ctx, field, obj)
-			if out.Values[i] == graphql.Null {
-				invalid = true
-			}
+			wg.Add(1)
+			go func(i int, field graphql.CollectedField) {
+				out.Values[i] = ec._Identity_displayName(ctx, field, obj)
+				if out.Values[i] == graphql.Null {
+					invalid = true
+				}
+				wg.Done()
+			}(i, field)
 		case "avatarUrl":
-			out.Values[i] = ec._Identity_avatarUrl(ctx, field, obj)
+			wg.Add(1)
+			go func(i int, field graphql.CollectedField) {
+				out.Values[i] = ec._Identity_avatarUrl(ctx, field, obj)
+				wg.Done()
+			}(i, field)
 		default:
 			panic("unknown field " + strconv.Quote(field.Name))
 		}
 	}
-
+	wg.Wait()
 	if invalid {
 		return graphql.Null
 	}
@@ -4718,7 +4698,7 @@ func (ec *executionContext) _Identity(ctx context.Context, sel ast.SelectionSet,
 }
 
 // nolint: vetshadow
-func (ec *executionContext) _Identity_name(ctx context.Context, field graphql.CollectedField, obj *identity.Identity) graphql.Marshaler {
+func (ec *executionContext) _Identity_name(ctx context.Context, field graphql.CollectedField, obj *identity.Interface) graphql.Marshaler {
 	ctx = ec.Tracer.StartFieldExecution(ctx, field)
 	defer func() { ec.Tracer.EndFieldExecution(ctx) }()
 	rctx := &graphql.ResolverContext{
@@ -4730,19 +4710,23 @@ func (ec *executionContext) _Identity_name(ctx context.Context, field graphql.Co
 	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
 	resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) {
 		ctx = rctx // use context from middleware stack in children
-		return obj.Name(), nil
+		return ec.resolvers.Identity().Name(rctx, obj)
 	})
 	if resTmp == nil {
 		return graphql.Null
 	}
-	res := resTmp.(string)
+	res := resTmp.(*string)
 	rctx.Result = res
 	ctx = ec.Tracer.StartFieldChildExecution(ctx)
-	return graphql.MarshalString(res)
+
+	if res == nil {
+		return graphql.Null
+	}
+	return graphql.MarshalString(*res)
 }
 
 // nolint: vetshadow
-func (ec *executionContext) _Identity_email(ctx context.Context, field graphql.CollectedField, obj *identity.Identity) graphql.Marshaler {
+func (ec *executionContext) _Identity_email(ctx context.Context, field graphql.CollectedField, obj *identity.Interface) graphql.Marshaler {
 	ctx = ec.Tracer.StartFieldExecution(ctx, field)
 	defer func() { ec.Tracer.EndFieldExecution(ctx) }()
 	rctx := &graphql.ResolverContext{
@@ -4754,19 +4738,23 @@ func (ec *executionContext) _Identity_email(ctx context.Context, field graphql.C
 	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
 	resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) {
 		ctx = rctx // use context from middleware stack in children
-		return obj.Email(), nil
+		return ec.resolvers.Identity().Email(rctx, obj)
 	})
 	if resTmp == nil {
 		return graphql.Null
 	}
-	res := resTmp.(string)
+	res := resTmp.(*string)
 	rctx.Result = res
 	ctx = ec.Tracer.StartFieldChildExecution(ctx)
-	return graphql.MarshalString(res)
+
+	if res == nil {
+		return graphql.Null
+	}
+	return graphql.MarshalString(*res)
 }
 
 // nolint: vetshadow
-func (ec *executionContext) _Identity_login(ctx context.Context, field graphql.CollectedField, obj *identity.Identity) graphql.Marshaler {
+func (ec *executionContext) _Identity_login(ctx context.Context, field graphql.CollectedField, obj *identity.Interface) graphql.Marshaler {
 	ctx = ec.Tracer.StartFieldExecution(ctx, field)
 	defer func() { ec.Tracer.EndFieldExecution(ctx) }()
 	rctx := &graphql.ResolverContext{
@@ -4778,19 +4766,23 @@ func (ec *executionContext) _Identity_login(ctx context.Context, field graphql.C
 	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
 	resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) {
 		ctx = rctx // use context from middleware stack in children
-		return obj.Login(), nil
+		return ec.resolvers.Identity().Login(rctx, obj)
 	})
 	if resTmp == nil {
 		return graphql.Null
 	}
-	res := resTmp.(string)
+	res := resTmp.(*string)
 	rctx.Result = res
 	ctx = ec.Tracer.StartFieldChildExecution(ctx)
-	return graphql.MarshalString(res)
+
+	if res == nil {
+		return graphql.Null
+	}
+	return graphql.MarshalString(*res)
 }
 
 // nolint: vetshadow
-func (ec *executionContext) _Identity_displayName(ctx context.Context, field graphql.CollectedField, obj *identity.Identity) graphql.Marshaler {
+func (ec *executionContext) _Identity_displayName(ctx context.Context, field graphql.CollectedField, obj *identity.Interface) graphql.Marshaler {
 	ctx = ec.Tracer.StartFieldExecution(ctx, field)
 	defer func() { ec.Tracer.EndFieldExecution(ctx) }()
 	rctx := &graphql.ResolverContext{
@@ -4802,7 +4794,7 @@ func (ec *executionContext) _Identity_displayName(ctx context.Context, field gra
 	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
 	resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) {
 		ctx = rctx // use context from middleware stack in children
-		return obj.DisplayName(), nil
+		return ec.resolvers.Identity().DisplayName(rctx, obj)
 	})
 	if resTmp == nil {
 		if !ec.HasError(rctx) {
@@ -4817,7 +4809,7 @@ func (ec *executionContext) _Identity_displayName(ctx context.Context, field gra
 }
 
 // nolint: vetshadow
-func (ec *executionContext) _Identity_avatarUrl(ctx context.Context, field graphql.CollectedField, obj *identity.Identity) graphql.Marshaler {
+func (ec *executionContext) _Identity_avatarUrl(ctx context.Context, field graphql.CollectedField, obj *identity.Interface) graphql.Marshaler {
 	ctx = ec.Tracer.StartFieldExecution(ctx, field)
 	defer func() { ec.Tracer.EndFieldExecution(ctx) }()
 	rctx := &graphql.ResolverContext{
@@ -4829,15 +4821,19 @@ func (ec *executionContext) _Identity_avatarUrl(ctx context.Context, field graph
 	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
 	resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) {
 		ctx = rctx // use context from middleware stack in children
-		return obj.AvatarURL(), nil
+		return ec.resolvers.Identity().AvatarURL(rctx, obj)
 	})
 	if resTmp == nil {
 		return graphql.Null
 	}
-	res := resTmp.(string)
+	res := resTmp.(*string)
 	rctx.Result = res
 	ctx = ec.Tracer.StartFieldChildExecution(ctx)
-	return graphql.MarshalString(res)
+
+	if res == nil {
+		return graphql.Null
+	}
+	return graphql.MarshalString(*res)
 }
 
 var labelChangeOperationImplementors = []string{"LabelChangeOperation", "Operation", "Authored"}
@@ -4943,18 +4939,11 @@ func (ec *executionContext) _LabelChangeOperation_author(ctx context.Context, fi
 		}
 		return graphql.Null
 	}
-	res := resTmp.(*identity.Identity)
+	res := resTmp.(identity.Interface)
 	rctx.Result = res
 	ctx = ec.Tracer.StartFieldChildExecution(ctx)
 
-	if res == nil {
-		if !ec.HasError(rctx) {
-			ec.Errorf(ctx, "must not be null")
-		}
-		return graphql.Null
-	}
-
-	return ec._Identity(ctx, field.Selections, res)
+	return ec._Identity(ctx, field.Selections, &res)
 }
 
 // nolint: vetshadow
@@ -5159,18 +5148,11 @@ func (ec *executionContext) _LabelChangeTimelineItem_author(ctx context.Context,
 		}
 		return graphql.Null
 	}
-	res := resTmp.(*identity.Identity)
+	res := resTmp.(identity.Interface)
 	rctx.Result = res
 	ctx = ec.Tracer.StartFieldChildExecution(ctx)
 
-	if res == nil {
-		if !ec.HasError(rctx) {
-			ec.Errorf(ctx, "must not be null")
-		}
-		return graphql.Null
-	}
-
-	return ec._Identity(ctx, field.Selections, res)
+	return ec._Identity(ctx, field.Selections, &res)
 }
 
 // nolint: vetshadow
@@ -6423,18 +6405,11 @@ func (ec *executionContext) _SetStatusOperation_author(ctx context.Context, fiel
 		}
 		return graphql.Null
 	}
-	res := resTmp.(*identity.Identity)
+	res := resTmp.(identity.Interface)
 	rctx.Result = res
 	ctx = ec.Tracer.StartFieldChildExecution(ctx)
 
-	if res == nil {
-		if !ec.HasError(rctx) {
-			ec.Errorf(ctx, "must not be null")
-		}
-		return graphql.Null
-	}
-
-	return ec._Identity(ctx, field.Selections, res)
+	return ec._Identity(ctx, field.Selections, &res)
 }
 
 // nolint: vetshadow
@@ -6593,18 +6568,11 @@ func (ec *executionContext) _SetStatusTimelineItem_author(ctx context.Context, f
 		}
 		return graphql.Null
 	}
-	res := resTmp.(*identity.Identity)
+	res := resTmp.(identity.Interface)
 	rctx.Result = res
 	ctx = ec.Tracer.StartFieldChildExecution(ctx)
 
-	if res == nil {
-		if !ec.HasError(rctx) {
-			ec.Errorf(ctx, "must not be null")
-		}
-		return graphql.Null
-	}
-
-	return ec._Identity(ctx, field.Selections, res)
+	return ec._Identity(ctx, field.Selections, &res)
 }
 
 // nolint: vetshadow
@@ -6764,18 +6732,11 @@ func (ec *executionContext) _SetTitleOperation_author(ctx context.Context, field
 		}
 		return graphql.Null
 	}
-	res := resTmp.(*identity.Identity)
+	res := resTmp.(identity.Interface)
 	rctx.Result = res
 	ctx = ec.Tracer.StartFieldChildExecution(ctx)
 
-	if res == nil {
-		if !ec.HasError(rctx) {
-			ec.Errorf(ctx, "must not be null")
-		}
-		return graphql.Null
-	}
-
-	return ec._Identity(ctx, field.Selections, res)
+	return ec._Identity(ctx, field.Selections, &res)
 }
 
 // nolint: vetshadow
@@ -6962,18 +6923,11 @@ func (ec *executionContext) _SetTitleTimelineItem_author(ctx context.Context, fi
 		}
 		return graphql.Null
 	}
-	res := resTmp.(*identity.Identity)
+	res := resTmp.(identity.Interface)
 	rctx.Result = res
 	ctx = ec.Tracer.StartFieldChildExecution(ctx)
 
-	if res == nil {
-		if !ec.HasError(rctx) {
-			ec.Errorf(ctx, "must not be null")
-		}
-		return graphql.Null
-	}
-
-	return ec._Identity(ctx, field.Selections, res)
+	return ec._Identity(ctx, field.Selections, &res)
 }
 
 // nolint: vetshadow

repository/repo.go 🔗

@@ -122,7 +122,7 @@ func prepareTreeEntries(entries []TreeEntry) bytes.Buffer {
 }
 
 func readTreeEntries(s string) ([]TreeEntry, error) {
-	split := strings.Split(s, "\n")
+	split := strings.Split(strings.TrimSpace(s), "\n")
 
 	casted := make([]TreeEntry, len(split))
 	for i, line := range split {

termui/bug_table.go 🔗

@@ -6,7 +6,6 @@ import (
 
 	"github.com/MichaelMure/git-bug/bug"
 	"github.com/MichaelMure/git-bug/cache"
-	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/util/colors"
 	"github.com/MichaelMure/git-bug/util/text"
 	"github.com/MichaelMure/gocui"
@@ -290,12 +289,9 @@ func (bt *bugTable) render(v *gocui.View, maxX int) {
 	columnWidths := bt.getColumnWidths(maxX)
 
 	for _, b := range bt.bugs {
-		person := &identity.Identity{}
 		snap := b.Snapshot()
-		if len(snap.Comments) > 0 {
-			create := snap.Comments[0]
-			person = create.Author
-		}
+		create := snap.Comments[0]
+		authorIdentity := create.Author
 
 		summaryTxt := fmt.Sprintf("C:%-2d L:%-2d",
 			len(snap.Comments)-1,
@@ -305,7 +301,7 @@ func (bt *bugTable) render(v *gocui.View, maxX int) {
 		id := text.LeftPadMaxLine(snap.HumanId(), columnWidths["id"], 1)
 		status := text.LeftPadMaxLine(snap.Status.String(), columnWidths["status"], 1)
 		title := text.LeftPadMaxLine(snap.Title, columnWidths["title"], 1)
-		author := text.LeftPadMaxLine(person.DisplayName(), columnWidths["author"], 1)
+		author := text.LeftPadMaxLine(authorIdentity.DisplayName(), columnWidths["author"], 1)
 		summary := text.LeftPadMaxLine(summaryTxt, columnWidths["summary"], 1)
 		lastEdit := text.LeftPadMaxLine(humanize.Time(snap.LastEditTime()), columnWidths["lastEdit"], 1)