more more wip

Michael MurΓ© created

Change summary

bridge/core/auth/credential.go      |   6 +
bridge/core/auth/credential_test.go |   2 
bridge/core/auth/options.go         |   2 
bridge/core/auth/token.go           |  13 ++
bridge/core/config.go               |  42 ++++++++++
bridge/github/config.go             |  37 --------
bridge/github/export.go             |  41 ++++++++-
bridge/github/export_test.go        |   2 
bridge/github/import.go             |   2 
bridge/launchpad/import.go          |   1 
bug/status.go                       |   2 
cache/bug_excerpt.go                |  18 ---
cache/filter.go                     |   3 
cache/identity_cache.go             |   2 
cache/identity_excerpt.go           |  17 ---
cache/query.go                      |   4 
cache/repo_cache.go                 |  31 ++++--
graphql/graph/gen_graph.go          | 126 +++++++++++++++++++++++++++++-
graphql/resolvers/identity.go       |   4 
graphql/schema/identity.graphql     |   4 
graphql/schema/root.graphql         |   2 
identity/bare.go                    |   8 
identity/identity.go                |  18 ++-
identity/identity_actions_test.go   |   8 
identity/identity_stub.go           |   4 
identity/identity_test.go           |  45 ++++++----
identity/interface.go               |   4 
identity/key.go                     |   5 +
identity/version.go                 |  12 +-
identity/version_test.go            |   3 
30 files changed, 313 insertions(+), 155 deletions(-)

Detailed changes

bridge/core/auth/credential.go πŸ”—

@@ -40,7 +40,10 @@ type Credential interface {
 	Kind() CredentialKind
 	CreateTime() time.Time
 	Validate() error
+
 	Metadata() map[string]string
+	GetMetadata(key string) (string, bool)
+	SetMetadata(key string, value string)
 
 	// Return all the specific properties of the credential that need to be saved into the configuration.
 	// This does not include Target, Kind, CreateTime and Metadata.
@@ -124,6 +127,9 @@ func metaFromConfig(configs map[string]string) map[string]string {
 			result[key] = val
 		}
 	}
+	if len(result) == 0 {
+		return nil
+	}
 	return result
 }
 

bridge/core/auth/credential_test.go πŸ”—

