identity: work on higher level now, cache, first two identity commands

Michael Muré created

Change summary

bridge/github/import.go         |  4 
bridge/launchpad/import.go      |  2 
cache/bug_cache.go              | 38 ++++++++---------
cache/identity_cache.go         | 26 +++++++++++
cache/repo_cache.go             | 68 +++++++++++++++++++++++-------
commands/id.go                  | 49 ----------------------
commands/user.go                | 56 +++++++++++++++++++++++++
commands/user_create.go         | 76 +++++++++++++++++++++++++++++++++++
doc/man/git-bug-bridge-bridge.1 | 29 -------------
doc/man/git-bug-user-create.1   | 29 +++++++++++++
doc/man/git-bug-user.1          |  8 +-
doc/man/git-bug.1               |  2 
doc/md/git-bug.md               |  2 
doc/md/git-bug_bridge_bridge.md | 22 ----------
doc/md/git-bug_close.md         | 22 ----------
doc/md/git-bug_new.md           | 25 -----------
doc/md/git-bug_open.md          | 22 ----------
doc/md/git-bug_user.md          |  7 +-
doc/md/git-bug_user_create.md   | 22 ++++++++++
identity/identity.go            | 16 ++++++
input/prompt.go                 | 44 ++++++++++++++++++++
misc/bash_completion/git-bug    | 63 +++++++++++++++++++---------
misc/zsh_completion/git-bug     |  5 +
23 files changed, 397 insertions(+), 240 deletions(-)

Detailed changes

bridge/github/import.go 🔗

@@ -599,7 +599,7 @@ func (gi *githubImporter) ensureCommentEdit(repo *cache.RepoCache, b *cache.BugC
 }
 
 // makePerson create a bug.Person from the Github data
