aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bridge/github/import.go138
-rw-r--r--bridge/launchpad/import.go40
-rw-r--r--bug/op_create_test.go2
-rw-r--r--bug/op_edit_comment_test.go2
-rw-r--r--bug/op_set_metadata_test.go2
-rw-r--r--bug/operation_iterator_test.go2
-rw-r--r--bug/operation_test.go10
-rw-r--r--cache/bug_cache.go4
-rw-r--r--cache/multi_repo_cache.go1
-rw-r--r--cache/repo_cache.go138
-rw-r--r--commands/ls.go4
-rw-r--r--commands/show.go2
-rw-r--r--doc/man/git-bug-id.129
-rw-r--r--doc/md/git-bug_id.md22
-rw-r--r--graphql/resolvers/identity.go36
-rw-r--r--graphql/resolvers/root.go4
-rw-r--r--identity/identity.go10
-rw-r--r--misc/bash_completion/git-bug21
-rw-r--r--misc/random_bugs/create_random_bugs.go2
-rw-r--r--misc/zsh_completion/git-bug2
20 files changed, 364 insertions, 107 deletions
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 [<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 [<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'