@@ -63,7 +63,7 @@ func TestCredential(t *testing.T) {
 
 	// Metadata
 
-	token4.Metadata()["key"] = "value"
+	token4.SetMetadata("key", "value")
 	err = Store(repo, token4)
 	assert.NoError(t, err)
 

bridge/core/auth/options.go πŸ”—

@@ -26,7 +26,7 @@ func (opts *options) Match(cred Credential) bool {
 	}
 
 	for key, val := range opts.meta {
-		if v, ok := cred.Metadata()[key]; !ok || v != val {
+		if v, ok := cred.GetMetadata(key); !ok || v != val {
 			return false
 		}
 	}

bridge/core/auth/token.go πŸ”—

@@ -30,7 +30,6 @@ func NewToken(value, target string) *Token {
 		target:     target,
 		createTime: time.Now(),
 		Value:      value,
-		meta:       make(map[string]string),
 	}
 }
 
@@ -88,6 +87,18 @@ func (t *Token) Metadata() map[string]string {
 	return t.meta
 }
 
+func (t *Token) GetMetadata(key string) (string, bool) {
+	val, ok := t.meta[key]
+	return val, ok
+}
+
+func (t *Token) SetMetadata(key string, value string) {
+	if t.meta == nil {
+		t.meta = make(map[string]string)
+	}
+	t.meta[key] = value
+}
+
 func (t *Token) toConfig() map[string]string {
 	return map[string]string{
 		tokenValueKey: t.Value,

bridge/core/config.go πŸ”—

@@ -1 +1,43 @@
 package core
+
+import (
+	"github.com/MichaelMure/git-bug/cache"
+	"github.com/MichaelMure/git-bug/identity"
+)
+
+func FinishConfig(repo *cache.RepoCache, metaKey string, login string) error {
+	// if no user exist with the given login metadata
+	_, err := repo.ResolveIdentityImmutableMetadata(metaKey, login)
+	if err != nil && err != identity.ErrIdentityNotExist {
+		// real error
+		return err
+	}
+	if err == nil {
+		// found an already valid user, all good
+		return nil
+	}
+
+	// if a default user exist, tag it with the login
+	user, err := repo.GetUserIdentity()
+	if err != nil && err != identity.ErrIdentityNotExist {
+		// real error
+		return err
+	}
+	if err == nil {
+		// found one
+		user.SetMetadata(metaKey, login)
+		return user.CommitAsNeeded()
+	}
+
+	// otherwise create a user with that metadata
+	i, err := repo.NewIdentityFromGitUserRaw(map[string]string{
+		metaKey: login,
+	})
+
+	err = repo.SetUserIdentity(i)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}

bridge/github/config.go πŸ”—

@@ -22,7 +22,6 @@ import (
 	"github.com/MichaelMure/git-bug/bridge/core"
 	"github.com/MichaelMure/git-bug/bridge/core/auth"
 	"github.com/MichaelMure/git-bug/cache"
-	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/input"
 	"github.com/MichaelMure/git-bug/repository"
 	"github.com/MichaelMure/git-bug/util/colors"
@@ -109,7 +108,7 @@ func (g *Github) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
 		}
 	case params.TokenRaw != "":
 		cred = auth.NewToken(params.TokenRaw, target)
-		cred.Metadata()[auth.MetaKeyLogin] = login
+		cred.SetMetadata(auth.MetaKeyLogin, login)
 	default:
 		cred, err = promptTokenOptions(repo, login, owner, project)
 		if err != nil {
@@ -140,34 +139,6 @@ func (g *Github) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
 		return nil, err
 	}
 
-	// TODO
-	func(login string) error {
-		// if no user exist with the given login
-		_, err := repo.ResolveIdentityLogin(login)
-		if err != nil && err != identity.ErrIdentityNotExist {
-			return err
-		}
-
-		// tag the default user with the github login, if any
-		user, err := repo.GetUserIdentity()
-		if err == identity.ErrNoIdentitySet {
-			return nil
-		}
-		if err != nil {
-			return err
-		}
-
-		userLogin, ok := user.ImmutableMetadata()[metaKeyGithubLogin]
-		if !ok {
-			user.SetMetadata()
-		}
-
-	}(login)
-
-	// Todo: if no user exist with the given login
-	// - tag the default user with the github login
-	// - add a command to manually tag a user ?
-
 	// don't forget to store the now known valid token
 	if !auth.IdExist(repo, cred.ID()) {
 		err = auth.Store(repo, cred)
@@ -176,7 +147,7 @@ func (g *Github) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
 		}
 	}
 
-	return conf, nil
+	return conf, core.FinishConfig(repo, metaKeyGithubLogin, login)
 }
 
 func (*Github) ValidateConfig(conf core.Configuration) error {
@@ -318,7 +289,7 @@ func promptTokenOptions(repo repository.RepoConfig, login, owner, project string
 				return nil, err
 			}
 			token := auth.NewToken(value, target)
-			token.Metadata()[auth.MetaKeyLogin] = login
+			token.SetMetadata(auth.MetaKeyLogin, login)
 			return token, nil
 		case 2:
 			value, err := loginAndRequestToken(login, owner, project)
@@ -326,7 +297,7 @@ func promptTokenOptions(repo repository.RepoConfig, login, owner, project string
 				return nil, err
 			}
 			token := auth.NewToken(value, target)
-			token.Metadata()[auth.MetaKeyLogin] = login
+			token.SetMetadata(auth.MetaKeyLogin, login)
 			return token, nil
 		default:
 			return creds[index-3], nil

bridge/github/export.go πŸ”—

@@ -21,7 +21,6 @@ import (
 	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/identity"
-	"github.com/MichaelMure/git-bug/repository"
 )
 
 var (
@@ -35,6 +34,13 @@ type githubExporter struct {
 	// cache identities clients
 	identityClient map[entity.Id]*githubv4.Client
 
+	// the client to use for non user-specific queries
+	// should be the client of the default user
+	defaultClient *githubv4.Client
+
+	// the token of the default user
+	defaultToken *auth.Token
+
 	// github repository ID
 	repositoryID string
 
@@ -53,12 +59,34 @@ func (ge *githubExporter) Init(repo *cache.RepoCache, conf core.Configuration) e
 	ge.cachedOperationIDs = make(map[entity.Id]string)
 	ge.cachedLabels = make(map[string]string)
 
+	user, err := repo.GetUserIdentity()
+	if err != nil {
+		return err
+	}
+
 	// preload all clients
-	err := ge.cacheAllClient(repo)
+	err = ge.cacheAllClient(repo)
 	if err != nil {
 		return err
 	}
 
+	ge.defaultClient, err = ge.getClientForIdentity(user.Id())
+	if err != nil {
+		return err
+	}
+
+	login := user.ImmutableMetadata()[metaKeyGithubLogin]
+	creds, err := auth.List(repo, auth.WithMeta(metaKeyGithubLogin, login), auth.WithTarget(target), auth.WithKind(auth.KindToken))
+	if err != nil {
+		return err
+	}
+
+	if len(creds) == 0 {
+		return ErrMissingIdentityToken
+	}
+
+	ge.defaultToken = creds[0].(*auth.Token)
+
 	return nil
 }
 
@@ -69,7 +97,7 @@ func (ge *githubExporter) cacheAllClient(repo *cache.RepoCache) error {
 	}
 
 	for _, cred := range creds {
-		login, ok := cred.Metadata()[auth.MetaKeyLogin]
+		login, ok := cred.GetMetadata(auth.MetaKeyLogin)
 		if !ok {
 			_, _ = fmt.Fprintf(os.Stderr, "credential %s is not tagged with Github login\n", cred.ID().Human())
 			continue
@@ -80,9 +108,9 @@ func (ge *githubExporter) cacheAllClient(repo *cache.RepoCache) error {
 			continue
 		}
 
-		if _, ok := ge.identityClient[cred.UserId()]; !ok {
+		if _, ok := ge.identityClient[user.Id()]; !ok {
 			client := buildClient(creds[0].(*auth.Token))
-			ge.identityClient[cred.UserId()] = client
+			ge.identityClient[user.Id()] = client
 		}
 	}
 
@@ -462,11 +490,12 @@ func (ge *githubExporter) cacheGithubLabels(ctx context.Context, gc *githubv4.Cl
 	for hasNextPage {
 		// create a new timeout context at each iteration
 		ctx, cancel := context.WithTimeout(ctx, defaultTimeout)
-		defer cancel()
 
 		if err := gc.Query(ctx, &q, variables); err != nil {
+			cancel()
 			return err
 		}
+		cancel()
 
 		for _, label := range q.Repository.Labels.Nodes {
 			ge.cachedLabels[label.Name] = label.ID

bridge/github/export_test.go πŸ”—

@@ -176,7 +176,7 @@ func TestPushPull(t *testing.T) {
 		return deleteRepository(projectName, envUser, envToken)
 	})
 
-	token := auth.NewToken(author.Id(), envToken, target)
+	token := auth.NewToken(envToken, target)
 	err = auth.Store(repo, token)
 	require.NoError(t, err)
 

bridge/github/import.go πŸ”—

@@ -543,7 +543,6 @@ func (gi *githubImporter) ensurePerson(repo *cache.RepoCache, actor *actor) (*ca
 	i, err = repo.NewIdentityRaw(
 		name,
 		email,
-		string(actor.Login),
 		string(actor.AvatarUrl),
 		map[string]string{
 			metaKeyGithubLogin: string(actor.Login),
@@ -590,7 +589,6 @@ func (gi *githubImporter) getGhost(repo *cache.RepoCache) (*cache.IdentityCache,
 	return repo.NewIdentityRaw(
 		name,
 		"",
-		string(q.User.Login),
 		string(q.User.AvatarUrl),
 		map[string]string{
 			metaKeyGithubLogin: string(q.User.Login),

bridge/launchpad/import.go πŸ”—

@@ -38,7 +38,6 @@ func (li *launchpadImporter) ensurePerson(repo *cache.RepoCache, owner LPPerson)
 	return repo.NewIdentityRaw(
 		owner.Name,
 		"",
-		owner.Login,
 		"",
 		map[string]string{
 			metaKeyLaunchpadLogin: owner.Login,

bug/status.go πŸ”—

@@ -44,7 +44,7 @@ func StatusFromString(str string) (Status, error) {
 	case "closed":
 		return ClosedStatus, nil
 	default:
-		return 0, fmt.Errorf("unknow status")
+		return 0, fmt.Errorf("unknown status")
 	}
 }
 

cache/bug_excerpt.go πŸ”—

@@ -2,7 +2,6 @@ package cache
 
 import (
 	"encoding/gob"
-	"fmt"
 
 	"github.com/MichaelMure/git-bug/bug"
 	"github.com/MichaelMure/git-bug/entity"
@@ -43,21 +42,11 @@ type BugExcerpt struct {
 
 // identity.Bare data are directly embedded in the bug excerpt
 type LegacyAuthorExcerpt struct {
-	Name  string
-	Login string
+	Name string
 }
 
 func (l LegacyAuthorExcerpt) DisplayName() string {
-	switch {
-	case l.Name == "" && l.Login != "":
-		return l.Login
-	case l.Name != "" && l.Login == "":
-		return l.Name
-	case l.Name != "" && l.Login != "":
-		return fmt.Sprintf("%s (%s)", l.Name, l.Login)
-	}
-
-	panic("invalid person data")
+	return l.Name
 }
 
 func NewBugExcerpt(b bug.Interface, snap *bug.Snapshot) *BugExcerpt {
@@ -95,8 +84,7 @@ func NewBugExcerpt(b bug.Interface, snap *bug.Snapshot) *BugExcerpt {
 		e.AuthorId = snap.Author.Id()
 	case *identity.Bare:
 		e.LegacyAuthor = LegacyAuthorExcerpt{
-			Login: snap.Author.Login(),
-			Name:  snap.Author.Name(),
+			Name: snap.Author.Name(),
 		}
 	default:
 		panic("unhandled identity type")

cache/filter.go πŸ”—

@@ -37,8 +37,7 @@ func AuthorFilter(query string) Filter {
 		}
 
 		// Legacy identity support
-		return strings.Contains(strings.ToLower(excerpt.LegacyAuthor.Name), query) ||
-			strings.Contains(strings.ToLower(excerpt.LegacyAuthor.Login), query)
+		return strings.Contains(strings.ToLower(excerpt.LegacyAuthor.Name), query)
 	}
 }
 

cache/identity_cache.go πŸ”—

@@ -21,7 +21,7 @@ func (i *IdentityCache) notifyUpdated() error {
 	return i.repoCache.identityUpdated(i.Identity.Id())
 }
 
-func (i *IdentityCache) Mutate(f func(identity.IdentityMutator) identity.IdentityMutator) error {
+func (i *IdentityCache) Mutate(f func(identity.Mutator) identity.Mutator) error {
 	i.Identity.Mutate(f)
 	return i.notifyUpdated()
 }

cache/identity_excerpt.go πŸ”—

@@ -2,7 +2,6 @@ package cache
 
 import (
 	"encoding/gob"
-	"fmt"
 	"strings"
 
 	"github.com/MichaelMure/git-bug/entity"
@@ -21,7 +20,6 @@ type IdentityExcerpt struct {
 	Id entity.Id
 
 	Name              string
-	Login             string
 	ImmutableMetadata map[string]string
 }
 
@@ -29,7 +27,6 @@ func NewIdentityExcerpt(i *identity.Identity) *IdentityExcerpt {
 	return &IdentityExcerpt{
 		Id:                i.Id(),
 		Name:              i.Name(),
-		Login:             i.Login(),
 		ImmutableMetadata: i.ImmutableMetadata(),
 	}
 }
@@ -37,23 +34,13 @@ func NewIdentityExcerpt(i *identity.Identity) *IdentityExcerpt {
 // DisplayName return a non-empty string to display, representing the
 // identity, based on the non-empty values.
 func (i *IdentityExcerpt) DisplayName() string {
-	switch {
-	case i.Name == "" && i.Login != "":
-		return i.Login
-	case i.Name != "" && i.Login == "":
-		return i.Name
-	case i.Name != "" && i.Login != "":
-		return fmt.Sprintf("%s (%s)", i.Name, i.Login)
-	}
-
-	panic("invalid person data")
+	return i.Name
 }
 
 // Match matches a query with the identity name, login and ID prefixes
 func (i *IdentityExcerpt) Match(query string) bool {
 	return i.Id.HasPrefix(query) ||
-		strings.Contains(strings.ToLower(i.Name), query) ||
-		strings.Contains(strings.ToLower(i.Login), query)
+		strings.Contains(strings.ToLower(i.Name), query)
 }
 
 /*

cache/query.go πŸ”—

@@ -91,7 +91,7 @@ func ParseQuery(query string) (*Query, error) {
 			sortingDone = true
 
 		default:
-			return nil, fmt.Errorf("unknow qualifier name %s", qualifierName)
+			return nil, fmt.Errorf("unknown qualifier name %s", qualifierName)
 		}
 	}
 
@@ -165,7 +165,7 @@ func (q *Query) parseSorting(query string) error {
 		q.OrderDirection = OrderAscending
 
 	default:
-		return fmt.Errorf("unknow sorting %s", query)
+		return fmt.Errorf("unknown sorting %s", query)
 	}
 
 	return nil

cache/repo_cache.go πŸ”—

@@ -789,12 +789,6 @@ func (c *RepoCache) ResolveIdentityImmutableMetadata(key string, value string) (
 	})
 }
 
-func (c *RepoCache) ResolveIdentityLogin(login string) (*IdentityCache, error) {
-	return c.ResolveIdentityMatcher(func(excerpt *IdentityExcerpt) bool {
-		return excerpt.Login == login
-	})
-}
-
 func (c *RepoCache) ResolveIdentityMatcher(f func(*IdentityExcerpt) bool) (*IdentityCache, error) {
 	// preallocate but empty
 	matching := make([]entity.Id, 0, 5)
@@ -869,21 +863,36 @@ func (c *RepoCache) IsUserIdentitySet() (bool, error) {
 	return identity.IsUserIdentitySet(c.repo)
 }
 
+func (c *RepoCache) NewIdentityFromGitUser() (*IdentityCache, error) {
+	return c.NewIdentityFromGitUserRaw(nil)
+}
+
+func (c *RepoCache) NewIdentityFromGitUserRaw(metadata map[string]string) (*IdentityCache, error) {
+	i, err := identity.NewFromGitUser(c.repo)
+	if err != nil {
+		return nil, err
+	}
+	return c.finishIdentity(i, metadata)
+}
+
 // NewIdentity create a new identity
 // The new identity is written in the repository (commit)
 func (c *RepoCache) NewIdentity(name string, email string) (*IdentityCache, error) {
-	return c.NewIdentityRaw(name, email, "", "", nil)
+	return c.NewIdentityRaw(name, email, "", nil)
 }
 
 // NewIdentityFull create a new identity
 // The new identity is written in the repository (commit)
-func (c *RepoCache) NewIdentityFull(name string, email string, login string, avatarUrl string) (*IdentityCache, error) {
-	return c.NewIdentityRaw(name, email, login, avatarUrl, nil)
+func (c *RepoCache) NewIdentityFull(name string, email string, avatarUrl string) (*IdentityCache, error) {
+	return c.NewIdentityRaw(name, email, avatarUrl, nil)
 }
 
-func (c *RepoCache) NewIdentityRaw(name string, email string, login string, avatarUrl string, metadata map[string]string) (*IdentityCache, error) {
-	i := identity.NewIdentityFull(name, email, login, avatarUrl)
+func (c *RepoCache) NewIdentityRaw(name string, email string, avatarUrl string, metadata map[string]string) (*IdentityCache, error) {
+	i := identity.NewIdentityFull(name, email, avatarUrl)
+	return c.finishIdentity(i, metadata)
+}
 
+func (c *RepoCache) finishIdentity(i *identity.Identity, metadata map[string]string) (*IdentityCache, error) {
 	for key, value := range metadata {
 		i.SetMetadata(key, value)
 	}

graphql/graph/gen_graph.go πŸ”—

@@ -210,7 +210,6 @@ type ComplexityRoot struct {
 		HumanID     func(childComplexity int) int
 		ID          func(childComplexity int) int
 		IsProtected func(childComplexity int) int
-		Login       func(childComplexity int) int
 		Name        func(childComplexity int) int
 	}
 
@@ -1139,13 +1138,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.Identity.IsProtected(childComplexity), true
 
-	case "Identity.login":
-		if e.complexity.Identity.Login == nil {
-			break
-		}
-
-		return e.complexity.Identity.Login(childComplexity), true
-
 	case "Identity.name":
 		if e.complexity.Identity.Name == nil {
 			break
@@ -2070,6 +2062,7 @@ type BugConnection {
 An edge in a connection.
 """
 type BugEdge {
+<<<<<<< HEAD
 	"""
 	A cursor for use in pagination.
 	"""
@@ -2078,6 +2071,105 @@ type BugEdge {
 	The item at the end of the edge.
 	"""
 	node: Bug!
+=======
+  """A cursor for use in pagination."""
+  cursor: String!
+  """The item at the end of the edge."""
+  node: Bug!
+}
+`},
+	&ast.Source{Name: "schema/identity.graphql", Input: `"""Represents an identity"""
+type Identity {
+    """The identifier for this identity"""
+    id: String!
+    """The human version (truncated) identifier for this identity"""
+    humanId: String!
+    """The name of the person, if known."""
+    name: String
+    """The email of the person, if known."""
+    email: String
+    """A non-empty string to display, representing the identity, based on the non-empty values."""
+    displayName: String!
+    """An url to an avatar"""
+    avatarUrl: String
+    """isProtected is true if the chain of git commits started to be signed.
+    If that's the case, only signed commit with a valid key for this identity can be added."""
+    isProtected: Boolean!
+}
+
+type IdentityConnection {
+    edges: [IdentityEdge!]!
+    nodes: [Identity!]!
+    pageInfo: PageInfo!
+    totalCount: Int!
+}
+
+type IdentityEdge {
+    cursor: String!
+    node: Identity!
+}`},
+	&ast.Source{Name: "schema/label.graphql", Input: `"""Label for a bug."""
+type Label {
+    """The name of the label."""
+    name: String!
+    """Color of the label."""
+    color: Color!
+}
+
+type LabelConnection {
+    edges: [LabelEdge!]!
+    nodes: [Label!]!
+    pageInfo: PageInfo!
+    totalCount: Int!
+}
+
+type LabelEdge {
+    cursor: String!
+    node: Label!
+}`},
+	&ast.Source{Name: "schema/mutations.graphql", Input: `input NewBugInput {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """"The name of the repository. If not set, the default repository is used."""
+    repoRef: String
+    """The title of the new bug."""
+    title: String!
+    """The first message of the new bug."""
+    message: String!
+    """The collection of file's hash required for the first message."""
+    files: [Hash!]
+}
+
+type NewBugPayload {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """The created bug."""
+    bug: Bug!
+    """The resulting operation."""
+    operation: CreateOperation!
+}
+
+input AddCommentInput {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """"The name of the repository. If not set, the default repository is used."""
+    repoRef: String
+    """The bug ID's prefix."""
+    prefix: String!
+    """The first message of the new bug."""
+    message: String!
+    """The collection of file's hash required for the first message."""
+    files: [Hash!]
+}
+
+type AddCommentPayload {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """The affected bug."""
+    bug: Bug!
+    """The resulting operation."""
+    operation: AddCommentOperation!
+>>>>>>> more more wip
 }
 input ChangeLabelInput {
 	"""
@@ -6215,6 +6307,7 @@ func (ec *executionContext) _Identity_email(ctx context.Context, field graphql.C
 	return ec.marshalOString2string(ctx, field.Selections, res)
 }
 
+<<<<<<< HEAD
 func (ec *executionContext) _Identity_login(ctx context.Context, field graphql.CollectedField, obj identity.Interface) (ret graphql.Marshaler) {
 	defer func() {
 		if r := recover(); r != nil {
@@ -6247,6 +6340,10 @@ func (ec *executionContext) _Identity_login(ctx context.Context, field graphql.C
 }
 
 func (ec *executionContext) _Identity_displayName(ctx context.Context, field graphql.CollectedField, obj identity.Interface) (ret graphql.Marshaler) {
+=======
+func (ec *executionContext) _Identity_displayName(ctx context.Context, field graphql.CollectedField, obj *identity.Interface) (ret graphql.Marshaler) {
+	ctx = ec.Tracer.StartFieldExecution(ctx, field)
+>>>>>>> more more wip
 	defer func() {
 		if r := recover(); r != nil {
 			ec.Error(ctx, ec.Recover(ctx, r))
@@ -11945,9 +12042,22 @@ func (ec *executionContext) _Identity(ctx context.Context, sel ast.SelectionSet,
 		case "name":
 			out.Values[i] = ec._Identity_name(ctx, field, obj)
 		case "email":
+<<<<<<< HEAD
 			out.Values[i] = ec._Identity_email(ctx, field, obj)
 		case "login":
 			out.Values[i] = ec._Identity_login(ctx, field, obj)
+=======
+			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._Identity_email(ctx, field, obj)
+				return res
+			})
+>>>>>>> more more wip
 		case "displayName":
 			out.Values[i] = ec._Identity_displayName(ctx, field, obj)
 			if out.Values[i] == graphql.Null {

graphql/resolvers/identity.go πŸ”—

@@ -14,7 +14,3 @@ type identityResolver struct{}
 func (identityResolver) ID(ctx context.Context, obj identity.Interface) (string, error) {
 	return obj.Id().String(), nil
 }
-
-func (identityResolver) HumanID(ctx context.Context, obj identity.Interface) (string, error) {
-	return obj.Id().Human(), nil
-}

graphql/schema/identity.graphql πŸ”—

@@ -8,9 +8,7 @@ type Identity {
     name: String
     """The email of the person, if known."""
     email: String
-    """The login of the person, if known."""
-    login: String
-    """A string containing the either the name of the person, its login or both"""
+    """A non-empty string to display, representing the identity, based on the non-empty values."""
     displayName: String!
     """An url to an avatar"""
     avatarUrl: String

graphql/schema/root.graphql πŸ”—

@@ -3,6 +3,8 @@ type Query {
     defaultRepository: Repository
     """Access a repository by reference/name."""
     repository(ref: String!): Repository
+
+    #TODO: connection for all repositories
 }
 
 type Mutation {

identity/bare.go πŸ”—

@@ -112,13 +112,13 @@ func (i *Bare) AvatarUrl() string {
 }
 
 // Keys return the last version of the valid keys
-func (i *Bare) Keys() []Key {
-	return []Key{}
+func (i *Bare) Keys() []*Key {
+	return nil
 }
 
 // ValidKeysAtTime return the set of keys valid at a given lamport time
-func (i *Bare) ValidKeysAtTime(time lamport.Time) []Key {
-	return []Key{}
+func (i *Bare) ValidKeysAtTime(_ lamport.Time) []*Key {
+	return nil
 }
 
 // DisplayName return a non-empty string to display, representing the

identity/identity.go πŸ”—

@@ -275,7 +275,7 @@ type Mutator struct {
 	Name      string
 	Email     string
 	AvatarUrl string
-	Keys      []Key
+	Keys      []*Key
 }
 
 // Mutate allow to create a new version of the Identity
@@ -507,13 +507,13 @@ func (i *Identity) AvatarUrl() string {
 }
 
 // Keys return the last version of the valid keys
-func (i *Identity) Keys() []Key {
+func (i *Identity) Keys() []*Key {
 	return i.lastVersion().keys
 }
 
 // ValidKeysAtTime return the set of keys valid at a given lamport time
-func (i *Identity) ValidKeysAtTime(time lamport.Time) []Key {
-	var result []Key
+func (i *Identity) ValidKeysAtTime(time lamport.Time) []*Key {
+	var result []*Key
 
 	for _, v := range i.versions {
 		if v.time > time {
@@ -550,11 +550,11 @@ func (i *Identity) LastModification() timestamp.Timestamp {
 }
 
 // SetMetadata store arbitrary metadata along the last not-commit Version.
-// If the Version has been commit to git already, a new version is added and will need to be
+// If the Version has been commit to git already, a new identical version is added and will need to be
 // commit.
 func (i *Identity) SetMetadata(key string, value string) {
 	if i.lastVersion().commitHash != "" {
-
+		i.versions = append(i.versions, i.lastVersion().Clone())
 	}
 	i.lastVersion().SetMetadata(key, value)
 }
@@ -588,3 +588,9 @@ func (i *Identity) MutableMetadata() map[string]string {
 
 	return metadata
 }
+
+// addVersionForTest add a new version to the identity
+// Only for testing !
+func (i *Identity) addVersionForTest(version *Version) {
+	i.versions = append(i.versions, version)
+}

identity/identity_actions_test.go πŸ”—

@@ -48,14 +48,14 @@ func TestPushPull(t *testing.T) {
 
 	// Update both
 
-	identity1.AddVersion(&Version{
+	identity1.addVersionForTest(&Version{
 		name:  "name1b",
 		email: "email1b",
 	})
 	err = identity1.Commit(repoA)
 	require.NoError(t, err)
 
-	identity2.AddVersion(&Version{
+	identity2.addVersionForTest(&Version{
 		name:  "name2b",
 		email: "email2b",
 	})
@@ -92,7 +92,7 @@ func TestPushPull(t *testing.T) {
 
 	// Concurrent update
 
-	identity1.AddVersion(&Version{
+	identity1.addVersionForTest(&Version{
 		name:  "name1c",
 		email: "email1c",
 	})
@@ -102,7 +102,7 @@ func TestPushPull(t *testing.T) {
 	identity1B, err := ReadLocal(repoB, identity1.Id())
 	require.NoError(t, err)
 
-	identity1B.AddVersion(&Version{
+	identity1B.addVersionForTest(&Version{
 		name:  "name1concurrent",
 		email: "email1concurrent",
 	})

identity/identity_stub.go πŸ”—

@@ -64,11 +64,11 @@ func (IdentityStub) AvatarUrl() string {
 	panic("identities needs to be properly loaded with identity.ReadLocal()")
 }
 
-func (IdentityStub) Keys() []Key {
+func (IdentityStub) Keys() []*Key {
 	panic("identities needs to be properly loaded with identity.ReadLocal()")
 }
 
-func (IdentityStub) ValidKeysAtTime(time lamport.Time) []Key {
+func (IdentityStub) ValidKeysAtTime(_ lamport.Time) []*Key {
 	panic("identities needs to be properly loaded with identity.ReadLocal()")
 }
 

identity/identity_test.go πŸ”—

@@ -44,7 +44,7 @@ func TestIdentityCommitLoad(t *testing.T) {
 				time:  100,
 				name:  "RenΓ© Descartes",
 				email: "rene.descartes@example.com",
-				keys: []Key{
+				keys: []*Key{
 					{PubKey: "pubkeyA"},
 				},
 			},
@@ -52,7 +52,7 @@ func TestIdentityCommitLoad(t *testing.T) {
 				time:  200,
 				name:  "RenΓ© Descartes",
 				email: "rene.descartes@example.com",
-				keys: []Key{
+				keys: []*Key{
 					{PubKey: "pubkeyB"},
 				},
 			},
@@ -60,7 +60,7 @@ func TestIdentityCommitLoad(t *testing.T) {
 				time:  201,
 				name:  "RenΓ© Descartes",
 				email: "rene.descartes@example.com",
-				keys: []Key{
+				keys: []*Key{
 					{PubKey: "pubkeyC"},
 				},
 			},
@@ -79,20 +79,25 @@ func TestIdentityCommitLoad(t *testing.T) {
 
 	// add more version
 
-	identity.AddVersion(&Version{
+	identity.Mutate(func(orig Mutator) Mutator {
+
+		return orig
+	})
+
+	identity.addVersionForTest(&Version{
 		time:  201,
 		name:  "RenΓ© Descartes",
 		email: "rene.descartes@example.com",
-		keys: []Key{
+		keys: []*Key{
 			{PubKey: "pubkeyD"},
 		},
 	})
 
-	identity.AddVersion(&Version{
+	identity.addVersionForTest(&Version{
 		time:  300,
 		name:  "RenΓ© Descartes",
 		email: "rene.descartes@example.com",
-		keys: []Key{
+		keys: []*Key{
 			{PubKey: "pubkeyE"},
 		},
 	})
@@ -123,7 +128,7 @@ func TestIdentity_ValidKeysAtTime(t *testing.T) {
 				time:  100,
 				name:  "RenΓ© Descartes",
 				email: "rene.descartes@example.com",
-				keys: []Key{
+				keys: []*Key{
 					{PubKey: "pubkeyA"},
 				},
 			},
@@ -131,7 +136,7 @@ func TestIdentity_ValidKeysAtTime(t *testing.T) {
 				time:  200,
 				name:  "RenΓ© Descartes",
 				email: "rene.descartes@example.com",
-				keys: []Key{
+				keys: []*Key{
 					{PubKey: "pubkeyB"},
 				},
 			},
@@ -139,7 +144,7 @@ func TestIdentity_ValidKeysAtTime(t *testing.T) {
 				time:  201,
 				name:  "RenΓ© Descartes",
 				email: "rene.descartes@example.com",
-				keys: []Key{
+				keys: []*Key{
 					{PubKey: "pubkeyC"},
 				},
 			},
@@ -147,7 +152,7 @@ func TestIdentity_ValidKeysAtTime(t *testing.T) {
 				time:  201,
 				name:  "RenΓ© Descartes",
 				email: "rene.descartes@example.com",
-				keys: []Key{
+				keys: []*Key{
 					{PubKey: "pubkeyD"},
 				},
 			},
@@ -155,7 +160,7 @@ func TestIdentity_ValidKeysAtTime(t *testing.T) {
 				time:  300,
 				name:  "RenΓ© Descartes",
 				email: "rene.descartes@example.com",
-				keys: []Key{
+				keys: []*Key{
 					{PubKey: "pubkeyE"},
 				},
 			},
@@ -163,13 +168,13 @@ func TestIdentity_ValidKeysAtTime(t *testing.T) {
 	}
 
 	assert.Nil(t, identity.ValidKeysAtTime(10))
-	assert.Equal(t, identity.ValidKeysAtTime(100), []Key{{PubKey: "pubkeyA"}})
-	assert.Equal(t, identity.ValidKeysAtTime(140), []Key{{PubKey: "pubkeyA"}})
-	assert.Equal(t, identity.ValidKeysAtTime(200), []Key{{PubKey: "pubkeyB"}})
-	assert.Equal(t, identity.ValidKeysAtTime(201), []Key{{PubKey: "pubkeyD"}})
-	assert.Equal(t, identity.ValidKeysAtTime(202), []Key{{PubKey: "pubkeyD"}})
-	assert.Equal(t, identity.ValidKeysAtTime(300), []Key{{PubKey: "pubkeyE"}})
-	assert.Equal(t, identity.ValidKeysAtTime(3000), []Key{{PubKey: "pubkeyE"}})
+	assert.Equal(t, identity.ValidKeysAtTime(100), []*Key{{PubKey: "pubkeyA"}})
+	assert.Equal(t, identity.ValidKeysAtTime(140), []*Key{{PubKey: "pubkeyA"}})
+	assert.Equal(t, identity.ValidKeysAtTime(200), []*Key{{PubKey: "pubkeyB"}})
+	assert.Equal(t, identity.ValidKeysAtTime(201), []*Key{{PubKey: "pubkeyD"}})
+	assert.Equal(t, identity.ValidKeysAtTime(202), []*Key{{PubKey: "pubkeyD"}})
+	assert.Equal(t, identity.ValidKeysAtTime(300), []*Key{{PubKey: "pubkeyE"}})
+	assert.Equal(t, identity.ValidKeysAtTime(3000), []*Key{{PubKey: "pubkeyE"}})
 }
 
 // Test the immutable or mutable metadata search
@@ -189,7 +194,7 @@ func TestMetadata(t *testing.T) {
 	assertHasKeyValue(t, identity.MutableMetadata(), "key1", "value1")
 
 	// try override
-	identity.AddVersion(&Version{
+	identity.addVersionForTest(&Version{
 		name:  "RenΓ© Descartes",
 		email: "rene.descartes@example.com",
 	})

identity/interface.go πŸ”—

@@ -21,10 +21,10 @@ type Interface interface {
 	AvatarUrl() string
 
 	// Keys return the last version of the valid keys
-	Keys() []Key
+	Keys() []*Key
 
 	// ValidKeysAtTime return the set of keys valid at a given lamport time
-	ValidKeysAtTime(time lamport.Time) []Key
+	ValidKeysAtTime(time lamport.Time) []*Key
 
 	// DisplayName return a non-empty string to display, representing the
 	// identity, based on the non-empty values.

identity/key.go πŸ”—

@@ -11,3 +11,8 @@ func (k *Key) Validate() error {
 
 	return nil
 }
+
+func (k *Key) Clone() *Key {
+	clone := *k
+	return &clone
+}

identity/version.go πŸ”—

@@ -30,7 +30,7 @@ type Version struct {
 	// The set of keys valid at that time, from this version onward, until they get removed
 	// in a new version. This allow to have multiple key for the same identity (e.g. one per
 	// device) as well as revoke key.
-	keys []Key
+	keys []*Key
 
 	// This optional array is here to ensure a better randomness of the identity id to avoid collisions.
 	// It has no functional purpose and should be ignored.
@@ -53,24 +53,22 @@ type VersionJSON struct {
 	Name      string            `json:"name,omitempty"`
 	Email     string            `json:"email,omitempty"`
 	AvatarUrl string            `json:"avatar_url,omitempty"`
-	Keys      []Key             `json:"pub_keys,omitempty"`
+	Keys      []*Key            `json:"pub_keys,omitempty"`
 	Nonce     []byte            `json:"nonce,omitempty"`
 	Metadata  map[string]string `json:"metadata,omitempty"`
 }
 
 // Make a deep copy
 func (v *Version) Clone() *Version {
-
 	clone := &Version{
 		name:      v.name,
 		email:     v.email,
 		avatarURL: v.avatarURL,
-		keys:      make([]Key, len(v.keys)),
-		metadata:  make(map[string]string),
+		keys:      make([]*Key, len(v.keys)),
 	}
 
-	for i, op := range opp.Operations {
-		clone.Operations[i] = op
+	for i, key := range v.keys {
+		clone.keys[i] = key.Clone()
 	}
 
 	return clone

identity/version_test.go πŸ”—

@@ -9,11 +9,10 @@ import (
 
 func TestVersionSerialize(t *testing.T) {
 	before := &Version{
-		login:     "login",
 		name:      "name",
 		email:     "email",
 		avatarURL: "avatarUrl",
-		keys: []Key{
+		keys: []*Key{
 			{
 				Fingerprint: "fingerprint1",
 				PubKey:      "pubkey1",