From 844616baf8dc628360942d57fd69f24e298e08da Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sat, 19 Jan 2019 16:01:06 +0100 Subject: identity: more progress and fixes --- bridge/github/import.go | 138 +++++++++++++++++++++++---------- bridge/launchpad/import.go | 40 ++++++++-- bug/op_create_test.go | 2 +- bug/op_edit_comment_test.go | 2 +- bug/op_set_metadata_test.go | 2 +- bug/operation_iterator_test.go | 2 +- bug/operation_test.go | 10 +-- cache/bug_cache.go | 4 + cache/multi_repo_cache.go | 1 + cache/repo_cache.go | 138 ++++++++++++++++++++++----------- commands/ls.go | 4 +- commands/show.go | 2 +- doc/man/git-bug-id.1 | 29 +++++++ doc/md/git-bug_id.md | 22 ++++++ graphql/resolvers/identity.go | 36 +++++++++ graphql/resolvers/root.go | 4 + identity/identity.go | 10 +++ misc/bash_completion/git-bug | 21 +++++ misc/random_bugs/create_random_bugs.go | 2 +- misc/zsh_completion/git-bug | 2 +- 20 files changed, 364 insertions(+), 107 deletions(-) create mode 100644 doc/man/git-bug-id.1 create mode 100644 doc/md/git-bug_id.md create mode 100644 graphql/resolvers/identity.go diff --git a/bridge/github/import.go b/bridge/github/import.go index de125793..43a8e3b5 100644 --- a/bridge/github/import.go +++ b/bridge/github/import.go @@ -21,14 +21,13 @@ const keyGithubLogin = "github-login" type githubImporter struct { client *githubv4.Client conf core.Configuration - ghost identity.Interface } func (gi *githubImporter) Init(conf core.Configuration) error { gi.conf = conf gi.client = buildClient(conf) - return gi.fetchGhost() + return nil } func (gi *githubImporter) ImportAll(repo *cache.RepoCache) error { @@ -71,7 +70,7 @@ func (gi *githubImporter) ImportAll(repo *cache.RepoCache) error { } for _, itemEdge := range q.Repository.Issues.Nodes[0].Timeline.Edges { - err = gi.ensureTimelineItem(b, itemEdge.Cursor, itemEdge.Node, variables) + err = gi.ensureTimelineItem(repo, b, itemEdge.Cursor, itemEdge.Node, variables) if err != nil { return err } @@ -114,6 +113,11 @@ func (gi *githubImporter) ensureIssue(repo *cache.RepoCache, issue issueTimeline return nil, err } + author, err := gi.makePerson(repo, issue.Author) + if err != nil { + return nil, err + } + // if there is no edit, the UserContentEdits given by github is empty. That // means that the original message is given by the issue message. // @@ -128,7 +132,7 @@ func (gi *githubImporter) ensureIssue(repo *cache.RepoCache, issue issueTimeline if len(issue.UserContentEdits.Nodes) == 0 { if err == bug.ErrBugNotExist { b, err = repo.NewBugRaw( - gi.makePerson(issue.Author), + author, issue.CreatedAt.Unix(), // Todo: this might not be the initial title, we need to query the // timeline to be sure @@ -140,7 +144,6 @@ func (gi *githubImporter) ensureIssue(repo *cache.RepoCache, issue issueTimeline keyGithubUrl: issue.Url.String(), }, ) - if err != nil { return nil, err } @@ -166,7 +169,7 @@ func (gi *githubImporter) ensureIssue(repo *cache.RepoCache, issue issueTimeline // we create the bug as soon as we have a legit first edition b, err = repo.NewBugRaw( - gi.makePerson(issue.Author), + author, issue.CreatedAt.Unix(), // Todo: this might not be the initial title, we need to query the // timeline to be sure @@ -189,7 +192,7 @@ func (gi *githubImporter) ensureIssue(repo *cache.RepoCache, issue issueTimeline return nil, err } - err = gi.ensureCommentEdit(b, target, edit) + err = gi.ensureCommentEdit(repo, b, target, edit) if err != nil { return nil, err } @@ -199,7 +202,7 @@ func (gi *githubImporter) ensureIssue(repo *cache.RepoCache, issue issueTimeline // if we still didn't get a legit edit, create the bug from the issue data if b == nil { return repo.NewBugRaw( - gi.makePerson(issue.Author), + author, issue.CreatedAt.Unix(), // Todo: this might not be the initial title, we need to query the // timeline to be sure @@ -248,7 +251,7 @@ func (gi *githubImporter) ensureIssue(repo *cache.RepoCache, issue issueTimeline // we create the bug as soon as we have a legit first edition b, err = repo.NewBugRaw( - gi.makePerson(issue.Author), + author, issue.CreatedAt.Unix(), // Todo: this might not be the initial title, we need to query the // timeline to be sure @@ -271,7 +274,7 @@ func (gi *githubImporter) ensureIssue(repo *cache.RepoCache, issue issueTimeline return nil, err } - err = gi.ensureCommentEdit(b, target, edit) + err = gi.ensureCommentEdit(repo, b, target, edit) if err != nil { return nil, err } @@ -289,7 +292,7 @@ func (gi *githubImporter) ensureIssue(repo *cache.RepoCache, issue issueTimeline // if we still didn't get a legit edit, create the bug from the issue data if b == nil { return repo.NewBugRaw( - gi.makePerson(issue.Author), + author, issue.CreatedAt.Unix(), // Todo: this might not be the initial title, we need to query the // timeline to be sure @@ -306,12 +309,12 @@ func (gi *githubImporter) ensureIssue(repo *cache.RepoCache, issue issueTimeline return b, nil } -func (gi *githubImporter) ensureTimelineItem(b *cache.BugCache, cursor githubv4.String, item timelineItem, rootVariables map[string]interface{}) error { +func (gi *githubImporter) ensureTimelineItem(repo *cache.RepoCache, b *cache.BugCache, cursor githubv4.String, item timelineItem, rootVariables map[string]interface{}) error { fmt.Printf("import %s\n", item.Typename) switch item.Typename { case "IssueComment": - return gi.ensureComment(b, cursor, item.IssueComment, rootVariables) + return gi.ensureComment(repo, b, cursor, item.IssueComment, rootVariables) case "LabeledEvent": id := parseId(item.LabeledEvent.Id) @@ -319,8 +322,12 @@ func (gi *githubImporter) ensureTimelineItem(b *cache.BugCache, cursor githubv4. if err != cache.ErrNoMatchingOp { return err } + author, err := gi.makePerson(repo, item.LabeledEvent.Actor) + if err != nil { + return err + } _, err = b.ChangeLabelsRaw( - gi.makePerson(item.LabeledEvent.Actor), + author, item.LabeledEvent.CreatedAt.Unix(), []string{ string(item.LabeledEvent.Label.Name), @@ -336,8 +343,12 @@ func (gi *githubImporter) ensureTimelineItem(b *cache.BugCache, cursor githubv4. if err != cache.ErrNoMatchingOp { return err } + author, err := gi.makePerson(repo, item.UnlabeledEvent.Actor) + if err != nil { + return err + } _, err = b.ChangeLabelsRaw( - gi.makePerson(item.UnlabeledEvent.Actor), + author, item.UnlabeledEvent.CreatedAt.Unix(), nil, []string{ @@ -353,8 +364,12 @@ func (gi *githubImporter) ensureTimelineItem(b *cache.BugCache, cursor githubv4. if err != cache.ErrNoMatchingOp { return err } + author, err := gi.makePerson(repo, item.ClosedEvent.Actor) + if err != nil { + return err + } return b.CloseRaw( - gi.makePerson(item.ClosedEvent.Actor), + author, item.ClosedEvent.CreatedAt.Unix(), map[string]string{keyGithubId: id}, ) @@ -365,8 +380,12 @@ func (gi *githubImporter) ensureTimelineItem(b *cache.BugCache, cursor githubv4. if err != cache.ErrNoMatchingOp { return err } + author, err := gi.makePerson(repo, item.ReopenedEvent.Actor) + if err != nil { + return err + } return b.OpenRaw( - gi.makePerson(item.ReopenedEvent.Actor), + author, item.ReopenedEvent.CreatedAt.Unix(), map[string]string{keyGithubId: id}, ) @@ -377,8 +396,12 @@ func (gi *githubImporter) ensureTimelineItem(b *cache.BugCache, cursor githubv4. if err != cache.ErrNoMatchingOp { return err } + author, err := gi.makePerson(repo, item.RenamedTitleEvent.Actor) + if err != nil { + return err + } return b.SetTitleRaw( - gi.makePerson(item.RenamedTitleEvent.Actor), + author, item.RenamedTitleEvent.CreatedAt.Unix(), string(item.RenamedTitleEvent.CurrentTitle), map[string]string{keyGithubId: id}, @@ -391,13 +414,18 @@ func (gi *githubImporter) ensureTimelineItem(b *cache.BugCache, cursor githubv4. return nil } -func (gi *githubImporter) ensureComment(b *cache.BugCache, cursor githubv4.String, comment issueComment, rootVariables map[string]interface{}) error { +func (gi *githubImporter) ensureComment(repo *cache.RepoCache, b *cache.BugCache, cursor githubv4.String, comment issueComment, rootVariables map[string]interface{}) error { target, err := b.ResolveTargetWithMetadata(keyGithubId, parseId(comment.Id)) if err != nil && err != cache.ErrNoMatchingOp { // real error return err } + author, err := gi.makePerson(repo, comment.Author) + if err != nil { + return err + } + // if there is no edit, the UserContentEdits given by github is empty. That // means that the original message is given by the comment message. // @@ -412,7 +440,7 @@ func (gi *githubImporter) ensureComment(b *cache.BugCache, cursor githubv4.Strin if len(comment.UserContentEdits.Nodes) == 0 { if err == cache.ErrNoMatchingOp { err = b.AddCommentRaw( - gi.makePerson(comment.Author), + author, comment.CreatedAt.Unix(), cleanupText(string(comment.Body)), nil, @@ -445,7 +473,7 @@ func (gi *githubImporter) ensureComment(b *cache.BugCache, cursor githubv4.Strin } err = b.AddCommentRaw( - gi.makePerson(comment.Author), + author, comment.CreatedAt.Unix(), cleanupText(string(*edit.Diff)), nil, @@ -459,7 +487,7 @@ func (gi *githubImporter) ensureComment(b *cache.BugCache, cursor githubv4.Strin } } - err := gi.ensureCommentEdit(b, target, edit) + err := gi.ensureCommentEdit(repo, b, target, edit) if err != nil { return err } @@ -501,7 +529,7 @@ func (gi *githubImporter) ensureComment(b *cache.BugCache, cursor githubv4.Strin continue } - err := gi.ensureCommentEdit(b, target, edit) + err := gi.ensureCommentEdit(repo, b, target, edit) if err != nil { return err } @@ -519,7 +547,7 @@ func (gi *githubImporter) ensureComment(b *cache.BugCache, cursor githubv4.Strin return nil } -func (gi *githubImporter) ensureCommentEdit(b *cache.BugCache, target git.Hash, edit userContentEdit) error { +func (gi *githubImporter) ensureCommentEdit(repo *cache.RepoCache, b *cache.BugCache, target git.Hash, edit userContentEdit) error { if edit.Diff == nil { // this happen if the event is older than early 2018, Github doesn't have the data before that. // Best we can do is to ignore the event. @@ -542,6 +570,11 @@ func (gi *githubImporter) ensureCommentEdit(b *cache.BugCache, target git.Hash, fmt.Println("import edition") + editor, err := gi.makePerson(repo, edit.Editor) + if err != nil { + return err + } + switch { case edit.DeletedAt != nil: // comment deletion, not supported yet @@ -549,7 +582,7 @@ func (gi *githubImporter) ensureCommentEdit(b *cache.BugCache, target git.Hash, case edit.DeletedAt == nil: // comment edition err := b.EditCommentRaw( - gi.makePerson(edit.Editor), + editor, edit.CreatedAt.Unix(), target, cleanupText(string(*edit.Diff)), @@ -566,10 +599,22 @@ 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) identity.Interface { +func (gi *githubImporter) makePerson(repo *cache.RepoCache, actor *actor) (*identity.Identity, 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 { - return gi.ghost + return gi.getGhost(repo) + } + + // Look first in the cache + i, err := repo.ResolveIdentityImmutableMetadata(keyGithubLogin, string(actor.Login)) + if err == nil { + return i, nil + } + if _, ok := err.(identity.ErrMultipleMatch); ok { + return nil, err } + var name string var email string @@ -589,24 +634,36 @@ func (gi *githubImporter) makePerson(actor *actor) identity.Interface { case "Bot": } - return bug.Person{ - Name: name, - Email: email, - Login: string(actor.Login), - AvatarUrl: string(actor.AvatarUrl), - } + return repo.NewIdentityRaw( + name, + email, + string(actor.Login), + string(actor.AvatarUrl), + map[string]string{ + keyGithubLogin: string(actor.Login), + }, + ) } -func (gi *githubImporter) fetchGhost() error { +func (gi *githubImporter) getGhost(repo *cache.RepoCache) (*identity.Identity, error) { + // Look first in the cache + i, err := repo.ResolveIdentityImmutableMetadata(keyGithubLogin, "ghost") + if err == nil { + return i, nil + } + if _, ok := err.(identity.ErrMultipleMatch); ok { + return nil, err + } + var q userQuery variables := map[string]interface{}{ "login": githubv4.String("ghost"), } - err := gi.client.Query(context.TODO(), &q, variables) + err = gi.client.Query(context.TODO(), &q, variables) if err != nil { - return err + return nil, err } var name string @@ -614,14 +671,15 @@ func (gi *githubImporter) fetchGhost() error { name = string(*q.User.Name) } - gi.ghost = identity.NewIdentityFull( + return repo.NewIdentityRaw( name, + string(q.User.Email), string(q.User.Login), string(q.User.AvatarUrl), - string(q.User.Email), + map[string]string{ + keyGithubLogin: string(q.User.Login), + }, ) - - return nil } // parseId convert the unusable githubv4.ID (an interface{}) into a string diff --git a/bridge/launchpad/import.go b/bridge/launchpad/import.go index 10d25e6c..e65186ed 100644 --- a/bridge/launchpad/import.go +++ b/bridge/launchpad/import.go @@ -7,6 +7,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/pkg/errors" ) @@ -20,14 +21,27 @@ func (li *launchpadImporter) Init(conf core.Configuration) error { } const keyLaunchpadID = "launchpad-id" +const keyLaunchpadLogin = "launchpad-login" -func (li *launchpadImporter) makePerson(owner LPPerson) bug.Person { - return bug.Person{ - Name: owner.Name, - Email: "", - Login: owner.Login, - AvatarUrl: "", +func (li *launchpadImporter) makePerson(repo *cache.RepoCache, owner LPPerson) (*identity.Identity, error) { + // Look first in the cache + i, err := repo.ResolveIdentityImmutableMetadata(keyLaunchpadLogin, owner.Login) + if err == nil { + return i, nil } + if _, ok := err.(identity.ErrMultipleMatch); ok { + return nil, err + } + + return repo.NewIdentityRaw( + owner.Name, + "", + owner.Login, + "", + map[string]string{ + keyLaunchpadLogin: owner.Login, + }, + ) } func (li *launchpadImporter) ImportAll(repo *cache.RepoCache) error { @@ -53,10 +67,15 @@ func (li *launchpadImporter) ImportAll(repo *cache.RepoCache) error { return err } + owner, err := li.makePerson(repo, lpBug.Owner) + if err != nil { + return err + } + if err == bug.ErrBugNotExist { createdAt, _ := time.Parse(time.RFC3339, lpBug.CreatedAt) b, err = repo.NewBugRaw( - li.makePerson(lpBug.Owner), + owner, createdAt.Unix(), lpBug.Title, lpBug.Description, @@ -94,10 +113,15 @@ func (li *launchpadImporter) ImportAll(repo *cache.RepoCache) error { continue } + owner, err := li.makePerson(repo, lpMessage.Owner) + if err != nil { + return err + } + // This is a new comment, we can add it. createdAt, _ := time.Parse(time.RFC3339, lpMessage.CreatedAt) err = b.AddCommentRaw( - li.makePerson(lpMessage.Owner), + owner, createdAt.Unix(), lpMessage.Content, nil, diff --git a/bug/op_create_test.go b/bug/op_create_test.go index 227dea27..aff58acc 100644 --- a/bug/op_create_test.go +++ b/bug/op_create_test.go @@ -11,7 +11,7 @@ import ( func TestCreate(t *testing.T) { snapshot := Snapshot{} - var rene = identity.NewBare("René Descartes", "rene@descartes.fr") + var rene = identity.NewIdentity("René Descartes", "rene@descartes.fr") unix := time.Now().Unix() diff --git a/bug/op_edit_comment_test.go b/bug/op_edit_comment_test.go index ba9bc9d5..7eee2fc1 100644 --- a/bug/op_edit_comment_test.go +++ b/bug/op_edit_comment_test.go @@ -11,7 +11,7 @@ import ( func TestEdit(t *testing.T) { snapshot := Snapshot{} - var rene = identity.NewBare("René Descartes", "rene@descartes.fr") + var rene = identity.NewIdentity("René Descartes", "rene@descartes.fr") unix := time.Now().Unix() diff --git a/bug/op_set_metadata_test.go b/bug/op_set_metadata_test.go index c6f5c3c1..6e62c9a3 100644 --- a/bug/op_set_metadata_test.go +++ b/bug/op_set_metadata_test.go @@ -11,7 +11,7 @@ import ( func TestSetMetadata(t *testing.T) { snapshot := Snapshot{} - var rene = identity.NewBare("René Descartes", "rene@descartes.fr") + var rene = identity.NewIdentity("René Descartes", "rene@descartes.fr") unix := time.Now().Unix() diff --git a/bug/operation_iterator_test.go b/bug/operation_iterator_test.go index 6b32cfc4..b8e1bf09 100644 --- a/bug/operation_iterator_test.go +++ b/bug/operation_iterator_test.go @@ -8,7 +8,7 @@ import ( ) var ( - rene = identity.NewBare("René Descartes", "rene@descartes.fr") + rene = identity.NewIdentity("René Descartes", "rene@descartes.fr") unix = time.Now().Unix() createOp = NewCreateOp(rene, unix, "title", "message", nil) diff --git a/bug/operation_test.go b/bug/operation_test.go index 0e2afc6c..083ccb1e 100644 --- a/bug/operation_test.go +++ b/bug/operation_test.go @@ -26,11 +26,11 @@ func TestValidate(t *testing.T) { bad := []Operation{ // opbase - NewSetStatusOp(identity.NewBare("", "rene@descartes.fr"), unix, ClosedStatus), - NewSetStatusOp(identity.NewBare("René Descartes\u001b", "rene@descartes.fr"), unix, ClosedStatus), - NewSetStatusOp(identity.NewBare("René Descartes", "rene@descartes.fr\u001b"), unix, ClosedStatus), - NewSetStatusOp(identity.NewBare("René \nDescartes", "rene@descartes.fr"), unix, ClosedStatus), - NewSetStatusOp(identity.NewBare("René Descartes", "rene@\ndescartes.fr"), unix, ClosedStatus), + NewSetStatusOp(identity.NewIdentity("", "rene@descartes.fr"), unix, ClosedStatus), + NewSetStatusOp(identity.NewIdentity("René Descartes\u001b", "rene@descartes.fr"), unix, ClosedStatus), + NewSetStatusOp(identity.NewIdentity("René Descartes", "rene@descartes.fr\u001b"), unix, ClosedStatus), + NewSetStatusOp(identity.NewIdentity("René \nDescartes", "rene@descartes.fr"), unix, ClosedStatus), + NewSetStatusOp(identity.NewIdentity("René Descartes", "rene@\ndescartes.fr"), unix, ClosedStatus), &CreateOperation{OpBase: OpBase{ Author: rene, UnixTime: 0, diff --git a/cache/bug_cache.go b/cache/bug_cache.go index 25ff000c..53c5c7d9 100644 --- a/cache/bug_cache.go +++ b/cache/bug_cache.go @@ -11,6 +11,10 @@ import ( "github.com/MichaelMure/git-bug/util/git" ) +// 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. +// 2. Maintain an up to date Snapshot available. type BugCache struct { repoCache *RepoCache bug *bug.WithSnapshot diff --git a/cache/multi_repo_cache.go b/cache/multi_repo_cache.go index ec435ff2..da1c26bd 100644 --- a/cache/multi_repo_cache.go +++ b/cache/multi_repo_cache.go @@ -8,6 +8,7 @@ import ( const lockfile = "lock" +// MultiRepoCache is the root cache, holding multiple RepoCache. type MultiRepoCache struct { repos map[string]*RepoCache } diff --git a/cache/repo_cache.go b/cache/repo_cache.go index 7d3e7d1d..e1a3d8f8 100644 --- a/cache/repo_cache.go +++ b/cache/repo_cache.go @@ -23,6 +23,20 @@ import ( const cacheFile = "cache" const formatVersion = 1 +// RepoCache is a cache for a Repository. This cache has multiple functions: +// +// 1. After being loaded, a Bug is kept in memory in the cache, allowing for fast +// access later. +// 2. The cache maintain on memory and on disk a pre-digested excerpt for each bug, +// allowing for fast querying the whole set of bugs without having to load +// them individually. +// 3. The cache guarantee that a single instance of a Bug is loaded at once, avoiding +// loss of data that we could have with multiple copies in the same process. +// 4. The same way, the cache maintain in memory a single copy of the loaded identities. +// +// The cache also protect the on-disk data by locking the git repository for its +// own usage, by writing a lock file. Of course, normal git operations are not +// affected, only git-bug related one. type RepoCache struct { // the underlying repo repo repository.ClockedRepo @@ -406,9 +420,14 @@ func (c *RepoCache) NewBugRaw(author *identity.Identity, unixTime int64, title s return nil, err } + if _, has := c.bugs[b.Id()]; has { + return nil, fmt.Errorf("bug %s already exist in the cache", b.Id()) + } + cached := NewBugCache(c, b) c.bugs[b.Id()] = cached + // force the write of the excerpt err = c.bugUpdated(b.Id()) if err != nil { return nil, err @@ -546,52 +565,81 @@ func (c *RepoCache) ResolveIdentity(id string) (*identity.Identity, error) { 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]) -// } +// ResolveIdentityPrefix retrieve an Identity matching an id prefix. +// It fails if multiple identities match. +func (c *RepoCache) ResolveIdentityPrefix(prefix string) (*identity.Identity, error) { + // preallocate but empty + matching := make([]string, 0, 5) + + for id := range c.identities { + if strings.HasPrefix(id, prefix) { + matching = append(matching, id) + } + } + + if len(matching) > 1 { + return nil, identity.ErrMultipleMatch{Matching: matching} + } + + if len(matching) == 0 { + return nil, identity.ErrIdentityNotExist + } + + return c.ResolveIdentity(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 +func (c *RepoCache) ResolveIdentityImmutableMetadata(key string, value string) (*identity.Identity, error) { + // preallocate but empty + matching := make([]string, 0, 5) + + for id, i := range c.identities { + if i.ImmutableMetadata()[key] == value { + matching = append(matching, id) + } + } + + if len(matching) > 1 { + return nil, identity.ErrMultipleMatch{Matching: matching} + } + + if len(matching) == 0 { + return nil, identity.ErrIdentityNotExist + } + + return c.ResolveIdentity(matching[0]) +} + +// 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) { + 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) { + 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) { + i := identity.NewIdentityFull(name, email, login, avatarUrl) + + for key, value := range metadata { + i.SetMetadata(key, value) + } + + err := i.Commit(c.repo) + if err != nil { + return nil, err + } + + if _, has := c.identities[i.Id()]; has { + return nil, fmt.Errorf("identity %s already exist in the cache", i.Id()) + } + + c.identities[i.Id()] = i + + return i, nil } diff --git a/commands/ls.go b/commands/ls.go index 2f621bc5..f641b58a 100644 --- a/commands/ls.go +++ b/commands/ls.go @@ -4,8 +4,8 @@ import ( "fmt" "strings" - "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/interrupt" "github.com/spf13/cobra" @@ -52,7 +52,7 @@ func runLsBug(cmd *cobra.Command, args []string) error { snapshot := b.Snapshot() - var author bug.Person + var author identity.Interface if len(snapshot.Comments) > 0 { create := snapshot.Comments[0] diff --git a/commands/show.go b/commands/show.go index 56717b3b..123a46dc 100644 --- a/commands/show.go +++ b/commands/show.go @@ -93,7 +93,7 @@ func runShowBug(cmd *cobra.Command, args []string) error { indent, i, comment.Author.DisplayName(), - comment.Author.Email, + comment.Author.Email(), ) if comment.Message == "" { diff --git a/doc/man/git-bug-id.1 b/doc/man/git-bug-id.1 new file mode 100644 index 00000000..259c4c48 --- /dev/null +++ b/doc/man/git-bug-id.1 @@ -0,0 +1,29 @@ +.TH "GIT-BUG" "1" "Jan 2019" "Generated from git-bug's source code" "" +.nh +.ad l + + +.SH NAME +.PP +git\-bug\-id \- Display or change the user identity + + +.SH SYNOPSIS +.PP +\fBgit\-bug id [] [flags]\fP + + +.SH DESCRIPTION +.PP +Display or change the user identity + + +.SH OPTIONS +.PP +\fB\-h\fP, \fB\-\-help\fP[=false] + help for id + + +.SH SEE ALSO +.PP +\fBgit\-bug(1)\fP diff --git a/doc/md/git-bug_id.md b/doc/md/git-bug_id.md new file mode 100644 index 00000000..09f8f276 --- /dev/null +++ b/doc/md/git-bug_id.md @@ -0,0 +1,22 @@ +## git-bug id + +Display or change the user identity + +### Synopsis + +Display or change the user identity + +``` +git-bug id [] [flags] +``` + +### Options + +``` + -h, --help help for id +``` + +### SEE ALSO + +* [git-bug](git-bug.md) - A bug tracker embedded in Git + diff --git a/graphql/resolvers/identity.go b/graphql/resolvers/identity.go new file mode 100644 index 00000000..cc68197f --- /dev/null +++ b/graphql/resolvers/identity.go @@ -0,0 +1,36 @@ +package resolvers + +import ( + "context" + + "github.com/MichaelMure/git-bug/identity" +) + +type identityResolver struct{} + +func (identityResolver) Name(ctx context.Context, obj *identity.Interface) (*string, error) { + return nilIfEmpty((*obj).Name()) +} + +func (identityResolver) Email(ctx context.Context, obj *identity.Interface) (*string, error) { + return nilIfEmpty((*obj).Email()) +} + +func (identityResolver) Login(ctx context.Context, obj *identity.Interface) (*string, error) { + return nilIfEmpty((*obj).Login()) +} + +func (identityResolver) DisplayName(ctx context.Context, obj *identity.Interface) (string, error) { + return (*obj).DisplayName(), nil +} + +func (identityResolver) AvatarURL(ctx context.Context, obj *identity.Interface) (*string, error) { + return nilIfEmpty((*obj).AvatarUrl()) +} + +func nilIfEmpty(s string) (*string, error) { + if s == "" { + return nil, nil + } + return &s, nil +} diff --git a/graphql/resolvers/root.go b/graphql/resolvers/root.go index 9b3a730b..cfdfe346 100644 --- a/graphql/resolvers/root.go +++ b/graphql/resolvers/root.go @@ -32,6 +32,10 @@ func (RootResolver) Bug() graph.BugResolver { return &bugResolver{} } +func (r RootResolver) Identity() graph.IdentityResolver { + return &identityResolver{} +} + func (RootResolver) CommentHistoryStep() graph.CommentHistoryStepResolver { return &commentHistoryStepResolver{} } diff --git a/identity/identity.go b/identity/identity.go index 3d523d38..313e3fd7 100644 --- a/identity/identity.go +++ b/identity/identity.go @@ -18,6 +18,16 @@ const identityConfigKey = "git-bug.identity" 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")) +} + +var _ Interface = &Identity{} + type Identity struct { id string Versions []*Version diff --git a/misc/bash_completion/git-bug b/misc/bash_completion/git-bug index 8551223d..98d94a35 100644 --- a/misc/bash_completion/git-bug +++ b/misc/bash_completion/git-bug @@ -450,6 +450,26 @@ _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" @@ -863,6 +883,7 @@ _git-bug_root_command() commands+=("commands") commands+=("comment") commands+=("deselect") + commands+=("id") commands+=("label") commands+=("ls") commands+=("ls-id") diff --git a/misc/random_bugs/create_random_bugs.go b/misc/random_bugs/create_random_bugs.go index f30a9d8a..085e89f0 100644 --- a/misc/random_bugs/create_random_bugs.go +++ b/misc/random_bugs/create_random_bugs.go @@ -138,7 +138,7 @@ func GenerateRandomOperationPacksWithSeed(packNumber int, opNumber int, seed int } func person() identity.Interface { - return identity.NewBare(fake.FullName(), fake.EmailAddress()) + return identity.NewIdentity(fake.FullName(), fake.EmailAddress()) } var persons []identity.Interface diff --git a/misc/zsh_completion/git-bug b/misc/zsh_completion/git-bug index a416ccef..d966b9be 100644 --- a/misc/zsh_completion/git-bug +++ b/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 label ls ls-label pull push select show status termui title version webui)' + _arguments '1: :(add bridge commands comment deselect id label ls ls-label pull push select show status termui title version webui)' ;; *) _arguments '*: :_files' -- cgit