-func (gi *githubImporter) makePerson(repo *cache.RepoCache, actor *actor) (*identity.Identity, error) {
+func (gi *githubImporter) makePerson(repo *cache.RepoCache, actor *actor) (*cache.IdentityCache, error) {
 	// When a user has been deleted, Github return a null actor, while displaying a profile named "ghost"
 	// in it's UI. So we need a special case to get it.
 	if actor == nil {
@@ -645,7 +645,7 @@ func (gi *githubImporter) makePerson(repo *cache.RepoCache, actor *actor) (*iden
 	)
 }
 
-func (gi *githubImporter) getGhost(repo *cache.RepoCache) (*identity.Identity, error) {
+func (gi *githubImporter) getGhost(repo *cache.RepoCache) (*cache.IdentityCache, error) {
 	// Look first in the cache
 	i, err := repo.ResolveIdentityImmutableMetadata(keyGithubLogin, "ghost")
 	if err == nil {

bridge/launchpad/import.go 🔗

@@ -23,7 +23,7 @@ func (li *launchpadImporter) Init(conf core.Configuration) error {
 const keyLaunchpadID = "launchpad-id"
 const keyLaunchpadLogin = "launchpad-login"
 
-func (li *launchpadImporter) makePerson(repo *cache.RepoCache, owner LPPerson) (*identity.Identity, error) {
+func (li *launchpadImporter) makePerson(repo *cache.RepoCache, owner LPPerson) (*cache.IdentityCache, error) {
 	// Look first in the cache
 	i, err := repo.ResolveIdentityImmutableMetadata(keyLaunchpadLogin, owner.Login)
 	if err == nil {

cache/bug_cache.go 🔗

@@ -5,8 +5,6 @@ import (
 	"strings"
 	"time"
 
-	"github.com/MichaelMure/git-bug/identity"
-
 	"github.com/MichaelMure/git-bug/bug"
 	"github.com/MichaelMure/git-bug/util/git"
 )
@@ -93,7 +91,7 @@ func (c *BugCache) AddComment(message string) error {
 }
 
 func (c *BugCache) AddCommentWithFiles(message string, files []git.Hash) error {
-	author, err := identity.GetUserIdentity(c.repoCache.repo)
+	author, err := c.repoCache.GetUserIdentity()
 	if err != nil {
 		return err
 	}
@@ -101,8 +99,8 @@ func (c *BugCache) AddCommentWithFiles(message string, files []git.Hash) error {
 	return c.AddCommentRaw(author, time.Now().Unix(), message, files, nil)
 }
 
-func (c *BugCache) AddCommentRaw(author identity.Interface, unixTime int64, message string, files []git.Hash, metadata map[string]string) error {
-	op, err := bug.AddCommentWithFiles(c.bug, author, unixTime, message, files)
+func (c *BugCache) AddCommentRaw(author *IdentityCache, unixTime int64, message string, files []git.Hash, metadata map[string]string) error {
+	op, err := bug.AddCommentWithFiles(c.bug, author.Identity, unixTime, message, files)
 	if err != nil {
 		return err
 	}
@@ -115,7 +113,7 @@ func (c *BugCache) AddCommentRaw(author identity.Interface, unixTime int64, mess
 }
 
 func (c *BugCache) ChangeLabels(added []string, removed []string) ([]bug.LabelChangeResult, error) {
-	author, err := identity.GetUserIdentity(c.repoCache.repo)
+	author, err := c.repoCache.GetUserIdentity()
 	if err != nil {
 		return nil, err
 	}
@@ -123,8 +121,8 @@ func (c *BugCache) ChangeLabels(added []string, removed []string) ([]bug.LabelCh
 	return c.ChangeLabelsRaw(author, time.Now().Unix(), added, removed, nil)
 }
 
-func (c *BugCache) ChangeLabelsRaw(author identity.Interface, unixTime int64, added []string, removed []string, metadata map[string]string) ([]bug.LabelChangeResult, error) {
-	changes, op, err := bug.ChangeLabels(c.bug, author, unixTime, added, removed)
+func (c *BugCache) ChangeLabelsRaw(author *IdentityCache, unixTime int64, added []string, removed []string, metadata map[string]string) ([]bug.LabelChangeResult, error) {
+	changes, op, err := bug.ChangeLabels(c.bug, author.Identity, unixTime, added, removed)
 	if err != nil {
 		return changes, err
 	}
@@ -142,7 +140,7 @@ func (c *BugCache) ChangeLabelsRaw(author identity.Interface, unixTime int64, ad
 }
 
 func (c *BugCache) Open() error {
-	author, err := identity.GetUserIdentity(c.repoCache.repo)
+	author, err := c.repoCache.GetUserIdentity()
 	if err != nil {
 		return err
 	}
@@ -150,8 +148,8 @@ func (c *BugCache) Open() error {
 	return c.OpenRaw(author, time.Now().Unix(), nil)
 }
 
-func (c *BugCache) OpenRaw(author identity.Interface, unixTime int64, metadata map[string]string) error {
-	op, err := bug.Open(c.bug, author, unixTime)
+func (c *BugCache) OpenRaw(author *IdentityCache, unixTime int64, metadata map[string]string) error {
+	op, err := bug.Open(c.bug, author.Identity, unixTime)
 	if err != nil {
 		return err
 	}
@@ -164,7 +162,7 @@ func (c *BugCache) OpenRaw(author identity.Interface, unixTime int64, metadata m
 }
 
 func (c *BugCache) Close() error {
-	author, err := identity.GetUserIdentity(c.repoCache.repo)
+	author, err := c.repoCache.GetUserIdentity()
 	if err != nil {
 		return err
 	}
@@ -172,8 +170,8 @@ func (c *BugCache) Close() error {
 	return c.CloseRaw(author, time.Now().Unix(), nil)
 }
 
-func (c *BugCache) CloseRaw(author identity.Interface, unixTime int64, metadata map[string]string) error {
-	op, err := bug.Close(c.bug, author, unixTime)
+func (c *BugCache) CloseRaw(author *IdentityCache, unixTime int64, metadata map[string]string) error {
+	op, err := bug.Close(c.bug, author.Identity, unixTime)
 	if err != nil {
 		return err
 	}
@@ -186,7 +184,7 @@ func (c *BugCache) CloseRaw(author identity.Interface, unixTime int64, metadata
 }
 
 func (c *BugCache) SetTitle(title string) error {
-	author, err := identity.GetUserIdentity(c.repoCache.repo)
+	author, err := c.repoCache.GetUserIdentity()
 	if err != nil {
 		return err
 	}
@@ -194,8 +192,8 @@ func (c *BugCache) SetTitle(title string) error {
 	return c.SetTitleRaw(author, time.Now().Unix(), title, nil)
 }
 
-func (c *BugCache) SetTitleRaw(author identity.Interface, unixTime int64, title string, metadata map[string]string) error {
-	op, err := bug.SetTitle(c.bug, author, unixTime, title)
+func (c *BugCache) SetTitleRaw(author *IdentityCache, unixTime int64, title string, metadata map[string]string) error {
+	op, err := bug.SetTitle(c.bug, author.Identity, unixTime, title)
 	if err != nil {
 		return err
 	}
@@ -208,7 +206,7 @@ func (c *BugCache) SetTitleRaw(author identity.Interface, unixTime int64, title
 }
 
 func (c *BugCache) EditComment(target git.Hash, message string) error {
-	author, err := identity.GetUserIdentity(c.repoCache.repo)
+	author, err := c.repoCache.GetUserIdentity()
 	if err != nil {
 		return err
 	}
@@ -216,8 +214,8 @@ func (c *BugCache) EditComment(target git.Hash, message string) error {
 	return c.EditCommentRaw(author, time.Now().Unix(), target, message, nil)
 }
 
-func (c *BugCache) EditCommentRaw(author identity.Interface, unixTime int64, target git.Hash, message string, metadata map[string]string) error {
-	op, err := bug.EditComment(c.bug, author, unixTime, target, message)
+func (c *BugCache) EditCommentRaw(author *IdentityCache, unixTime int64, target git.Hash, message string, metadata map[string]string) error {
+	op, err := bug.EditComment(c.bug, author.Identity, unixTime, target, message)
 	if err != nil {
 		return err
 	}

cache/identity_cache.go 🔗

@@ -0,0 +1,26 @@
+package cache
+
+import (
+	"github.com/MichaelMure/git-bug/identity"
+)
+
+// IdentityCache is a wrapper around an Identity. It provide multiple functions:
+type IdentityCache struct {
+	*identity.Identity
+	repoCache *RepoCache
+}
+
+func NewIdentityCache(repoCache *RepoCache, id *identity.Identity) *IdentityCache {
+	return &IdentityCache{
+		Identity:  id,
+		repoCache: repoCache,
+	}
+}
+
+func (i *IdentityCache) Commit() error {
+	return i.Identity.Commit(i.repoCache.repo)
+}
+
+func (i *IdentityCache) CommitAsNeeded() error {
+	return i.Identity.CommitAsNeeded(i.repoCache.repo)
+}

cache/repo_cache.go 🔗

@@ -45,13 +45,16 @@ type RepoCache struct {
 	// bug loaded in memory
 	bugs map[string]*BugCache
 	// identities loaded in memory
-	identities map[string]*identity.Identity
+	identities map[string]*IdentityCache
+	// the user identity's id, if known
+	userIdentityId string
 }
 
 func NewRepoCache(r repository.ClockedRepo) (*RepoCache, error) {
 	c := &RepoCache{
-		repo: r,
-		bugs: make(map[string]*BugCache),
+		repo:       r,
+		bugs:       make(map[string]*BugCache),
+		identities: make(map[string]*IdentityCache),
 	}
 
 	err := c.lock()
@@ -394,7 +397,7 @@ func (c *RepoCache) NewBug(title string, message string) (*BugCache, error) {
 // NewBugWithFiles create a new bug with attached files for the message
 // The new bug is written in the repository (commit)
 func (c *RepoCache) NewBugWithFiles(title string, message string, files []git.Hash) (*BugCache, error) {
-	author, err := identity.GetUserIdentity(c.repo)
+	author, err := c.GetUserIdentity()
 	if err != nil {
 		return nil, err
 	}
@@ -405,8 +408,8 @@ func (c *RepoCache) NewBugWithFiles(title string, message string, files []git.Ha
 // NewBugWithFilesMeta create a new bug with attached files for the message, as
 // well as metadata for the Create operation.
 // The new bug is written in the repository (commit)
-func (c *RepoCache) NewBugRaw(author identity.Interface, unixTime int64, title string, message string, files []git.Hash, metadata map[string]string) (*BugCache, error) {
-	b, op, err := bug.CreateWithFiles(author, unixTime, title, message, files)
+func (c *RepoCache) NewBugRaw(author *IdentityCache, unixTime int64, title string, message string, files []git.Hash, metadata map[string]string) (*BugCache, error) {
+	b, op, err := bug.CreateWithFiles(author.Identity, unixTime, title, message, files)
 	if err != nil {
 		return nil, err
 	}
@@ -549,7 +552,7 @@ func repoIsAvailable(repo repository.Repo) error {
 }
 
 // ResolveIdentity retrieve an identity matching the exact given id
-func (c *RepoCache) ResolveIdentity(id string) (*identity.Identity, error) {
+func (c *RepoCache) ResolveIdentity(id string) (*IdentityCache, error) {
 	cached, ok := c.identities[id]
 	if ok {
 		return cached, nil
@@ -560,14 +563,15 @@ func (c *RepoCache) ResolveIdentity(id string) (*identity.Identity, error) {
 		return nil, err
 	}
 
-	c.identities[id] = i
+	cached = NewIdentityCache(c, i)
+	c.identities[id] = cached
 
-	return i, nil
+	return cached, nil
 }
 
 // ResolveIdentityPrefix retrieve an Identity matching an id prefix.
 // It fails if multiple identities match.
-func (c *RepoCache) ResolveIdentityPrefix(prefix string) (*identity.Identity, error) {
+func (c *RepoCache) ResolveIdentityPrefix(prefix string) (*IdentityCache, error) {
 	// preallocate but empty
 	matching := make([]string, 0, 5)
 
@@ -590,7 +594,7 @@ func (c *RepoCache) ResolveIdentityPrefix(prefix string) (*identity.Identity, er
 
 // 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) (*identity.Identity, error) {
+func (c *RepoCache) ResolveIdentityImmutableMetadata(key string, value string) (*IdentityCache, error) {
 	// preallocate but empty
 	matching := make([]string, 0, 5)
 
@@ -611,19 +615,50 @@ func (c *RepoCache) ResolveIdentityImmutableMetadata(key string, value string) (
 	return c.ResolveIdentity(matching[0])
 }
 
+func (c *RepoCache) SetUserIdentity(i *IdentityCache) error {
+	err := identity.SetUserIdentity(c.repo, i.Identity)
+	if err != nil {
+		return err
+	}
+
+	c.userIdentityId = i.Id()
+
+	return nil
+}
+
+func (c *RepoCache) GetUserIdentity() (*IdentityCache, error) {
+	if c.userIdentityId != "" {
+		i, ok := c.identities[c.userIdentityId]
+		if ok {
+			return i, nil
+		}
+	}
+
+	i, err := identity.GetUserIdentity(c.repo)
+	if err != nil {
+		return nil, err
+	}
+
+	cached := NewIdentityCache(c, i)
+	c.identities[i.Id()] = cached
+	c.userIdentityId = i.Id()
+
+	return cached, nil
+}
+
 // NewIdentity create a new identity
 // The new identity is written in the repository (commit)
-func (c *RepoCache) NewIdentity(name string, email string) (*identity.Identity, error) {
+func (c *RepoCache) NewIdentity(name string, email string) (*IdentityCache, error) {
 	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) (*identity.Identity, error) {
+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) NewIdentityRaw(name string, email string, login string, avatarUrl string, metadata map[string]string) (*identity.Identity, error) {
+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)
 
 	for key, value := range metadata {
@@ -639,7 +674,8 @@ func (c *RepoCache) NewIdentityRaw(name string, email string, login string, avat
 		return nil, fmt.Errorf("identity %s already exist in the cache", i.Id())
 	}
 
-	c.identities[i.Id()] = i
+	cached := NewIdentityCache(c, i)
+	c.identities[i.Id()] = cached
 
-	return i, nil
+	return cached, nil
 }

commands/id.go 🔗

@@ -1,49 +0,0 @@
-package commands
-
-import (
-	"errors"
-	"fmt"
-
-	"github.com/MichaelMure/git-bug/identity"
-	"github.com/spf13/cobra"
-)
-
-func runId(cmd *cobra.Command, args []string) error {
-	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.ReadLocal(repo, args[0])
-	} else {
-		id, err = identity.GetUserIdentity(repo)
-	}
-
-	if err != nil {
-		return err
-	}
-
-	fmt.Printf("Id: %s\n", id.Id())
-	fmt.Printf("Identity: %s\n", id.DisplayName())
-	fmt.Printf("Name: %s\n", id.Name())
-	fmt.Printf("Login: %s\n", id.Login())
-	fmt.Printf("Email: %s\n", id.Email())
-	fmt.Printf("Protected: %v\n", id.IsProtected())
-
-	return nil
-}
-
-var idCmd = &cobra.Command{
-	Use:     "id [<id>]",
-	Short:   "Display or change the user identity",
-	PreRunE: loadRepo,
-	RunE:    runId,
-}
-
-func init() {
-	RootCmd.AddCommand(idCmd)
-	selectCmd.Flags().SortFlags = false
-}

commands/user.go 🔗

@@ -0,0 +1,56 @@
+package commands
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/MichaelMure/git-bug/cache"
+	"github.com/MichaelMure/git-bug/util/interrupt"
+	"github.com/spf13/cobra"
+)
+
+func runUser(cmd *cobra.Command, args []string) error {
+	backend, err := cache.NewRepoCache(repo)
+	if err != nil {
+		return err
+	}
+	defer backend.Close()
+	interrupt.RegisterCleaner(backend.Close)
+
+	if len(args) > 1 {
+		return errors.New("only one identity can be displayed at a time")
+	}
+
+	var id *cache.IdentityCache
+	if len(args) == 1 {
+		// TODO
+		return errors.New("this is not working yet, cache need to be hacked on")
+		id, err = backend.ResolveIdentityPrefix(args[0])
+	} else {
+		id, err = backend.GetUserIdentity()
+	}
+
+	if err != nil {
+		return err
+	}
+
+	fmt.Printf("Id: %s\n", id.Id())
+	fmt.Printf("Name: %s\n", id.Name())
+	fmt.Printf("Login: %s\n", id.Login())
+	fmt.Printf("Email: %s\n", id.Email())
+	fmt.Printf("Protected: %v\n", id.IsProtected())
+
+	return nil
+}
+
+var userCmd = &cobra.Command{
+	Use:     "user [<id>]",
+	Short:   "Display or change the user identity",
+	PreRunE: loadRepo,
+	RunE:    runUser,
+}
+
+func init() {
+	RootCmd.AddCommand(userCmd)
+	userCmd.Flags().SortFlags = false
+}

commands/user_create.go 🔗

@@ -0,0 +1,76 @@
+package commands
+
+import (
+	"fmt"
+
+	"github.com/MichaelMure/git-bug/cache"
+	"github.com/MichaelMure/git-bug/input"
+	"github.com/MichaelMure/git-bug/util/interrupt"
+	"github.com/spf13/cobra"
+)
+
+func runUserCreate(cmd *cobra.Command, args []string) error {
+	backend, err := cache.NewRepoCache(repo)
+	if err != nil {
+		return err
+	}
+	defer backend.Close()
+	interrupt.RegisterCleaner(backend.Close)
+
+	preName, err := backend.GetUserName()
+	if err != nil {
+		return err
+	}
+
+	name, err := input.PromptValueRequired("Name", preName)
+	if err != nil {
+		return err
+	}
+
+	preEmail, err := backend.GetUserEmail()
+	if err != nil {
+		return err
+	}
+
+	email, err := input.PromptValueRequired("Email", preEmail)
+	if err != nil {
+		return err
+	}
+
+	login, err := input.PromptValue("Avatar URL", "")
+	if err != nil {
+		return err
+	}
+
+	id, err := backend.NewIdentityRaw(name, email, "", login, nil)
+	if err != nil {
+		return err
+	}
+
+	err = id.CommitAsNeeded()
+	if err != nil {
+		return err
+	}
+
+	err = backend.SetUserIdentity(id)
+	if err != nil {
+		return err
+	}
+
+	fmt.Println()
+	fmt.Println(id.Id())
+
+	return nil
+}
+
+var userCreateCmd = &cobra.Command{
+	Use:     "create",
+	Short:   "Create a new identity",
+	PreRunE: loadRepo,
+	RunE:    runUserCreate,
+}
+
+func init() {
+	userCmd.AddCommand(userCreateCmd)
+	userCreateCmd.Flags().SortFlags = false
+}

doc/man/git-bug-bridge-bridge.1 🔗

@@ -1,29 +0,0 @@
-.TH "GIT-BUG" "1" "Sep 2018" "Generated from git-bug's source code" "" 
-.nh
-.ad l
-
-
-.SH NAME
-.PP
-git\-bug\-bridge\-bridge \- Configure and use bridges to other bug trackers
-
-
-.SH SYNOPSIS
-.PP
-\fBgit\-bug bridge bridge [flags]\fP
-
-
-.SH DESCRIPTION
-.PP
-Configure and use bridges to other bug trackers
-
-
-.SH OPTIONS
-.PP
-\fB\-h\fP, \fB\-\-help\fP[=false]
-    help for bridge
-
-
-.SH SEE ALSO
-.PP
-\fBgit\-bug\-bridge(1)\fP

doc/man/git-bug-user-create.1 🔗

@@ -0,0 +1,29 @@
+.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" "" 
+.nh
+.ad l
+
+
+.SH NAME
+.PP
+git\-bug\-user\-create \- Create a new identity
+
+
+.SH SYNOPSIS
+.PP
+\fBgit\-bug user create [flags]\fP
+
+
+.SH DESCRIPTION
+.PP
+Create a new identity
+
+
+.SH OPTIONS
+.PP
+\fB\-h\fP, \fB\-\-help\fP[=false]
+    help for create
+
+
+.SH SEE ALSO
+.PP
+\fBgit\-bug\-user(1)\fP

doc/man/git-bug-id.1 → doc/man/git-bug-user.1 🔗

@@ -5,12 +5,12 @@
 
 .SH NAME
 .PP
-git\-bug\-id \- Display or change the user identity
+git\-bug\-user \- Display or change the user identity
 
 
 .SH SYNOPSIS
 .PP
-\fBgit\-bug id [<id>] [flags]\fP
+\fBgit\-bug user [<id>] [flags]\fP
 
 
 .SH DESCRIPTION
@@ -21,9 +21,9 @@ Display or change the user identity
 .SH OPTIONS
 .PP
 \fB\-h\fP, \fB\-\-help\fP[=false]
-    help for id
+    help for user
 
 
 .SH SEE ALSO
 .PP
-\fBgit\-bug(1)\fP
+\fBgit\-bug(1)\fP, \fBgit\-bug\-user\-create(1)\fP

doc/man/git-bug.1 🔗

@@ -31,4 +31,4 @@ the same git remote your are already using to collaborate with other peoples.
 
 .SH SEE ALSO
 .PP
-\fBgit\-bug\-add(1)\fP, \fBgit\-bug\-bridge(1)\fP, \fBgit\-bug\-commands(1)\fP, \fBgit\-bug\-comment(1)\fP, \fBgit\-bug\-deselect(1)\fP, \fBgit\-bug\-id(1)\fP, \fBgit\-bug\-label(1)\fP, \fBgit\-bug\-ls(1)\fP, \fBgit\-bug\-ls\-label(1)\fP, \fBgit\-bug\-pull(1)\fP, \fBgit\-bug\-push(1)\fP, \fBgit\-bug\-select(1)\fP, \fBgit\-bug\-show(1)\fP, \fBgit\-bug\-status(1)\fP, \fBgit\-bug\-termui(1)\fP, \fBgit\-bug\-title(1)\fP, \fBgit\-bug\-version(1)\fP, \fBgit\-bug\-webui(1)\fP
+\fBgit\-bug\-add(1)\fP, \fBgit\-bug\-bridge(1)\fP, \fBgit\-bug\-commands(1)\fP, \fBgit\-bug\-comment(1)\fP, \fBgit\-bug\-deselect(1)\fP, \fBgit\-bug\-label(1)\fP, \fBgit\-bug\-ls(1)\fP, \fBgit\-bug\-ls\-label(1)\fP, \fBgit\-bug\-pull(1)\fP, \fBgit\-bug\-push(1)\fP, \fBgit\-bug\-select(1)\fP, \fBgit\-bug\-show(1)\fP, \fBgit\-bug\-status(1)\fP, \fBgit\-bug\-termui(1)\fP, \fBgit\-bug\-title(1)\fP, \fBgit\-bug\-user(1)\fP, \fBgit\-bug\-version(1)\fP, \fBgit\-bug\-webui(1)\fP

doc/md/git-bug.md 🔗

@@ -29,7 +29,6 @@ git-bug [flags]
 * [git-bug commands](git-bug_commands.md)	 - Display available commands
 * [git-bug comment](git-bug_comment.md)	 - Display or add comments
 * [git-bug deselect](git-bug_deselect.md)	 - Clear the implicitly selected bug
-* [git-bug id](git-bug_id.md)	 - Display or change the user identity
 * [git-bug label](git-bug_label.md)	 - Display, add or remove labels
 * [git-bug ls](git-bug_ls.md)	 - List bugs
 * [git-bug ls-id](git-bug_ls-id.md)	 - List Bug Id
@@ -41,6 +40,7 @@ git-bug [flags]
 * [git-bug status](git-bug_status.md)	 - Display or change a bug status
 * [git-bug termui](git-bug_termui.md)	 - Launch the terminal UI
 * [git-bug title](git-bug_title.md)	 - Display or change a title
+* [git-bug user](git-bug_user.md)	 - Display or change the user identity
 * [git-bug version](git-bug_version.md)	 - Show git-bug version information
 * [git-bug webui](git-bug_webui.md)	 - Launch the web UI
 

doc/md/git-bug_bridge_bridge.md 🔗

@@ -1,22 +0,0 @@
-## git-bug bridge bridge
-
-Configure and use bridges to other bug trackers
-
-### Synopsis
-
-Configure and use bridges to other bug trackers
-
-```
-git-bug bridge bridge [flags]
-```
-
-### Options
-
-```
-  -h, --help   help for bridge
-```
-
-### SEE ALSO
-
-* [git-bug bridge](git-bug_bridge.md)	 - Configure and use bridges to other bug trackers
-

doc/md/git-bug_close.md 🔗

@@ -1,22 +0,0 @@
-## git-bug close
-
-Mark the bug as closed
-
-### Synopsis
-
-Mark the bug as closed
-
-```
-git-bug close <id> [flags]
-```
-
-### Options
-
-```
-  -h, --help   help for close
-```
-
-### SEE ALSO
-
-* [git-bug](git-bug.md)	 - A bugtracker embedded in Git
-

doc/md/git-bug_new.md 🔗

@@ -1,25 +0,0 @@
-## git-bug new
-
-Create a new bug
-
-### Synopsis
-
-Create a new bug
-
-```
-git-bug new [flags]
-```
-
-### Options
-
-```
-  -t, --title string     Provide a title to describe the issue
-  -m, --message string   Provide a message to describe the issue
-  -F, --file string      Take the message from the given file. Use - to read the message from the standard input
-  -h, --help             help for new
-```
-
-### SEE ALSO
-
-* [git-bug](git-bug.md)	 - A bugtracker embedded in Git
-

doc/md/git-bug_open.md 🔗

@@ -1,22 +0,0 @@
-## git-bug open
-
-Mark the bug as open
-
-### Synopsis
-
-Mark the bug as open
-
-```
-git-bug open <id> [flags]
-```
-
-### Options
-
-```
-  -h, --help   help for open
-```
-
-### SEE ALSO
-
-* [git-bug](git-bug.md)	 - A bugtracker embedded in Git
-

doc/md/git-bug_id.md → doc/md/git-bug_user.md 🔗

@@ -1,4 +1,4 @@
-## git-bug id
+## git-bug user
 
 Display or change the user identity
 
@@ -7,16 +7,17 @@ Display or change the user identity
 Display or change the user identity
 
 ```
-git-bug id [<id>] [flags]
+git-bug user [<id>] [flags]
 ```
 
 ### Options
 
 ```
-  -h, --help   help for id
+  -h, --help   help for user
 ```
 
 ### SEE ALSO
 
 * [git-bug](git-bug.md)	 - A bug tracker embedded in Git
+* [git-bug user create](git-bug_user_create.md)	 - Create a new identity
 

doc/md/git-bug_user_create.md 🔗

@@ -0,0 +1,22 @@
+## git-bug user create
+
+Create a new identity
+
+### Synopsis
+
+Create a new identity
+
+```
+git-bug user create [flags]
+```
+
+### Options
+
+```
+  -h, --help   help for create
+```
+
+### SEE ALSO
+
+* [git-bug user](git-bug_user.md)	 - Display or change the user identity
+

identity/identity.go 🔗

@@ -204,8 +204,22 @@ func NewFromGitUser(repo repository.Repo) (*Identity, error) {
 	return NewIdentity(name, email), nil
 }
 
+// IsUserIdentitySet tell if the user identity is correctly set.
+func IsUserIdentitySet(repo repository.RepoCommon) (bool, error) {
+	configs, err := repo.ReadConfigs(identityConfigKey)
+	if err != nil {
+		return false, err
+	}
+
+	if len(configs) > 1 {
+		return false, fmt.Errorf("multiple identity config exist")
+	}
+
+	return len(configs) == 1, nil
+}
+
 // SetUserIdentity store the user identity's id in the git config
-func SetUserIdentity(repo repository.RepoCommon, identity Identity) error {
+func SetUserIdentity(repo repository.RepoCommon, identity *Identity) error {
 	return repo.StoreConfig(identityConfigKey, identity.Id())
 }
 

input/prompt.go 🔗

@@ -0,0 +1,44 @@
+package input
+
+import (
+	"bufio"
+	"fmt"
+	"os"
+	"strings"
+)
+
+func PromptValue(name string, preValue string) (string, error) {
+	return promptValue(name, preValue, false)
+}
+
+func PromptValueRequired(name string, preValue string) (string, error) {
+	return promptValue(name, preValue, true)
+}
+
+func promptValue(name string, preValue string, required bool) (string, error) {
+	for {
+		if preValue != "" {
+			fmt.Printf("%s [%s]: ", name, preValue)
+		} else {
+			fmt.Printf("%s: ", name)
+		}
+
+		line, err := bufio.NewReader(os.Stdin).ReadString('\n')
+		if err != nil {
+			return "", err
+		}
+
+		line = strings.TrimSpace(line)
+
+		if preValue != "" && line == "" {
+			return preValue, nil
+		}
+
+		if required && line == "" {
+			fmt.Printf("%s is empty\n", name)
+			continue
+		}
+
+		return line, nil
+	}
+}

misc/bash_completion/git-bug 🔗

@@ -450,26 +450,6 @@ _git-bug_deselect()
     noun_aliases=()
 }
 
-_git-bug_id()
-{
-    last_command="git-bug_id"
-
-    command_aliases=()
-
-    commands=()
-
-    flags=()
-    two_word_flags=()
-    local_nonpersistent_flags=()
-    flags_with_completion=()
-    flags_completion=()
-
-
-    must_have_one_flag=()
-    must_have_one_noun=()
-    noun_aliases=()
-}
-
 _git-bug_label_add()
 {
     last_command="git-bug_label_add"
@@ -819,6 +799,47 @@ _git-bug_title()
     noun_aliases=()
 }
 
+_git-bug_user_create()
+{
+    last_command="git-bug_user_create"
+
+    command_aliases=()
+
+    commands=()
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
+_git-bug_user()
+{
+    last_command="git-bug_user"
+
+    command_aliases=()
+
+    commands=()
+    commands+=("create")
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
 _git-bug_version()
 {
     last_command="git-bug_version"
@@ -883,7 +904,6 @@ _git-bug_root_command()
     commands+=("commands")
     commands+=("comment")
     commands+=("deselect")
-    commands+=("id")
     commands+=("label")
     commands+=("ls")
     commands+=("ls-id")
@@ -895,6 +915,7 @@ _git-bug_root_command()
     commands+=("status")
     commands+=("termui")
     commands+=("title")
+    commands+=("user")
     commands+=("version")
     commands+=("webui")
 

misc/zsh_completion/git-bug 🔗

@@ -8,7 +8,7 @@ case $state in
   level1)
     case $words[1] in
       git-bug)
-        _arguments '1: :(add bridge commands comment deselect id label ls ls-label pull push select show status termui title version webui)'
+        _arguments '1: :(add bridge commands comment deselect label ls ls-label pull push select show status termui title user version webui)'
       ;;
       *)
         _arguments '*: :_files'
@@ -32,6 +32,9 @@ case $state in
       title)
         _arguments '2: :(edit)'
       ;;
+      user)
+        _arguments '2: :(create)'
+      ;;
       *)
         _arguments '*: :_files'
       ;;