Merge pull request #89 from MichaelMure/identity

Michael MurΓ© created

WIP identity in git

Change summary

Gopkg.lock                                                         |  38 
Makefile                                                           |  11 
bridge/core/bridge.go                                              |  18 
bridge/github/import.go                                            | 204 
bridge/launchpad/import.go                                         |  44 
bridge/launchpad/launchpad.go                                      |   2 
bug/bug.go                                                         |  48 
bug/bug_actions.go                                                 |  62 
bug/bug_actions_test.go                                            | 343 
bug/bug_test.go                                                    |  57 
bug/comment.go                                                     |   6 
bug/identity.go                                                    |  27 
bug/label.go                                                       |   2 
bug/op_add_comment.go                                              |  63 
bug/op_add_comment_test.go                                         |  25 
bug/op_create.go                                                   |  68 
bug/op_create_test.go                                              |  37 
bug/op_edit_comment.go                                             |  69 
bug/op_edit_comment_test.go                                        |  34 
bug/op_label_change.go                                             |  66 
bug/op_label_change_test.go                                        |  25 
bug/op_noop.go                                                     |  53 
bug/op_noop_test.go                                                |  25 
bug/op_set_metadata.go                                             |  68 
bug/op_set_metadata_test.go                                        |  35 
bug/op_set_status.go                                               |  63 
bug/op_set_status_test.go                                          |  25 
bug/op_set_title.go                                                |  66 
bug/op_set_title_test.go                                           |  25 
bug/operation.go                                                   |  64 
bug/operation_iterator_test.go                                     |  40 
bug/operation_pack.go                                              |  47 
bug/operation_pack_test.go                                         |  44 
bug/operation_test.go                                              |  25 
bug/person.go                                                      |  95 
bug/snapshot.go                                                    |   3 
bug/timeline.go                                                    |  12 
cache/bug_cache.go                                                 | 109 
cache/bug_excerpt.go                                               |  41 
cache/filter.go                                                    |  49 
cache/identity_cache.go                                            |  43 
cache/identity_excerpt.go                                          |  70 
cache/multi_repo_cache.go                                          |   1 
cache/repo_cache.go                                                | 402 
commands/add.go                                                    |   2 
commands/bridge.go                                                 |   2 
commands/bridge_configure.go                                       |   2 
commands/bridge_pull.go                                            |   2 
commands/bridge_rm.go                                              |   2 
commands/commands.go                                               |   2 
commands/comment.go                                                |   2 
commands/comment_add.go                                            |   4 
commands/deselect.go                                               |   2 
commands/label.go                                                  |   2 
commands/label_add.go                                              |   4 
commands/label_rm.go                                               |   4 
commands/ls-labels.go                                              |   2 
commands/ls.go                                                     |   6 
commands/pull.go                                                   |   2 
commands/push.go                                                   |   2 
commands/root.go                                                   |  25 
commands/select.go                                                 |   2 
commands/select/select_test.go                                     | 129 
commands/show.go                                                   |   6 
commands/status.go                                                 |   2 
commands/status_close.go                                           |   4 
commands/status_open.go                                            |   4 
commands/termui.go                                                 |   4 
commands/title.go                                                  |   2 
commands/title_edit.go                                             |   4 
commands/user.go                                                   |  61 
commands/user_adopt.go                                             |  48 
commands/user_create.go                                            |  81 
commands/user_ls.go                                                |  45 
commands/version.go                                                |   2 
commands/webui.go                                                  |   2 
doc/gen_manpage.go                                                 |  15 
doc/gen_markdown.go                                                |  20 
doc/man/git-bug-add.1                                              |   6 
doc/man/git-bug-bridge-bridge.1                                    |  29 
doc/man/git-bug-bridge-configure.1                                 |   6 
doc/man/git-bug-bridge-pull.1                                      |   6 
doc/man/git-bug-bridge-rm.1                                        |   6 
doc/man/git-bug-bridge.1                                           |   6 
doc/man/git-bug-commands.1                                         |   6 
doc/man/git-bug-comment-add.1                                      |   6 
doc/man/git-bug-comment.1                                          |   6 
doc/man/git-bug-deselect.1                                         |   6 
doc/man/git-bug-label-add.1                                        |   6 
doc/man/git-bug-label-rm.1                                         |   6 
doc/man/git-bug-label.1                                            |   6 
doc/man/git-bug-ls-id.1                                            |  29 
doc/man/git-bug-ls-label.1                                         |   4 
doc/man/git-bug-ls.1                                               |   4 
doc/man/git-bug-pull.1                                             |   6 
doc/man/git-bug-push.1                                             |   6 
doc/man/git-bug-select.1                                           |   6 
doc/man/git-bug-show.1                                             |   6 
doc/man/git-bug-status-close.1                                     |   6 
doc/man/git-bug-status-open.1                                      |   6 
doc/man/git-bug-status.1                                           |   6 
doc/man/git-bug-termui.1                                           |   6 
doc/man/git-bug-title-edit.1                                       |   6 
doc/man/git-bug-title.1                                            |   6 
doc/man/git-bug-user-adopt.1                                       |  29 
doc/man/git-bug-user-create.1                                      |  29 
doc/man/git-bug-user-ls.1                                          |  29 
doc/man/git-bug-user.1                                             |  29 
doc/man/git-bug-version.1                                          |   6 
doc/man/git-bug-webui.1                                            |   6 
doc/man/git-bug.1                                                  |   6 
doc/md/git-bug.md                                                  |  37 
doc/md/git-bug_add.md                                              |   6 
doc/md/git-bug_bridge.md                                           |  12 
doc/md/git-bug_bridge_bridge.md                                    |  22 
doc/md/git-bug_bridge_configure.md                                 |   6 
doc/md/git-bug_bridge_pull.md                                      |   6 
doc/md/git-bug_bridge_rm.md                                        |   6 
doc/md/git-bug_close.md                                            |  22 
doc/md/git-bug_commands.md                                         |   6 
doc/md/git-bug_comment.md                                          |   8 
doc/md/git-bug_comment_add.md                                      |   6 
doc/md/git-bug_deselect.md                                         |   6 
doc/md/git-bug_label.md                                            |  10 
doc/md/git-bug_label_add.md                                        |   6 
doc/md/git-bug_label_rm.md                                         |   6 
doc/md/git-bug_ls-id.md                                            |  22 
doc/md/git-bug_ls-label.md                                         |   4 
doc/md/git-bug_ls.md                                               |   4 
doc/md/git-bug_new.md                                              |  25 
doc/md/git-bug_open.md                                             |  22 
doc/md/git-bug_pull.md                                             |   6 
doc/md/git-bug_push.md                                             |   6 
doc/md/git-bug_select.md                                           |   6 
doc/md/git-bug_show.md                                             |   6 
doc/md/git-bug_status.md                                           |  10 
doc/md/git-bug_status_close.md                                     |   6 
doc/md/git-bug_status_open.md                                      |   6 
doc/md/git-bug_termui.md                                           |   6 
doc/md/git-bug_title.md                                            |   8 
doc/md/git-bug_title_edit.md                                       |   6 
doc/md/git-bug_user.md                                             |  25 
doc/md/git-bug_user_adopt.md                                       |  22 
doc/md/git-bug_user_create.md                                      |  22 
doc/md/git-bug_user_ls.md                                          |  22 
doc/md/git-bug_version.md                                          |   6 
doc/md/git-bug_webui.md                                            |   6 
graphql/gqlgen.yml                                                 |  15 
graphql/graph/gen_graph.go                                         | 706 
graphql/graphql_test.go                                            |  22 
graphql/handler.go                                                 |   3 
graphql/resolvers/identity.go                                      |  44 
graphql/resolvers/mutation.go                                      |  10 
graphql/resolvers/person.go                                        |  37 
graphql/resolvers/root.go                                          |   4 
graphql/schema/bug.graphql                                         |  19 
graphql/schema/identity.graphql                                    |  18 
graphql/schema/operations.graphql                                  |  16 
graphql/schema/root.graphql                                        |   4 
graphql/schema/timeline.graphql                                    |  12 
identity/bare.go                                                   | 204 
identity/bare_test.go                                              |  32 
identity/common.go                                                 |  53 
identity/identity.go                                               | 584 
identity/identity_actions.go                                       | 187 
identity/identity_actions_test.go                                  | 151 
identity/identity_stub.go                                          | 104 
identity/identity_stub_test.go                                     |  23 
identity/identity_test.go                                          | 244 
identity/interface.go                                              |  58 
identity/key.go                                                    |  13 
identity/resolver.go                                               |  22 
identity/version.go                                                | 208 
identity/version_test.go                                           |  42 
input/prompt.go                                                    |  44 
misc/bash_completion/git-bug                                       |  84 
misc/gen_bash_completion.go                                        |   7 
misc/gen_zsh_completion.go                                         |   3 
misc/random_bugs/create_random_bugs.go                             |  60 
misc/random_bugs/main.go                                           |   2 
misc/zsh_completion/git-bug                                        |   5 
repository/git.go                                                  |  16 
repository/mock_repo.go                                            |  10 
repository/repo.go                                                 |   9 
repository/tree_entry_test.go                                      |   3 
termui/bug_table.go                                                |   9 
termui/label_select.go                                             |   2 
termui/show_bug.go                                                 |   6 
termui/termui.go                                                   |   6 
tests/read_bugs_test.go                                            |  42 
util/git/hash.go                                                   |   2 
util/test/repo.go                                                  |  79 
util/text/text.go                                                  |   7 
util/timestamp/timestamp.go                                        |   2 
vendor/github.com/go-test/deep/.gitignore                          |   2 
vendor/github.com/go-test/deep/.travis.yml                         |  13 
vendor/github.com/go-test/deep/CHANGES.md                          |   9 
vendor/github.com/go-test/deep/LICENSE                             |  21 
vendor/github.com/go-test/deep/README.md                           |  51 
vendor/github.com/go-test/deep/deep.go                             | 352 
vendor/github.com/google/go-cmp/LICENSE                            |  27 
vendor/github.com/google/go-cmp/cmp/compare.go                     | 553 
vendor/github.com/google/go-cmp/cmp/internal/diff/debug_disable.go |  17 
vendor/github.com/google/go-cmp/cmp/internal/diff/debug_enable.go  | 122 
vendor/github.com/google/go-cmp/cmp/internal/diff/diff.go          | 363 
vendor/github.com/google/go-cmp/cmp/internal/function/func.go      |  49 
vendor/github.com/google/go-cmp/cmp/internal/value/format.go       | 277 
vendor/github.com/google/go-cmp/cmp/internal/value/sort.go         | 111 
vendor/github.com/google/go-cmp/cmp/options.go                     | 453 
vendor/github.com/google/go-cmp/cmp/path.go                        | 309 
vendor/github.com/google/go-cmp/cmp/reporter.go                    |  53 
vendor/github.com/google/go-cmp/cmp/unsafe_panic.go                |  15 
vendor/github.com/google/go-cmp/cmp/unsafe_reflect.go              |  23 
vendor/gotest.tools/LICENSE                                        | 202 
vendor/gotest.tools/assert/assert.go                               | 311 
vendor/gotest.tools/assert/cmp/compare.go                          | 312 
vendor/gotest.tools/assert/cmp/result.go                           |  94 
vendor/gotest.tools/assert/result.go                               | 107 
vendor/gotest.tools/internal/difflib/LICENSE                       |  27 
vendor/gotest.tools/internal/difflib/difflib.go                    | 420 
vendor/gotest.tools/internal/format/diff.go                        | 161 
vendor/gotest.tools/internal/format/format.go                      |  27 
vendor/gotest.tools/internal/source/source.go                      | 163 
223 files changed, 5,489 insertions(+), 6,283 deletions(-)

Detailed changes

Gopkg.lock πŸ”—

@@ -83,14 +83,6 @@
   revision = "5b77d2a35fb0ede96d138fc9a99f5c9b6aef11b4"
   version = "v1.7.0"
 
-[[projects]]
-  digest = "1:7f89e0c888fb99c61055c646f5678aae645b0b0a1443d9b2dcd9964d850827ce"
-  name = "github.com/go-test/deep"
-  packages = ["."]
-  pruneopts = "UT"
-  revision = "6592d9cc0a499ad2d5f574fde80a2b5c5cc3b4f5"
-  version = "v1.0.1"
-
 [[projects]]
   digest = "1:97df918963298c287643883209a2c3f642e6593379f97ab400c2a2e219ab647d"
   name = "github.com/golang/protobuf"
@@ -99,19 +91,6 @@
   revision = "aa810b61a9c79d51363740d207bb46cf8e620ed5"
   version = "v1.2.0"
 
-[[projects]]
-  digest = "1:2e3c336fc7fde5c984d2841455a658a6d626450b1754a854b3b32e7a8f49a07a"
-  name = "github.com/google/go-cmp"
-  packages = [
-    "cmp",
-    "cmp/internal/diff",
-    "cmp/internal/function",
-    "cmp/internal/value",
-  ]
-  pruneopts = "UT"
-  revision = "3af367b6b30c263d47e8895973edcca9a49cf029"
-  version = "v0.2.0"
-
 [[projects]]
   digest = "1:c79fb010be38a59d657c48c6ba1d003a8aa651fa56b579d959d74573b7dff8e1"
   name = "github.com/gorilla/context"
@@ -440,20 +419,6 @@
   revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
   version = "v2.2.1"
 
-[[projects]]
-  digest = "1:225f565dc88a02cebe329d3a49d0ca125789091af952a5cc4fde6312c34ce44d"
-  name = "gotest.tools"
-  packages = [
-    "assert",
-    "assert/cmp",
-    "internal/difflib",
-    "internal/format",
-    "internal/source",
-  ]
-  pruneopts = "UT"
-  revision = "b6e20af1ed078cd01a6413b734051a292450b4cb"
-  version = "v2.1.0"
-
 [solve-meta]
   analyzer-name = "dep"
   analyzer-version = 1
@@ -466,9 +431,9 @@
     "github.com/cheekybits/genny/generic",
     "github.com/dustin/go-humanize",
     "github.com/fatih/color",
-    "github.com/go-test/deep",
     "github.com/gorilla/mux",
     "github.com/icrowley/fake",
+    "github.com/mattn/go-runewidth",
     "github.com/phayes/freeport",
     "github.com/pkg/errors",
     "github.com/shurcooL/githubv4",
@@ -485,7 +450,6 @@
     "github.com/vektah/gqlparser/ast",
     "golang.org/x/crypto/ssh/terminal",
     "golang.org/x/oauth2",
-    "gotest.tools/assert",
   ]
   solver-name = "gps-cdcl"
   solver-version = 1

Makefile πŸ”—

@@ -19,7 +19,7 @@ debug-build:
 
 install:
 	go generate
-	go install .
+	go install -ldflags "$(LDFLAGS)" .
 
 test:
 	go test -bench=. ./...
@@ -30,14 +30,19 @@ pack-webui:
 
 # produce a build that will fetch the web UI from the filesystem instead of from the binary
 debug-webui:
-	go build -tags=debugwebui
+	go build -ldflags "$(LDFLAGS)" -tags=debugwebui
 
 clean-local-bugs:
 	git for-each-ref refs/bugs/ | cut -f 2 | xargs -r -n 1 git update-ref -d
 	git for-each-ref refs/remotes/origin/bugs/ | cut -f 2 | xargs -r -n 1 git update-ref -d
-	rm -f .git/git-bug/cache
+	rm -f .git/git-bug/bug-cache
 
 clean-remote-bugs:
 	git ls-remote origin "refs/bugs/*" | cut -f 2 | xargs -r git push origin -d
 
+clean-local-identities:
+	git for-each-ref refs/identities/ | cut -f 2 | xargs -r -n 1 git update-ref -d
+	git for-each-ref refs/remotes/origin/identities/ | cut -f 2 | xargs -r -n 1 git update-ref -d
+	rm -f .git/git-bug/identity-cache
+
 .PHONY: build install test pack-webui debug-webui clean-local-bugs clean-remote-bugs

bridge/core/bridge.go πŸ”—

@@ -12,8 +12,10 @@ import (
 	"github.com/pkg/errors"
 )
 
-var ErrImportNorSupported = errors.New("import is not supported")
-var ErrExportNorSupported = errors.New("export is not supported")
+var ErrImportNotSupported = errors.New("import is not supported")
+var ErrExportNotSupported = errors.New("export is not supported")
+
+const bridgeConfigKeyPrefix = "git-bug.bridge"
 
 var bridgeImpl map[string]reflect.Type
 
@@ -114,12 +116,12 @@ func splitFullName(fullName string) (string, string, error) {
 // ConfiguredBridges return the list of bridge that are configured for the given
 // repo
 func ConfiguredBridges(repo repository.RepoCommon) ([]string, error) {
-	configs, err := repo.ReadConfigs("git-bug.bridge.")
+	configs, err := repo.ReadConfigs(bridgeConfigKeyPrefix + ".")
 	if err != nil {
 		return nil, errors.Wrap(err, "can't read configured bridges")
 	}
 
-	re, err := regexp.Compile(`git-bug.bridge.([^.]+\.[^.]+)`)
+	re, err := regexp.Compile(bridgeConfigKeyPrefix + `.([^.]+\.[^.]+)`)
 	if err != nil {
 		panic(err)
 	}
@@ -266,7 +268,7 @@ func (b *Bridge) ensureInit() error {
 func (b *Bridge) ImportAll() error {
 	importer := b.getImporter()
 	if importer == nil {
-		return ErrImportNorSupported
+		return ErrImportNotSupported
 	}
 
 	err := b.ensureConfig()
@@ -285,7 +287,7 @@ func (b *Bridge) ImportAll() error {
 func (b *Bridge) Import(id string) error {
 	importer := b.getImporter()
 	if importer == nil {
-		return ErrImportNorSupported
+		return ErrImportNotSupported
 	}
 
 	err := b.ensureConfig()
@@ -304,7 +306,7 @@ func (b *Bridge) Import(id string) error {
 func (b *Bridge) ExportAll() error {
 	exporter := b.getExporter()
 	if exporter == nil {
-		return ErrExportNorSupported
+		return ErrExportNotSupported
 	}
 
 	err := b.ensureConfig()
@@ -323,7 +325,7 @@ func (b *Bridge) ExportAll() error {
 func (b *Bridge) Export(id string) error {
 	exporter := b.getExporter()
 	if exporter == nil {
-		return ErrExportNorSupported
+		return ErrExportNotSupported
 	}
 
 	err := b.ensureConfig()

bridge/github/import.go πŸ”—

@@ -8,25 +8,26 @@ import (
 	"github.com/MichaelMure/git-bug/bridge/core"
 	"github.com/MichaelMure/git-bug/bug"
 	"github.com/MichaelMure/git-bug/cache"
+	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/util/git"
 	"github.com/shurcooL/githubv4"
 )
 
 const keyGithubId = "github-id"
 const keyGithubUrl = "github-url"
+const keyGithubLogin = "github-login"
 
 // githubImporter implement the Importer interface
 type githubImporter struct {
 	client *githubv4.Client
 	conf   core.Configuration
-	ghost  bug.Person
 }
 
 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 {
@@ -69,7 +70,10 @@ func (gi *githubImporter) ImportAll(repo *cache.RepoCache) error {
 		}
 
 		for _, itemEdge := range q.Repository.Issues.Nodes[0].Timeline.Edges {
-			gi.ensureTimelineItem(b, itemEdge.Cursor, itemEdge.Node, variables)
+			err = gi.ensureTimelineItem(repo, b, itemEdge.Cursor, itemEdge.Node, variables)
+			if err != nil {
+				return err
+			}
 		}
 
 		if !issue.Timeline.PageInfo.HasNextPage {
@@ -104,6 +108,11 @@ func (gi *githubImporter) Import(repo *cache.RepoCache, id string) error {
 func (gi *githubImporter) ensureIssue(repo *cache.RepoCache, issue issueTimeline, rootVariables map[string]interface{}) (*cache.BugCache, error) {
 	fmt.Printf("import issue: %s\n", issue.Title)
 
+	author, err := gi.ensurePerson(repo, issue.Author)
+	if err != nil {
+		return nil, err
+	}
+
 	b, err := repo.ResolveBugCreateMetadata(keyGithubId, parseId(issue.Id))
 	if err != nil && err != bug.ErrBugNotExist {
 		return nil, err
@@ -123,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
@@ -135,7 +144,6 @@ func (gi *githubImporter) ensureIssue(repo *cache.RepoCache, issue issueTimeline
 					keyGithubUrl: issue.Url.String(),
 				},
 			)
-
 			if err != nil {
 				return nil, err
 			}
@@ -161,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
@@ -179,12 +187,12 @@ func (gi *githubImporter) ensureIssue(repo *cache.RepoCache, issue issueTimeline
 			continue
 		}
 
-		target, err := b.ResolveTargetWithMetadata(keyGithubId, parseId(issue.Id))
+		target, err := b.ResolveOperationWithMetadata(keyGithubId, parseId(issue.Id))
 		if err != nil {
 			return nil, err
 		}
 
-		err = gi.ensureCommentEdit(b, target, edit)
+		err = gi.ensureCommentEdit(repo, b, target, edit)
 		if err != nil {
 			return nil, err
 		}
@@ -194,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
@@ -243,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
@@ -261,12 +269,12 @@ func (gi *githubImporter) ensureIssue(repo *cache.RepoCache, issue issueTimeline
 				continue
 			}
 
-			target, err := b.ResolveTargetWithMetadata(keyGithubId, parseId(issue.Id))
+			target, err := b.ResolveOperationWithMetadata(keyGithubId, parseId(issue.Id))
 			if err != nil {
 				return nil, err
 			}
 
-			err = gi.ensureCommentEdit(b, target, edit)
+			err = gi.ensureCommentEdit(repo, b, target, edit)
 			if err != nil {
 				return nil, err
 			}
@@ -284,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
@@ -301,21 +309,25 @@ 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)
-		_, err := b.ResolveTargetWithMetadata(keyGithubId, id)
+		_, err := b.ResolveOperationWithMetadata(keyGithubId, id)
 		if err != cache.ErrNoMatchingOp {
 			return err
 		}
-		_, err = b.ChangeLabelsRaw(
-			gi.makePerson(item.LabeledEvent.Actor),
+		author, err := gi.ensurePerson(repo, item.LabeledEvent.Actor)
+		if err != nil {
+			return err
+		}
+		_, _, err = b.ChangeLabelsRaw(
+			author,
 			item.LabeledEvent.CreatedAt.Unix(),
 			[]string{
 				string(item.LabeledEvent.Label.Name),
@@ -327,12 +339,16 @@ func (gi *githubImporter) ensureTimelineItem(b *cache.BugCache, cursor githubv4.
 
 	case "UnlabeledEvent":
 		id := parseId(item.UnlabeledEvent.Id)
-		_, err := b.ResolveTargetWithMetadata(keyGithubId, id)
+		_, err := b.ResolveOperationWithMetadata(keyGithubId, id)
 		if err != cache.ErrNoMatchingOp {
 			return err
 		}
-		_, err = b.ChangeLabelsRaw(
-			gi.makePerson(item.UnlabeledEvent.Actor),
+		author, err := gi.ensurePerson(repo, item.UnlabeledEvent.Actor)
+		if err != nil {
+			return err
+		}
+		_, _, err = b.ChangeLabelsRaw(
+			author,
 			item.UnlabeledEvent.CreatedAt.Unix(),
 			nil,
 			[]string{
@@ -344,40 +360,55 @@ func (gi *githubImporter) ensureTimelineItem(b *cache.BugCache, cursor githubv4.
 
 	case "ClosedEvent":
 		id := parseId(item.ClosedEvent.Id)
-		_, err := b.ResolveTargetWithMetadata(keyGithubId, id)
+		_, err := b.ResolveOperationWithMetadata(keyGithubId, id)
 		if err != cache.ErrNoMatchingOp {
 			return err
 		}
-		return b.CloseRaw(
-			gi.makePerson(item.ClosedEvent.Actor),
+		author, err := gi.ensurePerson(repo, item.ClosedEvent.Actor)
+		if err != nil {
+			return err
+		}
+		_, err = b.CloseRaw(
+			author,
 			item.ClosedEvent.CreatedAt.Unix(),
 			map[string]string{keyGithubId: id},
 		)
+		return err
 
 	case "ReopenedEvent":
 		id := parseId(item.ReopenedEvent.Id)
-		_, err := b.ResolveTargetWithMetadata(keyGithubId, id)
+		_, err := b.ResolveOperationWithMetadata(keyGithubId, id)
 		if err != cache.ErrNoMatchingOp {
 			return err
 		}
-		return b.OpenRaw(
-			gi.makePerson(item.ReopenedEvent.Actor),
+		author, err := gi.ensurePerson(repo, item.ReopenedEvent.Actor)
+		if err != nil {
+			return err
+		}
+		_, err = b.OpenRaw(
+			author,
 			item.ReopenedEvent.CreatedAt.Unix(),
 			map[string]string{keyGithubId: id},
 		)
+		return err
 
 	case "RenamedTitleEvent":
 		id := parseId(item.RenamedTitleEvent.Id)
-		_, err := b.ResolveTargetWithMetadata(keyGithubId, id)
+		_, err := b.ResolveOperationWithMetadata(keyGithubId, id)
 		if err != cache.ErrNoMatchingOp {
 			return err
 		}
-		return b.SetTitleRaw(
-			gi.makePerson(item.RenamedTitleEvent.Actor),
+		author, err := gi.ensurePerson(repo, item.RenamedTitleEvent.Actor)
+		if err != nil {
+			return err
+		}
+		_, err = b.SetTitleRaw(
+			author,
 			item.RenamedTitleEvent.CreatedAt.Unix(),
 			string(item.RenamedTitleEvent.CurrentTitle),
 			map[string]string{keyGithubId: id},
 		)
+		return err
 
 	default:
 		fmt.Println("ignore event ", item.Typename)
@@ -386,8 +417,14 @@ 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 {
-	target, err := b.ResolveTargetWithMetadata(keyGithubId, parseId(comment.Id))
+func (gi *githubImporter) ensureComment(repo *cache.RepoCache, b *cache.BugCache, cursor githubv4.String, comment issueComment, rootVariables map[string]interface{}) error {
+	author, err := gi.ensurePerson(repo, comment.Author)
+	if err != nil {
+		return err
+	}
+
+	var target git.Hash
+	target, err = b.ResolveOperationWithMetadata(keyGithubId, parseId(comment.Id))
 	if err != nil && err != cache.ErrNoMatchingOp {
 		// real error
 		return err
@@ -406,8 +443,8 @@ 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),
+			op, err := b.AddCommentRaw(
+				author,
 				comment.CreatedAt.Unix(),
 				cleanupText(string(comment.Body)),
 				nil,
@@ -415,7 +452,11 @@ func (gi *githubImporter) ensureComment(b *cache.BugCache, cursor githubv4.Strin
 					keyGithubId: parseId(comment.Id),
 				},
 			)
+			if err != nil {
+				return err
+			}
 
+			target, err = op.Hash()
 			if err != nil {
 				return err
 			}
@@ -439,8 +480,8 @@ func (gi *githubImporter) ensureComment(b *cache.BugCache, cursor githubv4.Strin
 				continue
 			}
 
-			err = b.AddCommentRaw(
-				gi.makePerson(comment.Author),
+			op, err := b.AddCommentRaw(
+				author,
 				comment.CreatedAt.Unix(),
 				cleanupText(string(*edit.Diff)),
 				nil,
@@ -452,9 +493,14 @@ func (gi *githubImporter) ensureComment(b *cache.BugCache, cursor githubv4.Strin
 			if err != nil {
 				return err
 			}
+
+			target, err = op.Hash()
+			if err != nil {
+				return err
+			}
 		}
 
-		err := gi.ensureCommentEdit(b, target, edit)
+		err := gi.ensureCommentEdit(repo, b, target, edit)
 		if err != nil {
 			return err
 		}
@@ -496,7 +542,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
 			}
@@ -514,18 +560,14 @@ 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.
 		return nil
 	}
 
-	if edit.Editor == nil {
-		return fmt.Errorf("no editor")
-	}
-
-	_, err := b.ResolveTargetWithMetadata(keyGithubId, parseId(edit.Id))
+	_, err := b.ResolveOperationWithMetadata(keyGithubId, parseId(edit.Id))
 	if err == nil {
 		// already imported
 		return nil
@@ -537,14 +579,19 @@ func (gi *githubImporter) ensureCommentEdit(b *cache.BugCache, target git.Hash,
 
 	fmt.Println("import edition")
 
+	editor, err := gi.ensurePerson(repo, edit.Editor)
+	if err != nil {
+		return err
+	}
+
 	switch {
 	case edit.DeletedAt != nil:
 		// comment deletion, not supported yet
 
 	case edit.DeletedAt == nil:
 		// comment edition
-		err := b.EditCommentRaw(
-			gi.makePerson(edit.Editor),
+		_, err := b.EditCommentRaw(
+			editor,
 			edit.CreatedAt.Unix(),
 			target,
 			cleanupText(string(*edit.Diff)),
@@ -560,11 +607,23 @@ func (gi *githubImporter) ensureCommentEdit(b *cache.BugCache, target git.Hash,
 	return nil
 }
 
-// makePerson create a bug.Person from the Github data
-func (gi *githubImporter) makePerson(actor *actor) bug.Person {
+// ensurePerson create a bug.Person from the Github data
+func (gi *githubImporter) ensurePerson(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 {
-		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
 
@@ -584,24 +643,36 @@ func (gi *githubImporter) makePerson(actor *actor) bug.Person {
 	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) (*cache.IdentityCache, 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
@@ -609,14 +680,15 @@ func (gi *githubImporter) fetchGhost() error {
 		name = string(*q.User.Name)
 	}
 
-	gi.ghost = bug.Person{
-		Name:      name,
-		Login:     string(q.User.Login),
-		AvatarUrl: string(q.User.AvatarUrl),
-		Email:     string(q.User.Email),
-	}
-
-	return nil
+	return repo.NewIdentityRaw(
+		name,
+		string(q.User.Email),
+		string(q.User.Login),
+		string(q.User.AvatarUrl),
+		map[string]string{
+			keyGithubLogin: string(q.User.Login),
+		},
+	)
 }
 
 // parseId convert the unusable githubv4.ID (an interface{}) into a string

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) ensurePerson(repo *cache.RepoCache, owner LPPerson) (*cache.IdentityCache, 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.ensurePerson(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,
@@ -81,7 +100,7 @@ func (li *launchpadImporter) ImportAll(repo *cache.RepoCache) error {
 		// The Launchpad API returns the bug description as the first
 		// comment, so skip it.
 		for _, lpMessage := range lpBug.Messages[1:] {
-			_, err := b.ResolveTargetWithMetadata(keyLaunchpadID, lpMessage.ID)
+			_, err := b.ResolveOperationWithMetadata(keyLaunchpadID, lpMessage.ID)
 			if err != nil && err != cache.ErrNoMatchingOp {
 				return errors.Wrapf(err, "failed to fetch comments for bug #%s", lpBugID)
 			}
@@ -94,10 +113,15 @@ func (li *launchpadImporter) ImportAll(repo *cache.RepoCache) error {
 				continue
 			}
 
+			owner, err := li.ensurePerson(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),
+			_, err = b.AddCommentRaw(
+				owner,
 				createdAt.Unix(),
 				lpMessage.Content,
 				nil,

bridge/launchpad/launchpad.go πŸ”—

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

bug/bug.go πŸ”—

@@ -6,10 +6,12 @@ import (
 	"fmt"
 	"strings"
 
+	"github.com/pkg/errors"
+
+	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/repository"
 	"github.com/MichaelMure/git-bug/util/git"
 	"github.com/MichaelMure/git-bug/util/lamport"
-	"github.com/pkg/errors"
 )
 
 const bugsRefPattern = "refs/bugs/"
@@ -113,13 +115,6 @@ func ReadRemoteBug(repo repository.ClockedRepo, remote string, id string) (*Bug,
 
 // readBug will read and parse a Bug from git
 func readBug(repo repository.ClockedRepo, ref string) (*Bug, error) {
-	hashes, err := repo.ListCommits(ref)
-
-	// TODO: this is not perfect, it might be a command invoke error
-	if err != nil {
-		return nil, ErrBugNotExist
-	}
-
 	refSplit := strings.Split(ref, "/")
 	id := refSplit[len(refSplit)-1]
 
@@ -127,6 +122,13 @@ func readBug(repo repository.ClockedRepo, ref string) (*Bug, error) {
 		return nil, fmt.Errorf("invalid ref length")
 	}
 
+	hashes, err := repo.ListCommits(ref)
+
+	// TODO: this is not perfect, it might be a command invoke error
+	if err != nil {
+		return nil, ErrBugNotExist
+	}
+
 	bug := Bug{
 		id: id,
 	}
@@ -217,6 +219,13 @@ func readBug(repo repository.ClockedRepo, ref string) (*Bug, error) {
 		bug.packs = append(bug.packs, *opp)
 	}
 
+	// Make sure that the identities are properly loaded
+	resolver := identity.NewSimpleResolver(repo)
+	err = bug.EnsureIdentities(resolver)
+	if err != nil {
+		return nil, err
+	}
+
 	return &bug, nil
 }
 
@@ -312,6 +321,11 @@ func (bug *Bug) Validate() error {
 		return fmt.Errorf("first operation should be a Create op")
 	}
 
+	// The bug ID should be the hash of the first commit
+	if len(bug.packs) > 0 && string(bug.packs[0].commitHash) != bug.id {
+		return fmt.Errorf("bug id should be the first commit hash")
+	}
+
 	// Check that there is no more CreateOp op
 	it := NewOperationIterator(bug)
 	createCount := 0
@@ -340,7 +354,8 @@ func (bug *Bug) HasPendingOp() bool {
 
 // Commit write the staging area in Git and move the operations to the packs
 func (bug *Bug) Commit(repo repository.ClockedRepo) error {
-	if bug.staging.IsEmpty() {
+
+	if !bug.NeedCommit() {
 		return fmt.Errorf("can't commit a bug with no pending operation")
 	}
 
@@ -450,12 +465,24 @@ func (bug *Bug) Commit(repo repository.ClockedRepo) error {
 		return err
 	}
 
+	bug.staging.commitHash = hash
 	bug.packs = append(bug.packs, bug.staging)
 	bug.staging = OperationPack{}
 
 	return nil
 }
 
+func (bug *Bug) CommitAsNeeded(repo repository.ClockedRepo) error {
+	if !bug.NeedCommit() {
+		return nil
+	}
+	return bug.Commit(repo)
+}
+
+func (bug *Bug) NeedCommit() bool {
+	return !bug.staging.IsEmpty()
+}
+
 func makeMediaTree(pack OperationPack) []repository.TreeEntry {
 	var tree []repository.TreeEntry
 	counter := 0
@@ -504,9 +531,8 @@ func (bug *Bug) Merge(repo repository.Repo, other Interface) (bool, error) {
 	}
 
 	ancestor, err := repo.FindCommonAncestor(bug.lastCommit, otherBug.lastCommit)
-
 	if err != nil {
-		return false, err
+		return false, errors.Wrap(err, "can't find common ancestor")
 	}
 
 	ancestorIndex := 0

bug/bug_actions.go πŸ”—

@@ -4,29 +4,68 @@ import (
 	"fmt"
 	"strings"
 
+	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/repository"
 	"github.com/pkg/errors"
 )
 
-// Fetch retrieve update from a remote
+// Note:
+//
+// For the actions (fetch/push/pull/merge/commit), this package act as a master for
+// the identity package and will also drive the needed identity actions. That is,
+// if bug.Push() is called, identity.Push will also be called to make sure that
+// the dependant identities are also present and up to date on the remote.
+//
+// I'm not entirely sure this is the correct way to do it, as it might introduce
+// too much complexity and hard coupling, but it does make this package easier
+// to use.
+
+// Fetch retrieve updates from a remote
 // This does not change the local bugs state
 func Fetch(repo repository.Repo, remote string) (string, error) {
+	stdout, err := identity.Fetch(repo, remote)
+	if err != nil {
+		return stdout, err
+	}
+
 	remoteRefSpec := fmt.Sprintf(bugsRemoteRefPattern, remote)
 	fetchRefSpec := fmt.Sprintf("%s*:%s*", bugsRefPattern, remoteRefSpec)
 
-	return repo.FetchRefs(remote, fetchRefSpec)
+	stdout2, err := repo.FetchRefs(remote, fetchRefSpec)
+
+	return stdout + "\n" + stdout2, err
 }
 
 // Push update a remote with the local changes
 func Push(repo repository.Repo, remote string) (string, error) {
-	return repo.PushRefs(remote, bugsRefPattern+"*")
+	stdout, err := identity.Push(repo, remote)
+	if err != nil {
+		return stdout, err
+	}
+
+	stdout2, err := repo.PushRefs(remote, bugsRefPattern+"*")
+
+	return stdout + "\n" + stdout2, err
 }
 
 // Pull will do a Fetch + MergeAll
-// This function won't give details on the underlying process. If you need more
-// use Fetch and MergeAll separately.
+// This function will return an error if a merge fail
 func Pull(repo repository.ClockedRepo, remote string) error {
-	_, err := Fetch(repo, remote)
+	_, err := identity.Fetch(repo, remote)
+	if err != nil {
+		return err
+	}
+
+	for merge := range identity.MergeAll(repo, remote) {
+		if merge.Err != nil {
+			return merge.Err
+		}
+		if merge.Status == identity.MergeStatusInvalid {
+			return errors.Errorf("merge failure: %s", merge.Reason)
+		}
+	}
+
+	_, err = Fetch(repo, remote)
 	if err != nil {
 		return err
 	}
@@ -35,12 +74,21 @@ func Pull(repo repository.ClockedRepo, remote string) error {
 		if merge.Err != nil {
 			return merge.Err
 		}
+		if merge.Status == MergeStatusInvalid {
+			return errors.Errorf("merge failure: %s", merge.Reason)
+		}
 	}
 
 	return nil
 }
 
-// MergeAll will merge all the available remote bug
+// MergeAll will merge all the available remote bug:
+//
+// - If the remote has new commit, the local bug is updated to match the same history
+//   (fast-forward update)
+// - if the local bug has new commits but the remote don't, nothing is changed
+// - if both local and remote bug have new commits (that is, we have a concurrent edition),
+//   new local commits are rewritten at the head of the remote history (that is, a rebase)
 func MergeAll(repo repository.ClockedRepo, remote string) <-chan MergeResult {
 	out := make(chan MergeResult)
 

bug/bug_actions_test.go πŸ”—

@@ -1,93 +1,31 @@
 package bug
 
 import (
-	"github.com/MichaelMure/git-bug/repository"
-	"github.com/stretchr/testify/assert"
-
-	"io/ioutil"
-	"log"
-	"os"
 	"testing"
-)
-
-func createRepo(bare bool) *repository.GitRepo {
-	dir, err := ioutil.TempDir("", "")
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	// fmt.Println("Creating repo:", dir)
-
-	var creator func(string) (*repository.GitRepo, error)
-
-	if bare {
-		creator = repository.InitBareGitRepo
-	} else {
-		creator = repository.InitGitRepo
-	}
-
-	repo, err := creator(dir)
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	if err := repo.StoreConfig("user.name", "testuser"); err != nil {
-		log.Fatal("failed to set user.name for test repository: ", err)
-	}
-	if err := repo.StoreConfig("user.email", "testuser@example.com"); err != nil {
-		log.Fatal("failed to set user.email for test repository: ", err)
-	}
-
-	return repo
-}
-
-func cleanupRepo(repo repository.Repo) error {
-	path := repo.GetPath()
-	// fmt.Println("Cleaning repo:", path)
-	return os.RemoveAll(path)
-}
-
-func setupRepos(t testing.TB) (repoA, repoB, remote *repository.GitRepo) {
-	repoA = createRepo(false)
-	repoB = createRepo(false)
-	remote = createRepo(true)
+	"time"
 
-	remoteAddr := "file://" + remote.GetPath()
-
-	err := repoA.AddRemote("origin", remoteAddr)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	err = repoB.AddRemote("origin", remoteAddr)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	return repoA, repoB, remote
-}
-
-func cleanupRepos(repoA, repoB, remote *repository.GitRepo) {
-	cleanupRepo(repoA)
-	cleanupRepo(repoB)
-	cleanupRepo(remote)
-}
+	"github.com/MichaelMure/git-bug/identity"
+	"github.com/MichaelMure/git-bug/util/test"
+	"github.com/stretchr/testify/require"
+)
 
 func TestPushPull(t *testing.T) {
-	repoA, repoB, remote := setupRepos(t)
-	defer cleanupRepos(repoA, repoB, remote)
+	repoA, repoB, remote := test.SetupReposAndRemote(t)
+	defer test.CleanupRepos(repoA, repoB, remote)
 
-	bug1, _, err := Create(rene, unix, "bug1", "message")
-	assert.Nil(t, err)
+	reneA := identity.NewIdentity("RenΓ© Descartes", "rene@descartes.fr")
+
+	bug1, _, err := Create(reneA, time.Now().Unix(), "bug1", "message")
+	require.NoError(t, err)
 	err = bug1.Commit(repoA)
-	assert.Nil(t, err)
+	require.NoError(t, err)
 
 	// A --> remote --> B
 	_, err = Push(repoA, "origin")
-	assert.Nil(t, err)
+	require.NoError(t, err)
 
 	err = Pull(repoB, "origin")
-	assert.Nil(t, err)
+	require.NoError(t, err)
 
 	bugs := allBugs(t, ReadAllLocalBugs(repoB))
 
@@ -96,16 +34,19 @@ func TestPushPull(t *testing.T) {
 	}
 
 	// B --> remote --> A
-	bug2, _, err := Create(rene, unix, "bug2", "message")
-	assert.Nil(t, err)
+	reneB, err := identity.ReadLocal(repoA, reneA.Id())
+	require.NoError(t, err)
+
+	bug2, _, err := Create(reneB, time.Now().Unix(), "bug2", "message")
+	require.NoError(t, err)
 	err = bug2.Commit(repoB)
-	assert.Nil(t, err)
+	require.NoError(t, err)
 
 	_, err = Push(repoB, "origin")
-	assert.Nil(t, err)
+	require.NoError(t, err)
 
 	err = Pull(repoA, "origin")
-	assert.Nil(t, err)
+	require.NoError(t, err)
 
 	bugs = allBugs(t, ReadAllLocalBugs(repoA))
 
@@ -136,41 +77,46 @@ func BenchmarkRebaseTheirs(b *testing.B) {
 }
 
 func _RebaseTheirs(t testing.TB) {
-	repoA, repoB, remote := setupRepos(t)
-	defer cleanupRepos(repoA, repoB, remote)
+	repoA, repoB, remote := test.SetupReposAndRemote(t)
+	defer test.CleanupRepos(repoA, repoB, remote)
+
+	reneA := identity.NewIdentity("RenΓ© Descartes", "rene@descartes.fr")
 
-	bug1, _, err := Create(rene, unix, "bug1", "message")
-	assert.Nil(t, err)
+	bug1, _, err := Create(reneA, time.Now().Unix(), "bug1", "message")
+	require.NoError(t, err)
 	err = bug1.Commit(repoA)
-	assert.Nil(t, err)
+	require.NoError(t, err)
 
 	// A --> remote
 	_, err = Push(repoA, "origin")
-	assert.Nil(t, err)
+	require.NoError(t, err)
 
 	// remote --> B
 	err = Pull(repoB, "origin")
-	assert.Nil(t, err)
+	require.NoError(t, err)
 
 	bug2, err := ReadLocalBug(repoB, bug1.Id())
-	assert.Nil(t, err)
-
-	_, err = AddComment(bug2, rene, unix, "message2")
-	assert.Nil(t, err)
-	_, err = AddComment(bug2, rene, unix, "message3")
-	assert.Nil(t, err)
-	_, err = AddComment(bug2, rene, unix, "message4")
-	assert.Nil(t, err)
+	require.NoError(t, err)
+
+	reneB, err := identity.ReadLocal(repoA, reneA.Id())
+	require.NoError(t, err)
+
+	_, err = AddComment(bug2, reneB, time.Now().Unix(), "message2")
+	require.NoError(t, err)
+	_, err = AddComment(bug2, reneB, time.Now().Unix(), "message3")
+	require.NoError(t, err)
+	_, err = AddComment(bug2, reneB, time.Now().Unix(), "message4")
+	require.NoError(t, err)
 	err = bug2.Commit(repoB)
-	assert.Nil(t, err)
+	require.NoError(t, err)
 
 	// B --> remote
 	_, err = Push(repoB, "origin")
-	assert.Nil(t, err)
+	require.NoError(t, err)
 
 	// remote --> A
 	err = Pull(repoA, "origin")
-	assert.Nil(t, err)
+	require.NoError(t, err)
 
 	bugs := allBugs(t, ReadAllLocalBugs(repoB))
 
@@ -179,7 +125,7 @@ func _RebaseTheirs(t testing.TB) {
 	}
 
 	bug3, err := ReadLocalBug(repoA, bug1.Id())
-	assert.Nil(t, err)
+	require.NoError(t, err)
 
 	if nbOps(bug3) != 4 {
 		t.Fatal("Unexpected number of operations")
@@ -197,52 +143,54 @@ func BenchmarkRebaseOurs(b *testing.B) {
 }
 
 func _RebaseOurs(t testing.TB) {
-	repoA, repoB, remote := setupRepos(t)
-	defer cleanupRepos(repoA, repoB, remote)
+	repoA, repoB, remote := test.SetupReposAndRemote(t)
+	defer test.CleanupRepos(repoA, repoB, remote)
 
-	bug1, _, err := Create(rene, unix, "bug1", "message")
-	assert.Nil(t, err)
+	reneA := identity.NewIdentity("RenΓ© Descartes", "rene@descartes.fr")
+
+	bug1, _, err := Create(reneA, time.Now().Unix(), "bug1", "message")
+	require.NoError(t, err)
 	err = bug1.Commit(repoA)
-	assert.Nil(t, err)
+	require.NoError(t, err)
 
 	// A --> remote
 	_, err = Push(repoA, "origin")
-	assert.Nil(t, err)
+	require.NoError(t, err)
 
 	// remote --> B
 	err = Pull(repoB, "origin")
-	assert.Nil(t, err)
-
-	_, err = AddComment(bug1, rene, unix, "message2")
-	assert.Nil(t, err)
-	_, err = AddComment(bug1, rene, unix, "message3")
-	assert.Nil(t, err)
-	_, err = AddComment(bug1, rene, unix, "message4")
-	assert.Nil(t, err)
+	require.NoError(t, err)
+
+	_, err = AddComment(bug1, reneA, time.Now().Unix(), "message2")
+	require.NoError(t, err)
+	_, err = AddComment(bug1, reneA, time.Now().Unix(), "message3")
+	require.NoError(t, err)
+	_, err = AddComment(bug1, reneA, time.Now().Unix(), "message4")
+	require.NoError(t, err)
 	err = bug1.Commit(repoA)
-	assert.Nil(t, err)
-
-	_, err = AddComment(bug1, rene, unix, "message5")
-	assert.Nil(t, err)
-	_, err = AddComment(bug1, rene, unix, "message6")
-	assert.Nil(t, err)
-	_, err = AddComment(bug1, rene, unix, "message7")
-	assert.Nil(t, err)
+	require.NoError(t, err)
+
+	_, err = AddComment(bug1, reneA, time.Now().Unix(), "message5")
+	require.NoError(t, err)
+	_, err = AddComment(bug1, reneA, time.Now().Unix(), "message6")
+	require.NoError(t, err)
+	_, err = AddComment(bug1, reneA, time.Now().Unix(), "message7")
+	require.NoError(t, err)
 	err = bug1.Commit(repoA)
-	assert.Nil(t, err)
-
-	_, err = AddComment(bug1, rene, unix, "message8")
-	assert.Nil(t, err)
-	_, err = AddComment(bug1, rene, unix, "message9")
-	assert.Nil(t, err)
-	_, err = AddComment(bug1, rene, unix, "message10")
-	assert.Nil(t, err)
+	require.NoError(t, err)
+
+	_, err = AddComment(bug1, reneA, time.Now().Unix(), "message8")
+	require.NoError(t, err)
+	_, err = AddComment(bug1, reneA, time.Now().Unix(), "message9")
+	require.NoError(t, err)
+	_, err = AddComment(bug1, reneA, time.Now().Unix(), "message10")
+	require.NoError(t, err)
 	err = bug1.Commit(repoA)
-	assert.Nil(t, err)
+	require.NoError(t, err)
 
 	// remote --> A
 	err = Pull(repoA, "origin")
-	assert.Nil(t, err)
+	require.NoError(t, err)
 
 	bugs := allBugs(t, ReadAllLocalBugs(repoA))
 
@@ -251,7 +199,7 @@ func _RebaseOurs(t testing.TB) {
 	}
 
 	bug2, err := ReadLocalBug(repoA, bug1.Id())
-	assert.Nil(t, err)
+	require.NoError(t, err)
 
 	if nbOps(bug2) != 10 {
 		t.Fatal("Unexpected number of operations")
@@ -278,86 +226,91 @@ func BenchmarkRebaseConflict(b *testing.B) {
 }
 
 func _RebaseConflict(t testing.TB) {
-	repoA, repoB, remote := setupRepos(t)
-	defer cleanupRepos(repoA, repoB, remote)
+	repoA, repoB, remote := test.SetupReposAndRemote(t)
+	defer test.CleanupRepos(repoA, repoB, remote)
+
+	reneA := identity.NewIdentity("RenΓ© Descartes", "rene@descartes.fr")
 
-	bug1, _, err := Create(rene, unix, "bug1", "message")
-	assert.Nil(t, err)
+	bug1, _, err := Create(reneA, time.Now().Unix(), "bug1", "message")
+	require.NoError(t, err)
 	err = bug1.Commit(repoA)
-	assert.Nil(t, err)
+	require.NoError(t, err)
 
 	// A --> remote
 	_, err = Push(repoA, "origin")
-	assert.Nil(t, err)
+	require.NoError(t, err)
 
 	// remote --> B
 	err = Pull(repoB, "origin")
-	assert.Nil(t, err)
-
-	_, err = AddComment(bug1, rene, unix, "message2")
-	assert.Nil(t, err)
-	_, err = AddComment(bug1, rene, unix, "message3")
-	assert.Nil(t, err)
-	_, err = AddComment(bug1, rene, unix, "message4")
-	assert.Nil(t, err)
+	require.NoError(t, err)
+
+	_, err = AddComment(bug1, reneA, time.Now().Unix(), "message2")
+	require.NoError(t, err)
+	_, err = AddComment(bug1, reneA, time.Now().Unix(), "message3")
+	require.NoError(t, err)
+	_, err = AddComment(bug1, reneA, time.Now().Unix(), "message4")
+	require.NoError(t, err)
 	err = bug1.Commit(repoA)
-	assert.Nil(t, err)
-
-	_, err = AddComment(bug1, rene, unix, "message5")
-	assert.Nil(t, err)
-	_, err = AddComment(bug1, rene, unix, "message6")
-	assert.Nil(t, err)
-	_, err = AddComment(bug1, rene, unix, "message7")
-	assert.Nil(t, err)
+	require.NoError(t, err)
+
+	_, err = AddComment(bug1, reneA, time.Now().Unix(), "message5")
+	require.NoError(t, err)
+	_, err = AddComment(bug1, reneA, time.Now().Unix(), "message6")
+	require.NoError(t, err)
+	_, err = AddComment(bug1, reneA, time.Now().Unix(), "message7")
+	require.NoError(t, err)
 	err = bug1.Commit(repoA)
-	assert.Nil(t, err)
-
-	_, err = AddComment(bug1, rene, unix, "message8")
-	assert.Nil(t, err)
-	_, err = AddComment(bug1, rene, unix, "message9")
-	assert.Nil(t, err)
-	_, err = AddComment(bug1, rene, unix, "message10")
-	assert.Nil(t, err)
+	require.NoError(t, err)
+
+	_, err = AddComment(bug1, reneA, time.Now().Unix(), "message8")
+	require.NoError(t, err)
+	_, err = AddComment(bug1, reneA, time.Now().Unix(), "message9")
+	require.NoError(t, err)
+	_, err = AddComment(bug1, reneA, time.Now().Unix(), "message10")
+	require.NoError(t, err)
 	err = bug1.Commit(repoA)
-	assert.Nil(t, err)
+	require.NoError(t, err)
 
 	bug2, err := ReadLocalBug(repoB, bug1.Id())
-	assert.Nil(t, err)
-
-	_, err = AddComment(bug2, rene, unix, "message11")
-	assert.Nil(t, err)
-	_, err = AddComment(bug2, rene, unix, "message12")
-	assert.Nil(t, err)
-	_, err = AddComment(bug2, rene, unix, "message13")
-	assert.Nil(t, err)
+	require.NoError(t, err)
+
+	reneB, err := identity.ReadLocal(repoA, reneA.Id())
+	require.NoError(t, err)
+
+	_, err = AddComment(bug2, reneB, time.Now().Unix(), "message11")
+	require.NoError(t, err)
+	_, err = AddComment(bug2, reneB, time.Now().Unix(), "message12")
+	require.NoError(t, err)
+	_, err = AddComment(bug2, reneB, time.Now().Unix(), "message13")
+	require.NoError(t, err)
 	err = bug2.Commit(repoB)
-	assert.Nil(t, err)
-
-	_, err = AddComment(bug2, rene, unix, "message14")
-	assert.Nil(t, err)
-	_, err = AddComment(bug2, rene, unix, "message15")
-	assert.Nil(t, err)
-	_, err = AddComment(bug2, rene, unix, "message16")
-	assert.Nil(t, err)
+	require.NoError(t, err)
+
+	_, err = AddComment(bug2, reneB, time.Now().Unix(), "message14")
+	require.NoError(t, err)
+	_, err = AddComment(bug2, reneB, time.Now().Unix(), "message15")
+	require.NoError(t, err)
+	_, err = AddComment(bug2, reneB, time.Now().Unix(), "message16")
+	require.NoError(t, err)
 	err = bug2.Commit(repoB)
-	assert.Nil(t, err)
-
-	_, err = AddComment(bug2, rene, unix, "message17")
-	assert.Nil(t, err)
-	_, err = AddComment(bug2, rene, unix, "message18")
-	assert.Nil(t, err)
-	_, err = AddComment(bug2, rene, unix, "message19")
-	assert.Nil(t, err)
+	require.NoError(t, err)
+
+	_, err = AddComment(bug2, reneB, time.Now().Unix(), "message17")
+	require.NoError(t, err)
+	_, err = AddComment(bug2, reneB, time.Now().Unix(), "message18")
+	require.NoError(t, err)
+	_, err = AddComment(bug2, reneB, time.Now().Unix(), "message19")
+	require.NoError(t, err)
 	err = bug2.Commit(repoB)
-	assert.Nil(t, err)
+	require.NoError(t, err)
 
 	// A --> remote
 	_, err = Push(repoA, "origin")
-	assert.Nil(t, err)
+	require.NoError(t, err)
 
 	// remote --> B
 	err = Pull(repoB, "origin")
-	assert.Nil(t, err)
+	require.NoError(t, err)
 
 	bugs := allBugs(t, ReadAllLocalBugs(repoB))
 
@@ -366,7 +319,7 @@ func _RebaseConflict(t testing.TB) {
 	}
 
 	bug3, err := ReadLocalBug(repoB, bug1.Id())
-	assert.Nil(t, err)
+	require.NoError(t, err)
 
 	if nbOps(bug3) != 19 {
 		t.Fatal("Unexpected number of operations")
@@ -374,11 +327,11 @@ func _RebaseConflict(t testing.TB) {
 
 	// B --> remote
 	_, err = Push(repoB, "origin")
-	assert.Nil(t, err)
+	require.NoError(t, err)
 
 	// remote --> A
 	err = Pull(repoA, "origin")
-	assert.Nil(t, err)
+	require.NoError(t, err)
 
 	bugs = allBugs(t, ReadAllLocalBugs(repoA))
 
@@ -387,7 +340,7 @@ func _RebaseConflict(t testing.TB) {
 	}
 
 	bug4, err := ReadLocalBug(repoA, bug1.Id())
-	assert.Nil(t, err)
+	require.NoError(t, err)
 
 	if nbOps(bug4) != 19 {
 		t.Fatal("Unexpected number of operations")

bug/bug_test.go πŸ”—

@@ -1,11 +1,12 @@
 package bug
 
 import (
+	"testing"
+	"time"
+
+	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/repository"
-	"github.com/go-test/deep"
 	"github.com/stretchr/testify/assert"
-
-	"testing"
 )
 
 func TestBugId(t *testing.T) {
@@ -13,6 +14,9 @@ func TestBugId(t *testing.T) {
 
 	bug1 := NewBug()
 
+	rene := identity.NewIdentity("RenΓ© Descartes", "rene@descartes.fr")
+	createOp := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil)
+
 	bug1.Append(createOp)
 
 	err := bug1.Commit(mockRepo)
@@ -29,6 +33,9 @@ func TestBugValidity(t *testing.T) {
 
 	bug1 := NewBug()
 
+	rene := identity.NewIdentity("RenΓ© Descartes", "rene@descartes.fr")
+	createOp := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil)
+
 	if bug1.Validate() == nil {
 		t.Fatal("Empty bug should be invalid")
 	}
@@ -56,9 +63,14 @@ func TestBugValidity(t *testing.T) {
 	}
 }
 
-func TestBugSerialisation(t *testing.T) {
+func TestBugCommitLoad(t *testing.T) {
 	bug1 := NewBug()
 
+	rene := identity.NewIdentity("RenΓ© Descartes", "rene@descartes.fr")
+	createOp := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil)
+	setTitleOp := NewSetTitleOp(rene, time.Now().Unix(), "title2", "title1")
+	addCommentOp := NewAddCommentOp(rene, time.Now().Unix(), "message2", nil)
+
 	bug1.Append(createOp)
 	bug1.Append(setTitleOp)
 	bug1.Append(setTitleOp)
@@ -70,25 +82,30 @@ func TestBugSerialisation(t *testing.T) {
 	assert.Nil(t, err)
 
 	bug2, err := ReadLocalBug(repo, bug1.Id())
-	if err != nil {
-		t.Error(err)
-	}
+	assert.NoError(t, err)
+	equivalentBug(t, bug1, bug2)
 
-	// ignore some fields
-	bug2.packs[0].commitHash = bug1.packs[0].commitHash
-	for i := range bug1.packs[0].Operations {
-		bug2.packs[0].Operations[i].base().hash = bug1.packs[0].Operations[i].base().hash
-	}
+	// add more op
 
-	// check hashes
-	for i := range bug1.packs[0].Operations {
-		if !bug2.packs[0].Operations[i].base().hash.IsValid() {
-			t.Fatal("invalid hash")
+	bug1.Append(setTitleOp)
+	bug1.Append(addCommentOp)
+
+	err = bug1.Commit(repo)
+	assert.Nil(t, err)
+
+	bug3, err := ReadLocalBug(repo, bug1.Id())
+	assert.NoError(t, err)
+	equivalentBug(t, bug1, bug3)
+}
+
+func equivalentBug(t *testing.T, expected, actual *Bug) {
+	assert.Equal(t, len(expected.packs), len(actual.packs))
+
+	for i := range expected.packs {
+		for j := range expected.packs[i].Operations {
+			actual.packs[i].Operations[j].base().hash = expected.packs[i].Operations[j].base().hash
 		}
 	}
 
-	deep.CompareUnexportedFields = true
-	if diff := deep.Equal(bug1, bug2); diff != nil {
-		t.Fatal(diff)
-	}
+	assert.Equal(t, expected, actual)
 }

bug/comment.go πŸ”—

@@ -1,19 +1,21 @@
 package bug
 
 import (
+	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/util/git"
+	"github.com/MichaelMure/git-bug/util/timestamp"
 	"github.com/dustin/go-humanize"
 )
 
 // Comment represent a comment in a Bug
 type Comment struct {
-	Author  Person
+	Author  identity.Interface
 	Message string
 	Files   []git.Hash
 
 	// Creation time of the comment.
 	// Should be used only for human display, never for ordering as we can't rely on it in a distributed system.
-	UnixTime Timestamp
+	UnixTime timestamp.Timestamp
 }
 
 // FormatTimeRel format the UnixTime of the comment for human consumption

bug/identity.go πŸ”—

@@ -0,0 +1,27 @@
+package bug
+
+import (
+	"github.com/MichaelMure/git-bug/identity"
+)
+
+// EnsureIdentities walk the graph of operations and make sure that all Identity
+// are properly loaded. That is, it replace all the IdentityStub with the full
+// Identity, loaded through a Resolver.
+func (bug *Bug) EnsureIdentities(resolver identity.Resolver) error {
+	it := NewOperationIterator(bug)
+
+	for it.Next() {
+		op := it.Value()
+		base := op.base()
+
+		if stub, ok := base.Author.(*identity.IdentityStub); ok {
+			i, err := resolver.ResolveIdentity(stub.Id())
+			if err != nil {
+				return err
+			}
+
+			base.Author = i
+		}
+	}
+	return nil
+}

bug/label.go πŸ”—

@@ -28,7 +28,7 @@ func (l *Label) UnmarshalGQL(v interface{}) error {
 
 // MarshalGQL implements the graphql.Marshaler interface
 func (l Label) MarshalGQL(w io.Writer) {
-	w.Write([]byte(`"` + l.String() + `"`))
+	_, _ = w.Write([]byte(`"` + l.String() + `"`))
 }
 
 func (l Label) Validate() error {

bug/op_add_comment.go πŸ”—

@@ -1,10 +1,13 @@
 package bug
 
 import (
+	"encoding/json"
 	"fmt"
 
+	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/util/git"
 	"github.com/MichaelMure/git-bug/util/text"
+	"github.com/MichaelMure/git-bug/util/timestamp"
 )
 
 var _ Operation = &AddCommentOperation{}
@@ -12,9 +15,9 @@ var _ Operation = &AddCommentOperation{}
 // AddCommentOperation will add a new comment in the bug
 type AddCommentOperation struct {
 	OpBase
-	Message string `json:"message"`
+	Message string
 	// TODO: change for a map[string]util.hash to store the filename ?
-	Files []git.Hash `json:"files"`
+	Files []git.Hash
 }
 
 func (op *AddCommentOperation) base() *OpBase {
@@ -30,7 +33,7 @@ func (op *AddCommentOperation) Apply(snapshot *Snapshot) {
 		Message:  op.Message,
 		Author:   op.Author,
 		Files:    op.Files,
-		UnixTime: Timestamp(op.UnixTime),
+		UnixTime: timestamp.Timestamp(op.UnixTime),
 	}
 
 	snapshot.Comments = append(snapshot.Comments, comment)
@@ -65,10 +68,58 @@ func (op *AddCommentOperation) Validate() error {
 	return nil
 }
 
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *AddCommentOperation) MarshalJSON() ([]byte, error) {
+	base, err := json.Marshal(op.OpBase)
+	if err != nil {
+		return nil, err
+	}
+
+	// revert back to a flat map to be able to add our own fields
+	var data map[string]interface{}
+	if err := json.Unmarshal(base, &data); err != nil {
+		return nil, err
+	}
+
+	data["message"] = op.Message
+	data["files"] = op.Files
+
+	return json.Marshal(data)
+}
+
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *AddCommentOperation) UnmarshalJSON(data []byte) error {
+	// Unmarshal OpBase and the op separately
+
+	base := OpBase{}
+	err := json.Unmarshal(data, &base)
+	if err != nil {
+		return err
+	}
+
+	aux := struct {
+		Message string     `json:"message"`
+		Files   []git.Hash `json:"files"`
+	}{}
+
+	err = json.Unmarshal(data, &aux)
+	if err != nil {
+		return err
+	}
+
+	op.OpBase = base
+	op.Message = aux.Message
+	op.Files = aux.Files
+
+	return nil
+}
+
 // Sign post method for gqlgen
 func (op *AddCommentOperation) IsAuthored() {}
 
-func NewAddCommentOp(author Person, unixTime int64, message string, files []git.Hash) *AddCommentOperation {
+func NewAddCommentOp(author identity.Interface, unixTime int64, message string, files []git.Hash) *AddCommentOperation {
 	return &AddCommentOperation{
 		OpBase:  newOpBase(AddCommentOp, author, unixTime),
 		Message: message,
@@ -82,11 +133,11 @@ type AddCommentTimelineItem struct {
 }
 
 // Convenience function to apply the operation
-func AddComment(b Interface, author Person, unixTime int64, message string) (*AddCommentOperation, error) {
+func AddComment(b Interface, author identity.Interface, unixTime int64, message string) (*AddCommentOperation, error) {
 	return AddCommentWithFiles(b, author, unixTime, message, nil)
 }
 
-func AddCommentWithFiles(b Interface, author Person, unixTime int64, message string, files []git.Hash) (*AddCommentOperation, error) {
+func AddCommentWithFiles(b Interface, author identity.Interface, unixTime int64, message string, files []git.Hash) (*AddCommentOperation, error) {
 	addCommentOp := NewAddCommentOp(author, unixTime, message, files)
 	if err := addCommentOp.Validate(); err != nil {
 		return nil, err

bug/op_add_comment_test.go πŸ”—

@@ -0,0 +1,25 @@
+package bug
+
+import (
+	"encoding/json"
+	"testing"
+	"time"
+
+	"github.com/MichaelMure/git-bug/identity"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestAddCommentSerialize(t *testing.T) {
+	var rene = identity.NewBare("RenΓ© Descartes", "rene@descartes.fr")
+	unix := time.Now().Unix()
+	before := NewAddCommentOp(rene, unix, "message", nil)
+
+	data, err := json.Marshal(before)
+	assert.NoError(t, err)
+
+	var after AddCommentOperation
+	err = json.Unmarshal(data, &after)
+	assert.NoError(t, err)
+
+	assert.Equal(t, before, &after)
+}

bug/op_create.go πŸ”—

@@ -1,11 +1,14 @@
 package bug
 
 import (
+	"encoding/json"
 	"fmt"
 	"strings"
 
+	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/util/git"
 	"github.com/MichaelMure/git-bug/util/text"
+	"github.com/MichaelMure/git-bug/util/timestamp"
 )
 
 var _ Operation = &CreateOperation{}
@@ -13,9 +16,9 @@ var _ Operation = &CreateOperation{}
 // CreateOperation define the initial creation of a bug
 type CreateOperation struct {
 	OpBase
-	Title   string     `json:"title"`
-	Message string     `json:"message"`
-	Files   []git.Hash `json:"files"`
+	Title   string
+	Message string
+	Files   []git.Hash
 }
 
 func (op *CreateOperation) base() *OpBase {
@@ -32,7 +35,7 @@ func (op *CreateOperation) Apply(snapshot *Snapshot) {
 	comment := Comment{
 		Message:  op.Message,
 		Author:   op.Author,
-		UnixTime: Timestamp(op.UnixTime),
+		UnixTime: timestamp.Timestamp(op.UnixTime),
 	}
 
 	snapshot.Comments = []Comment{comment}
@@ -81,10 +84,61 @@ func (op *CreateOperation) Validate() error {
 	return nil
 }
 
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *CreateOperation) MarshalJSON() ([]byte, error) {
+	base, err := json.Marshal(op.OpBase)
+	if err != nil {
+		return nil, err
+	}
+
+	// revert back to a flat map to be able to add our own fields
+	var data map[string]interface{}
+	if err := json.Unmarshal(base, &data); err != nil {
+		return nil, err
+	}
+
+	data["title"] = op.Title
+	data["message"] = op.Message
+	data["files"] = op.Files
+
+	return json.Marshal(data)
+}
+
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *CreateOperation) UnmarshalJSON(data []byte) error {
+	// Unmarshal OpBase and the op separately
+
+	base := OpBase{}
+	err := json.Unmarshal(data, &base)
+	if err != nil {
+		return err
+	}
+
+	aux := struct {
+		Title   string     `json:"title"`
+		Message string     `json:"message"`
+		Files   []git.Hash `json:"files"`
+	}{}
+
+	err = json.Unmarshal(data, &aux)
+	if err != nil {
+		return err
+	}
+
+	op.OpBase = base
+	op.Title = aux.Title
+	op.Message = aux.Message
+	op.Files = aux.Files
+
+	return nil
+}
+
 // Sign post method for gqlgen
 func (op *CreateOperation) IsAuthored() {}
 
-func NewCreateOp(author Person, unixTime int64, title, message string, files []git.Hash) *CreateOperation {
+func NewCreateOp(author identity.Interface, unixTime int64, title, message string, files []git.Hash) *CreateOperation {
 	return &CreateOperation{
 		OpBase:  newOpBase(CreateOp, author, unixTime),
 		Title:   title,
@@ -99,11 +153,11 @@ type CreateTimelineItem struct {
 }
 
 // Convenience function to apply the operation
-func Create(author Person, unixTime int64, title, message string) (*Bug, *CreateOperation, error) {
+func Create(author identity.Interface, unixTime int64, title, message string) (*Bug, *CreateOperation, error) {
 	return CreateWithFiles(author, unixTime, title, message, nil)
 }
 
-func CreateWithFiles(author Person, unixTime int64, title, message string, files []git.Hash) (*Bug, *CreateOperation, error) {
+func CreateWithFiles(author identity.Interface, unixTime int64, title, message string, files []git.Hash) (*Bug, *CreateOperation, error) {
 	newBug := NewBug()
 	createOp := NewCreateOp(author, unixTime, title, message, files)
 

bug/op_create_test.go πŸ”—

@@ -1,20 +1,19 @@
 package bug
 
 import (
+	"encoding/json"
 	"testing"
 	"time"
 
-	"github.com/go-test/deep"
+	"github.com/MichaelMure/git-bug/identity"
+	"github.com/MichaelMure/git-bug/util/timestamp"
+	"github.com/stretchr/testify/assert"
 )
 
 func TestCreate(t *testing.T) {
 	snapshot := Snapshot{}
 
-	var rene = Person{
-		Name:  "RenΓ© Descartes",
-		Email: "rene@descartes.fr",
-	}
-
+	rene := identity.NewBare("RenΓ© Descartes", "rene@descartes.fr")
 	unix := time.Now().Unix()
 
 	create := NewCreateOp(rene, unix, "title", "message", nil)
@@ -22,11 +21,9 @@ func TestCreate(t *testing.T) {
 	create.Apply(&snapshot)
 
 	hash, err := create.Hash()
-	if err != nil {
-		t.Fatal(err)
-	}
+	assert.NoError(t, err)
 
-	comment := Comment{Author: rene, Message: "message", UnixTime: Timestamp(create.UnixTime)}
+	comment := Comment{Author: rene, Message: "message", UnixTime: timestamp.Timestamp(create.UnixTime)}
 
 	expected := Snapshot{
 		Title: "title",
@@ -42,8 +39,20 @@ func TestCreate(t *testing.T) {
 		},
 	}
 
-	deep.CompareUnexportedFields = true
-	if diff := deep.Equal(snapshot, expected); diff != nil {
-		t.Fatal(diff)
-	}
+	assert.Equal(t, expected, snapshot)
+}
+
+func TestCreateSerialize(t *testing.T) {
+	var rene = identity.NewBare("RenΓ© Descartes", "rene@descartes.fr")
+	unix := time.Now().Unix()
+	before := NewCreateOp(rene, unix, "title", "message", nil)
+
+	data, err := json.Marshal(before)
+	assert.NoError(t, err)
+
+	var after CreateOperation
+	err = json.Unmarshal(data, &after)
+	assert.NoError(t, err)
+
+	assert.Equal(t, before, &after)
 }

bug/op_edit_comment.go πŸ”—

@@ -1,8 +1,12 @@
 package bug
 
 import (
+	"encoding/json"
 	"fmt"
 
+	"github.com/MichaelMure/git-bug/identity"
+	"github.com/MichaelMure/git-bug/util/timestamp"
+
 	"github.com/MichaelMure/git-bug/util/git"
 	"github.com/MichaelMure/git-bug/util/text"
 )
@@ -12,9 +16,9 @@ var _ Operation = &EditCommentOperation{}
 // EditCommentOperation will change a comment in the bug
 type EditCommentOperation struct {
 	OpBase
-	Target  git.Hash   `json:"target"`
-	Message string     `json:"message"`
-	Files   []git.Hash `json:"files"`
+	Target  git.Hash
+	Message string
+	Files   []git.Hash
 }
 
 func (op *EditCommentOperation) base() *OpBase {
@@ -55,7 +59,7 @@ func (op *EditCommentOperation) Apply(snapshot *Snapshot) {
 	comment := Comment{
 		Message:  op.Message,
 		Files:    op.Files,
-		UnixTime: Timestamp(op.UnixTime),
+		UnixTime: timestamp.Timestamp(op.UnixTime),
 	}
 
 	switch target.(type) {
@@ -92,10 +96,61 @@ func (op *EditCommentOperation) Validate() error {
 	return nil
 }
 
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *EditCommentOperation) MarshalJSON() ([]byte, error) {
+	base, err := json.Marshal(op.OpBase)
+	if err != nil {
+		return nil, err
+	}
+
+	// revert back to a flat map to be able to add our own fields
+	var data map[string]interface{}
+	if err := json.Unmarshal(base, &data); err != nil {
+		return nil, err
+	}
+
+	data["target"] = op.Target
+	data["message"] = op.Message
+	data["files"] = op.Files
+
+	return json.Marshal(data)
+}
+
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *EditCommentOperation) UnmarshalJSON(data []byte) error {
+	// Unmarshal OpBase and the op separately
+
+	base := OpBase{}
+	err := json.Unmarshal(data, &base)
+	if err != nil {
+		return err
+	}
+
+	aux := struct {
+		Target  git.Hash   `json:"target"`
+		Message string     `json:"message"`
+		Files   []git.Hash `json:"files"`
+	}{}
+
+	err = json.Unmarshal(data, &aux)
+	if err != nil {
+		return err
+	}
+
+	op.OpBase = base
+	op.Target = aux.Target
+	op.Message = aux.Message
+	op.Files = aux.Files
+
+	return nil
+}
+
 // Sign post method for gqlgen
 func (op *EditCommentOperation) IsAuthored() {}
 
-func NewEditCommentOp(author Person, unixTime int64, target git.Hash, message string, files []git.Hash) *EditCommentOperation {
+func NewEditCommentOp(author identity.Interface, unixTime int64, target git.Hash, message string, files []git.Hash) *EditCommentOperation {
 	return &EditCommentOperation{
 		OpBase:  newOpBase(EditCommentOp, author, unixTime),
 		Target:  target,
@@ -105,11 +160,11 @@ func NewEditCommentOp(author Person, unixTime int64, target git.Hash, message st
 }
 
 // Convenience function to apply the operation
-func EditComment(b Interface, author Person, unixTime int64, target git.Hash, message string) (*EditCommentOperation, error) {
+func EditComment(b Interface, author identity.Interface, unixTime int64, target git.Hash, message string) (*EditCommentOperation, error) {
 	return EditCommentWithFiles(b, author, unixTime, target, message, nil)
 }
 
-func EditCommentWithFiles(b Interface, author Person, unixTime int64, target git.Hash, message string, files []git.Hash) (*EditCommentOperation, error) {
+func EditCommentWithFiles(b Interface, author identity.Interface, unixTime int64, target git.Hash, message string, files []git.Hash) (*EditCommentOperation, error) {
 	editCommentOp := NewEditCommentOp(author, unixTime, target, message, files)
 	if err := editCommentOp.Validate(); err != nil {
 		return nil, err

bug/op_edit_comment_test.go πŸ”—

@@ -1,37 +1,32 @@
 package bug
 
 import (
+	"encoding/json"
 	"testing"
 	"time"
 
-	"gotest.tools/assert"
+	"github.com/MichaelMure/git-bug/identity"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestEdit(t *testing.T) {
 	snapshot := Snapshot{}
 
-	var rene = Person{
-		Name:  "RenΓ© Descartes",
-		Email: "rene@descartes.fr",
-	}
-
+	rene := identity.NewBare("RenΓ© Descartes", "rene@descartes.fr")
 	unix := time.Now().Unix()
 
 	create := NewCreateOp(rene, unix, "title", "create", nil)
 	create.Apply(&snapshot)
 
 	hash1, err := create.Hash()
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 
 	comment := NewAddCommentOp(rene, unix, "comment", nil)
 	comment.Apply(&snapshot)
 
 	hash2, err := comment.Hash()
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 
 	edit := NewEditCommentOp(rene, unix, hash1, "create edited", nil)
 	edit.Apply(&snapshot)
@@ -51,3 +46,18 @@ func TestEdit(t *testing.T) {
 	assert.Equal(t, snapshot.Comments[0].Message, "create edited")
 	assert.Equal(t, snapshot.Comments[1].Message, "comment edited")
 }
+
+func TestEditCommentSerialize(t *testing.T) {
+	var rene = identity.NewBare("RenΓ© Descartes", "rene@descartes.fr")
+	unix := time.Now().Unix()
+	before := NewEditCommentOp(rene, unix, "target", "message", nil)
+
+	data, err := json.Marshal(before)
+	assert.NoError(t, err)
+
+	var after EditCommentOperation
+	err = json.Unmarshal(data, &after)
+	assert.NoError(t, err)
+
+	assert.Equal(t, before, &after)
+}

bug/op_label_change.go πŸ”—

@@ -1,9 +1,13 @@
 package bug
 
 import (
+	"encoding/json"
 	"fmt"
 	"sort"
 
+	"github.com/MichaelMure/git-bug/identity"
+	"github.com/MichaelMure/git-bug/util/timestamp"
+
 	"github.com/MichaelMure/git-bug/util/git"
 	"github.com/pkg/errors"
 )
@@ -13,8 +17,8 @@ var _ Operation = &LabelChangeOperation{}
 // LabelChangeOperation define a Bug operation to add or remove labels
 type LabelChangeOperation struct {
 	OpBase
-	Added   []Label `json:"added"`
-	Removed []Label `json:"removed"`
+	Added   []Label
+	Removed []Label
 }
 
 func (op *LabelChangeOperation) base() *OpBase {
@@ -65,7 +69,7 @@ AddLoop:
 	item := &LabelChangeTimelineItem{
 		hash:     hash,
 		Author:   op.Author,
-		UnixTime: Timestamp(op.UnixTime),
+		UnixTime: timestamp.Timestamp(op.UnixTime),
 		Added:    op.Added,
 		Removed:  op.Removed,
 	}
@@ -97,10 +101,58 @@ func (op *LabelChangeOperation) Validate() error {
 	return nil
 }
 
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *LabelChangeOperation) MarshalJSON() ([]byte, error) {
+	base, err := json.Marshal(op.OpBase)
+	if err != nil {
+		return nil, err
+	}
+
+	// revert back to a flat map to be able to add our own fields
+	var data map[string]interface{}
+	if err := json.Unmarshal(base, &data); err != nil {
+		return nil, err
+	}
+
+	data["added"] = op.Added
+	data["removed"] = op.Removed
+
+	return json.Marshal(data)
+}
+
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *LabelChangeOperation) UnmarshalJSON(data []byte) error {
+	// Unmarshal OpBase and the op separately
+
+	base := OpBase{}
+	err := json.Unmarshal(data, &base)
+	if err != nil {
+		return err
+	}
+
+	aux := struct {
+		Added   []Label `json:"added"`
+		Removed []Label `json:"removed"`
+	}{}
+
+	err = json.Unmarshal(data, &aux)
+	if err != nil {
+		return err
+	}
+
+	op.OpBase = base
+	op.Added = aux.Added
+	op.Removed = aux.Removed
+
+	return nil
+}
+
 // Sign post method for gqlgen
 func (op *LabelChangeOperation) IsAuthored() {}
 
-func NewLabelChangeOperation(author Person, unixTime int64, added, removed []Label) *LabelChangeOperation {
+func NewLabelChangeOperation(author identity.Interface, unixTime int64, added, removed []Label) *LabelChangeOperation {
 	return &LabelChangeOperation{
 		OpBase:  newOpBase(LabelChangeOp, author, unixTime),
 		Added:   added,
@@ -110,8 +162,8 @@ func NewLabelChangeOperation(author Person, unixTime int64, added, removed []Lab
 
 type LabelChangeTimelineItem struct {
 	hash     git.Hash
-	Author   Person
-	UnixTime Timestamp
+	Author   identity.Interface
+	UnixTime timestamp.Timestamp
 	Added    []Label
 	Removed  []Label
 }
@@ -121,7 +173,7 @@ func (l LabelChangeTimelineItem) Hash() git.Hash {
 }
 
 // ChangeLabels is a convenience function to apply the operation
-func ChangeLabels(b Interface, author Person, unixTime int64, add, remove []string) ([]LabelChangeResult, *LabelChangeOperation, error) {
+func ChangeLabels(b Interface, author identity.Interface, unixTime int64, add, remove []string) ([]LabelChangeResult, *LabelChangeOperation, error) {
 	var added, removed []Label
 	var results []LabelChangeResult
 

bug/op_label_change_test.go πŸ”—

@@ -0,0 +1,25 @@
+package bug
+
+import (
+	"encoding/json"
+	"testing"
+	"time"
+
+	"github.com/MichaelMure/git-bug/identity"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestLabelChangeSerialize(t *testing.T) {
+	var rene = identity.NewBare("RenΓ© Descartes", "rene@descartes.fr")
+	unix := time.Now().Unix()
+	before := NewLabelChangeOperation(rene, unix, []Label{"added"}, []Label{"removed"})
+
+	data, err := json.Marshal(before)
+	assert.NoError(t, err)
+
+	var after LabelChangeOperation
+	err = json.Unmarshal(data, &after)
+	assert.NoError(t, err)
+
+	assert.Equal(t, before, &after)
+}

bug/op_noop.go πŸ”—

@@ -1,12 +1,17 @@
 package bug
 
-import "github.com/MichaelMure/git-bug/util/git"
+import (
+	"encoding/json"
+
+	"github.com/MichaelMure/git-bug/identity"
+	"github.com/MichaelMure/git-bug/util/git"
+)
 
 var _ Operation = &NoOpOperation{}
 
 // NoOpOperation is an operation that does not change the bug state. It can
 // however be used to store arbitrary metadata in the bug history, for example
-// to support a bridge feature
+// to support a bridge feature.
 type NoOpOperation struct {
 	OpBase
 }
@@ -27,17 +32,57 @@ func (op *NoOpOperation) Validate() error {
 	return opBaseValidate(op, NoOpOp)
 }
 
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *NoOpOperation) MarshalJSON() ([]byte, error) {
+	base, err := json.Marshal(op.OpBase)
+	if err != nil {
+		return nil, err
+	}
+
+	// revert back to a flat map to be able to add our own fields
+	var data map[string]interface{}
+	if err := json.Unmarshal(base, &data); err != nil {
+		return nil, err
+	}
+
+	return json.Marshal(data)
+}
+
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *NoOpOperation) UnmarshalJSON(data []byte) error {
+	// Unmarshal OpBase and the op separately
+
+	base := OpBase{}
+	err := json.Unmarshal(data, &base)
+	if err != nil {
+		return err
+	}
+
+	aux := struct{}{}
+
+	err = json.Unmarshal(data, &aux)
+	if err != nil {
+		return err
+	}
+
+	op.OpBase = base
+
+	return nil
+}
+
 // Sign post method for gqlgen
 func (op *NoOpOperation) IsAuthored() {}
 
-func NewNoOpOp(author Person, unixTime int64) *NoOpOperation {
+func NewNoOpOp(author identity.Interface, unixTime int64) *NoOpOperation {
 	return &NoOpOperation{
 		OpBase: newOpBase(NoOpOp, author, unixTime),
 	}
 }
 
 // Convenience function to apply the operation
-func NoOp(b Interface, author Person, unixTime int64, metadata map[string]string) (*NoOpOperation, error) {
+func NoOp(b Interface, author identity.Interface, unixTime int64, metadata map[string]string) (*NoOpOperation, error) {
 	op := NewNoOpOp(author, unixTime)
 
 	for key, value := range metadata {

bug/op_noop_test.go πŸ”—

@@ -0,0 +1,25 @@
+package bug
+
+import (
+	"encoding/json"
+	"testing"
+	"time"
+
+	"github.com/MichaelMure/git-bug/identity"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNoopSerialize(t *testing.T) {
+	var rene = identity.NewBare("RenΓ© Descartes", "rene@descartes.fr")
+	unix := time.Now().Unix()
+	before := NewNoOpOp(rene, unix)
+
+	data, err := json.Marshal(before)
+	assert.NoError(t, err)
+
+	var after NoOpOperation
+	err = json.Unmarshal(data, &after)
+	assert.NoError(t, err)
+
+	assert.Equal(t, before, &after)
+}

bug/op_set_metadata.go πŸ”—

@@ -1,13 +1,19 @@
 package bug
 
-import "github.com/MichaelMure/git-bug/util/git"
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/MichaelMure/git-bug/identity"
+	"github.com/MichaelMure/git-bug/util/git"
+)
 
 var _ Operation = &SetMetadataOperation{}
 
 type SetMetadataOperation struct {
 	OpBase
-	Target      git.Hash          `json:"target"`
-	NewMetadata map[string]string `json:"new_metadata"`
+	Target      git.Hash
+	NewMetadata map[string]string
 }
 
 func (op *SetMetadataOperation) base() *OpBase {
@@ -50,13 +56,65 @@ func (op *SetMetadataOperation) Validate() error {
 		return err
 	}
 
+	if !op.Target.IsValid() {
+		return fmt.Errorf("target hash is invalid")
+	}
+
+	return nil
+}
+
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *SetMetadataOperation) MarshalJSON() ([]byte, error) {
+	base, err := json.Marshal(op.OpBase)
+	if err != nil {
+		return nil, err
+	}
+
+	// revert back to a flat map to be able to add our own fields
+	var data map[string]interface{}
+	if err := json.Unmarshal(base, &data); err != nil {
+		return nil, err
+	}
+
+	data["target"] = op.Target
+	data["new_metadata"] = op.NewMetadata
+
+	return json.Marshal(data)
+}
+
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *SetMetadataOperation) UnmarshalJSON(data []byte) error {
+	// Unmarshal OpBase and the op separately
+
+	base := OpBase{}
+	err := json.Unmarshal(data, &base)
+	if err != nil {
+		return err
+	}
+
+	aux := struct {
+		Target      git.Hash          `json:"target"`
+		NewMetadata map[string]string `json:"new_metadata"`
+	}{}
+
+	err = json.Unmarshal(data, &aux)
+	if err != nil {
+		return err
+	}
+
+	op.OpBase = base
+	op.Target = aux.Target
+	op.NewMetadata = aux.NewMetadata
+
 	return nil
 }
 
 // Sign post method for gqlgen
 func (op *SetMetadataOperation) IsAuthored() {}
 
-func NewSetMetadataOp(author Person, unixTime int64, target git.Hash, newMetadata map[string]string) *SetMetadataOperation {
+func NewSetMetadataOp(author identity.Interface, unixTime int64, target git.Hash, newMetadata map[string]string) *SetMetadataOperation {
 	return &SetMetadataOperation{
 		OpBase:      newOpBase(SetMetadataOp, author, unixTime),
 		Target:      target,
@@ -65,7 +123,7 @@ func NewSetMetadataOp(author Person, unixTime int64, target git.Hash, newMetadat
 }
 
 // Convenience function to apply the operation
-func SetMetadata(b Interface, author Person, unixTime int64, target git.Hash, newMetadata map[string]string) (*SetMetadataOperation, error) {
+func SetMetadata(b Interface, author identity.Interface, unixTime int64, target git.Hash, newMetadata map[string]string) (*SetMetadataOperation, error) {
 	SetMetadataOp := NewSetMetadataOp(author, unixTime, target, newMetadata)
 	if err := SetMetadataOp.Validate(); err != nil {
 		return nil, err

bug/op_set_metadata_test.go πŸ”—

@@ -1,20 +1,19 @@
 package bug
 
 import (
+	"encoding/json"
 	"testing"
 	"time"
 
+	"github.com/MichaelMure/git-bug/identity"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestSetMetadata(t *testing.T) {
 	snapshot := Snapshot{}
 
-	var rene = Person{
-		Name:  "RenΓ© Descartes",
-		Email: "rene@descartes.fr",
-	}
-
+	rene := identity.NewBare("RenΓ© Descartes", "rene@descartes.fr")
 	unix := time.Now().Unix()
 
 	create := NewCreateOp(rene, unix, "title", "create", nil)
@@ -23,9 +22,7 @@ func TestSetMetadata(t *testing.T) {
 	snapshot.Operations = append(snapshot.Operations, create)
 
 	hash1, err := create.Hash()
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 
 	comment := NewAddCommentOp(rene, unix, "comment", nil)
 	comment.SetMetadata("key2", "value2")
@@ -33,9 +30,7 @@ func TestSetMetadata(t *testing.T) {
 	snapshot.Operations = append(snapshot.Operations, comment)
 
 	hash2, err := comment.Hash()
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 
 	op1 := NewSetMetadataOp(rene, unix, hash1, map[string]string{
 		"key":  "override",
@@ -96,3 +91,21 @@ func TestSetMetadata(t *testing.T) {
 	assert.Equal(t, commentMetadata["key2"], "value2")
 	assert.Equal(t, commentMetadata["key3"], "value3")
 }
+
+func TestSetMetadataSerialize(t *testing.T) {
+	var rene = identity.NewBare("RenΓ© Descartes", "rene@descartes.fr")
+	unix := time.Now().Unix()
+	before := NewSetMetadataOp(rene, unix, "message", map[string]string{
+		"key1": "value1",
+		"key2": "value2",
+	})
+
+	data, err := json.Marshal(before)
+	assert.NoError(t, err)
+
+	var after SetMetadataOperation
+	err = json.Unmarshal(data, &after)
+	assert.NoError(t, err)
+
+	assert.Equal(t, before, &after)
+}

bug/op_set_status.go πŸ”—

@@ -1,7 +1,11 @@
 package bug
 
 import (
+	"encoding/json"
+
+	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/util/git"
+	"github.com/MichaelMure/git-bug/util/timestamp"
 	"github.com/pkg/errors"
 )
 
@@ -10,7 +14,7 @@ var _ Operation = &SetStatusOperation{}
 // SetStatusOperation will change the status of a bug
 type SetStatusOperation struct {
 	OpBase
-	Status Status `json:"status"`
+	Status Status
 }
 
 func (op *SetStatusOperation) base() *OpBase {
@@ -34,7 +38,7 @@ func (op *SetStatusOperation) Apply(snapshot *Snapshot) {
 	item := &SetStatusTimelineItem{
 		hash:     hash,
 		Author:   op.Author,
-		UnixTime: Timestamp(op.UnixTime),
+		UnixTime: timestamp.Timestamp(op.UnixTime),
 		Status:   op.Status,
 	}
 
@@ -53,10 +57,55 @@ func (op *SetStatusOperation) Validate() error {
 	return nil
 }
 
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *SetStatusOperation) MarshalJSON() ([]byte, error) {
+	base, err := json.Marshal(op.OpBase)
+	if err != nil {
+		return nil, err
+	}
+
+	// revert back to a flat map to be able to add our own fields
+	var data map[string]interface{}
+	if err := json.Unmarshal(base, &data); err != nil {
+		return nil, err
+	}
+
+	data["status"] = op.Status
+
+	return json.Marshal(data)
+}
+
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *SetStatusOperation) UnmarshalJSON(data []byte) error {
+	// Unmarshal OpBase and the op separately
+
+	base := OpBase{}
+	err := json.Unmarshal(data, &base)
+	if err != nil {
+		return err
+	}
+
+	aux := struct {
+		Status Status `json:"status"`
+	}{}
+
+	err = json.Unmarshal(data, &aux)
+	if err != nil {
+		return err
+	}
+
+	op.OpBase = base
+	op.Status = aux.Status
+
+	return nil
+}
+
 // Sign post method for gqlgen
 func (op *SetStatusOperation) IsAuthored() {}
 
-func NewSetStatusOp(author Person, unixTime int64, status Status) *SetStatusOperation {
+func NewSetStatusOp(author identity.Interface, unixTime int64, status Status) *SetStatusOperation {
 	return &SetStatusOperation{
 		OpBase: newOpBase(SetStatusOp, author, unixTime),
 		Status: status,
@@ -65,8 +114,8 @@ func NewSetStatusOp(author Person, unixTime int64, status Status) *SetStatusOper
 
 type SetStatusTimelineItem struct {
 	hash     git.Hash
-	Author   Person
-	UnixTime Timestamp
+	Author   identity.Interface
+	UnixTime timestamp.Timestamp
 	Status   Status
 }
 
@@ -75,7 +124,7 @@ func (s SetStatusTimelineItem) Hash() git.Hash {
 }
 
 // Convenience function to apply the operation
-func Open(b Interface, author Person, unixTime int64) (*SetStatusOperation, error) {
+func Open(b Interface, author identity.Interface, unixTime int64) (*SetStatusOperation, error) {
 	op := NewSetStatusOp(author, unixTime, OpenStatus)
 	if err := op.Validate(); err != nil {
 		return nil, err
@@ -85,7 +134,7 @@ func Open(b Interface, author Person, unixTime int64) (*SetStatusOperation, erro
 }
 
 // Convenience function to apply the operation
-func Close(b Interface, author Person, unixTime int64) (*SetStatusOperation, error) {
+func Close(b Interface, author identity.Interface, unixTime int64) (*SetStatusOperation, error) {
 	op := NewSetStatusOp(author, unixTime, ClosedStatus)
 	if err := op.Validate(); err != nil {
 		return nil, err

bug/op_set_status_test.go πŸ”—

@@ -0,0 +1,25 @@
+package bug
+
+import (
+	"encoding/json"
+	"testing"
+	"time"
+
+	"github.com/MichaelMure/git-bug/identity"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestSetStatusSerialize(t *testing.T) {
+	var rene = identity.NewBare("RenΓ© Descartes", "rene@descartes.fr")
+	unix := time.Now().Unix()
+	before := NewSetStatusOp(rene, unix, ClosedStatus)
+
+	data, err := json.Marshal(before)
+	assert.NoError(t, err)
+
+	var after SetStatusOperation
+	err = json.Unmarshal(data, &after)
+	assert.NoError(t, err)
+
+	assert.Equal(t, before, &after)
+}

bug/op_set_title.go πŸ”—

@@ -1,9 +1,13 @@
 package bug
 
 import (
+	"encoding/json"
 	"fmt"
 	"strings"
 
+	"github.com/MichaelMure/git-bug/identity"
+	"github.com/MichaelMure/git-bug/util/timestamp"
+
 	"github.com/MichaelMure/git-bug/util/git"
 	"github.com/MichaelMure/git-bug/util/text"
 )
@@ -13,8 +17,8 @@ var _ Operation = &SetTitleOperation{}
 // SetTitleOperation will change the title of a bug
 type SetTitleOperation struct {
 	OpBase
-	Title string `json:"title"`
-	Was   string `json:"was"`
+	Title string
+	Was   string
 }
 
 func (op *SetTitleOperation) base() *OpBase {
@@ -38,7 +42,7 @@ func (op *SetTitleOperation) Apply(snapshot *Snapshot) {
 	item := &SetTitleTimelineItem{
 		hash:     hash,
 		Author:   op.Author,
-		UnixTime: Timestamp(op.UnixTime),
+		UnixTime: timestamp.Timestamp(op.UnixTime),
 		Title:    op.Title,
 		Was:      op.Was,
 	}
@@ -74,10 +78,58 @@ func (op *SetTitleOperation) Validate() error {
 	return nil
 }
 
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *SetTitleOperation) MarshalJSON() ([]byte, error) {
+	base, err := json.Marshal(op.OpBase)
+	if err != nil {
+		return nil, err
+	}
+
+	// revert back to a flat map to be able to add our own fields
+	var data map[string]interface{}
+	if err := json.Unmarshal(base, &data); err != nil {
+		return nil, err
+	}
+
+	data["title"] = op.Title
+	data["was"] = op.Was
+
+	return json.Marshal(data)
+}
+
+// Workaround to avoid the inner OpBase.MarshalJSON overriding the outer op
+// MarshalJSON
+func (op *SetTitleOperation) UnmarshalJSON(data []byte) error {
+	// Unmarshal OpBase and the op separately
+
+	base := OpBase{}
+	err := json.Unmarshal(data, &base)
+	if err != nil {
+		return err
+	}
+
+	aux := struct {
+		Title string `json:"title"`
+		Was   string `json:"was"`
+	}{}
+
+	err = json.Unmarshal(data, &aux)
+	if err != nil {
+		return err
+	}
+
+	op.OpBase = base
+	op.Title = aux.Title
+	op.Was = aux.Was
+
+	return nil
+}
+
 // Sign post method for gqlgen
 func (op *SetTitleOperation) IsAuthored() {}
 
-func NewSetTitleOp(author Person, unixTime int64, title string, was string) *SetTitleOperation {
+func NewSetTitleOp(author identity.Interface, unixTime int64, title string, was string) *SetTitleOperation {
 	return &SetTitleOperation{
 		OpBase: newOpBase(SetTitleOp, author, unixTime),
 		Title:  title,
@@ -87,8 +139,8 @@ func NewSetTitleOp(author Person, unixTime int64, title string, was string) *Set
 
 type SetTitleTimelineItem struct {
 	hash     git.Hash
-	Author   Person
-	UnixTime Timestamp
+	Author   identity.Interface
+	UnixTime timestamp.Timestamp
 	Title    string
 	Was      string
 }
@@ -98,7 +150,7 @@ func (s SetTitleTimelineItem) Hash() git.Hash {
 }
 
 // Convenience function to apply the operation
-func SetTitle(b Interface, author Person, unixTime int64, title string) (*SetTitleOperation, error) {
+func SetTitle(b Interface, author identity.Interface, unixTime int64, title string) (*SetTitleOperation, error) {
 	it := NewOperationIterator(b)
 
 	var lastTitleOp Operation

bug/op_set_title_test.go πŸ”—

@@ -0,0 +1,25 @@
+package bug
+
+import (
+	"encoding/json"
+	"testing"
+	"time"
+
+	"github.com/MichaelMure/git-bug/identity"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestSetTitleSerialize(t *testing.T) {
+	var rene = identity.NewBare("RenΓ© Descartes", "rene@descartes.fr")
+	unix := time.Now().Unix()
+	before := NewSetTitleOp(rene, unix, "title", "was")
+
+	data, err := json.Marshal(before)
+	assert.NoError(t, err)
+
+	var after SetTitleOperation
+	err = json.Unmarshal(data, &after)
+	assert.NoError(t, err)
+
+	assert.Equal(t, before, &after)
+}

bug/operation.go πŸ”—

@@ -6,6 +6,8 @@ import (
 	"fmt"
 	"time"
 
+	"github.com/MichaelMure/git-bug/identity"
+
 	"github.com/MichaelMure/git-bug/util/git"
 	"github.com/pkg/errors"
 )
@@ -76,19 +78,19 @@ func hashOperation(op Operation) (git.Hash, error) {
 
 // OpBase implement the common code for all operations
 type OpBase struct {
-	OperationType OperationType     `json:"type"`
-	Author        Person            `json:"author"`
-	UnixTime      int64             `json:"timestamp"`
-	Metadata      map[string]string `json:"metadata,omitempty"`
+	OperationType OperationType
+	Author        identity.Interface
+	UnixTime      int64
+	Metadata      map[string]string
 	// Not serialized. Store the op's hash in memory.
 	hash git.Hash
-	// Not serialized. Store the extra metadata compiled from SetMetadataOperation
-	// in memory.
+	// Not serialized. Store the extra metadata in memory,
+	// compiled from SetMetadataOperation.
 	extraMetadata map[string]string
 }
 
 // newOpBase is the constructor for an OpBase
-func newOpBase(opType OperationType, author Person, unixTime int64) OpBase {
+func newOpBase(opType OperationType, author identity.Interface, unixTime int64) OpBase {
 	return OpBase{
 		OperationType: opType,
 		Author:        author,
@@ -96,6 +98,46 @@ func newOpBase(opType OperationType, author Person, unixTime int64) OpBase {
 	}
 }
 
+func (op OpBase) MarshalJSON() ([]byte, error) {
+	return json.Marshal(struct {
+		OperationType OperationType      `json:"type"`
+		Author        identity.Interface `json:"author"`
+		UnixTime      int64              `json:"timestamp"`
+		Metadata      map[string]string  `json:"metadata,omitempty"`
+	}{
+		OperationType: op.OperationType,
+		Author:        op.Author,
+		UnixTime:      op.UnixTime,
+		Metadata:      op.Metadata,
+	})
+}
+
+func (op *OpBase) UnmarshalJSON(data []byte) error {
+	aux := struct {
+		OperationType OperationType     `json:"type"`
+		Author        json.RawMessage   `json:"author"`
+		UnixTime      int64             `json:"timestamp"`
+		Metadata      map[string]string `json:"metadata,omitempty"`
+	}{}
+
+	if err := json.Unmarshal(data, &aux); err != nil {
+		return err
+	}
+
+	// delegate the decoding of the identity
+	author, err := identity.UnmarshalJSON(aux.Author)
+	if err != nil {
+		return err
+	}
+
+	op.OperationType = aux.OperationType
+	op.Author = author
+	op.UnixTime = aux.UnixTime
+	op.Metadata = aux.Metadata
+
+	return nil
+}
+
 // Time return the time when the operation was added
 func (op *OpBase) Time() time.Time {
 	return time.Unix(op.UnixTime, 0)
@@ -117,14 +159,14 @@ func opBaseValidate(op Operation, opType OperationType) error {
 		return fmt.Errorf("incorrect operation type (expected: %v, actual: %v)", opType, op.base().OperationType)
 	}
 
-	if _, err := op.Hash(); err != nil {
-		return errors.Wrap(err, "op is not serializable")
-	}
-
 	if op.GetUnixTime() == 0 {
 		return fmt.Errorf("time not set")
 	}
 
+	if op.base().Author == nil {
+		return fmt.Errorf("author not set")
+	}
+
 	if err := op.base().Author.Validate(); err != nil {
 		return errors.Wrap(err, "author")
 	}

bug/operation_iterator_test.go πŸ”—

@@ -1,29 +1,39 @@
 package bug
 
 import (
+	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/repository"
+	"github.com/stretchr/testify/assert"
+
 	"testing"
 	"time"
 )
 
-var (
-	rene = Person{
-		Name:  "RenΓ© Descartes",
-		Email: "rene@descartes.fr",
-	}
+func ExampleOperationIterator() {
+	b := NewBug()
 
-	unix = time.Now().Unix()
+	// add operations
 
-	createOp      = NewCreateOp(rene, unix, "title", "message", nil)
-	setTitleOp    = NewSetTitleOp(rene, unix, "title2", "title1")
-	addCommentOp  = NewAddCommentOp(rene, unix, "message2", nil)
-	setStatusOp   = NewSetStatusOp(rene, unix, ClosedStatus)
-	labelChangeOp = NewLabelChangeOperation(rene, unix, []Label{"added"}, []Label{"removed"})
-)
+	it := NewOperationIterator(b)
+
+	for it.Next() {
+		// do something with each operations
+		_ = it.Value()
+	}
+}
 
 func TestOpIterator(t *testing.T) {
 	mockRepo := repository.NewMockRepoForTest()
 
+	rene := identity.NewIdentity("RenΓ© Descartes", "rene@descartes.fr")
+	unix := time.Now().Unix()
+
+	createOp := NewCreateOp(rene, unix, "title", "message", nil)
+	setTitleOp := NewSetTitleOp(rene, unix, "title2", "title1")
+	addCommentOp := NewAddCommentOp(rene, unix, "message2", nil)
+	setStatusOp := NewSetStatusOp(rene, unix, ClosedStatus)
+	labelChangeOp := NewLabelChangeOperation(rene, unix, []Label{"added"}, []Label{"removed"})
+
 	bug1 := NewBug()
 
 	// first pack
@@ -32,13 +42,15 @@ func TestOpIterator(t *testing.T) {
 	bug1.Append(addCommentOp)
 	bug1.Append(setStatusOp)
 	bug1.Append(labelChangeOp)
-	bug1.Commit(mockRepo)
+	err := bug1.Commit(mockRepo)
+	assert.NoError(t, err)
 
 	// second pack
 	bug1.Append(setTitleOp)
 	bug1.Append(setTitleOp)
 	bug1.Append(setTitleOp)
-	bug1.Commit(mockRepo)
+	err = bug1.Commit(mockRepo)
+	assert.NoError(t, err)
 
 	// staging
 	bug1.Append(setTitleOp)

bug/operation_pack.go πŸ”—

@@ -20,7 +20,7 @@ const formatVersion = 1
 type OperationPack struct {
 	Operations []Operation
 
-	// Private field so not serialized by gob
+	// Private field so not serialized
 	commitHash git.Hash
 }
 
@@ -57,6 +57,7 @@ func (opp *OperationPack) UnmarshalJSON(data []byte) error {
 			return err
 		}
 
+		// delegate to specialized unmarshal function
 		op, err := opp.unmarshalOp(raw, t.OperationType)
 		if err != nil {
 			return err
@@ -73,28 +74,36 @@ func (opp *OperationPack) UnmarshalJSON(data []byte) error {
 
 func (opp *OperationPack) unmarshalOp(raw []byte, _type OperationType) (Operation, error) {
 	switch _type {
+	case AddCommentOp:
+		op := &AddCommentOperation{}
+		err := json.Unmarshal(raw, &op)
+		return op, err
 	case CreateOp:
 		op := &CreateOperation{}
 		err := json.Unmarshal(raw, &op)
 		return op, err
-	case SetTitleOp:
-		op := &SetTitleOperation{}
+	case EditCommentOp:
+		op := &EditCommentOperation{}
 		err := json.Unmarshal(raw, &op)
 		return op, err
-	case AddCommentOp:
-		op := &AddCommentOperation{}
+	case LabelChangeOp:
+		op := &LabelChangeOperation{}
 		err := json.Unmarshal(raw, &op)
 		return op, err
-	case SetStatusOp:
-		op := &SetStatusOperation{}
+	case NoOpOp:
+		op := &NoOpOperation{}
 		err := json.Unmarshal(raw, &op)
 		return op, err
-	case LabelChangeOp:
-		op := &LabelChangeOperation{}
+	case SetMetadataOp:
+		op := &SetMetadataOperation{}
 		err := json.Unmarshal(raw, &op)
 		return op, err
-	case EditCommentOp:
-		op := &EditCommentOperation{}
+	case SetStatusOp:
+		op := &SetStatusOperation{}
+		err := json.Unmarshal(raw, &op)
+		return op, err
+	case SetTitleOp:
+		op := &SetTitleOperation{}
 		err := json.Unmarshal(raw, &op)
 		return op, err
 	default:
@@ -129,7 +138,21 @@ func (opp *OperationPack) Validate() error {
 
 // Write will serialize and store the OperationPack as a git blob and return
 // its hash
-func (opp *OperationPack) Write(repo repository.Repo) (git.Hash, error) {
+func (opp *OperationPack) Write(repo repository.ClockedRepo) (git.Hash, error) {
+	// make sure we don't write invalid data
+	err := opp.Validate()
+	if err != nil {
+		return "", errors.Wrap(err, "validation error")
+	}
+
+	// First, make sure that all the identities are properly Commit as well
+	for _, op := range opp.Operations {
+		err := op.base().Author.CommitAsNeeded(repo)
+		if err != nil {
+			return "", err
+		}
+	}
+
 	data, err := json.Marshal(opp)
 
 	if err != nil {

bug/operation_pack_test.go πŸ”—

@@ -3,51 +3,59 @@ package bug
 import (
 	"encoding/json"
 	"testing"
+	"time"
 
+	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/util/git"
-	"github.com/go-test/deep"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestOperationPackSerialize(t *testing.T) {
 	opp := &OperationPack{}
 
+	rene := identity.NewBare("RenΓ© Descartes", "rene@descartes.fr")
+	createOp := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil)
+	setTitleOp := NewSetTitleOp(rene, time.Now().Unix(), "title2", "title1")
+	addCommentOp := NewAddCommentOp(rene, time.Now().Unix(), "message2", nil)
+	setStatusOp := NewSetStatusOp(rene, time.Now().Unix(), ClosedStatus)
+	labelChangeOp := NewLabelChangeOperation(rene, time.Now().Unix(), []Label{"added"}, []Label{"removed"})
+
 	opp.Append(createOp)
 	opp.Append(setTitleOp)
 	opp.Append(addCommentOp)
 	opp.Append(setStatusOp)
 	opp.Append(labelChangeOp)
 
-	opMeta := NewCreateOp(rene, unix, "title", "message", nil)
+	opMeta := NewSetTitleOp(rene, time.Now().Unix(), "title3", "title2")
 	opMeta.SetMetadata("key", "value")
 	opp.Append(opMeta)
 
-	if len(opMeta.Metadata) != 1 {
-		t.Fatal()
-	}
+	assert.Equal(t, 1, len(opMeta.Metadata))
 
-	opFile := NewCreateOp(rene, unix, "title", "message", []git.Hash{
+	opFile := NewAddCommentOp(rene, time.Now().Unix(), "message", []git.Hash{
 		"abcdef",
 		"ghijkl",
 	})
 	opp.Append(opFile)
 
-	if len(opFile.Files) != 2 {
-		t.Fatal()
-	}
+	assert.Equal(t, 2, len(opFile.Files))
 
 	data, err := json.Marshal(opp)
-	if err != nil {
-		t.Fatal(err)
-	}
+	assert.NoError(t, err)
 
 	var opp2 *OperationPack
 	err = json.Unmarshal(data, &opp2)
-	if err != nil {
-		t.Fatal(err)
-	}
+	assert.NoError(t, err)
+
+	ensureHash(t, opp)
+
+	assert.Equal(t, opp, opp2)
+}
 
-	deep.CompareUnexportedFields = false
-	if diff := deep.Equal(opp, opp2); diff != nil {
-		t.Fatal(diff)
+func ensureHash(t *testing.T, opp *OperationPack) {
+	for _, op := range opp.Operations {
+		_, err := op.Hash()
+		require.NoError(t, err)
 	}
 }

bug/operation_test.go πŸ”—

@@ -2,13 +2,19 @@ package bug
 
 import (
 	"testing"
+	"time"
 
+	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/repository"
 	"github.com/MichaelMure/git-bug/util/git"
+	"github.com/MichaelMure/git-bug/util/test"
 	"github.com/stretchr/testify/require"
 )
 
 func TestValidate(t *testing.T) {
+	rene := identity.NewIdentity("RenΓ© Descartes", "rene@descartes.fr")
+	unix := time.Now().Unix()
+
 	good := []Operation{
 		NewCreateOp(rene, unix, "title", "message", nil),
 		NewSetTitleOp(rene, unix, "title2", "title1"),
@@ -25,11 +31,11 @@ func TestValidate(t *testing.T) {
 
 	bad := []Operation{
 		// opbase
-		NewSetStatusOp(Person{Name: "", Email: "rene@descartes.fr"}, unix, ClosedStatus),
-		NewSetStatusOp(Person{Name: "RenΓ© Descartes\u001b", Email: "rene@descartes.fr"}, unix, ClosedStatus),
-		NewSetStatusOp(Person{Name: "RenΓ© Descartes", Email: "rene@descartes.fr\u001b"}, unix, ClosedStatus),
-		NewSetStatusOp(Person{Name: "RenΓ© \nDescartes", Email: "rene@descartes.fr"}, unix, ClosedStatus),
-		NewSetStatusOp(Person{Name: "RenΓ© Descartes", Email: "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,
@@ -63,7 +69,8 @@ func TestValidate(t *testing.T) {
 }
 
 func TestMetadata(t *testing.T) {
-	op := NewCreateOp(rene, unix, "title", "message", nil)
+	rene := identity.NewIdentity("RenΓ© Descartes", "rene@descartes.fr")
+	op := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil)
 
 	op.SetMetadata("key", "value")
 
@@ -75,11 +82,13 @@ func TestMetadata(t *testing.T) {
 func TestHash(t *testing.T) {
 	repos := []repository.ClockedRepo{
 		repository.NewMockRepoForTest(),
-		createRepo(false),
+		test.CreateRepo(false),
 	}
 
 	for _, repo := range repos {
-		b, op, err := Create(rene, unix, "title", "message")
+		rene := identity.NewBare("RenΓ© Descartes", "rene@descartes.fr")
+
+		b, op, err := Create(rene, time.Now().Unix(), "title", "message")
 		require.Nil(t, err)
 
 		h1, err := op.Hash()

bug/person.go πŸ”—

@@ -1,95 +0,0 @@
-package bug
-
-import (
-	"errors"
-	"fmt"
-	"strings"
-
-	"github.com/MichaelMure/git-bug/repository"
-	"github.com/MichaelMure/git-bug/util/text"
-)
-
-type Person struct {
-	Name      string `json:"name"`
-	Email     string `json:"email"`
-	Login     string `json:"login"`
-	AvatarUrl string `json:"avatar_url"`
-}
-
-// GetUser will query the repository for user detail and build the corresponding Person
-func GetUser(repo repository.Repo) (Person, error) {
-	name, err := repo.GetUserName()
-	if err != nil {
-		return Person{}, err
-	}
-	if name == "" {
-		return Person{}, errors.New("User name is not configured in git yet. Please use `git config --global user.name \"John Doe\"`")
-	}
-
-	email, err := repo.GetUserEmail()
-	if err != nil {
-		return Person{}, err
-	}
-	if email == "" {
-		return Person{}, errors.New("User name is not configured in git yet. Please use `git config --global user.email johndoe@example.com`")
-	}
-
-	return Person{Name: name, Email: email}, nil
-}
-
-// Match tell is the Person match the given query string
-func (p Person) Match(query string) bool {
-	query = strings.ToLower(query)
-
-	return strings.Contains(strings.ToLower(p.Name), query) ||
-		strings.Contains(strings.ToLower(p.Login), query)
-}
-
-func (p Person) Validate() error {
-	if text.Empty(p.Name) && text.Empty(p.Login) {
-		return fmt.Errorf("either name or login should be set")
-	}
-
-	if strings.Contains(p.Name, "\n") {
-		return fmt.Errorf("name should be a single line")
-	}
-
-	if !text.Safe(p.Name) {
-		return fmt.Errorf("name is not fully printable")
-	}
-
-	if strings.Contains(p.Login, "\n") {
-		return fmt.Errorf("login should be a single line")
-	}
-
-	if !text.Safe(p.Login) {
-		return fmt.Errorf("login is not fully printable")
-	}
-
-	if strings.Contains(p.Email, "\n") {
-		return fmt.Errorf("email should be a single line")
-	}
-
-	if !text.Safe(p.Email) {
-		return fmt.Errorf("email is not fully printable")
-	}
-
-	if p.AvatarUrl != "" && !text.ValidUrl(p.AvatarUrl) {
-		return fmt.Errorf("avatarUrl is not a valid URL")
-	}
-
-	return nil
-}
-
-func (p Person) DisplayName() string {
-	switch {
-	case p.Name == "" && p.Login != "":
-		return p.Login
-	case p.Name != "" && p.Login == "":
-		return p.Name
-	case p.Name != "" && p.Login != "":
-		return fmt.Sprintf("%s (%s)", p.Name, p.Login)
-	}
-
-	panic("invalid person data")
-}

bug/snapshot.go πŸ”—

@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"time"
 
+	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/util/git"
 )
 
@@ -15,7 +16,7 @@ type Snapshot struct {
 	Title     string
 	Comments  []Comment
 	Labels    []Label
-	Author    Person
+	Author    identity.Interface
 	CreatedAt time.Time
 
 	Timeline []TimelineItem

bug/timeline.go πŸ”—

@@ -3,7 +3,9 @@ package bug
 import (
 	"strings"
 
+	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/util/git"
+	"github.com/MichaelMure/git-bug/util/timestamp"
 )
 
 type TimelineItem interface {
@@ -15,20 +17,20 @@ type TimelineItem interface {
 type CommentHistoryStep struct {
 	// The author of the edition, not necessarily the same as the author of the
 	// original comment
-	Author Person
+	Author identity.Interface
 	// The new message
 	Message  string
-	UnixTime Timestamp
+	UnixTime timestamp.Timestamp
 }
 
 // CommentTimelineItem is a TimelineItem that holds a Comment and its edition history
 type CommentTimelineItem struct {
 	hash      git.Hash
-	Author    Person
+	Author    identity.Interface
 	Message   string
 	Files     []git.Hash
-	CreatedAt Timestamp
-	LastEdit  Timestamp
+	CreatedAt timestamp.Timestamp
+	LastEdit  timestamp.Timestamp
 	History   []CommentHistoryStep
 }
 

cache/bug_cache.go πŸ”—

@@ -9,6 +9,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
@@ -53,8 +57,8 @@ func (e ErrMultipleMatchOp) Error() string {
 	return fmt.Sprintf("Multiple matching operation found:\n%s", strings.Join(casted, "\n"))
 }
 
-// ResolveTargetWithMetadata will find an operation that has the matching metadata
-func (c *BugCache) ResolveTargetWithMetadata(key string, value string) (git.Hash, error) {
+// ResolveOperationWithMetadata will find an operation that has the matching metadata
+func (c *BugCache) ResolveOperationWithMetadata(key string, value string) (git.Hash, error) {
 	// preallocate but empty
 	matching := make([]git.Hash, 0, 5)
 
@@ -82,45 +86,45 @@ func (c *BugCache) ResolveTargetWithMetadata(key string, value string) (git.Hash
 	return matching[0], nil
 }
 
-func (c *BugCache) AddComment(message string) error {
+func (c *BugCache) AddComment(message string) (*bug.AddCommentOperation, error) {
 	return c.AddCommentWithFiles(message, nil)
 }
 
-func (c *BugCache) AddCommentWithFiles(message string, files []git.Hash) error {
-	author, err := bug.GetUser(c.repoCache.repo)
+func (c *BugCache) AddCommentWithFiles(message string, files []git.Hash) (*bug.AddCommentOperation, error) {
+	author, err := c.repoCache.GetUserIdentity()
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	return c.AddCommentRaw(author, time.Now().Unix(), message, files, nil)
 }
 
-func (c *BugCache) AddCommentRaw(author bug.Person, 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) (*bug.AddCommentOperation, error) {
+	op, err := bug.AddCommentWithFiles(c.bug, author.Identity, unixTime, message, files)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	for key, value := range metadata {
 		op.SetMetadata(key, value)
 	}
 
-	return c.notifyUpdated()
+	return op, c.notifyUpdated()
 }
 
-func (c *BugCache) ChangeLabels(added []string, removed []string) ([]bug.LabelChangeResult, error) {
-	author, err := bug.GetUser(c.repoCache.repo)
+func (c *BugCache) ChangeLabels(added []string, removed []string) ([]bug.LabelChangeResult, *bug.LabelChangeOperation, error) {
+	author, err := c.repoCache.GetUserIdentity()
 	if err != nil {
-		return nil, err
+		return nil, nil, err
 	}
 
 	return c.ChangeLabelsRaw(author, time.Now().Unix(), added, removed, nil)
 }
 
-func (c *BugCache) ChangeLabelsRaw(author bug.Person, 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, *bug.LabelChangeOperation, error) {
+	changes, op, err := bug.ChangeLabels(c.bug, author.Identity, unixTime, added, removed)
 	if err != nil {
-		return changes, err
+		return changes, nil, err
 	}
 
 	for key, value := range metadata {
@@ -129,107 +133,112 @@ func (c *BugCache) ChangeLabelsRaw(author bug.Person, unixTime int64, added []st
 
 	err = c.notifyUpdated()
 	if err != nil {
-		return nil, err
+		return nil, nil, err
 	}
 
-	return changes, nil
+	return changes, op, nil
 }
 
-func (c *BugCache) Open() error {
-	author, err := bug.GetUser(c.repoCache.repo)
+func (c *BugCache) Open() (*bug.SetStatusOperation, error) {
+	author, err := c.repoCache.GetUserIdentity()
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	return c.OpenRaw(author, time.Now().Unix(), nil)
 }
 
-func (c *BugCache) OpenRaw(author bug.Person, 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) (*bug.SetStatusOperation, error) {
+	op, err := bug.Open(c.bug, author.Identity, unixTime)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	for key, value := range metadata {
 		op.SetMetadata(key, value)
 	}
 
-	return c.notifyUpdated()
+	return op, c.notifyUpdated()
 }
 
-func (c *BugCache) Close() error {
-	author, err := bug.GetUser(c.repoCache.repo)
+func (c *BugCache) Close() (*bug.SetStatusOperation, error) {
+	author, err := c.repoCache.GetUserIdentity()
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	return c.CloseRaw(author, time.Now().Unix(), nil)
 }
 
-func (c *BugCache) CloseRaw(author bug.Person, 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) (*bug.SetStatusOperation, error) {
+	op, err := bug.Close(c.bug, author.Identity, unixTime)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	for key, value := range metadata {
 		op.SetMetadata(key, value)
 	}
 
-	return c.notifyUpdated()
+	return op, c.notifyUpdated()
 }
 
-func (c *BugCache) SetTitle(title string) error {
-	author, err := bug.GetUser(c.repoCache.repo)
+func (c *BugCache) SetTitle(title string) (*bug.SetTitleOperation, error) {
+	author, err := c.repoCache.GetUserIdentity()
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	return c.SetTitleRaw(author, time.Now().Unix(), title, nil)
 }
 
-func (c *BugCache) SetTitleRaw(author bug.Person, 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) (*bug.SetTitleOperation, error) {
+	op, err := bug.SetTitle(c.bug, author.Identity, unixTime, title)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	for key, value := range metadata {
 		op.SetMetadata(key, value)
 	}
 
-	return c.notifyUpdated()
+	return op, c.notifyUpdated()
 }
 
-func (c *BugCache) EditComment(target git.Hash, message string) error {
-	author, err := bug.GetUser(c.repoCache.repo)
+func (c *BugCache) EditComment(target git.Hash, message string) (*bug.EditCommentOperation, error) {
+	author, err := c.repoCache.GetUserIdentity()
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	return c.EditCommentRaw(author, time.Now().Unix(), target, message, nil)
 }
 
-func (c *BugCache) EditCommentRaw(author bug.Person, 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) (*bug.EditCommentOperation, error) {
+	op, err := bug.EditComment(c.bug, author.Identity, unixTime, target, message)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	for key, value := range metadata {
 		op.SetMetadata(key, value)
 	}
 
-	return c.notifyUpdated()
+	return op, c.notifyUpdated()
 }
 
 func (c *BugCache) Commit() error {
-	return c.bug.Commit(c.repoCache.repo)
+	err := c.bug.Commit(c.repoCache.repo)
+	if err != nil {
+		return err
+	}
+	return c.notifyUpdated()
 }
 
 func (c *BugCache) CommitAsNeeded() error {
-	if c.bug.HasPendingOp() {
-		return c.bug.Commit(c.repoCache.repo)
+	err := c.bug.CommitAsNeeded(c.repoCache.repo)
+	if err != nil {
+		return err
 	}
-	return nil
+	return c.notifyUpdated()
 }

cache/bug_excerpt.go πŸ”—

@@ -4,9 +4,15 @@ import (
 	"encoding/gob"
 
 	"github.com/MichaelMure/git-bug/bug"
+	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/util/lamport"
 )
 
+// Package initialisation used to register the type for (de)serialization
+func init() {
+	gob.Register(BugExcerpt{})
+}
+
 // BugExcerpt hold a subset of the bug values to be able to sort and filter bugs
 // efficiently without having to read and compile each raw bugs.
 type BugExcerpt struct {
@@ -18,29 +24,52 @@ type BugExcerpt struct {
 	EditUnixTime      int64
 
 	Status bug.Status
-	Author bug.Person
 	Labels []bug.Label
 
+	// If author is identity.Bare, LegacyAuthor is set
+	// If author is identity.Identity, AuthorId is set and data is deported
+	// in a IdentityExcerpt
+	LegacyAuthor LegacyAuthorExcerpt
+	AuthorId     string
+
 	CreateMetadata map[string]string
 }
 
+// identity.Bare data are directly embedded in the bug excerpt
+type LegacyAuthorExcerpt struct {
+	Name  string
+	Login string
+}
+
 func NewBugExcerpt(b bug.Interface, snap *bug.Snapshot) *BugExcerpt {
-	return &BugExcerpt{
+	e := &BugExcerpt{
 		Id:                b.Id(),
 		CreateLamportTime: b.CreateLamportTime(),
 		EditLamportTime:   b.EditLamportTime(),
 		CreateUnixTime:    b.FirstOp().GetUnixTime(),
 		EditUnixTime:      snap.LastEditUnix(),
 		Status:            snap.Status,
-		Author:            snap.Author,
 		Labels:            snap.Labels,
 		CreateMetadata:    b.FirstOp().AllMetadata(),
 	}
+
+	switch snap.Author.(type) {
+	case *identity.Identity:
+		e.AuthorId = snap.Author.Id()
+	case *identity.Bare:
+		e.LegacyAuthor = LegacyAuthorExcerpt{
+			Login: snap.Author.Login(),
+			Name:  snap.Author.Name(),
+		}
+	default:
+		panic("unhandled identity type")
+	}
+
+	return e
 }
 
-// Package initialisation used to register the type for (de)serialization
-func init() {
-	gob.Register(BugExcerpt{})
+func (b *BugExcerpt) HumanId() string {
+	return bug.FormatHumanID(b.Id)
 }
 
 /*

cache/filter.go πŸ”—

@@ -1,11 +1,13 @@
 package cache
 
 import (
+	"strings"
+
 	"github.com/MichaelMure/git-bug/bug"
 )
 
-// Filter is a functor that match a subset of bugs
-type Filter func(excerpt *BugExcerpt) bool
+// Filter is a predicate that match a subset of bugs
+type Filter func(repoCache *RepoCache, excerpt *BugExcerpt) bool
 
 // StatusFilter return a Filter that match a bug status
 func StatusFilter(query string) (Filter, error) {
@@ -14,21 +16,36 @@ func StatusFilter(query string) (Filter, error) {
 		return nil, err
 	}
 
-	return func(excerpt *BugExcerpt) bool {
+	return func(repoCache *RepoCache, excerpt *BugExcerpt) bool {
 		return excerpt.Status == status
 	}, nil
 }
 
 // AuthorFilter return a Filter that match a bug author
 func AuthorFilter(query string) Filter {
-	return func(excerpt *BugExcerpt) bool {
-		return excerpt.Author.Match(query)
+	return func(repoCache *RepoCache, excerpt *BugExcerpt) bool {
+		query = strings.ToLower(query)
+
+		// Normal identity
+		if excerpt.AuthorId != "" {
+			author, ok := repoCache.identitiesExcerpts[excerpt.AuthorId]
+			if !ok {
+				panic("missing identity in the cache")
+			}
+
+			return strings.Contains(strings.ToLower(author.Name), query) ||
+				strings.Contains(strings.ToLower(author.Login), query)
+		}
+
+		// Legacy identity support
+		return strings.Contains(strings.ToLower(excerpt.LegacyAuthor.Name), query) ||
+			strings.Contains(strings.ToLower(excerpt.LegacyAuthor.Login), query)
 	}
 }
 
 // LabelFilter return a Filter that match a label
 func LabelFilter(label string) Filter {
-	return func(excerpt *BugExcerpt) bool {
+	return func(repoCache *RepoCache, excerpt *BugExcerpt) bool {
 		for _, l := range excerpt.Labels {
 			if string(l) == label {
 				return true
@@ -40,7 +57,7 @@ func LabelFilter(label string) Filter {
 
 // NoLabelFilter return a Filter that match the absence of labels
 func NoLabelFilter() Filter {
-	return func(excerpt *BugExcerpt) bool {
+	return func(repoCache *RepoCache, excerpt *BugExcerpt) bool {
 		return len(excerpt.Labels) == 0
 	}
 }
@@ -54,20 +71,20 @@ type Filters struct {
 }
 
 // Match check if a bug match the set of filters
-func (f *Filters) Match(excerpt *BugExcerpt) bool {
-	if match := f.orMatch(f.Status, excerpt); !match {
+func (f *Filters) Match(repoCache *RepoCache, excerpt *BugExcerpt) bool {
+	if match := f.orMatch(f.Status, repoCache, excerpt); !match {
 		return false
 	}
 
-	if match := f.orMatch(f.Author, excerpt); !match {
+	if match := f.orMatch(f.Author, repoCache, excerpt); !match {
 		return false
 	}
 
-	if match := f.orMatch(f.Label, excerpt); !match {
+	if match := f.orMatch(f.Label, repoCache, excerpt); !match {
 		return false
 	}
 
-	if match := f.andMatch(f.NoFilters, excerpt); !match {
+	if match := f.andMatch(f.NoFilters, repoCache, excerpt); !match {
 		return false
 	}
 
@@ -75,28 +92,28 @@ func (f *Filters) Match(excerpt *BugExcerpt) bool {
 }
 
 // Check if any of the filters provided match the bug
-func (*Filters) orMatch(filters []Filter, excerpt *BugExcerpt) bool {
+func (*Filters) orMatch(filters []Filter, repoCache *RepoCache, excerpt *BugExcerpt) bool {
 	if len(filters) == 0 {
 		return true
 	}
 
 	match := false
 	for _, f := range filters {
-		match = match || f(excerpt)
+		match = match || f(repoCache, excerpt)
 	}
 
 	return match
 }
 
 // Check if all of the filters provided match the bug
-func (*Filters) andMatch(filters []Filter, excerpt *BugExcerpt) bool {
+func (*Filters) andMatch(filters []Filter, repoCache *RepoCache, excerpt *BugExcerpt) bool {
 	if len(filters) == 0 {
 		return true
 	}
 
 	match := true
 	for _, f := range filters {
-		match = match && f(excerpt)
+		match = match && f(repoCache, excerpt)
 	}
 
 	return match

cache/identity_cache.go πŸ”—

@@ -0,0 +1,43 @@
+package cache
+
+import (
+	"github.com/MichaelMure/git-bug/identity"
+)
+
+// IdentityCache is a wrapper around an Identity for caching.
+type IdentityCache struct {
+	*identity.Identity
+	repoCache *RepoCache
+}
+
+func NewIdentityCache(repoCache *RepoCache, id *identity.Identity) *IdentityCache {
+	return &IdentityCache{
+		Identity:  id,
+		repoCache: repoCache,
+	}
+}
+
+func (i *IdentityCache) notifyUpdated() error {
+	return i.repoCache.identityUpdated(i.Identity.Id())
+}
+
+func (i *IdentityCache) AddVersion(version *identity.Version) error {
+	i.Identity.AddVersion(version)
+	return i.notifyUpdated()
+}
+
+func (i *IdentityCache) Commit() error {
+	err := i.Identity.Commit(i.repoCache.repo)
+	if err != nil {
+		return err
+	}
+	return i.notifyUpdated()
+}
+
+func (i *IdentityCache) CommitAsNeeded() error {
+	err := i.Identity.CommitAsNeeded(i.repoCache.repo)
+	if err != nil {
+		return err
+	}
+	return i.notifyUpdated()
+}

cache/identity_excerpt.go πŸ”—

@@ -0,0 +1,70 @@
+package cache
+
+import (
+	"encoding/gob"
+	"fmt"
+
+	"github.com/MichaelMure/git-bug/identity"
+)
+
+// Package initialisation used to register the type for (de)serialization
+func init() {
+	gob.Register(IdentityExcerpt{})
+}
+
+// IdentityExcerpt hold a subset of the identity values to be able to sort and
+// filter identities efficiently without having to read and compile each raw
+// identity.
+type IdentityExcerpt struct {
+	Id string
+
+	Name              string
+	Login             string
+	ImmutableMetadata map[string]string
+}
+
+func NewIdentityExcerpt(i *identity.Identity) *IdentityExcerpt {
+	return &IdentityExcerpt{
+		Id:                i.Id(),
+		Name:              i.Name(),
+		Login:             i.Login(),
+		ImmutableMetadata: i.ImmutableMetadata(),
+	}
+}
+
+func (i *IdentityExcerpt) HumanId() string {
+	return identity.FormatHumanID(i.Id)
+}
+
+// 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")
+}
+
+/*
+ * Sorting
+ */
+
+type IdentityById []*IdentityExcerpt
+
+func (b IdentityById) Len() int {
+	return len(b)
+}
+
+func (b IdentityById) Less(i, j int) bool {
+	return b[i].Id < b[j].Id
+}
+
+func (b IdentityById) Swap(i, j int) {
+	b[i], b[j] = b[j], b[i]
+}

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
 }

cache/repo_cache.go πŸ”—

@@ -14,27 +14,64 @@ import (
 	"time"
 
 	"github.com/MichaelMure/git-bug/bug"
+	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/repository"
 	"github.com/MichaelMure/git-bug/util/git"
 	"github.com/MichaelMure/git-bug/util/process"
 )
 
-const cacheFile = "cache"
-const formatVersion = 1
+const bugCacheFile = "bug-cache"
+const identityCacheFile = "identity-cache"
 
+// 1: original format
+// 2: added cache for identities with a reference in the bug cache
+const formatVersion = 2
+
+type ErrInvalidCacheFormat struct {
+	message string
+}
+
+func (e ErrInvalidCacheFormat) Error() string {
+	return e.message
+}
+
+// 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
+
 	// excerpt of bugs data for all bugs
-	excerpts map[string]*BugExcerpt
+	bugExcerpts map[string]*BugExcerpt
 	// bug loaded in memory
 	bugs map[string]*BugCache
+
+	// excerpt of identities data for all identities
+	identitiesExcerpts map[string]*IdentityExcerpt
+	// identities loaded in memory
+	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()
@@ -46,6 +83,9 @@ func NewRepoCache(r repository.ClockedRepo) (*RepoCache, error) {
 	if err == nil {
 		return c, nil
 	}
+	if _, ok := err.(ErrInvalidCacheFormat); ok {
+		return nil, err
+	}
 
 	err = c.buildCache()
 	if err != nil {
@@ -125,14 +165,38 @@ func (c *RepoCache) bugUpdated(id string) error {
 		panic("missing bug in the cache")
 	}
 
-	c.excerpts[id] = NewBugExcerpt(b.bug, b.Snapshot())
+	c.bugExcerpts[id] = NewBugExcerpt(b.bug, b.Snapshot())
 
-	return c.write()
+	// we only need to write the bug cache
+	return c.writeBugCache()
 }
 
-// load will try to read from the disk the bug cache file
+// identityUpdated is a callback to trigger when the excerpt of an identity
+// changed, that is each time an identity is updated
+func (c *RepoCache) identityUpdated(id string) error {
+	i, ok := c.identities[id]
+	if !ok {
+		panic("missing identity in the cache")
+	}
+
+	c.identitiesExcerpts[id] = NewIdentityExcerpt(i.Identity)
+
+	// we only need to write the identity cache
+	return c.writeIdentityCache()
+}
+
+// load will try to read from the disk all the cache files
 func (c *RepoCache) load() error {
-	f, err := os.Open(cacheFilePath(c.repo))
+	err := c.loadBugCache()
+	if err != nil {
+		return err
+	}
+	return c.loadIdentityCache()
+}
+
+// load will try to read from the disk the bug cache file
+func (c *RepoCache) loadBugCache() error {
+	f, err := os.Open(bugCacheFilePath(c.repo))
 	if err != nil {
 		return err
 	}
@@ -149,16 +213,56 @@ func (c *RepoCache) load() error {
 		return err
 	}
 
-	if aux.Version != 1 {
-		return fmt.Errorf("unknown cache format version %v", aux.Version)
+	if aux.Version != 2 {
+		return ErrInvalidCacheFormat{
+			message: fmt.Sprintf("unknown cache format version %v", aux.Version),
+		}
 	}
 
-	c.excerpts = aux.Excerpts
+	c.bugExcerpts = aux.Excerpts
 	return nil
 }
 
-// write will serialize on disk the bug cache file
+// load will try to read from the disk the identity cache file
+func (c *RepoCache) loadIdentityCache() error {
+	f, err := os.Open(identityCacheFilePath(c.repo))
+	if err != nil {
+		return err
+	}
+
+	decoder := gob.NewDecoder(f)
+
+	aux := struct {
+		Version  uint
+		Excerpts map[string]*IdentityExcerpt
+	}{}
+
+	err = decoder.Decode(&aux)
+	if err != nil {
+		return err
+	}
+
+	if aux.Version != 2 {
+		return ErrInvalidCacheFormat{
+			message: fmt.Sprintf("unknown cache format version %v", aux.Version),
+		}
+	}
+
+	c.identitiesExcerpts = aux.Excerpts
+	return nil
+}
+
+// write will serialize on disk all the cache files
 func (c *RepoCache) write() error {
+	err := c.writeBugCache()
+	if err != nil {
+		return err
+	}
+	return c.writeIdentityCache()
+}
+
+// write will serialize on disk the bug cache file
+func (c *RepoCache) writeBugCache() error {
 	var data bytes.Buffer
 
 	aux := struct {
@@ -166,7 +270,7 @@ func (c *RepoCache) write() error {
 		Excerpts map[string]*BugExcerpt
 	}{
 		Version:  formatVersion,
-		Excerpts: c.excerpts,
+		Excerpts: c.bugExcerpts,
 	}
 
 	encoder := gob.NewEncoder(&data)
@@ -176,7 +280,7 @@ func (c *RepoCache) write() error {
 		return err
 	}
 
-	f, err := os.Create(cacheFilePath(c.repo))
+	f, err := os.Create(bugCacheFilePath(c.repo))
 	if err != nil {
 		return err
 	}
@@ -189,14 +293,66 @@ func (c *RepoCache) write() error {
 	return f.Close()
 }
 
-func cacheFilePath(repo repository.Repo) string {
-	return path.Join(repo.GetPath(), ".git", "git-bug", cacheFile)
+// write will serialize on disk the identity cache file
+func (c *RepoCache) writeIdentityCache() error {
+	var data bytes.Buffer
+
+	aux := struct {
+		Version  uint
+		Excerpts map[string]*IdentityExcerpt
+	}{
+		Version:  formatVersion,
+		Excerpts: c.identitiesExcerpts,
+	}
+
+	encoder := gob.NewEncoder(&data)
+
+	err := encoder.Encode(aux)
+	if err != nil {
+		return err
+	}
+
+	f, err := os.Create(identityCacheFilePath(c.repo))
+	if err != nil {
+		return err
+	}
+
+	_, err = f.Write(data.Bytes())
+	if err != nil {
+		return err
+	}
+
+	return f.Close()
+}
+
+func bugCacheFilePath(repo repository.Repo) string {
+	return path.Join(repo.GetPath(), ".git", "git-bug", bugCacheFile)
+}
+
+func identityCacheFilePath(repo repository.Repo) string {
+	return path.Join(repo.GetPath(), ".git", "git-bug", identityCacheFile)
 }
 
 func (c *RepoCache) buildCache() error {
+	_, _ = fmt.Fprintf(os.Stderr, "Building identity cache... ")
+
+	c.identitiesExcerpts = make(map[string]*IdentityExcerpt)
+
+	allIdentities := identity.ReadAllLocalIdentities(c.repo)
+
+	for i := range allIdentities {
+		if i.Err != nil {
+			return i.Err
+		}
+
+		c.identitiesExcerpts[i.Identity.Id()] = NewIdentityExcerpt(i.Identity)
+	}
+
+	_, _ = fmt.Fprintln(os.Stderr, "Done.")
+
 	_, _ = fmt.Fprintf(os.Stderr, "Building bug cache... ")
 
-	c.excerpts = make(map[string]*BugExcerpt)
+	c.bugExcerpts = make(map[string]*BugExcerpt)
 
 	allBugs := bug.ReadAllLocalBugs(c.repo)
 
@@ -206,7 +362,7 @@ func (c *RepoCache) buildCache() error {
 		}
 
 		snap := b.Bug.Compile()
-		c.excerpts[b.Bug.Id()] = NewBugExcerpt(b.Bug, &snap)
+		c.bugExcerpts[b.Bug.Id()] = NewBugExcerpt(b.Bug, &snap)
 	}
 
 	_, _ = fmt.Fprintln(os.Stderr, "Done.")
@@ -231,13 +387,23 @@ func (c *RepoCache) ResolveBug(id string) (*BugCache, error) {
 	return cached, nil
 }
 
+// ResolveBugExcerpt retrieve a BugExcerpt matching the exact given id
+func (c *RepoCache) ResolveBugExcerpt(id string) (*BugExcerpt, error) {
+	e, ok := c.bugExcerpts[id]
+	if !ok {
+		return nil, bug.ErrBugNotExist
+	}
+
+	return e, nil
+}
+
 // ResolveBugPrefix retrieve a bug matching an id prefix. It fails if multiple
 // bugs match.
 func (c *RepoCache) ResolveBugPrefix(prefix string) (*BugCache, error) {
 	// preallocate but empty
 	matching := make([]string, 0, 5)
 
-	for id := range c.excerpts {
+	for id := range c.bugExcerpts {
 		if strings.HasPrefix(id, prefix) {
 			matching = append(matching, id)
 		}
@@ -261,7 +427,7 @@ func (c *RepoCache) ResolveBugCreateMetadata(key string, value string) (*BugCach
 	// preallocate but empty
 	matching := make([]string, 0, 5)
 
-	for id, excerpt := range c.excerpts {
+	for id, excerpt := range c.bugExcerpts {
 		if excerpt.CreateMetadata[key] == value {
 			matching = append(matching, id)
 		}
@@ -278,6 +444,7 @@ func (c *RepoCache) ResolveBugCreateMetadata(key string, value string) (*BugCach
 	return c.ResolveBug(matching[0])
 }
 
+// QueryBugs return the id of all Bug matching the given Query
 func (c *RepoCache) QueryBugs(query *Query) []string {
 	if query == nil {
 		return c.AllBugsIds()
@@ -285,8 +452,8 @@ func (c *RepoCache) QueryBugs(query *Query) []string {
 
 	var filtered []*BugExcerpt
 
-	for _, excerpt := range c.excerpts {
-		if query.Match(excerpt) {
+	for _, excerpt := range c.bugExcerpts {
+		if query.Match(c, excerpt) {
 			filtered = append(filtered, excerpt)
 		}
 	}
@@ -321,10 +488,10 @@ func (c *RepoCache) QueryBugs(query *Query) []string {
 
 // AllBugsIds return all known bug ids
 func (c *RepoCache) AllBugsIds() []string {
-	result := make([]string, len(c.excerpts))
+	result := make([]string, len(c.bugExcerpts))
 
 	i := 0
-	for _, excerpt := range c.excerpts {
+	for _, excerpt := range c.bugExcerpts {
 		result[i] = excerpt.Id
 		i++
 	}
@@ -332,11 +499,6 @@ func (c *RepoCache) AllBugsIds() []string {
 	return result
 }
 
-// ClearAllBugs clear all bugs kept in memory
-func (c *RepoCache) ClearAllBugs() {
-	c.bugs = make(map[string]*BugCache)
-}
-
 // ValidLabels list valid labels
 //
 // Note: in the future, a proper label policy could be implemented where valid
@@ -345,7 +507,7 @@ func (c *RepoCache) ClearAllBugs() {
 func (c *RepoCache) ValidLabels() []bug.Label {
 	set := map[bug.Label]interface{}{}
 
-	for _, excerpt := range c.excerpts {
+	for _, excerpt := range c.bugExcerpts {
 		for _, l := range excerpt.Labels {
 			set[l] = nil
 		}
@@ -376,7 +538,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 := bug.GetUser(c.repo)
+	author, err := c.GetUserIdentity()
 	if err != nil {
 		return nil, err
 	}
@@ -387,8 +549,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 bug.Person, 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
 	}
@@ -402,9 +564,14 @@ func (c *RepoCache) NewBugRaw(author bug.Person, unixTime int64, title string, m
 		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
@@ -421,6 +588,8 @@ func (c *RepoCache) Fetch(remote string) (string, error) {
 
 // MergeAll will merge all the available remote bug
 func (c *RepoCache) MergeAll(remote string) <-chan bug.MergeResult {
+	// TODO: add identities
+
 	out := make(chan bug.MergeResult)
 
 	// Intercept merge results to update the cache properly
@@ -441,7 +610,7 @@ func (c *RepoCache) MergeAll(remote string) <-chan bug.MergeResult {
 			case bug.MergeStatusNew, bug.MergeStatusUpdated:
 				b := result.Bug
 				snap := b.Compile()
-				c.excerpts[id] = NewBugExcerpt(b, &snap)
+				c.bugExcerpts[id] = NewBugExcerpt(b, &snap)
 			}
 		}
 
@@ -524,3 +693,166 @@ func repoIsAvailable(repo repository.Repo) error {
 
 	return nil
 }
+
+// ResolveIdentity retrieve an identity matching the exact given id
+func (c *RepoCache) ResolveIdentity(id string) (*IdentityCache, error) {
+	cached, ok := c.identities[id]
+	if ok {
+		return cached, nil
+	}
+
+	i, err := identity.ReadLocal(c.repo, id)
+	if err != nil {
+		return nil, err
+	}
+
+	cached = NewIdentityCache(c, i)
+	c.identities[id] = cached
+
+	return cached, nil
+}
+
+// ResolveIdentityExcerpt retrieve a IdentityExcerpt matching the exact given id
+func (c *RepoCache) ResolveIdentityExcerpt(id string) (*IdentityExcerpt, error) {
+	e, ok := c.identitiesExcerpts[id]
+	if !ok {
+		return nil, identity.ErrIdentityNotExist
+	}
+
+	return e, nil
+}
+
+// ResolveIdentityPrefix retrieve an Identity matching an id prefix.
+// It fails if multiple identities match.
+func (c *RepoCache) ResolveIdentityPrefix(prefix string) (*IdentityCache, error) {
+	// preallocate but empty
+	matching := make([]string, 0, 5)
+
+	for id := range c.identitiesExcerpts {
+		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) (*IdentityCache, error) {
+	// preallocate but empty
+	matching := make([]string, 0, 5)
+
+	for id, i := range c.identitiesExcerpts {
+		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])
+}
+
+// AllIdentityIds return all known identity ids
+func (c *RepoCache) AllIdentityIds() []string {
+	result := make([]string, len(c.identitiesExcerpts))
+
+	i := 0
+	for _, excerpt := range c.identitiesExcerpts {
+		result[i] = excerpt.Id
+		i++
+	}
+
+	return result
+}
+
+func (c *RepoCache) SetUserIdentity(i *IdentityCache) error {
+	err := identity.SetUserIdentity(c.repo, i.Identity)
+	if err != nil {
+		return err
+	}
+
+	// Make sure that everything is fine
+	if _, ok := c.identities[i.Id()]; !ok {
+		panic("SetUserIdentity while the identity is not from the cache, something is wrong")
+	}
+
+	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) (*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) (*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) (*IdentityCache, 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())
+	}
+
+	cached := NewIdentityCache(c, i)
+	c.identities[i.Id()] = cached
+
+	// force the write of the excerpt
+	err = c.identityUpdated(i.Id())
+	if err != nil {
+		return nil, err
+	}
+
+	return cached, nil
+}

commands/add.go πŸ”—

@@ -56,7 +56,7 @@ func runAddBug(cmd *cobra.Command, args []string) error {
 
 var addCmd = &cobra.Command{
 	Use:     "add",
-	Short:   "Create a new bug",
+	Short:   "Create a new bug.",
 	PreRunE: loadRepo,
 	RunE:    runAddBug,
 }

commands/bridge.go πŸ”—

@@ -31,7 +31,7 @@ func runBridge(cmd *cobra.Command, args []string) error {
 
 var bridgeCmd = &cobra.Command{
 	Use:     "bridge",
-	Short:   "Configure and use bridges to other bug trackers",
+	Short:   "Configure and use bridges to other bug trackers.",
 	PreRunE: loadRepo,
 	RunE:    runBridge,
 	Args:    cobra.NoArgs,

commands/bridge_configure.go πŸ”—

@@ -91,7 +91,7 @@ func promptName() (string, error) {
 
 var bridgeConfigureCmd = &cobra.Command{
 	Use:     "configure",
-	Short:   "Configure a new bridge",
+	Short:   "Configure a new bridge.",
 	PreRunE: loadRepo,
 	RunE:    runBridgeConfigure,
 }

commands/bridge_pull.go πŸ”—

@@ -38,7 +38,7 @@ func runBridgePull(cmd *cobra.Command, args []string) error {
 
 var bridgePullCmd = &cobra.Command{
 	Use:     "pull [<name>]",
-	Short:   "Pull updates",
+	Short:   "Pull updates.",
 	PreRunE: loadRepo,
 	RunE:    runBridgePull,
 }

commands/bridge_rm.go πŸ”—

@@ -25,7 +25,7 @@ func runBridgeRm(cmd *cobra.Command, args []string) error {
 
 var bridgeRmCmd = &cobra.Command{
 	Use:     "rm name <name>",
-	Short:   "Delete a configured bridge",
+	Short:   "Delete a configured bridge.",
 	PreRunE: loadRepo,
 	RunE:    runBridgeRm,
 	Args:    cobra.ExactArgs(1),

commands/commands.go πŸ”—

@@ -61,7 +61,7 @@ func runCommands(cmd *cobra.Command, args []string) error {
 
 var commandsCmd = &cobra.Command{
 	Use:   "commands [<option>...]",
-	Short: "Display available commands",
+	Short: "Display available commands.",
 	RunE:  runCommands,
 }
 

commands/comment.go πŸ”—

@@ -46,7 +46,7 @@ func commentsTextOutput(comments []bug.Comment) {
 
 var commentCmd = &cobra.Command{
 	Use:     "comment [<id>]",
-	Short:   "Display or add comments",
+	Short:   "Display or add comments.",
 	PreRunE: loadRepo,
 	RunE:    runComment,
 }

commands/comment_add.go πŸ”—

@@ -46,7 +46,7 @@ func runCommentAdd(cmd *cobra.Command, args []string) error {
 		}
 	}
 
-	err = b.AddComment(commentAddMessage)
+	_, err = b.AddComment(commentAddMessage)
 	if err != nil {
 		return err
 	}
@@ -56,7 +56,7 @@ func runCommentAdd(cmd *cobra.Command, args []string) error {
 
 var commentAddCmd = &cobra.Command{
 	Use:     "add [<id>]",
-	Short:   "Add a new comment",
+	Short:   "Add a new comment.",
 	PreRunE: loadRepo,
 	RunE:    runCommentAdd,
 }

commands/deselect.go πŸ”—

@@ -25,7 +25,7 @@ func runDeselect(cmd *cobra.Command, args []string) error {
 
 var deselectCmd = &cobra.Command{
 	Use:   "deselect",
-	Short: "Clear the implicitly selected bug",
+	Short: "Clear the implicitly selected bug.",
 	Example: `git bug select 2f15
 git bug comment
 git bug status

commands/label.go πŸ”—

@@ -33,7 +33,7 @@ func runLabel(cmd *cobra.Command, args []string) error {
 
 var labelCmd = &cobra.Command{
 	Use:     "label [<id>]",
-	Short:   "Display, add or remove labels",
+	Short:   "Display, add or remove labels.",
 	PreRunE: loadRepo,
 	RunE:    runLabel,
 }

commands/label_add.go πŸ”—

@@ -22,7 +22,7 @@ func runLabelAdd(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	changes, err := b.ChangeLabels(args, nil)
+	changes, _, err := b.ChangeLabels(args, nil)
 
 	for _, change := range changes {
 		fmt.Println(change)
@@ -37,7 +37,7 @@ func runLabelAdd(cmd *cobra.Command, args []string) error {
 
 var labelAddCmd = &cobra.Command{
 	Use:     "add [<id>] <label>[...]",
-	Short:   "Add a label",
+	Short:   "Add a label.",
 	PreRunE: loadRepo,
 	RunE:    runLabelAdd,
 }

commands/label_rm.go πŸ”—

@@ -22,7 +22,7 @@ func runLabelRm(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	changes, err := b.ChangeLabels(nil, args)
+	changes, _, err := b.ChangeLabels(nil, args)
 
 	for _, change := range changes {
 		fmt.Println(change)
@@ -37,7 +37,7 @@ func runLabelRm(cmd *cobra.Command, args []string) error {
 
 var labelRmCmd = &cobra.Command{
 	Use:     "rm [<id>] <label>[...]",
-	Short:   "Remove a label",
+	Short:   "Remove a label.",
 	PreRunE: loadRepo,
 	RunE:    runLabelRm,
 }

commands/ls-labels.go πŸ”—

@@ -27,7 +27,7 @@ func runLsLabel(cmd *cobra.Command, args []string) error {
 
 var lsLabelCmd = &cobra.Command{
 	Use:   "ls-label",
-	Short: "List valid labels",
+	Short: "List valid labels.",
 	Long: `List valid labels.
 
 Note: in the future, a proper label policy could be implemented where valid labels are defined in a configuration file. Until that, the default behavior is to return the list of labels already used.`,

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]
@@ -131,7 +131,7 @@ func lsQueryFromFlags() (*cache.Query, error) {
 
 var lsCmd = &cobra.Command{
 	Use:   "ls [<query>]",
-	Short: "List bugs",
+	Short: "List bugs.",
 	Long: `Display a summary of each bugs.
 
 You can pass an additional query to filter and order the list. This query can be expressed either with a simple query language or with flags.`,

commands/pull.go πŸ”—

@@ -54,7 +54,7 @@ func runPull(cmd *cobra.Command, args []string) error {
 // showCmd defines the "push" subcommand.
 var pullCmd = &cobra.Command{
 	Use:     "pull [<remote>]",
-	Short:   "Pull bugs update from a git remote",
+	Short:   "Pull bugs update from a git remote.",
 	PreRunE: loadRepo,
 	RunE:    runPull,
 }

commands/push.go πŸ”—

@@ -39,7 +39,7 @@ func runPush(cmd *cobra.Command, args []string) error {
 // showCmd defines the "push" subcommand.
 var pushCmd = &cobra.Command{
 	Use:     "push [<remote>]",
-	Short:   "Push bugs update to a git remote",
+	Short:   "Push bugs update to a git remote.",
 	PreRunE: loadRepo,
 	RunE:    runPush,
 }

commands/root.go πŸ”—

@@ -6,6 +6,7 @@ import (
 	"os"
 
 	"github.com/MichaelMure/git-bug/bug"
+	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/repository"
 	"github.com/spf13/cobra"
 )
@@ -18,7 +19,7 @@ var repo repository.ClockedRepo
 // RootCmd represents the base command when called without any subcommands
 var RootCmd = &cobra.Command{
 	Use:   rootCommandName,
-	Short: "A bug tracker embedded in Git",
+	Short: "A bug tracker embedded in Git.",
 	Long: `git-bug is a bug tracker embedded in git.
 
 git-bug use git objects to store the bug tracking separated from the files
@@ -53,6 +54,7 @@ func Execute() {
 	}
 }
 
+// loadRepo is a pre-run function that load the repository for use in a command
 func loadRepo(cmd *cobra.Command, args []string) error {
 	cwd, err := os.Getwd()
 	if err != nil {
@@ -70,3 +72,24 @@ func loadRepo(cmd *cobra.Command, args []string) error {
 
 	return nil
 }
+
+// loadRepoEnsureUser is the same as loadRepo, but also ensure that the user has configured
+// an identity. Use this pre-run function when an error after using the configured user won't
+// do.
+func loadRepoEnsureUser(cmd *cobra.Command, args []string) error {
+	err := loadRepo(cmd, args)
+	if err != nil {
+		return err
+	}
+
+	set, err := identity.IsUserIdentitySet(repo)
+	if err != nil {
+		return err
+	}
+
+	if !set {
+		return identity.ErrNoIdentitySet
+	}
+
+	return nil
+}

commands/select.go πŸ”—

@@ -41,7 +41,7 @@ func runSelect(cmd *cobra.Command, args []string) error {
 
 var selectCmd = &cobra.Command{
 	Use:   "select <id>",
-	Short: "Select a bug for implicit use in future commands",
+	Short: "Select a bug for implicit use in future commands.",
 	Example: `git bug select 2f15
 git bug comment
 git bug status

commands/select/select_test.go πŸ”—

@@ -1,113 +1,80 @@
 package _select
 
 import (
-	"io/ioutil"
-	"log"
 	"testing"
+	"time"
 
 	"github.com/MichaelMure/git-bug/cache"
-	"github.com/MichaelMure/git-bug/repository"
+	"github.com/MichaelMure/git-bug/util/test"
+	"github.com/stretchr/testify/require"
 )
 
 func TestSelect(t *testing.T) {
-	repo, err := cache.NewRepoCache(createRepo())
-	checkErr(t, err)
+	repo := test.CreateRepo(false)
 
-	_, _, err = ResolveBug(repo, []string{})
-	if err != ErrNoValidId {
-		t.Fatal("expected no valid id error, got", err)
-	}
+	repoCache, err := cache.NewRepoCache(repo)
+	require.NoError(t, err)
 
-	err = Select(repo, "invalid")
-	checkErr(t, err)
+	_, _, err = ResolveBug(repoCache, []string{})
+	require.Equal(t, ErrNoValidId, err)
 
-	_, _, err = ResolveBug(repo, []string{})
-	if err == nil {
-		t.Fatal("expected invalid bug error")
-	}
+	err = Select(repoCache, "invalid")
+	require.NoError(t, err)
+
+	// Resolve without a pattern should fail when no bug is selected
+	_, _, err = ResolveBug(repoCache, []string{})
+	require.Error(t, err)
 
 	// generate a bunch of bugs
+
+	rene, err := repoCache.NewIdentity("RenΓ© Descartes", "rene@descartes.fr")
+	require.NoError(t, err)
+
 	for i := 0; i < 10; i++ {
-		_, err := repo.NewBug("title", "message")
-		checkErr(t, err)
+		_, err := repoCache.NewBugRaw(rene, time.Now().Unix(), "title", "message", nil, nil)
+		require.NoError(t, err)
 	}
 
-	// two more for testing
-	b1, err := repo.NewBug("title", "message")
-	checkErr(t, err)
-	b2, err := repo.NewBug("title", "message")
-	checkErr(t, err)
+	// and two more for testing
+	b1, err := repoCache.NewBugRaw(rene, time.Now().Unix(), "title", "message", nil, nil)
+	require.NoError(t, err)
+	b2, err := repoCache.NewBugRaw(rene, time.Now().Unix(), "title", "message", nil, nil)
+	require.NoError(t, err)
 
-	err = Select(repo, b1.Id())
-	checkErr(t, err)
+	err = Select(repoCache, b1.Id())
+	require.NoError(t, err)
 
 	// normal select without args
-	b3, _, err := ResolveBug(repo, []string{})
-	checkErr(t, err)
-	if b3.Id() != b1.Id() {
-		t.Fatal("incorrect bug returned")
-	}
+	b3, _, err := ResolveBug(repoCache, []string{})
+	require.NoError(t, err)
+	require.Equal(t, b1.Id(), b3.Id())
 
 	// override selection with same id
-	b4, _, err := ResolveBug(repo, []string{b1.Id()})
-	checkErr(t, err)
-	if b4.Id() != b1.Id() {
-		t.Fatal("incorrect bug returned")
-	}
+	b4, _, err := ResolveBug(repoCache, []string{b1.Id()})
+	require.NoError(t, err)
+	require.Equal(t, b1.Id(), b4.Id())
 
 	// override selection with a prefix
-	b5, _, err := ResolveBug(repo, []string{b1.HumanId()})
-	checkErr(t, err)
-	if b5.Id() != b1.Id() {
-		t.Fatal("incorrect bug returned")
-	}
+	b5, _, err := ResolveBug(repoCache, []string{b1.HumanId()})
+	require.NoError(t, err)
+	require.Equal(t, b1.Id(), b5.Id())
 
 	// args that shouldn't override
-	b6, _, err := ResolveBug(repo, []string{"arg"})
-	checkErr(t, err)
-	if b6.Id() != b1.Id() {
-		t.Fatal("incorrect bug returned")
-	}
+	b6, _, err := ResolveBug(repoCache, []string{"arg"})
+	require.NoError(t, err)
+	require.Equal(t, b1.Id(), b6.Id())
 
 	// override with a different id
-	b7, _, err := ResolveBug(repo, []string{b2.Id()})
-	checkErr(t, err)
-	if b7.Id() != b2.Id() {
-		t.Fatal("incorrect bug returned")
-	}
-
-	err = Clear(repo)
-	checkErr(t, err)
+	b7, _, err := ResolveBug(repoCache, []string{b2.Id()})
+	require.NoError(t, err)
+	require.Equal(t, b2.Id(), b7.Id())
 
-	_, _, err = ResolveBug(repo, []string{})
-	if err == nil {
-		t.Fatal("expected invalid bug error")
-	}
-}
-
-func createRepo() *repository.GitRepo {
-	dir, err := ioutil.TempDir("", "")
-	if err != nil {
-		log.Fatal(err)
-	}
+	err = Clear(repoCache)
+	require.NoError(t, err)
 
-	repo, err := repository.InitGitRepo(dir)
-	if err != nil {
-		log.Fatal(err)
-	}
+	// Resolve without a pattern should error again after clearing the selected bug
+	_, _, err = ResolveBug(repoCache, []string{})
+	require.Error(t, err)
 
-	if err := repo.StoreConfig("user.name", "testuser"); err != nil {
-		log.Fatal("failed to set user.name for test repository: ", err)
-	}
-	if err := repo.StoreConfig("user.email", "testuser@example.com"); err != nil {
-		log.Fatal("failed to set user.email for test repository: ", err)
-	}
-
-	return repo
-}
-
-func checkErr(t testing.TB, err error) {
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, test.CleanupRepo(repo))
 }

commands/show.go πŸ”—

@@ -42,7 +42,7 @@ func runShowBug(cmd *cobra.Command, args []string) error {
 		case "author":
 			fmt.Printf("%s\n", firstComment.Author.DisplayName())
 		case "authorEmail":
-			fmt.Printf("%s\n", firstComment.Author.Email)
+			fmt.Printf("%s\n", firstComment.Author.Email())
 		case "createTime":
 			fmt.Printf("%s\n", firstComment.FormatTime())
 		case "id":
@@ -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 == "" {
@@ -113,7 +113,7 @@ func runShowBug(cmd *cobra.Command, args []string) error {
 
 var showCmd = &cobra.Command{
 	Use:     "show [<id>]",
-	Short:   "Display the details of a bug",
+	Short:   "Display the details of a bug.",
 	PreRunE: loadRepo,
 	RunE:    runShowBug,
 }

commands/status.go πŸ”—

@@ -31,7 +31,7 @@ func runStatus(cmd *cobra.Command, args []string) error {
 
 var statusCmd = &cobra.Command{
 	Use:     "status [<id>]",
-	Short:   "Display or change a bug status",
+	Short:   "Display or change a bug status.",
 	PreRunE: loadRepo,
 	RunE:    runStatus,
 }

commands/status_close.go πŸ”—

@@ -20,7 +20,7 @@ func runStatusClose(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	err = b.Close()
+	_, err = b.Close()
 	if err != nil {
 		return err
 	}
@@ -30,7 +30,7 @@ func runStatusClose(cmd *cobra.Command, args []string) error {
 
 var closeCmd = &cobra.Command{
 	Use:     "close [<id>]",
-	Short:   "Mark a bug as closed",
+	Short:   "Mark a bug as closed.",
 	PreRunE: loadRepo,
 	RunE:    runStatusClose,
 }

commands/status_open.go πŸ”—

@@ -20,7 +20,7 @@ func runStatusOpen(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	err = b.Open()
+	_, err = b.Open()
 	if err != nil {
 		return err
 	}
@@ -30,7 +30,7 @@ func runStatusOpen(cmd *cobra.Command, args []string) error {
 
 var openCmd = &cobra.Command{
 	Use:     "open [<id>]",
-	Short:   "Mark a bug as open",
+	Short:   "Mark a bug as open.",
 	PreRunE: loadRepo,
 	RunE:    runStatusOpen,
 }

commands/termui.go πŸ”—

@@ -20,8 +20,8 @@ func runTermUI(cmd *cobra.Command, args []string) error {
 
 var termUICmd = &cobra.Command{
 	Use:     "termui",
-	Short:   "Launch the terminal UI",
-	PreRunE: loadRepo,
+	Short:   "Launch the terminal UI.",
+	PreRunE: loadRepoEnsureUser,
 	RunE:    runTermUI,
 }
 

commands/title.go πŸ”—

@@ -31,7 +31,7 @@ func runTitle(cmd *cobra.Command, args []string) error {
 
 var titleCmd = &cobra.Command{
 	Use:     "title [<id>]",
-	Short:   "Display or change a title",
+	Short:   "Display or change a title.",
 	PreRunE: loadRepo,
 	RunE:    runTitle,
 }

commands/title_edit.go πŸ”—

@@ -44,7 +44,7 @@ func runTitleEdit(cmd *cobra.Command, args []string) error {
 		fmt.Println("No change, aborting.")
 	}
 
-	err = b.SetTitle(titleEditTitle)
+	_, err = b.SetTitle(titleEditTitle)
 	if err != nil {
 		return err
 	}
@@ -54,7 +54,7 @@ func runTitleEdit(cmd *cobra.Command, args []string) error {
 
 var titleEditCmd = &cobra.Command{
 	Use:     "edit [<id>]",
-	Short:   "Edit a title",
+	Short:   "Edit a title.",
 	PreRunE: loadRepo,
 	RunE:    runTitleEdit,
 }

commands/user.go πŸ”—

@@ -0,0 +1,61 @@
+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 {
+		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("Last modification: %s (lamport %d)\n",
+		id.LastModification().Time().Format("Mon Jan 2 15:04:05 2006 +0200"),
+		id.LastModificationLamport())
+	fmt.Println("Metadata:")
+	for key, value := range id.ImmutableMetadata() {
+		fmt.Printf("    %s --> %s\n", key, value)
+	}
+	// 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_adopt.go πŸ”—

@@ -0,0 +1,48 @@
+package commands
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/MichaelMure/git-bug/cache"
+	"github.com/MichaelMure/git-bug/util/interrupt"
+	"github.com/spf13/cobra"
+)
+
+func runUserAdopt(cmd *cobra.Command, args []string) error {
+	backend, err := cache.NewRepoCache(repo)
+	if err != nil {
+		return err
+	}
+	defer backend.Close()
+	interrupt.RegisterCleaner(backend.Close)
+
+	prefix := args[0]
+
+	i, err := backend.ResolveIdentityPrefix(prefix)
+	if err != nil {
+		return err
+	}
+
+	err = backend.SetUserIdentity(i)
+	if err != nil {
+		return err
+	}
+
+	_, _ = fmt.Fprintf(os.Stderr, "Your identity is now: %s\n", i.DisplayName())
+
+	return nil
+}
+
+var userAdoptCmd = &cobra.Command{
+	Use:     "adopt <id>",
+	Short:   "Adopt an existing identity as your own.",
+	PreRunE: loadRepo,
+	RunE:    runUserAdopt,
+	Args:    cobra.ExactArgs(1),
+}
+
+func init() {
+	userCmd.AddCommand(userAdoptCmd)
+	userAdoptCmd.Flags().SortFlags = false
+}

commands/user_create.go πŸ”—

@@ -0,0 +1,81 @@
+package commands
+
+import (
+	"fmt"
+	"os"
+
+	"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)
+
+	_, _ = fmt.Fprintf(os.Stderr, "Before creating a new identity, please be aware that "+
+		"you can also use an already existing one using \"git bug user adopt\". As an example, "+
+		"you can do that if your identity has already been created by an importer.\n\n")
+
+	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.Fprintln(os.Stderr)
+	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
+}

commands/user_ls.go πŸ”—

@@ -0,0 +1,45 @@
+package commands
+
+import (
+	"fmt"
+
+	"github.com/MichaelMure/git-bug/cache"
+	"github.com/MichaelMure/git-bug/util/colors"
+	"github.com/MichaelMure/git-bug/util/interrupt"
+	"github.com/spf13/cobra"
+)
+
+func runUserLs(cmd *cobra.Command, args []string) error {
+	backend, err := cache.NewRepoCache(repo)
+	if err != nil {
+		return err
+	}
+	defer backend.Close()
+	interrupt.RegisterCleaner(backend.Close)
+
+	for _, id := range backend.AllIdentityIds() {
+		i, err := backend.ResolveIdentityExcerpt(id)
+		if err != nil {
+			return err
+		}
+
+		fmt.Printf("%s %s\n",
+			colors.Cyan(i.HumanId()),
+			i.DisplayName(),
+		)
+	}
+
+	return nil
+}
+
+var userLsCmd = &cobra.Command{
+	Use:     "ls",
+	Short:   "List identities.",
+	PreRunE: loadRepo,
+	RunE:    runUserLs,
+}
+
+func init() {
+	userCmd.AddCommand(userLsCmd)
+	userLsCmd.Flags().SortFlags = false
+}

commands/version.go πŸ”—

@@ -41,7 +41,7 @@ func runVersionCmd(cmd *cobra.Command, args []string) {
 
 var versionCmd = &cobra.Command{
 	Use:   "version",
-	Short: "Show git-bug version information",
+	Short: "Show git-bug version information.",
 	Run:   runVersionCmd,
 }
 

commands/webui.go πŸ”—

@@ -224,7 +224,7 @@ func (gufh *gitUploadFileHandler) ServeHTTP(rw http.ResponseWriter, r *http.Requ
 
 var webUICmd = &cobra.Command{
 	Use:     "webui",
-	Short:   "Launch the web UI",
+	Short:   "Launch the web UI.",
 	PreRunE: loadRepo,
 	RunE:    runWebUI,
 }

doc/gen_manpage.go πŸ”—

@@ -7,6 +7,7 @@ import (
 	"log"
 	"os"
 	"path"
+	"path/filepath"
 
 	"github.com/MichaelMure/git-bug/commands"
 	"github.com/spf13/cobra/doc"
@@ -14,7 +15,7 @@ import (
 
 func main() {
 	cwd, _ := os.Getwd()
-	filepath := path.Join(cwd, "doc", "man")
+	dir := path.Join(cwd, "doc", "man")
 
 	header := &doc.GenManHeader{
 		Title:   "GIT-BUG",
@@ -24,7 +25,17 @@ func main() {
 
 	fmt.Println("Generating manpage ...")
 
-	err := doc.GenManTree(commands.RootCmd, header, filepath)
+	files, err := filepath.Glob(dir + "/*.1")
+	if err != nil {
+		log.Fatal(err)
+	}
+	for _, f := range files {
+		if err := os.Remove(f); err != nil {
+			log.Fatal(err)
+		}
+	}
+
+	err = doc.GenManTree(commands.RootCmd, header, dir)
 	if err != nil {
 		log.Fatal(err)
 	}

doc/gen_markdown.go πŸ”—

@@ -4,20 +4,32 @@ package main
 
 import (
 	"fmt"
-	"github.com/MichaelMure/git-bug/commands"
-	"github.com/spf13/cobra/doc"
 	"log"
 	"os"
 	"path"
+	"path/filepath"
+
+	"github.com/MichaelMure/git-bug/commands"
+	"github.com/spf13/cobra/doc"
 )
 
 func main() {
 	cwd, _ := os.Getwd()
-	filepath := path.Join(cwd, "doc", "md")
+	dir := path.Join(cwd, "doc", "md")
 
 	fmt.Println("Generating Markdown documentation ...")
 
-	err := doc.GenMarkdownTree(commands.RootCmd, filepath)
+	files, err := filepath.Glob(dir + "/*.md")
+	if err != nil {
+		log.Fatal(err)
+	}
+	for _, f := range files {
+		if err := os.Remove(f); err != nil {
+			log.Fatal(err)
+		}
+	}
+
+	err = doc.GenMarkdownTree(commands.RootCmd, dir)
 	if err != nil {
 		log.Fatal(err)
 	}

doc/man/git-bug-add.1 πŸ”—

@@ -1,11 +1,11 @@
-.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" "" 
+.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" "" 
 .nh
 .ad l
 
 
 .SH NAME
 .PP
-git\-bug\-add \- Create a new bug
+git\-bug\-add \- Create a new bug.
 
 
 .SH SYNOPSIS
@@ -15,7 +15,7 @@ git\-bug\-add \- Create a new bug
 
 .SH DESCRIPTION
 .PP
-Create a new bug
+Create a new bug.
 
 
 .SH OPTIONS

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-bridge-configure.1 πŸ”—

@@ -1,11 +1,11 @@
-.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" "" 
+.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" "" 
 .nh
 .ad l
 
 
 .SH NAME
 .PP
-git\-bug\-bridge\-configure \- Configure a new bridge
+git\-bug\-bridge\-configure \- Configure a new bridge.
 
 
 .SH SYNOPSIS
@@ -15,7 +15,7 @@ git\-bug\-bridge\-configure \- Configure a new bridge
 
 .SH DESCRIPTION
 .PP
-Configure a new bridge
+Configure a new bridge.
 
 
 .SH OPTIONS

doc/man/git-bug-bridge-pull.1 πŸ”—

@@ -1,11 +1,11 @@
-.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" "" 
+.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" "" 
 .nh
 .ad l
 
 
 .SH NAME
 .PP
-git\-bug\-bridge\-pull \- Pull updates
+git\-bug\-bridge\-pull \- Pull updates.
 
 
 .SH SYNOPSIS
@@ -15,7 +15,7 @@ git\-bug\-bridge\-pull \- Pull updates
 
 .SH DESCRIPTION
 .PP
-Pull updates
+Pull updates.
 
 
 .SH OPTIONS

doc/man/git-bug-bridge-rm.1 πŸ”—

@@ -1,11 +1,11 @@
-.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" "" 
+.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" "" 
 .nh
 .ad l
 
 
 .SH NAME
 .PP
-git\-bug\-bridge\-rm \- Delete a configured bridge
+git\-bug\-bridge\-rm \- Delete a configured bridge.
 
 
 .SH SYNOPSIS
@@ -15,7 +15,7 @@ git\-bug\-bridge\-rm \- Delete a configured bridge
 
 .SH DESCRIPTION
 .PP
-Delete a configured bridge
+Delete a configured bridge.
 
 
 .SH OPTIONS

doc/man/git-bug-bridge.1 πŸ”—

@@ -1,11 +1,11 @@
-.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" "" 
+.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" "" 
 .nh
 .ad l
 
 
 .SH NAME
 .PP
-git\-bug\-bridge \- Configure and use bridges to other bug trackers
+git\-bug\-bridge \- Configure and use bridges to other bug trackers.
 
 
 .SH SYNOPSIS
@@ -15,7 +15,7 @@ git\-bug\-bridge \- Configure and use bridges to other bug trackers
 
 .SH DESCRIPTION
 .PP
-Configure and use bridges to other bug trackers
+Configure and use bridges to other bug trackers.
 
 
 .SH OPTIONS

doc/man/git-bug-commands.1 πŸ”—

@@ -1,11 +1,11 @@
-.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" "" 
+.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" "" 
 .nh
 .ad l
 
 
 .SH NAME
 .PP
-git\-bug\-commands \- Display available commands
+git\-bug\-commands \- Display available commands.
 
 
 .SH SYNOPSIS
@@ -15,7 +15,7 @@ git\-bug\-commands \- Display available commands
 
 .SH DESCRIPTION
 .PP
-Display available commands
+Display available commands.
 
 
 .SH OPTIONS

doc/man/git-bug-comment-add.1 πŸ”—

@@ -1,11 +1,11 @@
-.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" "" 
+.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" "" 
 .nh
 .ad l
 
 
 .SH NAME
 .PP
-git\-bug\-comment\-add \- Add a new comment
+git\-bug\-comment\-add \- Add a new comment.
 
 
 .SH SYNOPSIS
@@ -15,7 +15,7 @@ git\-bug\-comment\-add \- Add a new comment
 
 .SH DESCRIPTION
 .PP
-Add a new comment
+Add a new comment.
 
 
 .SH OPTIONS

doc/man/git-bug-comment.1 πŸ”—

@@ -1,11 +1,11 @@
-.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" "" 
+.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" "" 
 .nh
 .ad l
 
 
 .SH NAME
 .PP
-git\-bug\-comment \- Display or add comments
+git\-bug\-comment \- Display or add comments.
 
 
 .SH SYNOPSIS
@@ -15,7 +15,7 @@ git\-bug\-comment \- Display or add comments
 
 .SH DESCRIPTION
 .PP
-Display or add comments
+Display or add comments.
 
 
 .SH OPTIONS

doc/man/git-bug-deselect.1 πŸ”—

@@ -1,11 +1,11 @@
-.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" "" 
+.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" "" 
 .nh
 .ad l
 
 
 .SH NAME
 .PP
-git\-bug\-deselect \- Clear the implicitly selected bug
+git\-bug\-deselect \- Clear the implicitly selected bug.
 
 
 .SH SYNOPSIS
@@ -15,7 +15,7 @@ git\-bug\-deselect \- Clear the implicitly selected bug
 
 .SH DESCRIPTION
 .PP
-Clear the implicitly selected bug
+Clear the implicitly selected bug.
 
 
 .SH OPTIONS

doc/man/git-bug-label-add.1 πŸ”—

@@ -1,11 +1,11 @@
-.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" "" 
+.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" "" 
 .nh
 .ad l
 
 
 .SH NAME
 .PP
-git\-bug\-label\-add \- Add a label
+git\-bug\-label\-add \- Add a label.
 
 
 .SH SYNOPSIS
@@ -15,7 +15,7 @@ git\-bug\-label\-add \- Add a label
 
 .SH DESCRIPTION
 .PP
-Add a label
+Add a label.
 
 
 .SH OPTIONS

doc/man/git-bug-label-rm.1 πŸ”—

@@ -1,11 +1,11 @@
-.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" "" 
+.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" "" 
 .nh
 .ad l
 
 
 .SH NAME
 .PP
-git\-bug\-label\-rm \- Remove a label
+git\-bug\-label\-rm \- Remove a label.
 
 
 .SH SYNOPSIS
@@ -15,7 +15,7 @@ git\-bug\-label\-rm \- Remove a label
 
 .SH DESCRIPTION
 .PP
-Remove a label
+Remove a label.
 
 
 .SH OPTIONS

doc/man/git-bug-label.1 πŸ”—

@@ -1,11 +1,11 @@
-.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" "" 
+.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" "" 
 .nh
 .ad l
 
 
 .SH NAME
 .PP
-git\-bug\-label \- Display, add or remove labels
+git\-bug\-label \- Display, add or remove labels.
 
 
 .SH SYNOPSIS
@@ -15,7 +15,7 @@ git\-bug\-label \- Display, add or remove labels
 
 .SH DESCRIPTION
 .PP
-Display, add or remove labels
+Display, add or remove labels.
 
 
 .SH OPTIONS

doc/man/git-bug-ls-id.1 πŸ”—

@@ -0,0 +1,29 @@
+.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" "" 
+.nh
+.ad l
+
+
+.SH NAME
+.PP
+git\-bug\-ls\-id \- List Bug Id
+
+
+.SH SYNOPSIS
+.PP
+\fBgit\-bug ls\-id [<prefix>] [flags]\fP
+
+
+.SH DESCRIPTION
+.PP
+List Bug Id
+
+
+.SH OPTIONS
+.PP
+\fB\-h\fP, \fB\-\-help\fP[=false]
+    help for ls\-id
+
+
+.SH SEE ALSO
+.PP
+\fBgit\-bug(1)\fP

doc/man/git-bug-ls-label.1 πŸ”—

@@ -1,11 +1,11 @@
-.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" "" 
+.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" "" 
 .nh
 .ad l
 
 
 .SH NAME
 .PP
-git\-bug\-ls\-label \- List valid labels
+git\-bug\-ls\-label \- List valid labels.
 
 
 .SH SYNOPSIS

doc/man/git-bug-ls.1 πŸ”—

@@ -1,11 +1,11 @@
-.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" "" 
+.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" "" 
 .nh
 .ad l
 
 
 .SH NAME
 .PP
-git\-bug\-ls \- List bugs
+git\-bug\-ls \- List bugs.
 
 
 .SH SYNOPSIS

doc/man/git-bug-pull.1 πŸ”—

@@ -1,11 +1,11 @@
-.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" "" 
+.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" "" 
 .nh
 .ad l
 
 
 .SH NAME
 .PP
-git\-bug\-pull \- Pull bugs update from a git remote
+git\-bug\-pull \- Pull bugs update from a git remote.
 
 
 .SH SYNOPSIS
@@ -15,7 +15,7 @@ git\-bug\-pull \- Pull bugs update from a git remote
 
 .SH DESCRIPTION
 .PP
-Pull bugs update from a git remote
+Pull bugs update from a git remote.
 
 
 .SH OPTIONS

doc/man/git-bug-push.1 πŸ”—

@@ -1,11 +1,11 @@
-.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" "" 
+.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" "" 
 .nh
 .ad l
 
 
 .SH NAME
 .PP
-git\-bug\-push \- Push bugs update to a git remote
+git\-bug\-push \- Push bugs update to a git remote.
 
 
 .SH SYNOPSIS
@@ -15,7 +15,7 @@ git\-bug\-push \- Push bugs update to a git remote
 
 .SH DESCRIPTION
 .PP
-Push bugs update to a git remote
+Push bugs update to a git remote.
 
 
 .SH OPTIONS

doc/man/git-bug-select.1 πŸ”—

@@ -1,11 +1,11 @@
-.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" "" 
+.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" "" 
 .nh
 .ad l
 
 
 .SH NAME
 .PP
-git\-bug\-select \- Select a bug for implicit use in future commands
+git\-bug\-select \- Select a bug for implicit use in future commands.
 
 
 .SH SYNOPSIS
@@ -15,7 +15,7 @@ git\-bug\-select \- Select a bug for implicit use in future commands
 
 .SH DESCRIPTION
 .PP
-Select a bug for implicit use in future commands
+Select a bug for implicit use in future commands.
 
 
 .SH OPTIONS

doc/man/git-bug-show.1 πŸ”—

@@ -1,11 +1,11 @@
-.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" "" 
+.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" "" 
 .nh
 .ad l
 
 
 .SH NAME
 .PP
-git\-bug\-show \- Display the details of a bug
+git\-bug\-show \- Display the details of a bug.
 
 
 .SH SYNOPSIS
@@ -15,7 +15,7 @@ git\-bug\-show \- Display the details of a bug
 
 .SH DESCRIPTION
 .PP
-Display the details of a bug
+Display the details of a bug.
 
 
 .SH OPTIONS

doc/man/git-bug-status-close.1 πŸ”—

@@ -1,11 +1,11 @@
-.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" "" 
+.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" "" 
 .nh
 .ad l
 
 
 .SH NAME
 .PP
-git\-bug\-status\-close \- Mark a bug as closed
+git\-bug\-status\-close \- Mark a bug as closed.
 
 
 .SH SYNOPSIS
@@ -15,7 +15,7 @@ git\-bug\-status\-close \- Mark a bug as closed
 
 .SH DESCRIPTION
 .PP
-Mark a bug as closed
+Mark a bug as closed.
 
 
 .SH OPTIONS

doc/man/git-bug-status-open.1 πŸ”—

@@ -1,11 +1,11 @@
-.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" "" 
+.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" "" 
 .nh
 .ad l
 
 
 .SH NAME
 .PP
-git\-bug\-status\-open \- Mark a bug as open
+git\-bug\-status\-open \- Mark a bug as open.
 
 
 .SH SYNOPSIS
@@ -15,7 +15,7 @@ git\-bug\-status\-open \- Mark a bug as open
 
 .SH DESCRIPTION
 .PP
-Mark a bug as open
+Mark a bug as open.
 
 
 .SH OPTIONS

doc/man/git-bug-status.1 πŸ”—

@@ -1,11 +1,11 @@
-.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" "" 
+.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" "" 
 .nh
 .ad l
 
 
 .SH NAME
 .PP
-git\-bug\-status \- Display or change a bug status
+git\-bug\-status \- Display or change a bug status.
 
 
 .SH SYNOPSIS
@@ -15,7 +15,7 @@ git\-bug\-status \- Display or change a bug status
 
 .SH DESCRIPTION
 .PP
-Display or change a bug status
+Display or change a bug status.
 
 
 .SH OPTIONS

doc/man/git-bug-termui.1 πŸ”—

@@ -1,11 +1,11 @@
-.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" "" 
+.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" "" 
 .nh
 .ad l
 
 
 .SH NAME
 .PP
-git\-bug\-termui \- Launch the terminal UI
+git\-bug\-termui \- Launch the terminal UI.
 
 
 .SH SYNOPSIS
@@ -15,7 +15,7 @@ git\-bug\-termui \- Launch the terminal UI
 
 .SH DESCRIPTION
 .PP
-Launch the terminal UI
+Launch the terminal UI.
 
 
 .SH OPTIONS

doc/man/git-bug-title-edit.1 πŸ”—

@@ -1,11 +1,11 @@
-.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" "" 
+.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" "" 
 .nh
 .ad l
 
 
 .SH NAME
 .PP
-git\-bug\-title\-edit \- Edit a title
+git\-bug\-title\-edit \- Edit a title.
 
 
 .SH SYNOPSIS
@@ -15,7 +15,7 @@ git\-bug\-title\-edit \- Edit a title
 
 .SH DESCRIPTION
 .PP
-Edit a title
+Edit a title.
 
 
 .SH OPTIONS

doc/man/git-bug-title.1 πŸ”—

@@ -1,11 +1,11 @@
-.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" "" 
+.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" "" 
 .nh
 .ad l
 
 
 .SH NAME
 .PP
-git\-bug\-title \- Display or change a title
+git\-bug\-title \- Display or change a title.
 
 
 .SH SYNOPSIS
@@ -15,7 +15,7 @@ git\-bug\-title \- Display or change a title
 
 .SH DESCRIPTION
 .PP
-Display or change a title
+Display or change a title.
 
 
 .SH OPTIONS

doc/man/git-bug-user-adopt.1 πŸ”—

@@ -0,0 +1,29 @@
+.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" "" 
+.nh
+.ad l
+
+
+.SH NAME
+.PP
+git\-bug\-user\-adopt \- Adopt an existing identity as your own.
+
+
+.SH SYNOPSIS
+.PP
+\fBgit\-bug user adopt <id> [flags]\fP
+
+
+.SH DESCRIPTION
+.PP
+Adopt an existing identity as your own.
+
+
+.SH OPTIONS
+.PP
+\fB\-h\fP, \fB\-\-help\fP[=false]
+    help for adopt
+
+
+.SH SEE ALSO
+.PP
+\fBgit\-bug\-user(1)\fP

doc/man/git-bug-user-create.1 πŸ”—

@@ -0,0 +1,29 @@
+.TH "GIT-BUG" "1" "Mar 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-user-ls.1 πŸ”—

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

doc/man/git-bug-user.1 πŸ”—

@@ -0,0 +1,29 @@
+.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" "" 
+.nh
+.ad l
+
+
+.SH NAME
+.PP
+git\-bug\-user \- Display or change the user identity.
+
+
+.SH SYNOPSIS
+.PP
+\fBgit\-bug user [<id>] [flags]\fP
+
+
+.SH DESCRIPTION
+.PP
+Display or change the user identity.
+
+
+.SH OPTIONS
+.PP
+\fB\-h\fP, \fB\-\-help\fP[=false]
+    help for user
+
+
+.SH SEE ALSO
+.PP
+\fBgit\-bug(1)\fP, \fBgit\-bug\-user\-adopt(1)\fP, \fBgit\-bug\-user\-create(1)\fP, \fBgit\-bug\-user\-ls(1)\fP

doc/man/git-bug-version.1 πŸ”—

@@ -1,11 +1,11 @@
-.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" "" 
+.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" "" 
 .nh
 .ad l
 
 
 .SH NAME
 .PP
-git\-bug\-version \- Show git\-bug version information
+git\-bug\-version \- Show git\-bug version information.
 
 
 .SH SYNOPSIS
@@ -15,7 +15,7 @@ git\-bug\-version \- Show git\-bug version information
 
 .SH DESCRIPTION
 .PP
-Show git\-bug version information
+Show git\-bug version information.
 
 
 .SH OPTIONS

doc/man/git-bug-webui.1 πŸ”—

@@ -1,11 +1,11 @@
-.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" "" 
+.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" "" 
 .nh
 .ad l
 
 
 .SH NAME
 .PP
-git\-bug\-webui \- Launch the web UI
+git\-bug\-webui \- Launch the web UI.
 
 
 .SH SYNOPSIS
@@ -15,7 +15,7 @@ git\-bug\-webui \- Launch the web UI
 
 .SH DESCRIPTION
 .PP
-Launch the web UI
+Launch the web UI.
 
 
 .SH OPTIONS

doc/man/git-bug.1 πŸ”—

@@ -1,11 +1,11 @@
-.TH "GIT-BUG" "1" "Feb 2019" "Generated from git-bug's source code" "" 
+.TH "GIT-BUG" "1" "Mar 2019" "Generated from git-bug's source code" "" 
 .nh
 .ad l
 
 
 .SH NAME
 .PP
-git\-bug \- A bug tracker embedded in Git
+git\-bug \- A bug tracker embedded in Git.
 
 
 .SH SYNOPSIS
@@ -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\-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\-id(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 πŸ”—

@@ -1,6 +1,6 @@
 ## git-bug
 
-A bug tracker embedded in Git
+A bug tracker embedded in Git.
 
 ### Synopsis
 
@@ -24,22 +24,23 @@ git-bug [flags]
 
 ### SEE ALSO
 
-* [git-bug add](git-bug_add.md)	 - Create a new bug
-* [git-bug bridge](git-bug_bridge.md)	 - Configure and use bridges to other bug trackers
-* [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 label](git-bug_label.md)	 - Display, add or remove labels
-* [git-bug ls](git-bug_ls.md)	 - List bugs
+* [git-bug add](git-bug_add.md)	 - Create a new bug.
+* [git-bug bridge](git-bug_bridge.md)	 - Configure and use bridges to other bug trackers.
+* [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 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
-* [git-bug ls-label](git-bug_ls-label.md)	 - List valid labels
-* [git-bug pull](git-bug_pull.md)	 - Pull bugs update from a git remote
-* [git-bug push](git-bug_push.md)	 - Push bugs update to a git remote
-* [git-bug select](git-bug_select.md)	 - Select a bug for implicit use in future commands
-* [git-bug show](git-bug_show.md)	 - Display the details of a bug
-* [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 version](git-bug_version.md)	 - Show git-bug version information
-* [git-bug webui](git-bug_webui.md)	 - Launch the web UI
+* [git-bug ls-label](git-bug_ls-label.md)	 - List valid labels.
+* [git-bug pull](git-bug_pull.md)	 - Pull bugs update from a git remote.
+* [git-bug push](git-bug_push.md)	 - Push bugs update to a git remote.
+* [git-bug select](git-bug_select.md)	 - Select a bug for implicit use in future commands.
+* [git-bug show](git-bug_show.md)	 - Display the details of a bug.
+* [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_add.md πŸ”—

@@ -1,10 +1,10 @@
 ## git-bug add
 
-Create a new bug
+Create a new bug.
 
 ### Synopsis
 
-Create a new bug
+Create a new bug.
 
 ```
 git-bug add [flags]
@@ -21,5 +21,5 @@ git-bug add [flags]
 
 ### SEE ALSO
 
-* [git-bug](git-bug.md)	 - A bug tracker embedded in Git
+* [git-bug](git-bug.md)	 - A bug tracker embedded in Git.
 

doc/md/git-bug_bridge.md πŸ”—

@@ -1,10 +1,10 @@
 ## git-bug bridge
 
-Configure and use bridges to other bug trackers
+Configure and use bridges to other bug trackers.
 
 ### Synopsis
 
-Configure and use bridges to other bug trackers
+Configure and use bridges to other bug trackers.
 
 ```
 git-bug bridge [flags]
@@ -18,8 +18,8 @@ git-bug bridge [flags]
 
 ### SEE ALSO
 
-* [git-bug](git-bug.md)	 - A bug tracker embedded in Git
-* [git-bug bridge configure](git-bug_bridge_configure.md)	 - Configure a new bridge
-* [git-bug bridge pull](git-bug_bridge_pull.md)	 - Pull updates
-* [git-bug bridge rm](git-bug_bridge_rm.md)	 - Delete a configured bridge
+* [git-bug](git-bug.md)	 - A bug tracker embedded in Git.
+* [git-bug bridge configure](git-bug_bridge_configure.md)	 - Configure a new bridge.
+* [git-bug bridge pull](git-bug_bridge_pull.md)	 - Pull updates.
+* [git-bug bridge rm](git-bug_bridge_rm.md)	 - Delete a configured bridge.
 

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_bridge_configure.md πŸ”—

@@ -1,10 +1,10 @@
 ## git-bug bridge configure
 
-Configure a new bridge
+Configure a new bridge.
 
 ### Synopsis
 
-Configure a new bridge
+Configure a new bridge.
 
 ```
 git-bug bridge configure [flags]
@@ -18,5 +18,5 @@ git-bug bridge configure [flags]
 
 ### SEE ALSO
 
-* [git-bug bridge](git-bug_bridge.md)	 - Configure and use bridges to other bug trackers
+* [git-bug bridge](git-bug_bridge.md)	 - Configure and use bridges to other bug trackers.
 

doc/md/git-bug_bridge_pull.md πŸ”—

@@ -1,10 +1,10 @@
 ## git-bug bridge pull
 
-Pull updates
+Pull updates.
 
 ### Synopsis
 
-Pull updates
+Pull updates.
 
 ```
 git-bug bridge pull [<name>] [flags]
@@ -18,5 +18,5 @@ git-bug bridge pull [<name>] [flags]
 
 ### SEE ALSO
 
-* [git-bug bridge](git-bug_bridge.md)	 - Configure and use bridges to other bug trackers
+* [git-bug bridge](git-bug_bridge.md)	 - Configure and use bridges to other bug trackers.
 

doc/md/git-bug_bridge_rm.md πŸ”—

@@ -1,10 +1,10 @@
 ## git-bug bridge rm
 
-Delete a configured bridge
+Delete a configured bridge.
 
 ### Synopsis
 
-Delete a configured bridge
+Delete a configured bridge.
 
 ```
 git-bug bridge rm name <name> [flags]
@@ -18,5 +18,5 @@ git-bug bridge rm name <name> [flags]
 
 ### SEE ALSO
 
-* [git-bug bridge](git-bug_bridge.md)	 - Configure and use bridges to other bug trackers
+* [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_commands.md πŸ”—

@@ -1,10 +1,10 @@
 ## git-bug commands
 
-Display available commands
+Display available commands.
 
 ### Synopsis
 
-Display available commands
+Display available commands.
 
 ```
 git-bug commands [<option>...] [flags]
@@ -19,5 +19,5 @@ git-bug commands [<option>...] [flags]
 
 ### SEE ALSO
 
-* [git-bug](git-bug.md)	 - A bug tracker embedded in Git
+* [git-bug](git-bug.md)	 - A bug tracker embedded in Git.
 

doc/md/git-bug_comment.md πŸ”—

@@ -1,10 +1,10 @@
 ## git-bug comment
 
-Display or add comments
+Display or add comments.
 
 ### Synopsis
 
-Display or add comments
+Display or add comments.
 
 ```
 git-bug comment [<id>] [flags]
@@ -18,6 +18,6 @@ git-bug comment [<id>] [flags]
 
 ### SEE ALSO
 
-* [git-bug](git-bug.md)	 - A bug tracker embedded in Git
-* [git-bug comment add](git-bug_comment_add.md)	 - Add a new comment
+* [git-bug](git-bug.md)	 - A bug tracker embedded in Git.
+* [git-bug comment add](git-bug_comment_add.md)	 - Add a new comment.
 

doc/md/git-bug_comment_add.md πŸ”—

@@ -1,10 +1,10 @@
 ## git-bug comment add
 
-Add a new comment
+Add a new comment.
 
 ### Synopsis
 
-Add a new comment
+Add a new comment.
 
 ```
 git-bug comment add [<id>] [flags]
@@ -20,5 +20,5 @@ git-bug comment add [<id>] [flags]
 
 ### SEE ALSO
 
-* [git-bug comment](git-bug_comment.md)	 - Display or add comments
+* [git-bug comment](git-bug_comment.md)	 - Display or add comments.
 

doc/md/git-bug_deselect.md πŸ”—

@@ -1,10 +1,10 @@
 ## git-bug deselect
 
-Clear the implicitly selected bug
+Clear the implicitly selected bug.
 
 ### Synopsis
 
-Clear the implicitly selected bug
+Clear the implicitly selected bug.
 
 ```
 git-bug deselect [flags]
@@ -28,5 +28,5 @@ git bug deselect
 
 ### SEE ALSO
 
-* [git-bug](git-bug.md)	 - A bug tracker embedded in Git
+* [git-bug](git-bug.md)	 - A bug tracker embedded in Git.
 

doc/md/git-bug_label.md πŸ”—

@@ -1,10 +1,10 @@
 ## git-bug label
 
-Display, add or remove labels
+Display, add or remove labels.
 
 ### Synopsis
 
-Display, add or remove labels
+Display, add or remove labels.
 
 ```
 git-bug label [<id>] [flags]
@@ -18,7 +18,7 @@ git-bug label [<id>] [flags]
 
 ### SEE ALSO
 
-* [git-bug](git-bug.md)	 - A bug tracker embedded in Git
-* [git-bug label add](git-bug_label_add.md)	 - Add a label
-* [git-bug label rm](git-bug_label_rm.md)	 - Remove a label
+* [git-bug](git-bug.md)	 - A bug tracker embedded in Git.
+* [git-bug label add](git-bug_label_add.md)	 - Add a label.
+* [git-bug label rm](git-bug_label_rm.md)	 - Remove a label.
 

doc/md/git-bug_label_add.md πŸ”—

@@ -1,10 +1,10 @@
 ## git-bug label add
 
-Add a label
+Add a label.
 
 ### Synopsis
 
-Add a label
+Add a label.
 
 ```
 git-bug label add [<id>] <label>[...] [flags]
@@ -18,5 +18,5 @@ git-bug label add [<id>] <label>[...] [flags]
 
 ### SEE ALSO
 
-* [git-bug label](git-bug_label.md)	 - Display, add or remove labels
+* [git-bug label](git-bug_label.md)	 - Display, add or remove labels.
 

doc/md/git-bug_label_rm.md πŸ”—

@@ -1,10 +1,10 @@
 ## git-bug label rm
 
-Remove a label
+Remove a label.
 
 ### Synopsis
 
-Remove a label
+Remove a label.
 
 ```
 git-bug label rm [<id>] <label>[...] [flags]
@@ -18,5 +18,5 @@ git-bug label rm [<id>] <label>[...] [flags]
 
 ### SEE ALSO
 
-* [git-bug label](git-bug_label.md)	 - Display, add or remove labels
+* [git-bug label](git-bug_label.md)	 - Display, add or remove labels.
 

doc/md/git-bug_ls-id.md πŸ”—

@@ -0,0 +1,22 @@
+## git-bug ls-id
+
+List Bug Id
+
+### Synopsis
+
+List Bug Id
+
+```
+git-bug ls-id [<prefix>] [flags]
+```
+
+### Options
+
+```
+  -h, --help   help for ls-id
+```
+
+### SEE ALSO
+
+* [git-bug](git-bug.md)	 - A bug tracker embedded in Git.
+

doc/md/git-bug_ls-label.md πŸ”—

@@ -1,6 +1,6 @@
 ## git-bug ls-label
 
-List valid labels
+List valid labels.
 
 ### Synopsis
 
@@ -20,5 +20,5 @@ git-bug ls-label [flags]
 
 ### SEE ALSO
 
-* [git-bug](git-bug.md)	 - A bug tracker embedded in Git
+* [git-bug](git-bug.md)	 - A bug tracker embedded in Git.
 

doc/md/git-bug_ls.md πŸ”—

@@ -1,6 +1,6 @@
 ## git-bug ls
 
-List bugs
+List bugs.
 
 ### Synopsis
 
@@ -37,5 +37,5 @@ git bug ls --status closed --by creation
 
 ### SEE ALSO
 
-* [git-bug](git-bug.md)	 - A bug tracker embedded in Git
+* [git-bug](git-bug.md)	 - A bug tracker 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_pull.md πŸ”—

@@ -1,10 +1,10 @@
 ## git-bug pull
 
-Pull bugs update from a git remote
+Pull bugs update from a git remote.
 
 ### Synopsis
 
-Pull bugs update from a git remote
+Pull bugs update from a git remote.
 
 ```
 git-bug pull [<remote>] [flags]
@@ -18,5 +18,5 @@ git-bug pull [<remote>] [flags]
 
 ### SEE ALSO
 
-* [git-bug](git-bug.md)	 - A bug tracker embedded in Git
+* [git-bug](git-bug.md)	 - A bug tracker embedded in Git.
 

doc/md/git-bug_push.md πŸ”—

@@ -1,10 +1,10 @@
 ## git-bug push
 
-Push bugs update to a git remote
+Push bugs update to a git remote.
 
 ### Synopsis
 
-Push bugs update to a git remote
+Push bugs update to a git remote.
 
 ```
 git-bug push [<remote>] [flags]
@@ -18,5 +18,5 @@ git-bug push [<remote>] [flags]
 
 ### SEE ALSO
 
-* [git-bug](git-bug.md)	 - A bug tracker embedded in Git
+* [git-bug](git-bug.md)	 - A bug tracker embedded in Git.
 

doc/md/git-bug_select.md πŸ”—

@@ -1,10 +1,10 @@
 ## git-bug select
 
-Select a bug for implicit use in future commands
+Select a bug for implicit use in future commands.
 
 ### Synopsis
 
-Select a bug for implicit use in future commands
+Select a bug for implicit use in future commands.
 
 ```
 git-bug select <id> [flags]
@@ -27,5 +27,5 @@ git bug status
 
 ### SEE ALSO
 
-* [git-bug](git-bug.md)	 - A bug tracker embedded in Git
+* [git-bug](git-bug.md)	 - A bug tracker embedded in Git.
 

doc/md/git-bug_show.md πŸ”—

@@ -1,10 +1,10 @@
 ## git-bug show
 
-Display the details of a bug
+Display the details of a bug.
 
 ### Synopsis
 
-Display the details of a bug
+Display the details of a bug.
 
 ```
 git-bug show [<id>] [flags]
@@ -19,5 +19,5 @@ git-bug show [<id>] [flags]
 
 ### SEE ALSO
 
-* [git-bug](git-bug.md)	 - A bug tracker embedded in Git
+* [git-bug](git-bug.md)	 - A bug tracker embedded in Git.
 

doc/md/git-bug_status.md πŸ”—

@@ -1,10 +1,10 @@
 ## git-bug status
 
-Display or change a bug status
+Display or change a bug status.
 
 ### Synopsis
 
-Display or change a bug status
+Display or change a bug status.
 
 ```
 git-bug status [<id>] [flags]
@@ -18,7 +18,7 @@ git-bug status [<id>] [flags]
 
 ### SEE ALSO
 
-* [git-bug](git-bug.md)	 - A bug tracker embedded in Git
-* [git-bug status close](git-bug_status_close.md)	 - Mark a bug as closed
-* [git-bug status open](git-bug_status_open.md)	 - Mark a bug as open
+* [git-bug](git-bug.md)	 - A bug tracker embedded in Git.
+* [git-bug status close](git-bug_status_close.md)	 - Mark a bug as closed.
+* [git-bug status open](git-bug_status_open.md)	 - Mark a bug as open.
 

doc/md/git-bug_status_close.md πŸ”—

@@ -1,10 +1,10 @@
 ## git-bug status close
 
-Mark a bug as closed
+Mark a bug as closed.
 
 ### Synopsis
 
-Mark a bug as closed
+Mark a bug as closed.
 
 ```
 git-bug status close [<id>] [flags]
@@ -18,5 +18,5 @@ git-bug status close [<id>] [flags]
 
 ### SEE ALSO
 
-* [git-bug status](git-bug_status.md)	 - Display or change a bug status
+* [git-bug status](git-bug_status.md)	 - Display or change a bug status.
 

doc/md/git-bug_status_open.md πŸ”—

@@ -1,10 +1,10 @@
 ## git-bug status open
 
-Mark a bug as open
+Mark a bug as open.
 
 ### Synopsis
 
-Mark a bug as open
+Mark a bug as open.
 
 ```
 git-bug status open [<id>] [flags]
@@ -18,5 +18,5 @@ git-bug status open [<id>] [flags]
 
 ### SEE ALSO
 
-* [git-bug status](git-bug_status.md)	 - Display or change a bug status
+* [git-bug status](git-bug_status.md)	 - Display or change a bug status.
 

doc/md/git-bug_termui.md πŸ”—

@@ -1,10 +1,10 @@
 ## git-bug termui
 
-Launch the terminal UI
+Launch the terminal UI.
 
 ### Synopsis
 
-Launch the terminal UI
+Launch the terminal UI.
 
 ```
 git-bug termui [flags]
@@ -18,5 +18,5 @@ git-bug termui [flags]
 
 ### SEE ALSO
 
-* [git-bug](git-bug.md)	 - A bug tracker embedded in Git
+* [git-bug](git-bug.md)	 - A bug tracker embedded in Git.
 

doc/md/git-bug_title.md πŸ”—

@@ -1,10 +1,10 @@
 ## git-bug title
 
-Display or change a title
+Display or change a title.
 
 ### Synopsis
 
-Display or change a title
+Display or change a title.
 
 ```
 git-bug title [<id>] [flags]
@@ -18,6 +18,6 @@ git-bug title [<id>] [flags]
 
 ### SEE ALSO
 
-* [git-bug](git-bug.md)	 - A bug tracker embedded in Git
-* [git-bug title edit](git-bug_title_edit.md)	 - Edit a title
+* [git-bug](git-bug.md)	 - A bug tracker embedded in Git.
+* [git-bug title edit](git-bug_title_edit.md)	 - Edit a title.
 

doc/md/git-bug_title_edit.md πŸ”—

@@ -1,10 +1,10 @@
 ## git-bug title edit
 
-Edit a title
+Edit a title.
 
 ### Synopsis
 
-Edit a title
+Edit a title.
 
 ```
 git-bug title edit [<id>] [flags]
@@ -19,5 +19,5 @@ git-bug title edit [<id>] [flags]
 
 ### SEE ALSO
 
-* [git-bug title](git-bug_title.md)	 - Display or change a title
+* [git-bug title](git-bug_title.md)	 - Display or change a title.
 

doc/md/git-bug_user.md πŸ”—

@@ -0,0 +1,25 @@
+## git-bug user
+
+Display or change the user identity.
+
+### Synopsis
+
+Display or change the user identity.
+
+```
+git-bug user [<id>] [flags]
+```
+
+### Options
+
+```
+  -h, --help   help for user
+```
+
+### SEE ALSO
+
+* [git-bug](git-bug.md)	 - A bug tracker embedded in Git.
+* [git-bug user adopt](git-bug_user_adopt.md)	 - Adopt an existing identity as your own.
+* [git-bug user create](git-bug_user_create.md)	 - Create a new identity.
+* [git-bug user ls](git-bug_user_ls.md)	 - List identities.
+

doc/md/git-bug_user_adopt.md πŸ”—

@@ -0,0 +1,22 @@
+## git-bug user adopt
+
+Adopt an existing identity as your own.
+
+### Synopsis
+
+Adopt an existing identity as your own.
+
+```
+git-bug user adopt <id> [flags]
+```
+
+### Options
+
+```
+  -h, --help   help for adopt
+```
+
+### SEE ALSO
+
+* [git-bug user](git-bug_user.md)	 - Display or change the user 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.
+

doc/md/git-bug_user_ls.md πŸ”—

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

doc/md/git-bug_version.md πŸ”—

@@ -1,10 +1,10 @@
 ## git-bug version
 
-Show git-bug version information
+Show git-bug version information.
 
 ### Synopsis
 
-Show git-bug version information
+Show git-bug version information.
 
 ```
 git-bug version [flags]
@@ -21,5 +21,5 @@ git-bug version [flags]
 
 ### SEE ALSO
 
-* [git-bug](git-bug.md)	 - A bug tracker embedded in Git
+* [git-bug](git-bug.md)	 - A bug tracker embedded in Git.
 

doc/md/git-bug_webui.md πŸ”—

@@ -1,10 +1,10 @@
 ## git-bug webui
 
-Launch the web UI
+Launch the web UI.
 
 ### Synopsis
 
-Launch the web UI
+Launch the web UI.
 
 ```
 git-bug webui [flags]
@@ -19,5 +19,5 @@ git-bug webui [flags]
 
 ### SEE ALSO
 
-* [git-bug](git-bug.md)	 - A bug tracker embedded in Git
+* [git-bug](git-bug.md)	 - A bug tracker embedded in Git.
 

graphql/gqlgen.yml πŸ”—

@@ -1,4 +1,4 @@
-schema: "*.graphql"
+schema: "schema/*.graphql"
 exec:
   filename: graph/gen_graph.go
 model:
@@ -13,17 +13,8 @@ models:
     model: github.com/MichaelMure/git-bug/bug.Snapshot
   Comment:
     model: github.com/MichaelMure/git-bug/bug.Comment
-  Person:
-    model: github.com/MichaelMure/git-bug/bug.Person
-    fields:
-      name:
-        resolver: true
-      email:
-        resolver: true
-      login:
-        resolver: true
-      avatarUrl:
-        resolver: true
+  Identity:
+    model: github.com/MichaelMure/git-bug/identity.Interface
   Label:
     model: github.com/MichaelMure/git-bug/bug.Label
   Hash:

graphql/graph/gen_graph.go πŸ”—

@@ -15,6 +15,7 @@ import (
 	"github.com/99designs/gqlgen/graphql/introspection"
 	"github.com/MichaelMure/git-bug/bug"
 	"github.com/MichaelMure/git-bug/graphql/models"
+	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/util/git"
 	"github.com/vektah/gqlparser"
 	"github.com/vektah/gqlparser/ast"
@@ -43,10 +44,10 @@ type ResolverRoot interface {
 	CreateOperation() CreateOperationResolver
 	CreateTimelineItem() CreateTimelineItemResolver
 	EditCommentOperation() EditCommentOperationResolver
+	Identity() IdentityResolver
 	LabelChangeOperation() LabelChangeOperationResolver
 	LabelChangeTimelineItem() LabelChangeTimelineItemResolver
 	Mutation() MutationResolver
-	Person() PersonResolver
 	Query() QueryResolver
 	Repository() RepositoryResolver
 	SetStatusOperation() SetStatusOperationResolver
@@ -158,6 +159,16 @@ type ComplexityRoot struct {
 		Files   func(childComplexity int) int
 	}
 
+	Identity struct {
+		Id          func(childComplexity int) int
+		Name        func(childComplexity int) int
+		Email       func(childComplexity int) int
+		Login       func(childComplexity int) int
+		DisplayName func(childComplexity int) int
+		AvatarUrl   func(childComplexity int) int
+		IsProtected func(childComplexity int) int
+	}
+
 	LabelChangeOperation struct {
 		Hash    func(childComplexity int) int
 		Author  func(childComplexity int) int
@@ -203,14 +214,6 @@ type ComplexityRoot struct {
 		EndCursor       func(childComplexity int) int
 	}
 
-	Person struct {
-		Name        func(childComplexity int) int
-		Email       func(childComplexity int) int
-		Login       func(childComplexity int) int
-		DisplayName func(childComplexity int) int
-		AvatarUrl   func(childComplexity int) int
-	}
-
 	Query struct {
 		DefaultRepository func(childComplexity int) int
 		Repository        func(childComplexity int, id string) int
@@ -292,6 +295,15 @@ type CreateTimelineItemResolver interface {
 type EditCommentOperationResolver interface {
 	Date(ctx context.Context, obj *bug.EditCommentOperation) (time.Time, error)
 }
+type IdentityResolver interface {
+	ID(ctx context.Context, obj *identity.Interface) (string, error)
+	Name(ctx context.Context, obj *identity.Interface) (*string, error)
+	Email(ctx context.Context, obj *identity.Interface) (*string, error)
+	Login(ctx context.Context, obj *identity.Interface) (*string, error)
+	DisplayName(ctx context.Context, obj *identity.Interface) (string, error)
+	AvatarURL(ctx context.Context, obj *identity.Interface) (*string, error)
+	IsProtected(ctx context.Context, obj *identity.Interface) (bool, error)
+}
 type LabelChangeOperationResolver interface {
 	Date(ctx context.Context, obj *bug.LabelChangeOperation) (time.Time, error)
 }
@@ -307,13 +319,6 @@ type MutationResolver interface {
 	SetTitle(ctx context.Context, repoRef *string, prefix string, title string) (bug.Snapshot, error)
 	Commit(ctx context.Context, repoRef *string, prefix string) (bug.Snapshot, error)
 }
-type PersonResolver interface {
-	Name(ctx context.Context, obj *bug.Person) (*string, error)
-	Email(ctx context.Context, obj *bug.Person) (*string, error)
-	Login(ctx context.Context, obj *bug.Person) (*string, error)
-
-	AvatarURL(ctx context.Context, obj *bug.Person) (*string, error)
-}
 type QueryResolver interface {
 	DefaultRepository(ctx context.Context) (*models.Repository, error)
 	Repository(ctx context.Context, id string) (*models.Repository, error)
@@ -1453,6 +1458,55 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.EditCommentOperation.Files(childComplexity), true
 
+	case "Identity.id":
+		if e.complexity.Identity.Id == nil {
+			break
+		}
+
+		return e.complexity.Identity.Id(childComplexity), true
+
+	case "Identity.name":
+		if e.complexity.Identity.Name == nil {
+			break
+		}
+
+		return e.complexity.Identity.Name(childComplexity), true
+
+	case "Identity.email":
+		if e.complexity.Identity.Email == nil {
+			break
+		}
+
+		return e.complexity.Identity.Email(childComplexity), true
+
+	case "Identity.login":
+		if e.complexity.Identity.Login == nil {
+			break
+		}
+
+		return e.complexity.Identity.Login(childComplexity), true
+
+	case "Identity.displayName":
+		if e.complexity.Identity.DisplayName == nil {
+			break
+		}
+
+		return e.complexity.Identity.DisplayName(childComplexity), true
+
+	case "Identity.avatarUrl":
+		if e.complexity.Identity.AvatarUrl == nil {
+			break
+		}
+
+		return e.complexity.Identity.AvatarUrl(childComplexity), true
+
+	case "Identity.isProtected":
+		if e.complexity.Identity.IsProtected == nil {
+			break
+		}
+
+		return e.complexity.Identity.IsProtected(childComplexity), true
+
 	case "LabelChangeOperation.hash":
 		if e.complexity.LabelChangeOperation.Hash == nil {
 			break
@@ -1677,41 +1731,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.PageInfo.EndCursor(childComplexity), true
 
-	case "Person.name":
-		if e.complexity.Person.Name == nil {
-			break
-		}
-
-		return e.complexity.Person.Name(childComplexity), true
-
-	case "Person.email":
-		if e.complexity.Person.Email == nil {
-			break
-		}
-
-		return e.complexity.Person.Email(childComplexity), true
-
-	case "Person.login":
-		if e.complexity.Person.Login == nil {
-			break
-		}
-
-		return e.complexity.Person.Login(childComplexity), true
-
-	case "Person.displayName":
-		if e.complexity.Person.DisplayName == nil {
-			break
-		}
-
-		return e.complexity.Person.DisplayName(childComplexity), true
-
-	case "Person.avatarUrl":
-		if e.complexity.Person.AvatarUrl == nil {
-			break
-		}
-
-		return e.complexity.Person.AvatarUrl(childComplexity), true
-
 	case "Query.defaultRepository":
 		if e.complexity.Query.DefaultRepository == nil {
 			break
@@ -2072,11 +2091,11 @@ func (ec *executionContext) _AddCommentOperation_author(ctx context.Context, fie
 		}
 		return graphql.Null
 	}
-	res := resTmp.(bug.Person)
+	res := resTmp.(identity.Interface)
 	rctx.Result = res
 	ctx = ec.Tracer.StartFieldChildExecution(ctx)
 
-	return ec._Person(ctx, field.Selections, &res)
+	return ec._Identity(ctx, field.Selections, &res)
 }
 
 // nolint: vetshadow
@@ -2296,11 +2315,11 @@ func (ec *executionContext) _AddCommentTimelineItem_author(ctx context.Context,
 		}
 		return graphql.Null
 	}
-	res := resTmp.(bug.Person)
+	res := resTmp.(identity.Interface)
 	rctx.Result = res
 	ctx = ec.Tracer.StartFieldChildExecution(ctx)
 
-	return ec._Person(ctx, field.Selections, &res)
+	return ec._Identity(ctx, field.Selections, &res)
 }
 
 // nolint: vetshadow
@@ -2800,11 +2819,11 @@ func (ec *executionContext) _Bug_author(ctx context.Context, field graphql.Colle
 		}
 		return graphql.Null
 	}
-	res := resTmp.(bug.Person)
+	res := resTmp.(identity.Interface)
 	rctx.Result = res
 	ctx = ec.Tracer.StartFieldChildExecution(ctx)
 
-	return ec._Person(ctx, field.Selections, &res)
+	return ec._Identity(ctx, field.Selections, &res)
 }
 
 // nolint: vetshadow
@@ -3334,11 +3353,11 @@ func (ec *executionContext) _Comment_author(ctx context.Context, field graphql.C
 		}
 		return graphql.Null
 	}
-	res := resTmp.(bug.Person)
+	res := resTmp.(identity.Interface)
 	rctx.Result = res
 	ctx = ec.Tracer.StartFieldChildExecution(ctx)
 
-	return ec._Person(ctx, field.Selections, &res)
+	return ec._Identity(ctx, field.Selections, &res)
 }
 
 // nolint: vetshadow
@@ -3916,11 +3935,11 @@ func (ec *executionContext) _CreateOperation_author(ctx context.Context, field g
 		}
 		return graphql.Null
 	}
-	res := resTmp.(bug.Person)
+	res := resTmp.(identity.Interface)
 	rctx.Result = res
 	ctx = ec.Tracer.StartFieldChildExecution(ctx)
 
-	return ec._Person(ctx, field.Selections, &res)
+	return ec._Identity(ctx, field.Selections, &res)
 }
 
 // nolint: vetshadow
@@ -4167,11 +4186,11 @@ func (ec *executionContext) _CreateTimelineItem_author(ctx context.Context, fiel
 		}
 		return graphql.Null
 	}
-	res := resTmp.(bug.Person)
+	res := resTmp.(identity.Interface)
 	rctx.Result = res
 	ctx = ec.Tracer.StartFieldChildExecution(ctx)
 
-	return ec._Person(ctx, field.Selections, &res)
+	return ec._Identity(ctx, field.Selections, &res)
 }
 
 // nolint: vetshadow
@@ -4513,11 +4532,11 @@ func (ec *executionContext) _EditCommentOperation_author(ctx context.Context, fi
 		}
 		return graphql.Null
 	}
-	res := resTmp.(bug.Person)
+	res := resTmp.(identity.Interface)
 	rctx.Result = res
 	ctx = ec.Tracer.StartFieldChildExecution(ctx)
 
-	return ec._Person(ctx, field.Selections, &res)
+	return ec._Identity(ctx, field.Selections, &res)
 }
 
 // nolint: vetshadow
@@ -4637,6 +4656,276 @@ func (ec *executionContext) _EditCommentOperation_files(ctx context.Context, fie
 	return arr1
 }
 
+var identityImplementors = []string{"Identity"}
+
+// nolint: gocyclo, errcheck, gas, goconst
+func (ec *executionContext) _Identity(ctx context.Context, sel ast.SelectionSet, obj *identity.Interface) graphql.Marshaler {
+	fields := graphql.CollectFields(ctx, sel, identityImplementors)
+
+	var wg sync.WaitGroup
+	out := graphql.NewOrderedMap(len(fields))
+	invalid := false
+	for i, field := range fields {
+		out.Keys[i] = field.Alias
+
+		switch field.Name {
+		case "__typename":
+			out.Values[i] = graphql.MarshalString("Identity")
+		case "id":
+			wg.Add(1)
+			go func(i int, field graphql.CollectedField) {
+				out.Values[i] = ec._Identity_id(ctx, field, obj)
+				if out.Values[i] == graphql.Null {
+					invalid = true
+				}
+				wg.Done()
+			}(i, field)
+		case "name":
+			wg.Add(1)
+			go func(i int, field graphql.CollectedField) {
+				out.Values[i] = ec._Identity_name(ctx, field, obj)
+				wg.Done()
+			}(i, field)
+		case "email":
+			wg.Add(1)
+			go func(i int, field graphql.CollectedField) {
+				out.Values[i] = ec._Identity_email(ctx, field, obj)
+				wg.Done()
+			}(i, field)
+		case "login":
+			wg.Add(1)
+			go func(i int, field graphql.CollectedField) {
+				out.Values[i] = ec._Identity_login(ctx, field, obj)
+				wg.Done()
+			}(i, field)
+		case "displayName":
+			wg.Add(1)
+			go func(i int, field graphql.CollectedField) {
+				out.Values[i] = ec._Identity_displayName(ctx, field, obj)
+				if out.Values[i] == graphql.Null {
+					invalid = true
+				}
+				wg.Done()
+			}(i, field)
+		case "avatarUrl":
+			wg.Add(1)
+			go func(i int, field graphql.CollectedField) {
+				out.Values[i] = ec._Identity_avatarUrl(ctx, field, obj)
+				wg.Done()
+			}(i, field)
+		case "isProtected":
+			wg.Add(1)
+			go func(i int, field graphql.CollectedField) {
+				out.Values[i] = ec._Identity_isProtected(ctx, field, obj)
+				if out.Values[i] == graphql.Null {
+					invalid = true
+				}
+				wg.Done()
+			}(i, field)
+		default:
+			panic("unknown field " + strconv.Quote(field.Name))
+		}
+	}
+	wg.Wait()
+	if invalid {
+		return graphql.Null
+	}
+	return out
+}
+
+// nolint: vetshadow
+func (ec *executionContext) _Identity_id(ctx context.Context, field graphql.CollectedField, obj *identity.Interface) graphql.Marshaler {
+	ctx = ec.Tracer.StartFieldExecution(ctx, field)
+	defer func() { ec.Tracer.EndFieldExecution(ctx) }()
+	rctx := &graphql.ResolverContext{
+		Object: "Identity",
+		Args:   nil,
+		Field:  field,
+	}
+	ctx = graphql.WithResolverContext(ctx, rctx)
+	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
+	resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return ec.resolvers.Identity().ID(rctx, obj)
+	})
+	if resTmp == nil {
+		if !ec.HasError(rctx) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(string)
+	rctx.Result = res
+	ctx = ec.Tracer.StartFieldChildExecution(ctx)
+	return graphql.MarshalString(res)
+}
+
+// nolint: vetshadow
+func (ec *executionContext) _Identity_name(ctx context.Context, field graphql.CollectedField, obj *identity.Interface) graphql.Marshaler {
+	ctx = ec.Tracer.StartFieldExecution(ctx, field)
+	defer func() { ec.Tracer.EndFieldExecution(ctx) }()
+	rctx := &graphql.ResolverContext{
+		Object: "Identity",
+		Args:   nil,
+		Field:  field,
+	}
+	ctx = graphql.WithResolverContext(ctx, rctx)
+	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
+	resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return ec.resolvers.Identity().Name(rctx, obj)
+	})
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.(*string)
+	rctx.Result = res
+	ctx = ec.Tracer.StartFieldChildExecution(ctx)
+
+	if res == nil {
+		return graphql.Null
+	}
+	return graphql.MarshalString(*res)
+}
+
+// nolint: vetshadow
+func (ec *executionContext) _Identity_email(ctx context.Context, field graphql.CollectedField, obj *identity.Interface) graphql.Marshaler {
+	ctx = ec.Tracer.StartFieldExecution(ctx, field)
+	defer func() { ec.Tracer.EndFieldExecution(ctx) }()
+	rctx := &graphql.ResolverContext{
+		Object: "Identity",
+		Args:   nil,
+		Field:  field,
+	}
+	ctx = graphql.WithResolverContext(ctx, rctx)
+	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
+	resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return ec.resolvers.Identity().Email(rctx, obj)
+	})
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.(*string)
+	rctx.Result = res
+	ctx = ec.Tracer.StartFieldChildExecution(ctx)
+
+	if res == nil {
+		return graphql.Null
+	}
+	return graphql.MarshalString(*res)
+}
+
+// nolint: vetshadow
+func (ec *executionContext) _Identity_login(ctx context.Context, field graphql.CollectedField, obj *identity.Interface) graphql.Marshaler {
+	ctx = ec.Tracer.StartFieldExecution(ctx, field)
+	defer func() { ec.Tracer.EndFieldExecution(ctx) }()
+	rctx := &graphql.ResolverContext{
+		Object: "Identity",
+		Args:   nil,
+		Field:  field,
+	}
+	ctx = graphql.WithResolverContext(ctx, rctx)
+	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
+	resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return ec.resolvers.Identity().Login(rctx, obj)
+	})
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.(*string)
+	rctx.Result = res
+	ctx = ec.Tracer.StartFieldChildExecution(ctx)
+
+	if res == nil {
+		return graphql.Null
+	}
+	return graphql.MarshalString(*res)
+}
+
+// nolint: vetshadow
+func (ec *executionContext) _Identity_displayName(ctx context.Context, field graphql.CollectedField, obj *identity.Interface) graphql.Marshaler {
+	ctx = ec.Tracer.StartFieldExecution(ctx, field)
+	defer func() { ec.Tracer.EndFieldExecution(ctx) }()
+	rctx := &graphql.ResolverContext{
+		Object: "Identity",
+		Args:   nil,
+		Field:  field,
+	}
+	ctx = graphql.WithResolverContext(ctx, rctx)
+	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
+	resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return ec.resolvers.Identity().DisplayName(rctx, obj)
+	})
+	if resTmp == nil {
+		if !ec.HasError(rctx) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(string)
+	rctx.Result = res
+	ctx = ec.Tracer.StartFieldChildExecution(ctx)
+	return graphql.MarshalString(res)
+}
+
+// nolint: vetshadow
+func (ec *executionContext) _Identity_avatarUrl(ctx context.Context, field graphql.CollectedField, obj *identity.Interface) graphql.Marshaler {
+	ctx = ec.Tracer.StartFieldExecution(ctx, field)
+	defer func() { ec.Tracer.EndFieldExecution(ctx) }()
+	rctx := &graphql.ResolverContext{
+		Object: "Identity",
+		Args:   nil,
+		Field:  field,
+	}
+	ctx = graphql.WithResolverContext(ctx, rctx)
+	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
+	resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return ec.resolvers.Identity().AvatarURL(rctx, obj)
+	})
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.(*string)
+	rctx.Result = res
+	ctx = ec.Tracer.StartFieldChildExecution(ctx)
+
+	if res == nil {
+		return graphql.Null
+	}
+	return graphql.MarshalString(*res)
+}
+
+// nolint: vetshadow
+func (ec *executionContext) _Identity_isProtected(ctx context.Context, field graphql.CollectedField, obj *identity.Interface) graphql.Marshaler {
+	ctx = ec.Tracer.StartFieldExecution(ctx, field)
+	defer func() { ec.Tracer.EndFieldExecution(ctx) }()
+	rctx := &graphql.ResolverContext{
+		Object: "Identity",
+		Args:   nil,
+		Field:  field,
+	}
+	ctx = graphql.WithResolverContext(ctx, rctx)
+	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
+	resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return ec.resolvers.Identity().IsProtected(rctx, obj)
+	})
+	if resTmp == nil {
+		if !ec.HasError(rctx) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(bool)
+	rctx.Result = res
+	ctx = ec.Tracer.StartFieldChildExecution(ctx)
+	return graphql.MarshalBoolean(res)
+}
+
 var labelChangeOperationImplementors = []string{"LabelChangeOperation", "Operation", "Authored"}
 
 // nolint: gocyclo, errcheck, gas, goconst
@@ -4740,11 +5029,11 @@ func (ec *executionContext) _LabelChangeOperation_author(ctx context.Context, fi
 		}
 		return graphql.Null
 	}
-	res := resTmp.(bug.Person)
+	res := resTmp.(identity.Interface)
 	rctx.Result = res
 	ctx = ec.Tracer.StartFieldChildExecution(ctx)
 
-	return ec._Person(ctx, field.Selections, &res)
+	return ec._Identity(ctx, field.Selections, &res)
 }
 
 // nolint: vetshadow
@@ -4949,11 +5238,11 @@ func (ec *executionContext) _LabelChangeTimelineItem_author(ctx context.Context,
 		}
 		return graphql.Null
 	}
-	res := resTmp.(bug.Person)
+	res := resTmp.(identity.Interface)
 	rctx.Result = res
 	ctx = ec.Tracer.StartFieldChildExecution(ctx)
 
-	return ec._Person(ctx, field.Selections, &res)
+	return ec._Identity(ctx, field.Selections, &res)
 }
 
 // nolint: vetshadow
@@ -5820,200 +6109,6 @@ func (ec *executionContext) _PageInfo_endCursor(ctx context.Context, field graph
 	return graphql.MarshalString(res)
 }
 
-var personImplementors = []string{"Person"}
-
-// nolint: gocyclo, errcheck, gas, goconst
-func (ec *executionContext) _Person(ctx context.Context, sel ast.SelectionSet, obj *bug.Person) graphql.Marshaler {
-	fields := graphql.CollectFields(ctx, sel, personImplementors)
-
-	var wg sync.WaitGroup
-	out := graphql.NewOrderedMap(len(fields))
-	invalid := false
-	for i, field := range fields {
-		out.Keys[i] = field.Alias
-
-		switch field.Name {
-		case "__typename":
-			out.Values[i] = graphql.MarshalString("Person")
-		case "name":
-			wg.Add(1)
-			go func(i int, field graphql.CollectedField) {
-				out.Values[i] = ec._Person_name(ctx, field, obj)
-				wg.Done()
-			}(i, field)
-		case "email":
-			wg.Add(1)
-			go func(i int, field graphql.CollectedField) {
-				out.Values[i] = ec._Person_email(ctx, field, obj)
-				wg.Done()
-			}(i, field)
-		case "login":
-			wg.Add(1)
-			go func(i int, field graphql.CollectedField) {
-				out.Values[i] = ec._Person_login(ctx, field, obj)
-				wg.Done()
-			}(i, field)
-		case "displayName":
-			out.Values[i] = ec._Person_displayName(ctx, field, obj)
-			if out.Values[i] == graphql.Null {
-				invalid = true
-			}
-		case "avatarUrl":
-			wg.Add(1)
-			go func(i int, field graphql.CollectedField) {
-				out.Values[i] = ec._Person_avatarUrl(ctx, field, obj)
-				wg.Done()
-			}(i, field)
-		default:
-			panic("unknown field " + strconv.Quote(field.Name))
-		}
-	}
-	wg.Wait()
-	if invalid {
-		return graphql.Null
-	}
-	return out
-}
-
-// nolint: vetshadow
-func (ec *executionContext) _Person_name(ctx context.Context, field graphql.CollectedField, obj *bug.Person) graphql.Marshaler {
-	ctx = ec.Tracer.StartFieldExecution(ctx, field)
-	defer func() { ec.Tracer.EndFieldExecution(ctx) }()
-	rctx := &graphql.ResolverContext{
-		Object: "Person",
-		Args:   nil,
-		Field:  field,
-	}
-	ctx = graphql.WithResolverContext(ctx, rctx)
-	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
-	resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) {
-		ctx = rctx // use context from middleware stack in children
-		return ec.resolvers.Person().Name(rctx, obj)
-	})
-	if resTmp == nil {
-		return graphql.Null
-	}
-	res := resTmp.(*string)
-	rctx.Result = res
-	ctx = ec.Tracer.StartFieldChildExecution(ctx)
-
-	if res == nil {
-		return graphql.Null
-	}
-	return graphql.MarshalString(*res)
-}
-
-// nolint: vetshadow
-func (ec *executionContext) _Person_email(ctx context.Context, field graphql.CollectedField, obj *bug.Person) graphql.Marshaler {
-	ctx = ec.Tracer.StartFieldExecution(ctx, field)
-	defer func() { ec.Tracer.EndFieldExecution(ctx) }()
-	rctx := &graphql.ResolverContext{
-		Object: "Person",
-		Args:   nil,
-		Field:  field,
-	}
-	ctx = graphql.WithResolverContext(ctx, rctx)
-	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
-	resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) {
-		ctx = rctx // use context from middleware stack in children
-		return ec.resolvers.Person().Email(rctx, obj)
-	})
-	if resTmp == nil {
-		return graphql.Null
-	}
-	res := resTmp.(*string)
-	rctx.Result = res
-	ctx = ec.Tracer.StartFieldChildExecution(ctx)
-
-	if res == nil {
-		return graphql.Null
-	}
-	return graphql.MarshalString(*res)
-}
-
-// nolint: vetshadow
-func (ec *executionContext) _Person_login(ctx context.Context, field graphql.CollectedField, obj *bug.Person) graphql.Marshaler {
-	ctx = ec.Tracer.StartFieldExecution(ctx, field)
-	defer func() { ec.Tracer.EndFieldExecution(ctx) }()
-	rctx := &graphql.ResolverContext{
-		Object: "Person",
-		Args:   nil,
-		Field:  field,
-	}
-	ctx = graphql.WithResolverContext(ctx, rctx)
-	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
-	resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) {
-		ctx = rctx // use context from middleware stack in children
-		return ec.resolvers.Person().Login(rctx, obj)
-	})
-	if resTmp == nil {
-		return graphql.Null
-	}
-	res := resTmp.(*string)
-	rctx.Result = res
-	ctx = ec.Tracer.StartFieldChildExecution(ctx)
-
-	if res == nil {
-		return graphql.Null
-	}
-	return graphql.MarshalString(*res)
-}
-
-// nolint: vetshadow
-func (ec *executionContext) _Person_displayName(ctx context.Context, field graphql.CollectedField, obj *bug.Person) graphql.Marshaler {
-	ctx = ec.Tracer.StartFieldExecution(ctx, field)
-	defer func() { ec.Tracer.EndFieldExecution(ctx) }()
-	rctx := &graphql.ResolverContext{
-		Object: "Person",
-		Args:   nil,
-		Field:  field,
-	}
-	ctx = graphql.WithResolverContext(ctx, rctx)
-	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
-	resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) {
-		ctx = rctx // use context from middleware stack in children
-		return obj.DisplayName(), nil
-	})
-	if resTmp == nil {
-		if !ec.HasError(rctx) {
-			ec.Errorf(ctx, "must not be null")
-		}
-		return graphql.Null
-	}
-	res := resTmp.(string)
-	rctx.Result = res
-	ctx = ec.Tracer.StartFieldChildExecution(ctx)
-	return graphql.MarshalString(res)
-}
-
-// nolint: vetshadow
-func (ec *executionContext) _Person_avatarUrl(ctx context.Context, field graphql.CollectedField, obj *bug.Person) graphql.Marshaler {
-	ctx = ec.Tracer.StartFieldExecution(ctx, field)
-	defer func() { ec.Tracer.EndFieldExecution(ctx) }()
-	rctx := &graphql.ResolverContext{
-		Object: "Person",
-		Args:   nil,
-		Field:  field,
-	}
-	ctx = graphql.WithResolverContext(ctx, rctx)
-	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
-	resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) {
-		ctx = rctx // use context from middleware stack in children
-		return ec.resolvers.Person().AvatarURL(rctx, obj)
-	})
-	if resTmp == nil {
-		return graphql.Null
-	}
-	res := resTmp.(*string)
-	rctx.Result = res
-	ctx = ec.Tracer.StartFieldChildExecution(ctx)
-
-	if res == nil {
-		return graphql.Null
-	}
-	return graphql.MarshalString(*res)
-}
-
 var queryImplementors = []string{"Query"}
 
 // nolint: gocyclo, errcheck, gas, goconst
@@ -6400,11 +6495,11 @@ func (ec *executionContext) _SetStatusOperation_author(ctx context.Context, fiel
 		}
 		return graphql.Null
 	}
-	res := resTmp.(bug.Person)
+	res := resTmp.(identity.Interface)
 	rctx.Result = res
 	ctx = ec.Tracer.StartFieldChildExecution(ctx)
 
-	return ec._Person(ctx, field.Selections, &res)
+	return ec._Identity(ctx, field.Selections, &res)
 }
 
 // nolint: vetshadow
@@ -6563,11 +6658,11 @@ func (ec *executionContext) _SetStatusTimelineItem_author(ctx context.Context, f
 		}
 		return graphql.Null
 	}
-	res := resTmp.(bug.Person)
+	res := resTmp.(identity.Interface)
 	rctx.Result = res
 	ctx = ec.Tracer.StartFieldChildExecution(ctx)
 
-	return ec._Person(ctx, field.Selections, &res)
+	return ec._Identity(ctx, field.Selections, &res)
 }
 
 // nolint: vetshadow
@@ -6727,11 +6822,11 @@ func (ec *executionContext) _SetTitleOperation_author(ctx context.Context, field
 		}
 		return graphql.Null
 	}
-	res := resTmp.(bug.Person)
+	res := resTmp.(identity.Interface)
 	rctx.Result = res
 	ctx = ec.Tracer.StartFieldChildExecution(ctx)
 
-	return ec._Person(ctx, field.Selections, &res)
+	return ec._Identity(ctx, field.Selections, &res)
 }
 
 // nolint: vetshadow
@@ -6918,11 +7013,11 @@ func (ec *executionContext) _SetTitleTimelineItem_author(ctx context.Context, fi
 		}
 		return graphql.Null
 	}
-	res := resTmp.(bug.Person)
+	res := resTmp.(identity.Interface)
 	rctx.Result = res
 	ctx = ec.Tracer.StartFieldChildExecution(ctx)
 
-	return ec._Person(ctx, field.Selections, &res)
+	return ec._Identity(ctx, field.Selections, &res)
 }
 
 // nolint: vetshadow
@@ -8862,24 +8957,10 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er
 }
 
 var parsedSchema = gqlparser.MustLoadSchema(
-	&ast.Source{Name: "bug.graphql", Input: `"""Represents an person"""
-type Person {
-  """The name of the person, if known."""
-  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"""
-  displayName: String!
-  """An url to an avatar"""
-  avatarUrl: String
-}
-
-"""Represents a comment on a bug."""
+	&ast.Source{Name: "schema/bug.graphql", Input: `"""Represents a comment on a bug."""
 type Comment implements Authored {
   """The author of this comment."""
-  author: Person!
+  author: Identity!
 
   """The message of this comment."""
   message: String!
@@ -8911,7 +8992,7 @@ type Bug {
   status: Status!
   title: String!
   labels: [Label!]!
-  author: Person!
+  author: Identity!
   createdAt: Time!
   lastEdit: Time!
 
@@ -8983,14 +9064,31 @@ type Repository {
   ): BugConnection!
   bug(prefix: String!): Bug
 }
-
 `},
-	&ast.Source{Name: "operations.graphql", Input: `"""An operation applied to a bug."""
+	&ast.Source{Name: "schema/identity.graphql", Input: `"""Represents an identity"""
+type Identity {
+    """The identifier for this identity"""
+    id: String!
+    """The name of the person, if known."""
+    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"""
+    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!
+}`},
+	&ast.Source{Name: "schema/operations.graphql", Input: `"""An operation applied to a bug."""
 interface Operation {
     """The hash of the operation"""
     hash: Hash!
     """The operations author."""
-    author: Person!
+    author: Identity!
     """The datetime when this operation was issued."""
     date: Time!
 }
@@ -9017,7 +9115,7 @@ type CreateOperation implements Operation & Authored {
     """The hash of the operation"""
     hash: Hash!
     """The author of this object."""
-    author: Person!
+    author: Identity!
     """The datetime when this operation was issued."""
     date: Time!
 
@@ -9030,7 +9128,7 @@ type SetTitleOperation implements Operation & Authored {
     """The hash of the operation"""
     hash: Hash!
     """The author of this object."""
-    author: Person!
+    author: Identity!
     """The datetime when this operation was issued."""
     date: Time!
 
@@ -9042,7 +9140,7 @@ type AddCommentOperation implements Operation & Authored {
     """The hash of the operation"""
     hash: Hash!
     """The author of this object."""
-    author: Person!
+    author: Identity!
     """The datetime when this operation was issued."""
     date: Time!
 
@@ -9054,7 +9152,7 @@ type EditCommentOperation implements Operation & Authored {
     """The hash of the operation"""
     hash: Hash!
     """The author of this object."""
-    author: Person!
+    author: Identity!
     """The datetime when this operation was issued."""
     date: Time!
 
@@ -9067,7 +9165,7 @@ type SetStatusOperation implements Operation & Authored {
     """The hash of the operation"""
     hash: Hash!
     """The author of this object."""
-    author: Person!
+    author: Identity!
     """The datetime when this operation was issued."""
     date: Time!
 
@@ -9078,14 +9176,15 @@ type LabelChangeOperation implements Operation & Authored {
     """The hash of the operation"""
     hash: Hash!
     """The author of this object."""
-    author: Person!
+    author: Identity!
     """The datetime when this operation was issued."""
     date: Time!
 
     added: [Label!]!
     removed: [Label!]!
-}`},
-	&ast.Source{Name: "root.graphql", Input: `scalar Time
+}
+`},
+	&ast.Source{Name: "schema/root.graphql", Input: `scalar Time
 scalar Label
 scalar Hash
 
@@ -9104,7 +9203,7 @@ type PageInfo {
 """An object that has an author."""
 interface Authored {
     """The author of this object."""
-    author: Person!
+    author: Identity!
 }
 
 type Query {
@@ -9122,8 +9221,9 @@ type Mutation {
     setTitle(repoRef: String, prefix: String!, title: String!): Bug!
 
     commit(repoRef: String, prefix: String!): Bug!
-}`},
-	&ast.Source{Name: "timeline.graphql", Input: `"""An item in the timeline of events"""
+}
+`},
+	&ast.Source{Name: "schema/timeline.graphql", Input: `"""An item in the timeline of events"""
 interface TimelineItem {
     """The hash of the source operation"""
     hash: Hash!
@@ -9157,7 +9257,7 @@ type TimelineItemEdge {
 type CreateTimelineItem implements TimelineItem {
     """The hash of the source operation"""
     hash: Hash!
-    author: Person!
+    author: Identity!
     message: String!
     messageIsEmpty: Boolean!
     files: [Hash!]!

tests/graphql_test.go β†’ graphql/graphql_test.go πŸ”—

@@ -1,18 +1,32 @@
-package tests
+package graphql
 
 import (
 	"net/http/httptest"
 	"testing"
 
-	"github.com/MichaelMure/git-bug/graphql"
 	"github.com/MichaelMure/git-bug/graphql/models"
+	"github.com/MichaelMure/git-bug/misc/random_bugs"
+	"github.com/MichaelMure/git-bug/repository"
+	"github.com/MichaelMure/git-bug/util/test"
 	"github.com/vektah/gqlgen/client"
 )
 
+func CreateFilledRepo(bugNumber int) repository.ClockedRepo {
+	repo := test.CreateRepo(false)
+
+	var seed int64 = 42
+	options := random_bugs.DefaultOptions()
+
+	options.BugNumber = bugNumber
+
+	random_bugs.CommitRandomBugsWithSeed(repo, options, seed)
+	return repo
+}
+
 func TestQueries(t *testing.T) {
-	repo := createFilledRepo(10)
+	repo := CreateFilledRepo(10)
 
-	handler, err := graphql.NewHandler(repo)
+	handler, err := NewHandler(repo)
 	if err != nil {
 		t.Fatal(err)
 	}

graphql/handler.go πŸ”—

@@ -4,11 +4,12 @@
 package graphql
 
 import (
+	"net/http"
+
 	"github.com/99designs/gqlgen/handler"
 	"github.com/MichaelMure/git-bug/graphql/graph"
 	"github.com/MichaelMure/git-bug/graphql/resolvers"
 	"github.com/MichaelMure/git-bug/repository"
-	"net/http"
 )
 
 // Handler is the root GraphQL http handler

graphql/resolvers/identity.go πŸ”—

@@ -0,0 +1,44 @@
+package resolvers
+
+import (
+	"context"
+
+	"github.com/MichaelMure/git-bug/identity"
+)
+
+type identityResolver struct{}
+
+func (identityResolver) ID(ctx context.Context, obj *identity.Interface) (string, error) {
+	return (*obj).Id(), nil
+}
+
+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 (identityResolver) IsProtected(ctx context.Context, obj *identity.Interface) (bool, error) {
+	return (*obj).IsProtected(), nil
+}
+
+func nilIfEmpty(s string) (*string, error) {
+	if s == "" {
+		return nil, nil
+	}
+	return &s, nil
+}

graphql/resolvers/mutation.go πŸ”—

@@ -68,7 +68,7 @@ func (r mutationResolver) AddComment(ctx context.Context, repoRef *string, prefi
 		return bug.Snapshot{}, err
 	}
 
-	err = b.AddCommentWithFiles(message, files)
+	_, err = b.AddCommentWithFiles(message, files)
 	if err != nil {
 		return bug.Snapshot{}, err
 	}
@@ -89,7 +89,7 @@ func (r mutationResolver) ChangeLabels(ctx context.Context, repoRef *string, pre
 		return bug.Snapshot{}, err
 	}
 
-	_, err = b.ChangeLabels(added, removed)
+	_, _, err = b.ChangeLabels(added, removed)
 	if err != nil {
 		return bug.Snapshot{}, err
 	}
@@ -110,7 +110,7 @@ func (r mutationResolver) Open(ctx context.Context, repoRef *string, prefix stri
 		return bug.Snapshot{}, err
 	}
 
-	err = b.Open()
+	_, err = b.Open()
 	if err != nil {
 		return bug.Snapshot{}, err
 	}
@@ -131,7 +131,7 @@ func (r mutationResolver) Close(ctx context.Context, repoRef *string, prefix str
 		return bug.Snapshot{}, err
 	}
 
-	err = b.Close()
+	_, err = b.Close()
 	if err != nil {
 		return bug.Snapshot{}, err
 	}
@@ -152,7 +152,7 @@ func (r mutationResolver) SetTitle(ctx context.Context, repoRef *string, prefix
 		return bug.Snapshot{}, err
 	}
 
-	err = b.SetTitle(title)
+	_, err = b.SetTitle(title)
 	if err != nil {
 		return bug.Snapshot{}, err
 	}

graphql/resolvers/person.go πŸ”—

@@ -1,37 +0,0 @@
-package resolvers
-
-import (
-	"context"
-
-	"github.com/MichaelMure/git-bug/bug"
-)
-
-type personResolver struct{}
-
-func (personResolver) Name(ctx context.Context, obj *bug.Person) (*string, error) {
-	if obj.Name == "" {
-		return nil, nil
-	}
-	return &obj.Name, nil
-}
-
-func (personResolver) Email(ctx context.Context, obj *bug.Person) (*string, error) {
-	if obj.Email == "" {
-		return nil, nil
-	}
-	return &obj.Email, nil
-}
-
-func (personResolver) Login(ctx context.Context, obj *bug.Person) (*string, error) {
-	if obj.Login == "" {
-		return nil, nil
-	}
-	return &obj.Login, nil
-}
-
-func (personResolver) AvatarURL(ctx context.Context, obj *bug.Person) (*string, error) {
-	if obj.AvatarUrl == "" {
-		return nil, nil
-	}
-	return &obj.AvatarUrl, nil
-}

graphql/resolvers/root.go πŸ”—

@@ -32,8 +32,8 @@ func (RootResolver) Bug() graph.BugResolver {
 	return &bugResolver{}
 }
 
-func (r RootResolver) Person() graph.PersonResolver {
-	return &personResolver{}
+func (r RootResolver) Identity() graph.IdentityResolver {
+	return &identityResolver{}
 }
 
 func (RootResolver) CommentHistoryStep() graph.CommentHistoryStepResolver {

graphql/bug.graphql β†’ graphql/schema/bug.graphql πŸ”—

@@ -1,21 +1,7 @@
-"""Represents an person"""
-type Person {
-  """The name of the person, if known."""
-  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"""
-  displayName: String!
-  """An url to an avatar"""
-  avatarUrl: String
-}
-
 """Represents a comment on a bug."""
 type Comment implements Authored {
   """The author of this comment."""
-  author: Person!
+  author: Identity!
 
   """The message of this comment."""
   message: String!
@@ -47,7 +33,7 @@ type Bug {
   status: Status!
   title: String!
   labels: [Label!]!
-  author: Person!
+  author: Identity!
   createdAt: Time!
   lastEdit: Time!
 
@@ -119,4 +105,3 @@ type Repository {
   ): BugConnection!
   bug(prefix: String!): Bug
 }
-

graphql/schema/identity.graphql πŸ”—

@@ -0,0 +1,18 @@
+"""Represents an identity"""
+type Identity {
+    """The identifier for this identity"""
+    id: String!
+    """The name of the person, if known."""
+    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"""
+    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!
+}

graphql/operations.graphql β†’ graphql/schema/operations.graphql πŸ”—

@@ -3,7 +3,7 @@ interface Operation {
     """The hash of the operation"""
     hash: Hash!
     """The operations author."""
-    author: Person!
+    author: Identity!
     """The datetime when this operation was issued."""
     date: Time!
 }
@@ -30,7 +30,7 @@ type CreateOperation implements Operation & Authored {
     """The hash of the operation"""
     hash: Hash!
     """The author of this object."""
-    author: Person!
+    author: Identity!
     """The datetime when this operation was issued."""
     date: Time!
 
@@ -43,7 +43,7 @@ type SetTitleOperation implements Operation & Authored {
     """The hash of the operation"""
     hash: Hash!
     """The author of this object."""
-    author: Person!
+    author: Identity!
     """The datetime when this operation was issued."""
     date: Time!
 
@@ -55,7 +55,7 @@ type AddCommentOperation implements Operation & Authored {
     """The hash of the operation"""
     hash: Hash!
     """The author of this object."""
-    author: Person!
+    author: Identity!
     """The datetime when this operation was issued."""
     date: Time!
 
@@ -67,7 +67,7 @@ type EditCommentOperation implements Operation & Authored {
     """The hash of the operation"""
     hash: Hash!
     """The author of this object."""
-    author: Person!
+    author: Identity!
     """The datetime when this operation was issued."""
     date: Time!
 
@@ -80,7 +80,7 @@ type SetStatusOperation implements Operation & Authored {
     """The hash of the operation"""
     hash: Hash!
     """The author of this object."""
-    author: Person!
+    author: Identity!
     """The datetime when this operation was issued."""
     date: Time!
 
@@ -91,10 +91,10 @@ type LabelChangeOperation implements Operation & Authored {
     """The hash of the operation"""
     hash: Hash!
     """The author of this object."""
-    author: Person!
+    author: Identity!
     """The datetime when this operation was issued."""
     date: Time!
 
     added: [Label!]!
     removed: [Label!]!
-}
+}

graphql/root.graphql β†’ graphql/schema/root.graphql πŸ”—

@@ -17,7 +17,7 @@ type PageInfo {
 """An object that has an author."""
 interface Authored {
     """The author of this object."""
-    author: Person!
+    author: Identity!
 }
 
 type Query {
@@ -35,4 +35,4 @@ type Mutation {
     setTitle(repoRef: String, prefix: String!, title: String!): Bug!
 
     commit(repoRef: String, prefix: String!): Bug!
-}
+}

graphql/timeline.graphql β†’ graphql/schema/timeline.graphql πŸ”—

@@ -32,7 +32,7 @@ type TimelineItemEdge {
 type CreateTimelineItem implements TimelineItem {
     """The hash of the source operation"""
     hash: Hash!
-    author: Person!
+    author: Identity!
     message: String!
     messageIsEmpty: Boolean!
     files: [Hash!]!
@@ -46,7 +46,7 @@ type CreateTimelineItem implements TimelineItem {
 type AddCommentTimelineItem implements TimelineItem {
     """The hash of the source operation"""
     hash: Hash!
-    author: Person!
+    author: Identity!
     message: String!
     messageIsEmpty: Boolean!
     files: [Hash!]!
@@ -60,7 +60,7 @@ type AddCommentTimelineItem implements TimelineItem {
 type LabelChangeTimelineItem implements TimelineItem {
     """The hash of the source operation"""
     hash: Hash!
-    author: Person!
+    author: Identity!
     date: Time!
     added: [Label!]!
     removed: [Label!]!
@@ -70,7 +70,7 @@ type LabelChangeTimelineItem implements TimelineItem {
 type SetStatusTimelineItem implements TimelineItem {
     """The hash of the source operation"""
     hash: Hash!
-    author: Person!
+    author: Identity!
     date: Time!
     status: Status!
 }
@@ -79,8 +79,8 @@ type SetStatusTimelineItem implements TimelineItem {
 type SetTitleTimelineItem implements TimelineItem {
     """The hash of the source operation"""
     hash: Hash!
-    author: Person!
+    author: Identity!
     date: Time!
     title: String!
     was: String!
-}
+}

identity/bare.go πŸ”—

@@ -0,0 +1,204 @@
+package identity
+
+import (
+	"crypto/sha256"
+	"encoding/json"
+	"fmt"
+	"strings"
+
+	"github.com/MichaelMure/git-bug/repository"
+	"github.com/MichaelMure/git-bug/util/lamport"
+	"github.com/MichaelMure/git-bug/util/text"
+	"github.com/MichaelMure/git-bug/util/timestamp"
+)
+
+var _ Interface = &Bare{}
+
+// Bare is a very minimal identity, designed to be fully embedded directly along
+// other data.
+//
+// in particular, this identity is designed to be compatible with the handling of
+// identities in the early version of git-bug.
+type Bare struct {
+	id        string
+	name      string
+	email     string
+	login     string
+	avatarUrl string
+}
+
+func NewBare(name string, email string) *Bare {
+	return &Bare{name: name, email: email}
+}
+
+func NewBareFull(name string, email string, login string, avatarUrl string) *Bare {
+	return &Bare{name: name, email: email, login: login, avatarUrl: avatarUrl}
+}
+
+type bareIdentityJSON struct {
+	Name      string `json:"name,omitempty"`
+	Email     string `json:"email,omitempty"`
+	Login     string `json:"login,omitempty"`
+	AvatarUrl string `json:"avatar_url,omitempty"`
+}
+
+func (i *Bare) MarshalJSON() ([]byte, error) {
+	return json.Marshal(bareIdentityJSON{
+		Name:      i.name,
+		Email:     i.email,
+		Login:     i.login,
+		AvatarUrl: i.avatarUrl,
+	})
+}
+
+func (i *Bare) UnmarshalJSON(data []byte) error {
+	aux := bareIdentityJSON{}
+
+	if err := json.Unmarshal(data, &aux); err != nil {
+		return err
+	}
+
+	i.name = aux.Name
+	i.email = aux.Email
+	i.login = aux.Login
+	i.avatarUrl = aux.AvatarUrl
+
+	return nil
+}
+
+// Id return the Identity identifier
+func (i *Bare) Id() string {
+	// We don't have a proper ID at hand, so let's hash all the data to get one.
+	// Hopefully the
+
+	if i.id != "" {
+		return i.id
+	}
+
+	data, err := json.Marshal(i)
+	if err != nil {
+		panic(err)
+	}
+
+	h := fmt.Sprintf("%x", sha256.New().Sum(data)[:16])
+	i.id = string(h)
+
+	return i.id
+}
+
+// HumanId return the Identity identifier truncated for human consumption
+func (i *Bare) HumanId() string {
+	return FormatHumanID(i.Id())
+}
+
+// Name return the last version of the name
+func (i *Bare) Name() string {
+	return i.name
+}
+
+// Email return the last version of the email
+func (i *Bare) Email() string {
+	return i.email
+}
+
+// Login return the last version of the login
+func (i *Bare) Login() string {
+	return i.login
+}
+
+// AvatarUrl return the last version of the Avatar URL
+func (i *Bare) AvatarUrl() string {
+	return i.avatarUrl
+}
+
+// Keys return the last version of the valid keys
+func (i *Bare) Keys() []Key {
+	return []Key{}
+}
+
+// ValidKeysAtTime return the set of keys valid at a given lamport time
+func (i *Bare) ValidKeysAtTime(time lamport.Time) []Key {
+	return []Key{}
+}
+
+// DisplayName return a non-empty string to display, representing the
+// identity, based on the non-empty values.
+func (i *Bare) 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")
+}
+
+// Validate check if the Identity data is valid
+func (i *Bare) Validate() error {
+	if text.Empty(i.name) && text.Empty(i.login) {
+		return fmt.Errorf("either name or login should be set")
+	}
+
+	if strings.Contains(i.name, "\n") {
+		return fmt.Errorf("name should be a single line")
+	}
+
+	if !text.Safe(i.name) {
+		return fmt.Errorf("name is not fully printable")
+	}
+
+	if strings.Contains(i.login, "\n") {
+		return fmt.Errorf("login should be a single line")
+	}
+
+	if !text.Safe(i.login) {
+		return fmt.Errorf("login is not fully printable")
+	}
+
+	if strings.Contains(i.email, "\n") {
+		return fmt.Errorf("email should be a single line")
+	}
+
+	if !text.Safe(i.email) {
+		return fmt.Errorf("email is not fully printable")
+	}
+
+	if i.avatarUrl != "" && !text.ValidUrl(i.avatarUrl) {
+		return fmt.Errorf("avatarUrl is not a valid URL")
+	}
+
+	return nil
+}
+
+// Write the identity into the Repository. In particular, this ensure that
+// the Id is properly set.
+func (i *Bare) Commit(repo repository.ClockedRepo) error {
+	// Nothing to do, everything is directly embedded
+	return nil
+}
+
+// If needed, write the identity into the Repository. In particular, this
+// ensure that the Id is properly set.
+func (i *Bare) CommitAsNeeded(repo repository.ClockedRepo) error {
+	// Nothing to do, everything is directly embedded
+	return nil
+}
+
+// IsProtected return 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.
+func (i *Bare) IsProtected() bool {
+	return false
+}
+
+// LastModificationLamportTime return the Lamport time at which the last version of the identity became valid.
+func (i *Bare) LastModificationLamport() lamport.Time {
+	return 0
+}
+
+// LastModification return the timestamp at which the last version of the identity became valid.
+func (i *Bare) LastModification() timestamp.Timestamp {
+	return 0
+}

identity/bare_test.go πŸ”—

@@ -0,0 +1,32 @@
+package identity
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestBare_Id(t *testing.T) {
+	i := NewBare("name", "email")
+	id := i.Id()
+	assert.Equal(t, "7b226e616d65223a226e616d65222c22", id)
+}
+
+func TestBareSerialize(t *testing.T) {
+	before := &Bare{
+		login:     "login",
+		email:     "email",
+		name:      "name",
+		avatarUrl: "avatar",
+	}
+
+	data, err := json.Marshal(before)
+	assert.NoError(t, err)
+
+	var after Bare
+	err = json.Unmarshal(data, &after)
+	assert.NoError(t, err)
+
+	assert.Equal(t, before, &after)
+}

identity/common.go πŸ”—

@@ -0,0 +1,53 @@
+package identity
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"strings"
+)
+
+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"))
+}
+
+// Custom unmarshaling function to allow package user to delegate
+// the decoding of an Identity and distinguish between an Identity
+// and a Bare.
+//
+// If the given message has a "id" field, it's considered being a proper Identity.
+func UnmarshalJSON(raw json.RawMessage) (Interface, error) {
+	aux := &IdentityStub{}
+
+	// First try to decode and load as a normal Identity
+	err := json.Unmarshal(raw, &aux)
+	if err == nil && aux.Id() != "" {
+		return aux, nil
+	}
+
+	// abort if we have an error other than the wrong type
+	if _, ok := err.(*json.UnmarshalTypeError); err != nil && !ok {
+		return nil, err
+	}
+
+	// Fallback on a legacy Bare identity
+	var b Bare
+
+	err = json.Unmarshal(raw, &b)
+	if err == nil && (b.name != "" || b.login != "") {
+		return &b, nil
+	}
+
+	// abort if we have an error other than the wrong type
+	if _, ok := err.(*json.UnmarshalTypeError); err != nil && !ok {
+		return nil, err
+	}
+
+	return nil, fmt.Errorf("unknown identity type")
+}

identity/identity.go πŸ”—

@@ -0,0 +1,584 @@
+// Package identity contains the identity data model and low-level related functions
+package identity
+
+import (
+	"encoding/json"
+	"fmt"
+	"os"
+	"strings"
+	"time"
+
+	"github.com/MichaelMure/git-bug/util/timestamp"
+	"github.com/pkg/errors"
+
+	"github.com/MichaelMure/git-bug/repository"
+	"github.com/MichaelMure/git-bug/util/git"
+	"github.com/MichaelMure/git-bug/util/lamport"
+)
+
+const identityRefPattern = "refs/identities/"
+const identityRemoteRefPattern = "refs/remotes/%s/identities/"
+const versionEntryName = "version"
+const identityConfigKey = "git-bug.identity"
+
+const idLength = 40
+const humanIdLength = 7
+
+var ErrNonFastForwardMerge = errors.New("non fast-forward identity merge")
+var ErrNoIdentitySet = errors.New("user identity first needs to be created using \"git bug user create\" or \"git bug user adopt\"")
+var ErrMultipleIdentitiesSet = errors.New("multiple user identities set")
+
+var _ Interface = &Identity{}
+
+type Identity struct {
+	// Id used as unique identifier
+	id string
+
+	// all the successive version of the identity
+	versions []*Version
+
+	// not serialized
+	lastCommit git.Hash
+}
+
+func NewIdentity(name string, email string) *Identity {
+	return &Identity{
+		versions: []*Version{
+			{
+				name:  name,
+				email: email,
+				nonce: makeNonce(20),
+			},
+		},
+	}
+}
+
+func NewIdentityFull(name string, email string, login string, avatarUrl string) *Identity {
+	return &Identity{
+		versions: []*Version{
+			{
+				name:      name,
+				email:     email,
+				login:     login,
+				avatarURL: avatarUrl,
+				nonce:     makeNonce(20),
+			},
+		},
+	}
+}
+
+// MarshalJSON will only serialize the id
+func (i *Identity) MarshalJSON() ([]byte, error) {
+	return json.Marshal(&IdentityStub{
+		id: i.Id(),
+	})
+}
+
+// UnmarshalJSON will only read the id
+// Users of this package are expected to run Load() to load
+// the remaining data from the identities data in git.
+func (i *Identity) UnmarshalJSON(data []byte) error {
+	panic("identity should be loaded with identity.UnmarshalJSON")
+}
+
+// ReadLocal load a local Identity from the identities data available in git
+func ReadLocal(repo repository.Repo, id string) (*Identity, error) {
+	ref := fmt.Sprintf("%s%s", identityRefPattern, id)
+	return read(repo, ref)
+}
+
+// ReadRemote load a remote Identity from the identities data available in git
+func ReadRemote(repo repository.Repo, remote string, id string) (*Identity, error) {
+	ref := fmt.Sprintf(identityRemoteRefPattern, remote) + id
+	return read(repo, ref)
+}
+
+// read will load and parse an identity from git
+func read(repo repository.Repo, ref string) (*Identity, error) {
+	refSplit := strings.Split(ref, "/")
+	id := refSplit[len(refSplit)-1]
+
+	if len(id) != idLength {
+		return nil, fmt.Errorf("invalid ref length")
+	}
+
+	hashes, err := repo.ListCommits(ref)
+
+	// TODO: this is not perfect, it might be a command invoke error
+	if err != nil {
+		return nil, ErrIdentityNotExist
+	}
+
+	i := &Identity{
+		id: id,
+	}
+
+	for _, hash := range hashes {
+		entries, err := repo.ListEntries(hash)
+		if err != nil {
+			return nil, errors.Wrap(err, "can't list git tree entries")
+		}
+
+		if len(entries) != 1 {
+			return nil, fmt.Errorf("invalid identity data at hash %s", hash)
+		}
+
+		entry := entries[0]
+
+		if entry.Name != versionEntryName {
+			return nil, fmt.Errorf("invalid identity data at hash %s", hash)
+		}
+
+		data, err := repo.ReadData(entry.Hash)
+		if err != nil {
+			return nil, errors.Wrap(err, "failed to read git blob data")
+		}
+
+		var version Version
+		err = json.Unmarshal(data, &version)
+
+		if err != nil {
+			return nil, errors.Wrapf(err, "failed to decode Identity version json %s", hash)
+		}
+
+		// tag the version with the commit hash
+		version.commitHash = hash
+		i.lastCommit = hash
+
+		i.versions = append(i.versions, &version)
+	}
+
+	return i, nil
+}
+
+type StreamedIdentity struct {
+	Identity *Identity
+	Err      error
+}
+
+// ReadAllLocalIdentities read and parse all local Identity
+func ReadAllLocalIdentities(repo repository.ClockedRepo) <-chan StreamedIdentity {
+	return readAllIdentities(repo, identityRefPattern)
+}
+
+// ReadAllRemoteIdentities read and parse all remote Identity for a given remote
+func ReadAllRemoteIdentities(repo repository.ClockedRepo, remote string) <-chan StreamedIdentity {
+	refPrefix := fmt.Sprintf(identityRemoteRefPattern, remote)
+	return readAllIdentities(repo, refPrefix)
+}
+
+// Read and parse all available bug with a given ref prefix
+func readAllIdentities(repo repository.ClockedRepo, refPrefix string) <-chan StreamedIdentity {
+	out := make(chan StreamedIdentity)
+
+	go func() {
+		defer close(out)
+
+		refs, err := repo.ListRefs(refPrefix)
+		if err != nil {
+			out <- StreamedIdentity{Err: err}
+			return
+		}
+
+		for _, ref := range refs {
+			b, err := read(repo, ref)
+
+			if err != nil {
+				out <- StreamedIdentity{Err: err}
+				return
+			}
+
+			out <- StreamedIdentity{Identity: b}
+		}
+	}()
+
+	return out
+}
+
+// NewFromGitUser will query the repository for user detail and
+// build the corresponding Identity
+func NewFromGitUser(repo repository.Repo) (*Identity, error) {
+	name, err := repo.GetUserName()
+	if err != nil {
+		return nil, err
+	}
+	if name == "" {
+		return nil, errors.New("user name is not configured in git yet. Please use `git config --global user.name \"John Doe\"`")
+	}
+
+	email, err := repo.GetUserEmail()
+	if err != nil {
+		return nil, err
+	}
+	if email == "" {
+		return nil, errors.New("user name is not configured in git yet. Please use `git config --global user.email johndoe@example.com`")
+	}
+
+	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, ErrMultipleIdentitiesSet
+	}
+
+	return len(configs) == 1, nil
+}
+
+// SetUserIdentity store the user identity's id in the git config
+func SetUserIdentity(repo repository.RepoCommon, identity *Identity) error {
+	return repo.StoreConfig(identityConfigKey, identity.Id())
+}
+
+// GetUserIdentity read the current user identity, set with a git config entry
+func GetUserIdentity(repo repository.Repo) (*Identity, error) {
+	configs, err := repo.ReadConfigs(identityConfigKey)
+	if err != nil {
+		return nil, err
+	}
+
+	if len(configs) == 0 {
+		return nil, ErrNoIdentitySet
+	}
+
+	if len(configs) > 1 {
+		return nil, ErrMultipleIdentitiesSet
+	}
+
+	var id string
+	for _, val := range configs {
+		id = val
+	}
+
+	i, err := ReadLocal(repo, id)
+	if err == ErrIdentityNotExist {
+		innerErr := repo.RmConfigs(identityConfigKey)
+		if innerErr != nil {
+			_, _ = fmt.Fprintln(os.Stderr, errors.Wrap(innerErr, "can't clear user identity").Error())
+		}
+		return nil, err
+	}
+
+	return i, nil
+}
+
+func (i *Identity) AddVersion(version *Version) {
+	i.versions = append(i.versions, version)
+}
+
+// Write the identity into the Repository. In particular, this ensure that
+// the Id is properly set.
+func (i *Identity) Commit(repo repository.ClockedRepo) error {
+	// Todo: check for mismatch between memory and commit data
+
+	if !i.NeedCommit() {
+		return fmt.Errorf("can't commit an identity with no pending version")
+	}
+
+	if err := i.Validate(); err != nil {
+		return errors.Wrap(err, "can't commit an identity with invalid data")
+	}
+
+	for _, v := range i.versions {
+		if v.commitHash != "" {
+			i.lastCommit = v.commitHash
+			// ignore already commit versions
+			continue
+		}
+
+		// get the times where new versions starts to be valid
+		v.time = repo.EditTime()
+		v.unixTime = time.Now().Unix()
+
+		blobHash, err := v.Write(repo)
+		if err != nil {
+			return err
+		}
+
+		// Make a git tree referencing the blob
+		tree := []repository.TreeEntry{
+			{ObjectType: repository.Blob, Hash: blobHash, Name: versionEntryName},
+		}
+
+		treeHash, err := repo.StoreTree(tree)
+		if err != nil {
+			return err
+		}
+
+		var commitHash git.Hash
+		if i.lastCommit != "" {
+			commitHash, err = repo.StoreCommitWithParent(treeHash, i.lastCommit)
+		} else {
+			commitHash, err = repo.StoreCommit(treeHash)
+		}
+
+		if err != nil {
+			return err
+		}
+
+		i.lastCommit = commitHash
+		v.commitHash = commitHash
+
+		// if it was the first commit, use the commit hash as the Identity id
+		if i.id == "" {
+			i.id = string(commitHash)
+		}
+	}
+
+	if i.id == "" {
+		panic("identity with no id")
+	}
+
+	ref := fmt.Sprintf("%s%s", identityRefPattern, i.id)
+	err := repo.UpdateRef(ref, i.lastCommit)
+
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (i *Identity) CommitAsNeeded(repo repository.ClockedRepo) error {
+	if !i.NeedCommit() {
+		return nil
+	}
+	return i.Commit(repo)
+}
+
+func (i *Identity) NeedCommit() bool {
+	for _, v := range i.versions {
+		if v.commitHash == "" {
+			return true
+		}
+	}
+
+	return false
+}
+
+// Merge will merge a different version of the same Identity
+//
+// To make sure that an Identity history can't be altered, a strict fast-forward
+// only policy is applied here. As an Identity should be tied to a single user, this
+// should work in practice but it does leave a possibility that a user would edit his
+// Identity from two different repo concurrently and push the changes in a non-centralized
+// network of repositories. In this case, it would result in some of the repo accepting one
+// version and some other accepting another, preventing the network in general to converge
+// to the same result. This would create a sort of partition of the network, and manual
+// cleaning would be required.
+//
+// An alternative approach would be to have a determinist rebase:
+// - any commits present in both local and remote version would be kept, never changed.
+// - newer commits would be merged in a linear chain of commits, ordered based on the
+//   Lamport time
+//
+// However, this approach leave the possibility, in the case of a compromised crypto keys,
+// of forging a new version with a bogus Lamport time to be inserted before a legit version,
+// invalidating the correct version and hijacking the Identity. There would only be a short
+// period of time where this would be possible (before the network converge) but I'm not
+// confident enough to implement that. I choose the strict fast-forward only approach,
+// despite it's potential problem with two different version as mentioned above.
+func (i *Identity) Merge(repo repository.Repo, other *Identity) (bool, error) {
+	if i.id != other.id {
+		return false, errors.New("merging unrelated identities is not supported")
+	}
+
+	if i.lastCommit == "" || other.lastCommit == "" {
+		return false, errors.New("can't merge identities that has never been stored")
+	}
+
+	modified := false
+	for j, otherVersion := range other.versions {
+		// if there is more version in other, take them
+		if len(i.versions) == j {
+			i.versions = append(i.versions, otherVersion)
+			i.lastCommit = otherVersion.commitHash
+			modified = true
+		}
+
+		// we have a non fast-forward merge.
+		// as explained in the doc above, refusing to merge
+		if i.versions[j].commitHash != otherVersion.commitHash {
+			return false, ErrNonFastForwardMerge
+		}
+	}
+
+	if modified {
+		err := repo.UpdateRef(identityRefPattern+i.id, i.lastCommit)
+		if err != nil {
+			return false, err
+		}
+	}
+
+	return false, nil
+}
+
+// Validate check if the Identity data is valid
+func (i *Identity) Validate() error {
+	lastTime := lamport.Time(0)
+
+	if len(i.versions) == 0 {
+		return fmt.Errorf("no version")
+	}
+
+	for _, v := range i.versions {
+		if err := v.Validate(); err != nil {
+			return err
+		}
+
+		if v.commitHash != "" && v.time < lastTime {
+			return fmt.Errorf("non-chronological version (%d --> %d)", lastTime, v.time)
+		}
+
+		lastTime = v.time
+	}
+
+	// The identity ID should be the hash of the first commit
+	if i.versions[0].commitHash != "" && string(i.versions[0].commitHash) != i.id {
+		return fmt.Errorf("identity id should be the first commit hash")
+	}
+
+	return nil
+}
+
+func (i *Identity) lastVersion() *Version {
+	if len(i.versions) <= 0 {
+		panic("no version at all")
+	}
+
+	return i.versions[len(i.versions)-1]
+}
+
+// Id return the Identity identifier
+func (i *Identity) Id() string {
+	if i.id == "" {
+		// simply panic as it would be a coding error
+		// (using an id of an identity not stored yet)
+		panic("no id yet")
+	}
+	return i.id
+}
+
+// HumanId return the Identity identifier truncated for human consumption
+func (i *Identity) HumanId() string {
+	return FormatHumanID(i.Id())
+}
+
+func FormatHumanID(id string) string {
+	format := fmt.Sprintf("%%.%ds", humanIdLength)
+	return fmt.Sprintf(format, id)
+}
+
+// Name return the last version of the name
+func (i *Identity) Name() string {
+	return i.lastVersion().name
+}
+
+// Email return the last version of the email
+func (i *Identity) Email() string {
+	return i.lastVersion().email
+}
+
+// Login return the last version of the login
+func (i *Identity) Login() string {
+	return i.lastVersion().login
+}
+
+// AvatarUrl return the last version of the Avatar URL
+func (i *Identity) AvatarUrl() string {
+	return i.lastVersion().avatarURL
+}
+
+// Keys return the last version of the valid keys
+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
+
+	for _, v := range i.versions {
+		if v.time > time {
+			return result
+		}
+
+		result = v.keys
+	}
+
+	return result
+}
+
+// DisplayName return a non-empty string to display, representing the
+// identity, based on the non-empty values.
+func (i *Identity) 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")
+}
+
+// IsProtected return 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.
+func (i *Identity) IsProtected() bool {
+	// Todo
+	return false
+}
+
+// LastModificationLamportTime return the Lamport time at which the last version of the identity became valid.
+func (i *Identity) LastModificationLamport() lamport.Time {
+	return i.lastVersion().time
+}
+
+// LastModification return the timestamp at which the last version of the identity became valid.
+func (i *Identity) LastModification() timestamp.Timestamp {
+	return timestamp.Timestamp(i.lastVersion().unixTime)
+}
+
+// SetMetadata store arbitrary metadata along the last defined Version.
+// If the Version has been commit to git already, it won't be overwritten.
+func (i *Identity) SetMetadata(key string, value string) {
+	i.lastVersion().SetMetadata(key, value)
+}
+
+// ImmutableMetadata return all metadata for this Identity, accumulated from each Version.
+// If multiple value are found, the first defined takes precedence.
+func (i *Identity) ImmutableMetadata() map[string]string {
+	metadata := make(map[string]string)
+
+	for _, version := range i.versions {
+		for key, value := range version.metadata {
+			if _, has := metadata[key]; !has {
+				metadata[key] = value
+			}
+		}
+	}
+
+	return metadata
+}
+
+// MutableMetadata return all metadata for this Identity, accumulated from each Version.
+// If multiple value are found, the last defined takes precedence.
+func (i *Identity) MutableMetadata() map[string]string {
+	metadata := make(map[string]string)
+
+	for _, version := range i.versions {
+		for key, value := range version.metadata {
+			metadata[key] = value
+		}
+	}
+
+	return metadata
+}

identity/identity_actions.go πŸ”—

@@ -0,0 +1,187 @@
+package identity
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/MichaelMure/git-bug/repository"
+	"github.com/pkg/errors"
+)
+
+// Fetch retrieve updates from a remote
+// This does not change the local identities state
+func Fetch(repo repository.Repo, remote string) (string, error) {
+	remoteRefSpec := fmt.Sprintf(identityRemoteRefPattern, remote)
+	fetchRefSpec := fmt.Sprintf("%s*:%s*", identityRefPattern, remoteRefSpec)
+
+	return repo.FetchRefs(remote, fetchRefSpec)
+}
+
+// Push update a remote with the local changes
+func Push(repo repository.Repo, remote string) (string, error) {
+	return repo.PushRefs(remote, identityRefPattern+"*")
+}
+
+// Pull will do a Fetch + MergeAll
+// This function will return an error if a merge fail
+func Pull(repo repository.ClockedRepo, remote string) error {
+	_, err := Fetch(repo, remote)
+	if err != nil {
+		return err
+	}
+
+	for merge := range MergeAll(repo, remote) {
+		if merge.Err != nil {
+			return merge.Err
+		}
+		if merge.Status == MergeStatusInvalid {
+			return errors.Errorf("merge failure: %s", merge.Reason)
+		}
+	}
+
+	return nil
+}
+
+// MergeAll will merge all the available remote identity
+func MergeAll(repo repository.ClockedRepo, remote string) <-chan MergeResult {
+	out := make(chan MergeResult)
+
+	go func() {
+		defer close(out)
+
+		remoteRefSpec := fmt.Sprintf(identityRemoteRefPattern, remote)
+		remoteRefs, err := repo.ListRefs(remoteRefSpec)
+
+		if err != nil {
+			out <- MergeResult{Err: err}
+			return
+		}
+
+		for _, remoteRef := range remoteRefs {
+			refSplitted := strings.Split(remoteRef, "/")
+			id := refSplitted[len(refSplitted)-1]
+
+			remoteIdentity, err := read(repo, remoteRef)
+
+			if err != nil {
+				out <- newMergeInvalidStatus(id, errors.Wrap(err, "remote identity is not readable").Error())
+				continue
+			}
+
+			// Check for error in remote data
+			if err := remoteIdentity.Validate(); err != nil {
+				out <- newMergeInvalidStatus(id, errors.Wrap(err, "remote identity is invalid").Error())
+				continue
+			}
+
+			localRef := identityRefPattern + remoteIdentity.Id()
+			localExist, err := repo.RefExist(localRef)
+
+			if err != nil {
+				out <- newMergeError(err, id)
+				continue
+			}
+
+			// the identity is not local yet, simply create the reference
+			if !localExist {
+				err := repo.CopyRef(remoteRef, localRef)
+
+				if err != nil {
+					out <- newMergeError(err, id)
+					return
+				}
+
+				out <- newMergeStatus(MergeStatusNew, id, remoteIdentity)
+				continue
+			}
+
+			localIdentity, err := read(repo, localRef)
+
+			if err != nil {
+				out <- newMergeError(errors.Wrap(err, "local identity is not readable"), id)
+				return
+			}
+
+			updated, err := localIdentity.Merge(repo, remoteIdentity)
+
+			if err != nil {
+				out <- newMergeInvalidStatus(id, errors.Wrap(err, "merge failed").Error())
+				return
+			}
+
+			if updated {
+				out <- newMergeStatus(MergeStatusUpdated, id, localIdentity)
+			} else {
+				out <- newMergeStatus(MergeStatusNothing, id, localIdentity)
+			}
+		}
+	}()
+
+	return out
+}
+
+// MergeStatus represent the result of a merge operation of a bug
+type MergeStatus int
+
+const (
+	_ MergeStatus = iota
+	MergeStatusNew
+	MergeStatusInvalid
+	MergeStatusUpdated
+	MergeStatusNothing
+)
+
+// Todo: share a generalized MergeResult with the bug package ?
+type MergeResult struct {
+	// Err is set when a terminal error occur in the process
+	Err error
+
+	Id     string
+	Status MergeStatus
+
+	// Only set for invalid status
+	Reason string
+
+	// Not set for invalid status
+	Identity *Identity
+}
+
+func (mr MergeResult) String() string {
+	switch mr.Status {
+	case MergeStatusNew:
+		return "new"
+	case MergeStatusInvalid:
+		return fmt.Sprintf("invalid data: %s", mr.Reason)
+	case MergeStatusUpdated:
+		return "updated"
+	case MergeStatusNothing:
+		return "nothing to do"
+	default:
+		panic("unknown merge status")
+	}
+}
+
+func newMergeError(err error, id string) MergeResult {
+	return MergeResult{
+		Err: err,
+		Id:  id,
+	}
+}
+
+func newMergeStatus(status MergeStatus, id string, identity *Identity) MergeResult {
+	return MergeResult{
+		Id:     id,
+		Status: status,
+
+		// Identity is not set for an invalid merge result
+		Identity: identity,
+	}
+}
+
+func newMergeInvalidStatus(id string, reason string) MergeResult {
+	return MergeResult{
+		Id:     id,
+		Status: MergeStatusInvalid,
+		Reason: reason,
+	}
+}

identity/identity_actions_test.go πŸ”—

@@ -0,0 +1,151 @@
+package identity
+
+import (
+	"testing"
+
+	"github.com/MichaelMure/git-bug/util/test"
+	"github.com/stretchr/testify/require"
+)
+
+func TestPushPull(t *testing.T) {
+	repoA, repoB, remote := test.SetupReposAndRemote(t)
+	defer test.CleanupRepos(repoA, repoB, remote)
+
+	identity1 := NewIdentity("name1", "email1")
+	err := identity1.Commit(repoA)
+	require.NoError(t, err)
+
+	// A --> remote --> B
+	_, err = Push(repoA, "origin")
+	require.NoError(t, err)
+
+	err = Pull(repoB, "origin")
+	require.NoError(t, err)
+
+	identities := allIdentities(t, ReadAllLocalIdentities(repoB))
+
+	if len(identities) != 1 {
+		t.Fatal("Unexpected number of bugs")
+	}
+
+	// B --> remote --> A
+	identity2 := NewIdentity("name2", "email2")
+	err = identity2.Commit(repoB)
+	require.NoError(t, err)
+
+	_, err = Push(repoB, "origin")
+	require.NoError(t, err)
+
+	err = Pull(repoA, "origin")
+	require.NoError(t, err)
+
+	identities = allIdentities(t, ReadAllLocalIdentities(repoA))
+
+	if len(identities) != 2 {
+		t.Fatal("Unexpected number of bugs")
+	}
+
+	// Update both
+
+	identity1.AddVersion(&Version{
+		name:  "name1b",
+		email: "email1b",
+	})
+	err = identity1.Commit(repoA)
+	require.NoError(t, err)
+
+	identity2.AddVersion(&Version{
+		name:  "name2b",
+		email: "email2b",
+	})
+	err = identity2.Commit(repoB)
+	require.NoError(t, err)
+
+	//  A --> remote --> B
+
+	_, err = Push(repoA, "origin")
+	require.NoError(t, err)
+
+	err = Pull(repoB, "origin")
+	require.NoError(t, err)
+
+	identities = allIdentities(t, ReadAllLocalIdentities(repoB))
+
+	if len(identities) != 2 {
+		t.Fatal("Unexpected number of bugs")
+	}
+
+	// B --> remote --> A
+
+	_, err = Push(repoB, "origin")
+	require.NoError(t, err)
+
+	err = Pull(repoA, "origin")
+	require.NoError(t, err)
+
+	identities = allIdentities(t, ReadAllLocalIdentities(repoA))
+
+	if len(identities) != 2 {
+		t.Fatal("Unexpected number of bugs")
+	}
+
+	// Concurrent update
+
+	identity1.AddVersion(&Version{
+		name:  "name1c",
+		email: "email1c",
+	})
+	err = identity1.Commit(repoA)
+	require.NoError(t, err)
+
+	identity1B, err := ReadLocal(repoB, identity1.Id())
+	require.NoError(t, err)
+
+	identity1B.AddVersion(&Version{
+		name:  "name1concurrent",
+		email: "email1concurrent",
+	})
+	err = identity1B.Commit(repoB)
+	require.NoError(t, err)
+
+	//  A --> remote --> B
+
+	_, err = Push(repoA, "origin")
+	require.NoError(t, err)
+
+	// Pulling a non-fast-forward update should fail
+	err = Pull(repoB, "origin")
+	require.Error(t, err)
+
+	identities = allIdentities(t, ReadAllLocalIdentities(repoB))
+
+	if len(identities) != 2 {
+		t.Fatal("Unexpected number of bugs")
+	}
+
+	// B --> remote --> A
+
+	// Pushing a non-fast-forward update should fail
+	_, err = Push(repoB, "origin")
+	require.Error(t, err)
+
+	err = Pull(repoA, "origin")
+	require.NoError(t, err)
+
+	identities = allIdentities(t, ReadAllLocalIdentities(repoA))
+
+	if len(identities) != 2 {
+		t.Fatal("Unexpected number of bugs")
+	}
+}
+
+func allIdentities(t testing.TB, identities <-chan StreamedIdentity) []*Identity {
+	var result []*Identity
+	for streamed := range identities {
+		if streamed.Err != nil {
+			t.Fatal(streamed.Err)
+		}
+		result = append(result, streamed.Identity)
+	}
+	return result
+}

identity/identity_stub.go πŸ”—

@@ -0,0 +1,104 @@
+package identity
+
+import (
+	"encoding/json"
+
+	"github.com/MichaelMure/git-bug/repository"
+	"github.com/MichaelMure/git-bug/util/lamport"
+	"github.com/MichaelMure/git-bug/util/timestamp"
+)
+
+var _ Interface = &IdentityStub{}
+
+// IdentityStub is an almost empty Identity, holding only the id.
+// When a normal Identity is serialized into JSON, only the id is serialized.
+// All the other data are stored in git in a chain of commit + a ref.
+// When this JSON is deserialized, an IdentityStub is returned instead, to be replaced
+// later by the proper Identity, loaded from the Repo.
+type IdentityStub struct {
+	id string
+}
+
+func (i *IdentityStub) MarshalJSON() ([]byte, error) {
+	return json.Marshal(struct {
+		Id string `json:"id"`
+	}{
+		Id: i.id,
+	})
+}
+
+func (i *IdentityStub) UnmarshalJSON(data []byte) error {
+	aux := struct {
+		Id string `json:"id"`
+	}{}
+
+	if err := json.Unmarshal(data, &aux); err != nil {
+		return err
+	}
+
+	i.id = aux.Id
+
+	return nil
+}
+
+// Id return the Identity identifier
+func (i *IdentityStub) Id() string {
+	return i.id
+}
+
+// HumanId return the Identity identifier truncated for human consumption
+func (i *IdentityStub) HumanId() string {
+	return FormatHumanID(i.Id())
+}
+
+func (IdentityStub) Name() string {
+	panic("identities needs to be properly loaded with identity.ReadLocal()")
+}
+
+func (IdentityStub) Email() string {
+	panic("identities needs to be properly loaded with identity.ReadLocal()")
+}
+
+func (IdentityStub) Login() string {
+	panic("identities needs to be properly loaded with identity.ReadLocal()")
+}
+
+func (IdentityStub) AvatarUrl() string {
+	panic("identities needs to be properly loaded with identity.ReadLocal()")
+}
+
+func (IdentityStub) Keys() []Key {
+	panic("identities needs to be properly loaded with identity.ReadLocal()")
+}
+
+func (IdentityStub) ValidKeysAtTime(time lamport.Time) []Key {
+	panic("identities needs to be properly loaded with identity.ReadLocal()")
+}
+
+func (IdentityStub) DisplayName() string {
+	panic("identities needs to be properly loaded with identity.ReadLocal()")
+}
+
+func (IdentityStub) Validate() error {
+	panic("identities needs to be properly loaded with identity.ReadLocal()")
+}
+
+func (IdentityStub) Commit(repo repository.ClockedRepo) error {
+	panic("identities needs to be properly loaded with identity.ReadLocal()")
+}
+
+func (i *IdentityStub) CommitAsNeeded(repo repository.ClockedRepo) error {
+	panic("identities needs to be properly loaded with identity.ReadLocal()")
+}
+
+func (IdentityStub) IsProtected() bool {
+	panic("identities needs to be properly loaded with identity.ReadLocal()")
+}
+
+func (i *IdentityStub) LastModificationLamport() lamport.Time {
+	panic("identities needs to be properly loaded with identity.ReadLocal()")
+}
+
+func (i *IdentityStub) LastModification() timestamp.Timestamp {
+	panic("identities needs to be properly loaded with identity.ReadLocal()")
+}

identity/identity_stub_test.go πŸ”—

@@ -0,0 +1,23 @@
+package identity
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestIdentityStubSerialize(t *testing.T) {
+	before := &IdentityStub{
+		id: "id1234",
+	}
+
+	data, err := json.Marshal(before)
+	assert.NoError(t, err)
+
+	var after IdentityStub
+	err = json.Unmarshal(data, &after)
+	assert.NoError(t, err)
+
+	assert.Equal(t, before, &after)
+}

identity/identity_test.go πŸ”—

@@ -0,0 +1,244 @@
+package identity
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/MichaelMure/git-bug/repository"
+	"github.com/stretchr/testify/assert"
+)
+
+// Test the commit and load of an Identity with multiple versions
+func TestIdentityCommitLoad(t *testing.T) {
+	mockRepo := repository.NewMockRepoForTest()
+
+	// single version
+
+	identity := &Identity{
+		versions: []*Version{
+			{
+				name:  "RenΓ© Descartes",
+				email: "rene.descartes@example.com",
+			},
+		},
+	}
+
+	err := identity.Commit(mockRepo)
+
+	assert.Nil(t, err)
+	assert.NotEmpty(t, identity.id)
+
+	loaded, err := ReadLocal(mockRepo, identity.id)
+	assert.Nil(t, err)
+	commitsAreSet(t, loaded)
+	assert.Equal(t, identity, loaded)
+
+	// multiple version
+
+	identity = &Identity{
+		versions: []*Version{
+			{
+				time:  100,
+				name:  "RenΓ© Descartes",
+				email: "rene.descartes@example.com",
+				keys: []Key{
+					{PubKey: "pubkeyA"},
+				},
+			},
+			{
+				time:  200,
+				name:  "RenΓ© Descartes",
+				email: "rene.descartes@example.com",
+				keys: []Key{
+					{PubKey: "pubkeyB"},
+				},
+			},
+			{
+				time:  201,
+				name:  "RenΓ© Descartes",
+				email: "rene.descartes@example.com",
+				keys: []Key{
+					{PubKey: "pubkeyC"},
+				},
+			},
+		},
+	}
+
+	err = identity.Commit(mockRepo)
+
+	assert.Nil(t, err)
+	assert.NotEmpty(t, identity.id)
+
+	loaded, err = ReadLocal(mockRepo, identity.id)
+	assert.Nil(t, err)
+	commitsAreSet(t, loaded)
+	assert.Equal(t, identity, loaded)
+
+	// add more version
+
+	identity.AddVersion(&Version{
+		time:  201,
+		name:  "RenΓ© Descartes",
+		email: "rene.descartes@example.com",
+		keys: []Key{
+			{PubKey: "pubkeyD"},
+		},
+	})
+
+	identity.AddVersion(&Version{
+		time:  300,
+		name:  "RenΓ© Descartes",
+		email: "rene.descartes@example.com",
+		keys: []Key{
+			{PubKey: "pubkeyE"},
+		},
+	})
+
+	err = identity.Commit(mockRepo)
+
+	assert.Nil(t, err)
+	assert.NotEmpty(t, identity.id)
+
+	loaded, err = ReadLocal(mockRepo, identity.id)
+	assert.Nil(t, err)
+	commitsAreSet(t, loaded)
+	assert.Equal(t, identity, loaded)
+}
+
+func commitsAreSet(t *testing.T, identity *Identity) {
+	for _, version := range identity.versions {
+		assert.NotEmpty(t, version.commitHash)
+	}
+}
+
+// Test that the correct crypto keys are returned for a given lamport time
+func TestIdentity_ValidKeysAtTime(t *testing.T) {
+	identity := Identity{
+		versions: []*Version{
+			{
+				time:  100,
+				name:  "RenΓ© Descartes",
+				email: "rene.descartes@example.com",
+				keys: []Key{
+					{PubKey: "pubkeyA"},
+				},
+			},
+			{
+				time:  200,
+				name:  "RenΓ© Descartes",
+				email: "rene.descartes@example.com",
+				keys: []Key{
+					{PubKey: "pubkeyB"},
+				},
+			},
+			{
+				time:  201,
+				name:  "RenΓ© Descartes",
+				email: "rene.descartes@example.com",
+				keys: []Key{
+					{PubKey: "pubkeyC"},
+				},
+			},
+			{
+				time:  201,
+				name:  "RenΓ© Descartes",
+				email: "rene.descartes@example.com",
+				keys: []Key{
+					{PubKey: "pubkeyD"},
+				},
+			},
+			{
+				time:  300,
+				name:  "RenΓ© Descartes",
+				email: "rene.descartes@example.com",
+				keys: []Key{
+					{PubKey: "pubkeyE"},
+				},
+			},
+		},
+	}
+
+	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"}})
+}
+
+// Test the immutable or mutable metadata search
+func TestMetadata(t *testing.T) {
+	mockRepo := repository.NewMockRepoForTest()
+
+	identity := NewIdentity("RenΓ© Descartes", "rene.descartes@example.com")
+
+	identity.SetMetadata("key1", "value1")
+	assertHasKeyValue(t, identity.ImmutableMetadata(), "key1", "value1")
+	assertHasKeyValue(t, identity.MutableMetadata(), "key1", "value1")
+
+	err := identity.Commit(mockRepo)
+	assert.NoError(t, err)
+
+	assertHasKeyValue(t, identity.ImmutableMetadata(), "key1", "value1")
+	assertHasKeyValue(t, identity.MutableMetadata(), "key1", "value1")
+
+	// try override
+	identity.AddVersion(&Version{
+		name:  "RenΓ© Descartes",
+		email: "rene.descartes@example.com",
+	})
+
+	identity.SetMetadata("key1", "value2")
+	assertHasKeyValue(t, identity.ImmutableMetadata(), "key1", "value1")
+	assertHasKeyValue(t, identity.MutableMetadata(), "key1", "value2")
+
+	err = identity.Commit(mockRepo)
+	assert.NoError(t, err)
+
+	// reload
+	loaded, err := ReadLocal(mockRepo, identity.id)
+	assert.Nil(t, err)
+
+	assertHasKeyValue(t, loaded.ImmutableMetadata(), "key1", "value1")
+	assertHasKeyValue(t, loaded.MutableMetadata(), "key1", "value2")
+}
+
+func assertHasKeyValue(t *testing.T, metadata map[string]string, key, value string) {
+	val, ok := metadata[key]
+	assert.True(t, ok)
+	assert.Equal(t, val, value)
+}
+
+func TestJSON(t *testing.T) {
+	mockRepo := repository.NewMockRepoForTest()
+
+	identity := &Identity{
+		versions: []*Version{
+			{
+				name:  "RenΓ© Descartes",
+				email: "rene.descartes@example.com",
+			},
+		},
+	}
+
+	// commit to make sure we have an ID
+	err := identity.Commit(mockRepo)
+	assert.Nil(t, err)
+	assert.NotEmpty(t, identity.id)
+
+	// serialize
+	data, err := json.Marshal(identity)
+	assert.NoError(t, err)
+
+	// deserialize, got a IdentityStub with the same id
+	var i Interface
+	i, err = UnmarshalJSON(data)
+	assert.NoError(t, err)
+	assert.Equal(t, identity.id, i.Id())
+
+	// make sure we can load the identity properly
+	i, err = ReadLocal(mockRepo, i.Id())
+	assert.NoError(t, err)
+}

identity/interface.go πŸ”—

@@ -0,0 +1,58 @@
+package identity
+
+import (
+	"github.com/MichaelMure/git-bug/repository"
+	"github.com/MichaelMure/git-bug/util/lamport"
+	"github.com/MichaelMure/git-bug/util/timestamp"
+)
+
+type Interface interface {
+	// Id return the Identity identifier
+	Id() string
+
+	// HumanId return the Identity identifier truncated for human consumption
+	HumanId() string
+
+	// Name return the last version of the name
+	Name() string
+
+	// Email return the last version of the email
+	Email() string
+
+	// Login return the last version of the login
+	Login() string
+
+	// AvatarUrl return the last version of the Avatar URL
+	AvatarUrl() string
+
+	// Keys return the last version of the valid keys
+	Keys() []Key
+
+	// ValidKeysAtTime return the set of keys valid at a given lamport time
+	ValidKeysAtTime(time lamport.Time) []Key
+
+	// DisplayName return a non-empty string to display, representing the
+	// identity, based on the non-empty values.
+	DisplayName() string
+
+	// Validate check if the Identity data is valid
+	Validate() error
+
+	// Write the identity into the Repository. In particular, this ensure that
+	// the Id is properly set.
+	Commit(repo repository.ClockedRepo) error
+
+	// If needed, write the identity into the Repository. In particular, this
+	// ensure that the Id is properly set.
+	CommitAsNeeded(repo repository.ClockedRepo) error
+
+	// IsProtected return 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() bool
+
+	// LastModificationLamportTime return the Lamport time at which the last version of the identity became valid.
+	LastModificationLamport() lamport.Time
+
+	// LastModification return the timestamp at which the last version of the identity became valid.
+	LastModification() timestamp.Timestamp
+}

identity/key.go πŸ”—

@@ -0,0 +1,13 @@
+package identity
+
+type Key struct {
+	// The GPG fingerprint of the key
+	Fingerprint string `json:"fingerprint"`
+	PubKey      string `json:"pub_key"`
+}
+
+func (k *Key) Validate() error {
+	// Todo
+
+	return nil
+}

identity/resolver.go πŸ”—

@@ -0,0 +1,22 @@
+package identity
+
+import "github.com/MichaelMure/git-bug/repository"
+
+// Resolver define the interface of an Identity resolver, able to load
+// an identity from, for example, a repo or a cache.
+type Resolver interface {
+	ResolveIdentity(id string) (Interface, error)
+}
+
+// DefaultResolver is a Resolver loading Identities directly from a Repo
+type SimpleResolver struct {
+	repo repository.Repo
+}
+
+func NewSimpleResolver(repo repository.Repo) *SimpleResolver {
+	return &SimpleResolver{repo: repo}
+}
+
+func (r *SimpleResolver) ResolveIdentity(id string) (Interface, error) {
+	return ReadLocal(r.repo, id)
+}

identity/version.go πŸ”—

@@ -0,0 +1,208 @@
+package identity
+
+import (
+	"crypto/rand"
+	"encoding/json"
+	"fmt"
+	"strings"
+
+	"github.com/MichaelMure/git-bug/repository"
+	"github.com/MichaelMure/git-bug/util/git"
+	"github.com/MichaelMure/git-bug/util/lamport"
+	"github.com/MichaelMure/git-bug/util/text"
+	"github.com/pkg/errors"
+)
+
+const formatVersion = 1
+
+// Version is a complete set of information about an Identity at a point in time.
+type Version struct {
+	// The lamport time at which this version become effective
+	// The reference time is the bug edition lamport clock
+	// It must be the first field in this struct due to https://github.com/golang/go/issues/599
+	time     lamport.Time
+	unixTime int64
+
+	name      string
+	email     string
+	login     string
+	avatarURL string
+
+	// 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
+
+	// 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.
+	// It is advised to fill this array if there is not enough entropy, e.g. if there is no keys.
+	nonce []byte
+
+	// A set of arbitrary key/value to store metadata about a version or about an Identity in general.
+	metadata map[string]string
+
+	// Not serialized
+	commitHash git.Hash
+}
+
+type VersionJSON struct {
+	// Additional field to version the data
+	FormatVersion uint `json:"version"`
+
+	Time      lamport.Time      `json:"time"`
+	UnixTime  int64             `json:"unix_time"`
+	Name      string            `json:"name,omitempty"`
+	Email     string            `json:"email,omitempty"`
+	Login     string            `json:"login,omitempty"`
+	AvatarUrl string            `json:"avatar_url,omitempty"`
+	Keys      []Key             `json:"pub_keys,omitempty"`
+	Nonce     []byte            `json:"nonce,omitempty"`
+	Metadata  map[string]string `json:"metadata,omitempty"`
+}
+
+func (v *Version) MarshalJSON() ([]byte, error) {
+	return json.Marshal(VersionJSON{
+		FormatVersion: formatVersion,
+		Time:          v.time,
+		UnixTime:      v.unixTime,
+		Name:          v.name,
+		Email:         v.email,
+		Login:         v.login,
+		AvatarUrl:     v.avatarURL,
+		Keys:          v.keys,
+		Nonce:         v.nonce,
+		Metadata:      v.metadata,
+	})
+}
+
+func (v *Version) UnmarshalJSON(data []byte) error {
+	var aux VersionJSON
+
+	if err := json.Unmarshal(data, &aux); err != nil {
+		return err
+	}
+
+	if aux.FormatVersion != formatVersion {
+		return fmt.Errorf("unknown format version %v", aux.FormatVersion)
+	}
+
+	v.time = aux.Time
+	v.unixTime = aux.UnixTime
+	v.name = aux.Name
+	v.email = aux.Email
+	v.login = aux.Login
+	v.avatarURL = aux.AvatarUrl
+	v.keys = aux.Keys
+	v.nonce = aux.Nonce
+	v.metadata = aux.Metadata
+
+	return nil
+}
+
+func (v *Version) Validate() error {
+	// time must be set after a commit
+	if v.commitHash != "" && v.unixTime == 0 {
+		return fmt.Errorf("unix time not set")
+	}
+	if v.commitHash != "" && v.time == 0 {
+		return fmt.Errorf("lamport time not set")
+	}
+
+	if text.Empty(v.name) && text.Empty(v.login) {
+		return fmt.Errorf("either name or login should be set")
+	}
+
+	if strings.Contains(v.name, "\n") {
+		return fmt.Errorf("name should be a single line")
+	}
+
+	if !text.Safe(v.name) {
+		return fmt.Errorf("name is not fully printable")
+	}
+
+	if strings.Contains(v.login, "\n") {
+		return fmt.Errorf("login should be a single line")
+	}
+
+	if !text.Safe(v.login) {
+		return fmt.Errorf("login is not fully printable")
+	}
+
+	if strings.Contains(v.email, "\n") {
+		return fmt.Errorf("email should be a single line")
+	}
+
+	if !text.Safe(v.email) {
+		return fmt.Errorf("email is not fully printable")
+	}
+
+	if v.avatarURL != "" && !text.ValidUrl(v.avatarURL) {
+		return fmt.Errorf("avatarUrl is not a valid URL")
+	}
+
+	if len(v.nonce) > 64 {
+		return fmt.Errorf("nonce is too big")
+	}
+
+	for _, k := range v.keys {
+		if err := k.Validate(); err != nil {
+			return errors.Wrap(err, "invalid key")
+		}
+	}
+
+	return nil
+}
+
+// Write will serialize and store the Version as a git blob and return
+// its hash
+func (v *Version) Write(repo repository.Repo) (git.Hash, error) {
+	// make sure we don't write invalid data
+	err := v.Validate()
+	if err != nil {
+		return "", errors.Wrap(err, "validation error")
+	}
+
+	data, err := json.Marshal(v)
+
+	if err != nil {
+		return "", err
+	}
+
+	hash, err := repo.StoreData(data)
+
+	if err != nil {
+		return "", err
+	}
+
+	return hash, nil
+}
+
+func makeNonce(len int) []byte {
+	result := make([]byte, len)
+	_, err := rand.Read(result)
+	if err != nil {
+		panic(err)
+	}
+	return result
+}
+
+// SetMetadata store arbitrary metadata about a version or an Identity in general
+// If the Version has been commit to git already, it won't be overwritten.
+func (v *Version) SetMetadata(key string, value string) {
+	if v.metadata == nil {
+		v.metadata = make(map[string]string)
+	}
+
+	v.metadata[key] = value
+}
+
+// GetMetadata retrieve arbitrary metadata about the Version
+func (v *Version) GetMetadata(key string) (string, bool) {
+	val, ok := v.metadata[key]
+	return val, ok
+}
+
+// AllMetadata return all metadata for this Identity
+func (v *Version) AllMetadata() map[string]string {
+	return v.metadata
+}

identity/version_test.go πŸ”—

@@ -0,0 +1,42 @@
+package identity
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestVersionSerialize(t *testing.T) {
+	before := &Version{
+		login:     "login",
+		name:      "name",
+		email:     "email",
+		avatarURL: "avatarUrl",
+		keys: []Key{
+			{
+				Fingerprint: "fingerprint1",
+				PubKey:      "pubkey1",
+			},
+			{
+				Fingerprint: "fingerprint2",
+				PubKey:      "pubkey2",
+			},
+		},
+		nonce: makeNonce(20),
+		metadata: map[string]string{
+			"key1": "value1",
+			"key2": "value2",
+		},
+		time: 3,
+	}
+
+	data, err := json.Marshal(before)
+	assert.NoError(t, err)
+
+	var after Version
+	err = json.Unmarshal(data, &after)
+	assert.NoError(t, err)
+
+	assert.Equal(t, before, &after)
+}

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.Fprintf(os.Stderr, "%s [%s]: ", name, preValue)
+		} else {
+			_, _ = fmt.Fprintf(os.Stderr, "%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.Fprintf(os.Stderr, "%s is empty\n", name)
+			continue
+		}
+
+		return line, nil
+	}
+}

misc/bash_completion/git-bug πŸ”—

@@ -799,6 +799,89 @@ _git-bug_title()
     noun_aliases=()
 }
 
+_git-bug_user_adopt()
+{
+    last_command="git-bug_user_adopt"
+
+    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_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_ls()
+{
+    last_command="git-bug_user_ls"
+
+    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+=("adopt")
+    commands+=("create")
+    commands+=("ls")
+
+    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"
@@ -874,6 +957,7 @@ _git-bug_root_command()
     commands+=("status")
     commands+=("termui")
     commands+=("title")
+    commands+=("user")
     commands+=("version")
     commands+=("webui")
 

misc/gen_bash_completion.go πŸ”—

@@ -4,19 +4,20 @@ package main
 
 import (
 	"fmt"
-	"github.com/MichaelMure/git-bug/commands"
 	"log"
 	"os"
 	"path"
+
+	"github.com/MichaelMure/git-bug/commands"
 )
 
 func main() {
 	cwd, _ := os.Getwd()
-	filepath := path.Join(cwd, "misc", "bash_completion", "git-bug")
+	dir := path.Join(cwd, "misc", "bash_completion", "git-bug")
 
 	fmt.Println("Generating bash completion file ...")
 
-	err := commands.RootCmd.GenBashCompletionFile(filepath)
+	err := commands.RootCmd.GenBashCompletionFile(dir)
 	if err != nil {
 		log.Fatal(err)
 	}

misc/gen_zsh_completion.go πŸ”—

@@ -4,10 +4,11 @@ package main
 
 import (
 	"fmt"
-	"github.com/MichaelMure/git-bug/commands"
 	"log"
 	"os"
 	"path"
+
+	"github.com/MichaelMure/git-bug/commands"
 )
 
 func main() {

misc/random_bugs/create_random_bugs.go πŸ”—

@@ -6,11 +6,12 @@ import (
 	"time"
 
 	"github.com/MichaelMure/git-bug/bug"
+	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/repository"
 	"github.com/icrowley/fake"
 )
 
-type opsGenerator func(bug.Interface, bug.Person)
+type opsGenerator func(bug.Interface, identity.Interface)
 
 type Options struct {
 	BugNumber    int
@@ -33,7 +34,9 @@ func CommitRandomBugs(repo repository.ClockedRepo, opts Options) {
 }
 
 func CommitRandomBugsWithSeed(repo repository.ClockedRepo, opts Options, seed int64) {
-	bugs := GenerateRandomBugsWithSeed(opts, seed)
+	generateRandomPersons(repo, opts.PersonNumber)
+
+	bugs := generateRandomBugsWithSeed(opts, seed)
 
 	for _, b := range bugs {
 		err := b.Commit(repo)
@@ -43,11 +46,7 @@ func CommitRandomBugsWithSeed(repo repository.ClockedRepo, opts Options, seed in
 	}
 }
 
-func GenerateRandomBugs(opts Options) []*bug.Bug {
-	return GenerateRandomBugsWithSeed(opts, time.Now().UnixNano())
-}
-
-func GenerateRandomBugsWithSeed(opts Options, seed int64) []*bug.Bug {
+func generateRandomBugsWithSeed(opts Options, seed int64) []*bug.Bug {
 	rand.Seed(seed)
 	fake.Seed(seed)
 
@@ -66,7 +65,7 @@ func GenerateRandomBugsWithSeed(opts Options, seed int64) []*bug.Bug {
 		addedLabels = []string{}
 
 		b, _, err := bug.Create(
-			randomPerson(opts.PersonNumber),
+			randomPerson(),
 			time.Now().Unix(),
 			fake.Sentence(),
 			paragraphs(),
@@ -84,7 +83,7 @@ func GenerateRandomBugsWithSeed(opts Options, seed int64) []*bug.Bug {
 
 		for j := 0; j < nOps; j++ {
 			index := rand.Intn(len(opsGenerators))
-			opsGenerators[index](b, randomPerson(opts.PersonNumber))
+			opsGenerators[index](b, randomPerson())
 		}
 
 		result[i] = b
@@ -100,6 +99,9 @@ func GenerateRandomOperationPacks(packNumber int, opNumber int) []*bug.Operation
 func GenerateRandomOperationPacksWithSeed(packNumber int, opNumber int, seed int64) []*bug.OperationPack {
 	// Note: this is a bit crude, only generate a Create + Comments
 
+	panic("this piece of code needs to be updated to make sure that the identities " +
+		"are properly commit before usage. That is, generateRandomPersons() need to be called.")
+
 	rand.Seed(seed)
 	fake.Seed(seed)
 
@@ -111,7 +113,7 @@ func GenerateRandomOperationPacksWithSeed(packNumber int, opNumber int, seed int
 		var op bug.Operation
 
 		op = bug.NewCreateOp(
-			randomPerson(5),
+			randomPerson(),
 			time.Now().Unix(),
 			fake.Sentence(),
 			paragraphs(),
@@ -122,7 +124,7 @@ func GenerateRandomOperationPacksWithSeed(packNumber int, opNumber int, seed int
 
 		for j := 0; j < opNumber-1; j++ {
 			op = bug.NewAddCommentOp(
-				randomPerson(5),
+				randomPerson(),
 				time.Now().Unix(),
 				paragraphs(),
 				nil,
@@ -136,24 +138,26 @@ func GenerateRandomOperationPacksWithSeed(packNumber int, opNumber int, seed int
 	return result
 }
 
-func person() bug.Person {
-	return bug.Person{
-		Name:  fake.FullName(),
-		Email: fake.EmailAddress(),
-	}
+func person() identity.Interface {
+	return identity.NewIdentity(fake.FullName(), fake.EmailAddress())
 }
 
-var persons []bug.Person
+var persons []identity.Interface
 
-func randomPerson(personNumber int) bug.Person {
-	if len(persons) == 0 {
-		persons = make([]bug.Person, personNumber)
-		for i := range persons {
-			persons[i] = person()
+func generateRandomPersons(repo repository.ClockedRepo, n int) {
+	persons = make([]identity.Interface, n)
+	for i := range persons {
+		p := person()
+		err := p.Commit(repo)
+		if err != nil {
+			panic(err)
 		}
+		persons[i] = p
 	}
+}
 
-	index := rand.Intn(personNumber)
+func randomPerson() identity.Interface {
+	index := rand.Intn(len(persons))
 	return persons[index]
 }
 
@@ -162,25 +166,25 @@ func paragraphs() string {
 	return strings.Replace(p, "\t", "\n\n", -1)
 }
 
-func comment(b bug.Interface, p bug.Person) {
+func comment(b bug.Interface, p identity.Interface) {
 	_, _ = bug.AddComment(b, p, time.Now().Unix(), paragraphs())
 }
 
-func title(b bug.Interface, p bug.Person) {
+func title(b bug.Interface, p identity.Interface) {
 	_, _ = bug.SetTitle(b, p, time.Now().Unix(), fake.Sentence())
 }
 
-func open(b bug.Interface, p bug.Person) {
+func open(b bug.Interface, p identity.Interface) {
 	_, _ = bug.Open(b, p, time.Now().Unix())
 }
 
-func close(b bug.Interface, p bug.Person) {
+func close(b bug.Interface, p identity.Interface) {
 	_, _ = bug.Close(b, p, time.Now().Unix())
 }
 
 var addedLabels []string
 
-func labels(b bug.Interface, p bug.Person) {
+func labels(b bug.Interface, p identity.Interface) {
 	var removed []string
 	nbRemoved := rand.Intn(3)
 	for nbRemoved > 0 && len(addedLabels) > 0 {

misc/random_bugs/main.go πŸ”—

@@ -17,7 +17,7 @@ func main() {
 		panic(err)
 	}
 
-	repo, err := repository.NewGitRepo(dir, func(repo *repository.GitRepo) error {
+	repo, err := repository.NewGitRepo(dir, func(repo repository.ClockedRepo) error {
 		return nil
 	})
 	if err != nil {

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 label ls ls-id 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: :(adopt create ls)'
+      ;;
       *)
         _arguments '*: :_files'
       ;;

repository/git.go πŸ”—

@@ -20,6 +20,8 @@ const editClockFile = "/.git/git-bug/edit-clock"
 // ErrNotARepo is the error returned when the git repo root wan't be found
 var ErrNotARepo = errors.New("not a git repository")
 
+var _ ClockedRepo = &GitRepo{}
+
 // GitRepo represents an instance of a (local) git repository.
 type GitRepo struct {
 	Path        string
@@ -29,7 +31,7 @@ type GitRepo struct {
 
 // Run the given git command with the given I/O reader/writers, returning an error if it fails.
 func (repo *GitRepo) runGitCommandWithIO(stdin io.Reader, stdout, stderr io.Writer, args ...string) error {
-	//fmt.Println("Running git", strings.Join(args, " "))
+	// fmt.Printf("[%s] Running git %s\n", repo.Path, strings.Join(args, " "))
 
 	cmd := exec.Command("git", args...)
 	cmd.Dir = repo.Path
@@ -202,7 +204,7 @@ func (repo *GitRepo) ReadConfigs(keyPrefix string) (map[string]string, error) {
 
 // RmConfigs remove all key/value pair matching the key prefix
 func (repo *GitRepo) RmConfigs(keyPrefix string) error {
-	_, err := repo.runGitCommand("config", "--remove-section", keyPrefix)
+	_, err := repo.runGitCommand("config", "--unset-all", keyPrefix)
 
 	return err
 }
@@ -440,11 +442,21 @@ func (repo *GitRepo) WriteClocks() error {
 	return nil
 }
 
+// CreateTime return the current value of the creation clock
+func (repo *GitRepo) CreateTime() lamport.Time {
+	return repo.createClock.Time()
+}
+
 // CreateTimeIncrement increment the creation clock and return the new value.
 func (repo *GitRepo) CreateTimeIncrement() (lamport.Time, error) {
 	return repo.createClock.Increment()
 }
 
+// EditTime return the current value of the edit clock
+func (repo *GitRepo) EditTime() lamport.Time {
+	return repo.editClock.Time()
+}
+
 // EditTimeIncrement increment the edit clock and return the new value.
 func (repo *GitRepo) EditTimeIncrement() (lamport.Time, error) {
 	return repo.editClock.Increment()

repository/mock_repo.go πŸ”—

@@ -9,6 +9,8 @@ import (
 	"github.com/MichaelMure/git-bug/util/lamport"
 )
 
+var _ ClockedRepo = &mockRepoForTest{}
+
 // mockRepoForTest defines an instance of Repo that can be used for testing.
 type mockRepoForTest struct {
 	config      map[string]string
@@ -227,10 +229,18 @@ func (r *mockRepoForTest) WriteClocks() error {
 	return nil
 }
 
+func (r *mockRepoForTest) CreateTime() lamport.Time {
+	return r.createClock.Time()
+}
+
 func (r *mockRepoForTest) CreateTimeIncrement() (lamport.Time, error) {
 	return r.createClock.Increment(), nil
 }
 
+func (r *mockRepoForTest) EditTime() lamport.Time {
+	return r.editClock.Time()
+}
+
 func (r *mockRepoForTest) EditTimeIncrement() (lamport.Time, error) {
 	return r.editClock.Increment(), nil
 }

repository/repo.go πŸ”—

@@ -83,6 +83,7 @@ type Repo interface {
 	GetTreeHash(commit git.Hash) (git.Hash, error)
 }
 
+// ClockedRepo is a Repo that also has Lamport clocks
 type ClockedRepo interface {
 	Repo
 
@@ -92,9 +93,15 @@ type ClockedRepo interface {
 	// WriteClocks write the clocks values into the repo
 	WriteClocks() error
 
+	// CreateTime return the current value of the creation clock
+	CreateTime() lamport.Time
+
 	// CreateTimeIncrement increment the creation clock and return the new value.
 	CreateTimeIncrement() (lamport.Time, error)
 
+	// EditTime return the current value of the edit clock
+	EditTime() lamport.Time
+
 	// EditTimeIncrement increment the edit clock and return the new value.
 	EditTimeIncrement() (lamport.Time, error)
 
@@ -122,7 +129,7 @@ func prepareTreeEntries(entries []TreeEntry) bytes.Buffer {
 }
 
 func readTreeEntries(s string) ([]TreeEntry, error) {
-	split := strings.Split(s, "\n")
+	split := strings.Split(strings.TrimSpace(s), "\n")
 
 	casted := make([]TreeEntry, len(split))
 	for i, line := range split {

repository/tree_entry_test.go πŸ”—

@@ -1,8 +1,9 @@
 package repository
 
 import (
-	"github.com/MichaelMure/git-bug/util/git"
 	"testing"
+
+	"github.com/MichaelMure/git-bug/util/git"
 )
 
 func TestTreeEntryFormat(t *testing.T) {

termui/bug_table.go πŸ”—

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

termui/label_select.go πŸ”—

@@ -296,7 +296,7 @@ func (ls *labelSelect) saveAndReturn(g *gocui.Gui, v *gocui.View) error {
 		}
 	}
 
-	if _, err := ls.bug.ChangeLabels(newLabels, rmLabels); err != nil {
+	if _, _, err := ls.bug.ChangeLabels(newLabels, rmLabels); err != nil {
 		ui.msgPopup.Activate(msgPopupErrorTitle, err.Error())
 	}
 

termui/show_bug.go πŸ”—

@@ -622,9 +622,11 @@ func (sb *showBug) setTitle(g *gocui.Gui, v *gocui.View) error {
 func (sb *showBug) toggleOpenClose(g *gocui.Gui, v *gocui.View) error {
 	switch sb.bug.Snapshot().Status {
 	case bug.OpenStatus:
-		return sb.bug.Close()
+		_, err := sb.bug.Close()
+		return err
 	case bug.ClosedStatus:
-		return sb.bug.Open()
+		_, err := sb.bug.Open()
+		return err
 	default:
 		return nil
 	}

termui/termui.go πŸ”—

@@ -226,7 +226,7 @@ func addCommentWithEditor(bug *cache.BugCache) error {
 	if err == input.ErrEmptyMessage {
 		ui.msgPopup.Activate(msgPopupErrorTitle, "Empty message, aborting.")
 	} else {
-		err := bug.AddComment(message)
+		_, err := bug.AddComment(message)
 		if err != nil {
 			return err
 		}
@@ -261,7 +261,7 @@ func editCommentWithEditor(bug *cache.BugCache, target git.Hash, preMessage stri
 	} else if message == preMessage {
 		ui.msgPopup.Activate(msgPopupErrorTitle, "No changes found, aborting.")
 	} else {
-		err := bug.EditComment(target, message)
+		_, err := bug.EditComment(target, message)
 		if err != nil {
 			return err
 		}
@@ -298,7 +298,7 @@ func setTitleWithEditor(bug *cache.BugCache) error {
 	} else if title == snap.Title {
 		ui.msgPopup.Activate(msgPopupErrorTitle, "No change, aborting.")
 	} else {
-		err := bug.SetTitle(title)
+		_, err := bug.SetTitle(title)
 		if err != nil {
 			return err
 		}

tests/read_bugs_test.go πŸ”—

@@ -1,48 +1,16 @@
 package tests
 
 import (
-	"io/ioutil"
-	"log"
 	"testing"
 
 	"github.com/MichaelMure/git-bug/bug"
 	"github.com/MichaelMure/git-bug/misc/random_bugs"
 	"github.com/MichaelMure/git-bug/repository"
+	"github.com/MichaelMure/git-bug/util/test"
 )
 
-func createRepo(bare bool) *repository.GitRepo {
-	dir, err := ioutil.TempDir("", "")
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	// fmt.Println("Creating repo:", dir)
-
-	var creator func(string) (*repository.GitRepo, error)
-
-	if bare {
-		creator = repository.InitBareGitRepo
-	} else {
-		creator = repository.InitGitRepo
-	}
-
-	repo, err := creator(dir)
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	if err := repo.StoreConfig("user.name", "testuser"); err != nil {
-		log.Fatal("failed to set user.name for test repository: ", err)
-	}
-	if err := repo.StoreConfig("user.email", "testuser@example.com"); err != nil {
-		log.Fatal("failed to set user.email for test repository: ", err)
-	}
-
-	return repo
-}
-
-func createFilledRepo(bugNumber int) repository.ClockedRepo {
-	repo := createRepo(false)
+func CreateFilledRepo(bugNumber int) repository.ClockedRepo {
+	repo := test.CreateRepo(false)
 
 	var seed int64 = 42
 	options := random_bugs.DefaultOptions()
@@ -54,7 +22,7 @@ func createFilledRepo(bugNumber int) repository.ClockedRepo {
 }
 
 func TestReadBugs(t *testing.T) {
-	repo := createFilledRepo(15)
+	repo := CreateFilledRepo(15)
 	bugs := bug.ReadAllLocalBugs(repo)
 	for b := range bugs {
 		if b.Err != nil {
@@ -64,7 +32,7 @@ func TestReadBugs(t *testing.T) {
 }
 
 func benchmarkReadBugs(bugNumber int, t *testing.B) {
-	repo := createFilledRepo(bugNumber)
+	repo := CreateFilledRepo(bugNumber)
 	t.ResetTimer()
 
 	for n := 0; n < t.N; n++ {

util/git/hash.go πŸ”—

@@ -30,7 +30,7 @@ func (h *Hash) UnmarshalGQL(v interface{}) error {
 
 // MarshalGQL implement the Marshaler interface for gqlgen
 func (h Hash) MarshalGQL(w io.Writer) {
-	w.Write([]byte(`"` + h.String() + `"`))
+	_, _ = w.Write([]byte(`"` + h.String() + `"`))
 }
 
 // IsValid tell if the hash is valid

util/test/repo.go πŸ”—

@@ -0,0 +1,79 @@
+package test
+
+import (
+	"io/ioutil"
+	"log"
+	"os"
+	"testing"
+
+	"github.com/MichaelMure/git-bug/repository"
+)
+
+func CreateRepo(bare bool) *repository.GitRepo {
+	dir, err := ioutil.TempDir("", "")
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	// fmt.Println("Creating repo:", dir)
+
+	var creator func(string) (*repository.GitRepo, error)
+
+	if bare {
+		creator = repository.InitBareGitRepo
+	} else {
+		creator = repository.InitGitRepo
+	}
+
+	repo, err := creator(dir)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	if err := repo.StoreConfig("user.name", "testuser"); err != nil {
+		log.Fatal("failed to set user.name for test repository: ", err)
+	}
+	if err := repo.StoreConfig("user.email", "testuser@example.com"); err != nil {
+		log.Fatal("failed to set user.email for test repository: ", err)
+	}
+
+	return repo
+}
+
+func CleanupRepo(repo repository.Repo) error {
+	path := repo.GetPath()
+	// fmt.Println("Cleaning repo:", path)
+	return os.RemoveAll(path)
+}
+
+func SetupReposAndRemote(t testing.TB) (repoA, repoB, remote *repository.GitRepo) {
+	repoA = CreateRepo(false)
+	repoB = CreateRepo(false)
+	remote = CreateRepo(true)
+
+	remoteAddr := "file://" + remote.GetPath()
+
+	err := repoA.AddRemote("origin", remoteAddr)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	err = repoB.AddRemote("origin", remoteAddr)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	return repoA, repoB, remote
+}
+
+func CleanupRepos(repoA, repoB, remote *repository.GitRepo) {
+	if err := CleanupRepo(repoA); err != nil {
+		log.Println(err)
+	}
+	if err := CleanupRepo(repoB); err != nil {
+		log.Println(err)
+	}
+	if err := CleanupRepo(remote); err != nil {
+		log.Println(err)
+	}
+}

util/text/text.go πŸ”—

@@ -323,10 +323,3 @@ func splitWord(word string, length int) (string, string) {
 
 	return string(result), string(leftover)
 }
-
-func minInt(a, b int) int {
-	if a > b {
-		return b
-	}
-	return a
-}

vendor/github.com/go-test/deep/.travis.yml πŸ”—

@@ -1,13 +0,0 @@
-language: go
-
-go:
-  - 1.7
-  - 1.8
-  - 1.9
-
-before_install:
-  - go get github.com/mattn/goveralls
-  - go get golang.org/x/tools/cover
-
-script:
-  - $HOME/gopath/bin/goveralls -service=travis-ci

vendor/github.com/go-test/deep/LICENSE πŸ”—

@@ -1,21 +0,0 @@
-MIT License
-
-Copyright 2015-2017 Daniel Nichter
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.

vendor/github.com/go-test/deep/README.md πŸ”—

@@ -1,51 +0,0 @@
-# Deep Variable Equality for Humans
-
-[![Go Report Card](https://goreportcard.com/badge/github.com/go-test/deep)](https://goreportcard.com/report/github.com/go-test/deep) [![Build Status](https://travis-ci.org/go-test/deep.svg?branch=master)](https://travis-ci.org/go-test/deep) [![Coverage Status](https://coveralls.io/repos/github/go-test/deep/badge.svg?branch=master)](https://coveralls.io/github/go-test/deep?branch=master) [![GoDoc](https://godoc.org/github.com/go-test/deep?status.svg)](https://godoc.org/github.com/go-test/deep)
-
-This package provides a single function: `deep.Equal`. It's like [reflect.DeepEqual](http://golang.org/pkg/reflect/#DeepEqual) but much friendlier to humans (or any sentient being) for two reason:
-
-* `deep.Equal` returns a list of differences
-* `deep.Equal` does not compare unexported fields (by default)
-
-`reflect.DeepEqual` is good (like all things Golang!), but it's a game of [Hunt the Wumpus](https://en.wikipedia.org/wiki/Hunt_the_Wumpus). For large maps, slices, and structs, finding the difference is difficult.
-
-`deep.Equal` doesn't play games with you, it lists the differences:
-
-```go
-package main_test
-
-import (
-	"testing"
-	"github.com/go-test/deep"
-)
-
-type T struct {
-	Name    string
-	Numbers []float64
-}
-
-func TestDeepEqual(t *testing.T) {
-	// Can you spot the difference?
-	t1 := T{
-		Name:    "Isabella",
-		Numbers: []float64{1.13459, 2.29343, 3.010100010},
-	}
-	t2 := T{
-		Name:    "Isabella",
-		Numbers: []float64{1.13459, 2.29843, 3.010100010},
-	}
-
-	if diff := deep.Equal(t1, t2); diff != nil {
-		t.Error(diff)
-	}
-}
-```
-
-
-```
-$ go test
---- FAIL: TestDeepEqual (0.00s)
-        main_test.go:25: [Numbers.slice[1]: 2.29343 != 2.29843]
-```
-
-The difference is in `Numbers.slice[1]`: the two values aren't equal using Go `==`.

vendor/github.com/go-test/deep/deep.go πŸ”—

@@ -1,352 +0,0 @@
-// Package deep provides function deep.Equal which is like reflect.DeepEqual but
-// returns a list of differences. This is helpful when comparing complex types
-// like structures and maps.
-package deep
-
-import (
-	"errors"
-	"fmt"
-	"log"
-	"reflect"
-	"strings"
-)
-
-var (
-	// FloatPrecision is the number of decimal places to round float values
-	// to when comparing.
-	FloatPrecision = 10
-
-	// MaxDiff specifies the maximum number of differences to return.
-	MaxDiff = 10
-
-	// MaxDepth specifies the maximum levels of a struct to recurse into.
-	MaxDepth = 10
-
-	// LogErrors causes errors to be logged to STDERR when true.
-	LogErrors = false
-
-	// CompareUnexportedFields causes unexported struct fields, like s in
-	// T{s int}, to be comparsed when true.
-	CompareUnexportedFields = false
-)
-
-var (
-	// ErrMaxRecursion is logged when MaxDepth is reached.
-	ErrMaxRecursion = errors.New("recursed to MaxDepth")
-
-	// ErrTypeMismatch is logged when Equal passed two different types of values.
-	ErrTypeMismatch = errors.New("variables are different reflect.Type")
-
-	// ErrNotHandled is logged when a primitive Go kind is not handled.
-	ErrNotHandled = errors.New("cannot compare the reflect.Kind")
-)
-
-type cmp struct {
-	diff        []string
-	buff        []string
-	floatFormat string
-}
-
-var errorType = reflect.TypeOf((*error)(nil)).Elem()
-
-// Equal compares variables a and b, recursing into their structure up to
-// MaxDepth levels deep, and returns a list of differences, or nil if there are
-// none. Some differences may not be found if an error is also returned.
-//
-// If a type has an Equal method, like time.Equal, it is called to check for
-// equality.
-func Equal(a, b interface{}) []string {
-	aVal := reflect.ValueOf(a)
-	bVal := reflect.ValueOf(b)
-	c := &cmp{
-		diff:        []string{},
-		buff:        []string{},
-		floatFormat: fmt.Sprintf("%%.%df", FloatPrecision),
-	}
-	if a == nil && b == nil {
-		return nil
-	} else if a == nil && b != nil {
-		c.saveDiff(b, "<nil pointer>")
-	} else if a != nil && b == nil {
-		c.saveDiff(a, "<nil pointer>")
-	}
-	if len(c.diff) > 0 {
-		return c.diff
-	}
-
-	c.equals(aVal, bVal, 0)
-	if len(c.diff) > 0 {
-		return c.diff // diffs
-	}
-	return nil // no diffs
-}
-
-func (c *cmp) equals(a, b reflect.Value, level int) {
-	if level > MaxDepth {
-		logError(ErrMaxRecursion)
-		return
-	}
-
-	// Check if one value is nil, e.g. T{x: *X} and T.x is nil
-	if !a.IsValid() || !b.IsValid() {
-		if a.IsValid() && !b.IsValid() {
-			c.saveDiff(a.Type(), "<nil pointer>")
-		} else if !a.IsValid() && b.IsValid() {
-			c.saveDiff("<nil pointer>", b.Type())
-		}
-		return
-	}
-
-	// If differenet types, they can't be equal
-	aType := a.Type()
-	bType := b.Type()
-	if aType != bType {
-		c.saveDiff(aType, bType)
-		logError(ErrTypeMismatch)
-		return
-	}
-
-	// Primitive https://golang.org/pkg/reflect/#Kind
-	aKind := a.Kind()
-	bKind := b.Kind()
-
-	// If both types implement the error interface, compare the error strings.
-	// This must be done before dereferencing because the interface is on a
-	// pointer receiver.
-	if aType.Implements(errorType) && bType.Implements(errorType) {
-		if a.Elem().IsValid() && b.Elem().IsValid() { // both err != nil
-			aString := a.MethodByName("Error").Call(nil)[0].String()
-			bString := b.MethodByName("Error").Call(nil)[0].String()
-			if aString != bString {
-				c.saveDiff(aString, bString)
-			}
-			return
-		}
-	}
-
-	// Dereference pointers and interface{}
-	if aElem, bElem := (aKind == reflect.Ptr || aKind == reflect.Interface),
-		(bKind == reflect.Ptr || bKind == reflect.Interface); aElem || bElem {
-
-		if aElem {
-			a = a.Elem()
-		}
-
-		if bElem {
-			b = b.Elem()
-		}
-
-		c.equals(a, b, level+1)
-		return
-	}
-
-	// Types with an Equal(), like time.Time.
-	eqFunc := a.MethodByName("Equal")
-	if eqFunc.IsValid() {
-		retVals := eqFunc.Call([]reflect.Value{b})
-		if !retVals[0].Bool() {
-			c.saveDiff(a, b)
-		}
-		return
-	}
-
-	switch aKind {
-
-	/////////////////////////////////////////////////////////////////////
-	// Iterable kinds
-	/////////////////////////////////////////////////////////////////////
-
-	case reflect.Struct:
-		/*
-			The variables are structs like:
-				type T struct {
-					FirstName string
-					LastName  string
-				}
-			Type = <pkg>.T, Kind = reflect.Struct
-
-			Iterate through the fields (FirstName, LastName), recurse into their values.
-		*/
-		for i := 0; i < a.NumField(); i++ {
-			if aType.Field(i).PkgPath != "" && !CompareUnexportedFields {
-				continue // skip unexported field, e.g. s in type T struct {s string}
-			}
-
-			c.push(aType.Field(i).Name) // push field name to buff
-
-			// Get the Value for each field, e.g. FirstName has Type = string,
-			// Kind = reflect.String.
-			af := a.Field(i)
-			bf := b.Field(i)
-
-			// Recurse to compare the field values
-			c.equals(af, bf, level+1)
-
-			c.pop() // pop field name from buff
-
-			if len(c.diff) >= MaxDiff {
-				break
-			}
-		}
-	case reflect.Map:
-		/*
-			The variables are maps like:
-				map[string]int{
-					"foo": 1,
-					"bar": 2,
-				}
-			Type = map[string]int, Kind = reflect.Map
-
-			Or:
-				type T map[string]int{}
-			Type = <pkg>.T, Kind = reflect.Map
-
-			Iterate through the map keys (foo, bar), recurse into their values.
-		*/
-
-		if a.IsNil() || b.IsNil() {
-			if a.IsNil() && !b.IsNil() {
-				c.saveDiff("<nil map>", b)
-			} else if !a.IsNil() && b.IsNil() {
-				c.saveDiff(a, "<nil map>")
-			}
-			return
-		}
-
-		if a.Pointer() == b.Pointer() {
-			return
-		}
-
-		for _, key := range a.MapKeys() {
-			c.push(fmt.Sprintf("map[%s]", key))
-
-			aVal := a.MapIndex(key)
-			bVal := b.MapIndex(key)
-			if bVal.IsValid() {
-				c.equals(aVal, bVal, level+1)
-			} else {
-				c.saveDiff(aVal, "<does not have key>")
-			}
-
-			c.pop()
-
-			if len(c.diff) >= MaxDiff {
-				return
-			}
-		}
-
-		for _, key := range b.MapKeys() {
-			if aVal := a.MapIndex(key); aVal.IsValid() {
-				continue
-			}
-
-			c.push(fmt.Sprintf("map[%s]", key))
-			c.saveDiff("<does not have key>", b.MapIndex(key))
-			c.pop()
-			if len(c.diff) >= MaxDiff {
-				return
-			}
-		}
-	case reflect.Array:
-		n := a.Len()
-		for i := 0; i < n; i++ {
-			c.push(fmt.Sprintf("array[%d]", i))
-			c.equals(a.Index(i), b.Index(i), level+1)
-			c.pop()
-			if len(c.diff) >= MaxDiff {
-				break
-			}
-		}
-	case reflect.Slice:
-		if a.IsNil() || b.IsNil() {
-			if a.IsNil() && !b.IsNil() {
-				c.saveDiff("<nil slice>", b)
-			} else if !a.IsNil() && b.IsNil() {
-				c.saveDiff(a, "<nil slice>")
-			}
-			return
-		}
-
-		if a.Pointer() == b.Pointer() {
-			return
-		}
-
-		aLen := a.Len()
-		bLen := b.Len()
-		n := aLen
-		if bLen > aLen {
-			n = bLen
-		}
-		for i := 0; i < n; i++ {
-			c.push(fmt.Sprintf("slice[%d]", i))
-			if i < aLen && i < bLen {
-				c.equals(a.Index(i), b.Index(i), level+1)
-			} else if i < aLen {
-				c.saveDiff(a.Index(i), "<no value>")
-			} else {
-				c.saveDiff("<no value>", b.Index(i))
-			}
-			c.pop()
-			if len(c.diff) >= MaxDiff {
-				break
-			}
-		}
-
-	/////////////////////////////////////////////////////////////////////
-	// Primitive kinds
-	/////////////////////////////////////////////////////////////////////
-
-	case reflect.Float32, reflect.Float64:
-		// Avoid 0.04147685731961082 != 0.041476857319611
-		// 6 decimal places is close enough
-		aval := fmt.Sprintf(c.floatFormat, a.Float())
-		bval := fmt.Sprintf(c.floatFormat, b.Float())
-		if aval != bval {
-			c.saveDiff(a.Float(), b.Float())
-		}
-	case reflect.Bool:
-		if a.Bool() != b.Bool() {
-			c.saveDiff(a.Bool(), b.Bool())
-		}
-	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
-		if a.Int() != b.Int() {
-			c.saveDiff(a.Int(), b.Int())
-		}
-	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
-		if a.Uint() != b.Uint() {
-			c.saveDiff(a.Uint(), b.Uint())
-		}
-	case reflect.String:
-		if a.String() != b.String() {
-			c.saveDiff(a.String(), b.String())
-		}
-
-	default:
-		logError(ErrNotHandled)
-	}
-}
-
-func (c *cmp) push(name string) {
-	c.buff = append(c.buff, name)
-}
-
-func (c *cmp) pop() {
-	if len(c.buff) > 0 {
-		c.buff = c.buff[0 : len(c.buff)-1]
-	}
-}
-
-func (c *cmp) saveDiff(aval, bval interface{}) {
-	if len(c.buff) > 0 {
-		varName := strings.Join(c.buff, ".")
-		c.diff = append(c.diff, fmt.Sprintf("%s: %v != %v", varName, aval, bval))
-	} else {
-		c.diff = append(c.diff, fmt.Sprintf("%v != %v", aval, bval))
-	}
-}
-
-func logError(err error) {
-	if LogErrors {
-		log.Println(err)
-	}
-}

vendor/github.com/google/go-cmp/LICENSE πŸ”—

@@ -1,27 +0,0 @@
-Copyright (c) 2017 The Go Authors. All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are
-met:
-
-   * Redistributions of source code must retain the above copyright
-notice, this list of conditions and the following disclaimer.
-   * Redistributions in binary form must reproduce the above
-copyright notice, this list of conditions and the following disclaimer
-in the documentation and/or other materials provided with the
-distribution.
-   * Neither the name of Google Inc. nor the names of its
-contributors may be used to endorse or promote products derived from
-this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

vendor/github.com/google/go-cmp/cmp/compare.go πŸ”—

@@ -1,553 +0,0 @@
-// Copyright 2017, The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE.md file.
-
-// Package cmp determines equality of values.
-//
-// This package is intended to be a more powerful and safer alternative to
-// reflect.DeepEqual for comparing whether two values are semantically equal.
-//
-// The primary features of cmp are:
-//
-// β€’ When the default behavior of equality does not suit the needs of the test,
-// custom equality functions can override the equality operation.
-// For example, an equality function may report floats as equal so long as they
-// are within some tolerance of each other.
-//
-// β€’ Types that have an Equal method may use that method to determine equality.
-// This allows package authors to determine the equality operation for the types
-// that they define.
-//
-// β€’ If no custom equality functions are used and no Equal method is defined,
-// equality is determined by recursively comparing the primitive kinds on both
-// values, much like reflect.DeepEqual. Unlike reflect.DeepEqual, unexported
-// fields are not compared by default; they result in panics unless suppressed
-// by using an Ignore option (see cmpopts.IgnoreUnexported) or explicitly compared
-// using the AllowUnexported option.
-package cmp
-
-import (
-	"fmt"
-	"reflect"
-
-	"github.com/google/go-cmp/cmp/internal/diff"
-	"github.com/google/go-cmp/cmp/internal/function"
-	"github.com/google/go-cmp/cmp/internal/value"
-)
-
-// BUG(dsnet): Maps with keys containing NaN values cannot be properly compared due to
-// the reflection package's inability to retrieve such entries. Equal will panic
-// anytime it comes across a NaN key, but this behavior may change.
-//
-// See https://golang.org/issue/11104 for more details.
-
-var nothing = reflect.Value{}
-
-// Equal reports whether x and y are equal by recursively applying the
-// following rules in the given order to x and y and all of their sub-values:
-//
-// β€’ If two values are not of the same type, then they are never equal
-// and the overall result is false.
-//
-// β€’ Let S be the set of all Ignore, Transformer, and Comparer options that
-// remain after applying all path filters, value filters, and type filters.
-// If at least one Ignore exists in S, then the comparison is ignored.
-// If the number of Transformer and Comparer options in S is greater than one,
-// then Equal panics because it is ambiguous which option to use.
-// If S contains a single Transformer, then use that to transform the current
-// values and recursively call Equal on the output values.
-// If S contains a single Comparer, then use that to compare the current values.
-// Otherwise, evaluation proceeds to the next rule.
-//
-// β€’ If the values have an Equal method of the form "(T) Equal(T) bool" or
-// "(T) Equal(I) bool" where T is assignable to I, then use the result of
-// x.Equal(y) even if x or y is nil.
-// Otherwise, no such method exists and evaluation proceeds to the next rule.
-//
-// β€’ Lastly, try to compare x and y based on their basic kinds.
-// Simple kinds like booleans, integers, floats, complex numbers, strings, and
-// channels are compared using the equivalent of the == operator in Go.
-// Functions are only equal if they are both nil, otherwise they are unequal.
-// Pointers are equal if the underlying values they point to are also equal.
-// Interfaces are equal if their underlying concrete values are also equal.
-//
-// Structs are equal if all of their fields are equal. If a struct contains
-// unexported fields, Equal panics unless the AllowUnexported option is used or
-// an Ignore option (e.g., cmpopts.IgnoreUnexported) ignores that field.
-//
-// Arrays, slices, and maps are equal if they are both nil or both non-nil
-// with the same length and the elements at each index or key are equal.
-// Note that a non-nil empty slice and a nil slice are not equal.
-// To equate empty slices and maps, consider using cmpopts.EquateEmpty.
-// Map keys are equal according to the == operator.
-// To use custom comparisons for map keys, consider using cmpopts.SortMaps.
-func Equal(x, y interface{}, opts ...Option) bool {
-	s := newState(opts)
-	s.compareAny(reflect.ValueOf(x), reflect.ValueOf(y))
-	return s.result.Equal()
-}
-
-// Diff returns a human-readable report of the differences between two values.
-// It returns an empty string if and only if Equal returns true for the same
-// input values and options. The output string will use the "-" symbol to
-// indicate elements removed from x, and the "+" symbol to indicate elements
-// added to y.
-//
-// Do not depend on this output being stable.
-func Diff(x, y interface{}, opts ...Option) string {
-	r := new(defaultReporter)
-	opts = Options{Options(opts), r}
-	eq := Equal(x, y, opts...)
-	d := r.String()
-	if (d == "") != eq {
-		panic("inconsistent difference and equality results")
-	}
-	return d
-}
-
-type state struct {
-	// These fields represent the "comparison state".
-	// Calling statelessCompare must not result in observable changes to these.
-	result   diff.Result // The current result of comparison
-	curPath  Path        // The current path in the value tree
-	reporter reporter    // Optional reporter used for difference formatting
-
-	// dynChecker triggers pseudo-random checks for option correctness.
-	// It is safe for statelessCompare to mutate this value.
-	dynChecker dynChecker
-
-	// These fields, once set by processOption, will not change.
-	exporters map[reflect.Type]bool // Set of structs with unexported field visibility
-	opts      Options               // List of all fundamental and filter options
-}
-
-func newState(opts []Option) *state {
-	s := new(state)
-	for _, opt := range opts {
-		s.processOption(opt)
-	}
-	return s
-}
-
-func (s *state) processOption(opt Option) {
-	switch opt := opt.(type) {
-	case nil:
-	case Options:
-		for _, o := range opt {
-			s.processOption(o)
-		}
-	case coreOption:
-		type filtered interface {
-			isFiltered() bool
-		}
-		if fopt, ok := opt.(filtered); ok && !fopt.isFiltered() {
-			panic(fmt.Sprintf("cannot use an unfiltered option: %v", opt))
-		}
-		s.opts = append(s.opts, opt)
-	case visibleStructs:
-		if s.exporters == nil {
-			s.exporters = make(map[reflect.Type]bool)
-		}
-		for t := range opt {
-			s.exporters[t] = true
-		}
-	case reporter:
-		if s.reporter != nil {
-			panic("difference reporter already registered")
-		}
-		s.reporter = opt
-	default:
-		panic(fmt.Sprintf("unknown option %T", opt))
-	}
-}
-
-// statelessCompare compares two values and returns the result.
-// This function is stateless in that it does not alter the current result,
-// or output to any registered reporters.
-func (s *state) statelessCompare(vx, vy reflect.Value) diff.Result {
-	// We do not save and restore the curPath because all of the compareX
-	// methods should properly push and pop from the path.
-	// It is an implementation bug if the contents of curPath differs from
-	// when calling this function to when returning from it.
-
-	oldResult, oldReporter := s.result, s.reporter
-	s.result = diff.Result{} // Reset result
-	s.reporter = nil         // Remove reporter to avoid spurious printouts
-	s.compareAny(vx, vy)
-	res := s.result
-	s.result, s.reporter = oldResult, oldReporter
-	return res
-}
-
-func (s *state) compareAny(vx, vy reflect.Value) {
-	// TODO: Support cyclic data structures.
-
-	// Rule 0: Differing types are never equal.
-	if !vx.IsValid() || !vy.IsValid() {
-		s.report(vx.IsValid() == vy.IsValid(), vx, vy)
-		return
-	}
-	if vx.Type() != vy.Type() {
-		s.report(false, vx, vy) // Possible for path to be empty
-		return
-	}
-	t := vx.Type()
-	if len(s.curPath) == 0 {
-		s.curPath.push(&pathStep{typ: t})
-		defer s.curPath.pop()
-	}
-	vx, vy = s.tryExporting(vx, vy)
-
-	// Rule 1: Check whether an option applies on this node in the value tree.
-	if s.tryOptions(vx, vy, t) {
-		return
-	}
-
-	// Rule 2: Check whether the type has a valid Equal method.
-	if s.tryMethod(vx, vy, t) {
-		return
-	}
-
-	// Rule 3: Recursively descend into each value's underlying kind.
-	switch t.Kind() {
-	case reflect.Bool:
-		s.report(vx.Bool() == vy.Bool(), vx, vy)
-		return
-	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
-		s.report(vx.Int() == vy.Int(), vx, vy)
-		return
-	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
-		s.report(vx.Uint() == vy.Uint(), vx, vy)
-		return
-	case reflect.Float32, reflect.Float64:
-		s.report(vx.Float() == vy.Float(), vx, vy)
-		return
-	case reflect.Complex64, reflect.Complex128:
-		s.report(vx.Complex() == vy.Complex(), vx, vy)
-		return
-	case reflect.String:
-		s.report(vx.String() == vy.String(), vx, vy)
-		return
-	case reflect.Chan, reflect.UnsafePointer:
-		s.report(vx.Pointer() == vy.Pointer(), vx, vy)
-		return
-	case reflect.Func:
-		s.report(vx.IsNil() && vy.IsNil(), vx, vy)
-		return
-	case reflect.Ptr:
-		if vx.IsNil() || vy.IsNil() {
-			s.report(vx.IsNil() && vy.IsNil(), vx, vy)
-			return
-		}
-		s.curPath.push(&indirect{pathStep{t.Elem()}})
-		defer s.curPath.pop()
-		s.compareAny(vx.Elem(), vy.Elem())
-		return
-	case reflect.Interface:
-		if vx.IsNil() || vy.IsNil() {
-			s.report(vx.IsNil() && vy.IsNil(), vx, vy)
-			return
-		}
-		if vx.Elem().Type() != vy.Elem().Type() {
-			s.report(false, vx.Elem(), vy.Elem())
-			return
-		}
-		s.curPath.push(&typeAssertion{pathStep{vx.Elem().Type()}})
-		defer s.curPath.pop()
-		s.compareAny(vx.Elem(), vy.Elem())
-		return
-	case reflect.Slice:
-		if vx.IsNil() || vy.IsNil() {
-			s.report(vx.IsNil() && vy.IsNil(), vx, vy)
-			return
-		}
-		fallthrough
-	case reflect.Array:
-		s.compareArray(vx, vy, t)
-		return
-	case reflect.Map:
-		s.compareMap(vx, vy, t)
-		return
-	case reflect.Struct:
-		s.compareStruct(vx, vy, t)
-		return
-	default:
-		panic(fmt.Sprintf("%v kind not handled", t.Kind()))
-	}
-}
-
-func (s *state) tryExporting(vx, vy reflect.Value) (reflect.Value, reflect.Value) {
-	if sf, ok := s.curPath[len(s.curPath)-1].(*structField); ok && sf.unexported {
-		if sf.force {
-			// Use unsafe pointer arithmetic to get read-write access to an
-			// unexported field in the struct.
-			vx = unsafeRetrieveField(sf.pvx, sf.field)
-			vy = unsafeRetrieveField(sf.pvy, sf.field)
-		} else {
-			// We are not allowed to export the value, so invalidate them
-			// so that tryOptions can panic later if not explicitly ignored.
-			vx = nothing
-			vy = nothing
-		}
-	}
-	return vx, vy
-}
-
-func (s *state) tryOptions(vx, vy reflect.Value, t reflect.Type) bool {
-	// If there were no FilterValues, we will not detect invalid inputs,
-	// so manually check for them and append invalid if necessary.
-	// We still evaluate the options since an ignore can override invalid.
-	opts := s.opts
-	if !vx.IsValid() || !vy.IsValid() {
-		opts = Options{opts, invalid{}}
-	}
-
-	// Evaluate all filters and apply the remaining options.
-	if opt := opts.filter(s, vx, vy, t); opt != nil {
-		opt.apply(s, vx, vy)
-		return true
-	}
-	return false
-}
-
-func (s *state) tryMethod(vx, vy reflect.Value, t reflect.Type) bool {
-	// Check if this type even has an Equal method.
-	m, ok := t.MethodByName("Equal")
-	if !ok || !function.IsType(m.Type, function.EqualAssignable) {
-		return false
-	}
-
-	eq := s.callTTBFunc(m.Func, vx, vy)
-	s.report(eq, vx, vy)
-	return true
-}
-
-func (s *state) callTRFunc(f, v reflect.Value) reflect.Value {
-	v = sanitizeValue(v, f.Type().In(0))
-	if !s.dynChecker.Next() {
-		return f.Call([]reflect.Value{v})[0]
-	}
-
-	// Run the function twice and ensure that we get the same results back.
-	// We run in goroutines so that the race detector (if enabled) can detect
-	// unsafe mutations to the input.
-	c := make(chan reflect.Value)
-	go detectRaces(c, f, v)
-	want := f.Call([]reflect.Value{v})[0]
-	if got := <-c; !s.statelessCompare(got, want).Equal() {
-		// To avoid false-positives with non-reflexive equality operations,
-		// we sanity check whether a value is equal to itself.
-		if !s.statelessCompare(want, want).Equal() {
-			return want
-		}
-		fn := getFuncName(f.Pointer())
-		panic(fmt.Sprintf("non-deterministic function detected: %s", fn))
-	}
-	return want
-}
-
-func (s *state) callTTBFunc(f, x, y reflect.Value) bool {
-	x = sanitizeValue(x, f.Type().In(0))
-	y = sanitizeValue(y, f.Type().In(1))
-	if !s.dynChecker.Next() {
-		return f.Call([]reflect.Value{x, y})[0].Bool()
-	}
-
-	// Swapping the input arguments is sufficient to check that
-	// f is symmetric and deterministic.
-	// We run in goroutines so that the race detector (if enabled) can detect
-	// unsafe mutations to the input.
-	c := make(chan reflect.Value)
-	go detectRaces(c, f, y, x)
-	want := f.Call([]reflect.Value{x, y})[0].Bool()
-	if got := <-c; !got.IsValid() || got.Bool() != want {
-		fn := getFuncName(f.Pointer())
-		panic(fmt.Sprintf("non-deterministic or non-symmetric function detected: %s", fn))
-	}
-	return want
-}
-
-func detectRaces(c chan<- reflect.Value, f reflect.Value, vs ...reflect.Value) {
-	var ret reflect.Value
-	defer func() {
-		recover() // Ignore panics, let the other call to f panic instead
-		c <- ret
-	}()
-	ret = f.Call(vs)[0]
-}
-
-// sanitizeValue converts nil interfaces of type T to those of type R,
-// assuming that T is assignable to R.
-// Otherwise, it returns the input value as is.
-func sanitizeValue(v reflect.Value, t reflect.Type) reflect.Value {
-	// TODO(dsnet): Remove this hacky workaround.
-	// See https://golang.org/issue/22143
-	if v.Kind() == reflect.Interface && v.IsNil() && v.Type() != t {
-		return reflect.New(t).Elem()
-	}
-	return v
-}
-
-func (s *state) compareArray(vx, vy reflect.Value, t reflect.Type) {
-	step := &sliceIndex{pathStep{t.Elem()}, 0, 0}
-	s.curPath.push(step)
-
-	// Compute an edit-script for slices vx and vy.
-	es := diff.Difference(vx.Len(), vy.Len(), func(ix, iy int) diff.Result {
-		step.xkey, step.ykey = ix, iy
-		return s.statelessCompare(vx.Index(ix), vy.Index(iy))
-	})
-
-	// Report the entire slice as is if the arrays are of primitive kind,
-	// and the arrays are different enough.
-	isPrimitive := false
-	switch t.Elem().Kind() {
-	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
-		reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr,
-		reflect.Bool, reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128:
-		isPrimitive = true
-	}
-	if isPrimitive && es.Dist() > (vx.Len()+vy.Len())/4 {
-		s.curPath.pop() // Pop first since we are reporting the whole slice
-		s.report(false, vx, vy)
-		return
-	}
-
-	// Replay the edit-script.
-	var ix, iy int
-	for _, e := range es {
-		switch e {
-		case diff.UniqueX:
-			step.xkey, step.ykey = ix, -1
-			s.report(false, vx.Index(ix), nothing)
-			ix++
-		case diff.UniqueY:
-			step.xkey, step.ykey = -1, iy
-			s.report(false, nothing, vy.Index(iy))
-			iy++
-		default:
-			step.xkey, step.ykey = ix, iy
-			if e == diff.Identity {
-				s.report(true, vx.Index(ix), vy.Index(iy))
-			} else {
-				s.compareAny(vx.Index(ix), vy.Index(iy))
-			}
-			ix++
-			iy++
-		}
-	}
-	s.curPath.pop()
-	return
-}
-
-func (s *state) compareMap(vx, vy reflect.Value, t reflect.Type) {
-	if vx.IsNil() || vy.IsNil() {
-		s.report(vx.IsNil() && vy.IsNil(), vx, vy)
-		return
-	}
-
-	// We combine and sort the two map keys so that we can perform the
-	// comparisons in a deterministic order.
-	step := &mapIndex{pathStep: pathStep{t.Elem()}}
-	s.curPath.push(step)
-	defer s.curPath.pop()
-	for _, k := range value.SortKeys(append(vx.MapKeys(), vy.MapKeys()...)) {
-		step.key = k
-		vvx := vx.MapIndex(k)
-		vvy := vy.MapIndex(k)
-		switch {
-		case vvx.IsValid() && vvy.IsValid():
-			s.compareAny(vvx, vvy)
-		case vvx.IsValid() && !vvy.IsValid():
-			s.report(false, vvx, nothing)
-		case !vvx.IsValid() && vvy.IsValid():
-			s.report(false, nothing, vvy)
-		default:
-			// It is possible for both vvx and vvy to be invalid if the
-			// key contained a NaN value in it. There is no way in
-			// reflection to be able to retrieve these values.
-			// See https://golang.org/issue/11104
-			panic(fmt.Sprintf("%#v has map key with NaNs", s.curPath))
-		}
-	}
-}
-
-func (s *state) compareStruct(vx, vy reflect.Value, t reflect.Type) {
-	var vax, vay reflect.Value // Addressable versions of vx and vy
-
-	step := &structField{}
-	s.curPath.push(step)
-	defer s.curPath.pop()
-	for i := 0; i < t.NumField(); i++ {
-		vvx := vx.Field(i)
-		vvy := vy.Field(i)
-		step.typ = t.Field(i).Type
-		step.name = t.Field(i).Name
-		step.idx = i
-		step.unexported = !isExported(step.name)
-		if step.unexported {
-			// Defer checking of unexported fields until later to give an
-			// Ignore a chance to ignore the field.
-			if !vax.IsValid() || !vay.IsValid() {
-				// For unsafeRetrieveField to work, the parent struct must
-				// be addressable. Create a new copy of the values if
-				// necessary to make them addressable.
-				vax = makeAddressable(vx)
-				vay = makeAddressable(vy)
-			}
-			step.force = s.exporters[t]
-			step.pvx = vax
-			step.pvy = vay
-			step.field = t.Field(i)
-		}
-		s.compareAny(vvx, vvy)
-	}
-}
-
-// report records the result of a single comparison.
-// It also calls Report if any reporter is registered.
-func (s *state) report(eq bool, vx, vy reflect.Value) {
-	if eq {
-		s.result.NSame++
-	} else {
-		s.result.NDiff++
-	}
-	if s.reporter != nil {
-		s.reporter.Report(vx, vy, eq, s.curPath)
-	}
-}
-
-// dynChecker tracks the state needed to periodically perform checks that
-// user provided functions are symmetric and deterministic.
-// The zero value is safe for immediate use.
-type dynChecker struct{ curr, next int }
-
-// Next increments the state and reports whether a check should be performed.
-//
-// Checks occur every Nth function call, where N is a triangular number:
-//	0 1 3 6 10 15 21 28 36 45 55 66 78 91 105 120 136 153 171 190 ...
-// See https://en.wikipedia.org/wiki/Triangular_number
-//
-// This sequence ensures that the cost of checks drops significantly as
-// the number of functions calls grows larger.
-func (dc *dynChecker) Next() bool {
-	ok := dc.curr == dc.next
-	if ok {
-		dc.curr = 0
-		dc.next++
-	}
-	dc.curr++
-	return ok
-}
-
-// makeAddressable returns a value that is always addressable.
-// It returns the input verbatim if it is already addressable,
-// otherwise it creates a new value and returns an addressable copy.
-func makeAddressable(v reflect.Value) reflect.Value {
-	if v.CanAddr() {
-		return v
-	}
-	vc := reflect.New(v.Type()).Elem()
-	vc.Set(v)
-	return vc
-}

vendor/github.com/google/go-cmp/cmp/internal/diff/debug_disable.go πŸ”—

@@ -1,17 +0,0 @@
-// Copyright 2017, The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE.md file.
-
-// +build !debug
-
-package diff
-
-var debug debugger
-
-type debugger struct{}
-
-func (debugger) Begin(_, _ int, f EqualFunc, _, _ *EditScript) EqualFunc {
-	return f
-}
-func (debugger) Update() {}
-func (debugger) Finish() {}

vendor/github.com/google/go-cmp/cmp/internal/diff/debug_enable.go πŸ”—

@@ -1,122 +0,0 @@
-// Copyright 2017, The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE.md file.
-
-// +build debug
-
-package diff
-
-import (
-	"fmt"
-	"strings"
-	"sync"
-	"time"
-)
-
-// The algorithm can be seen running in real-time by enabling debugging:
-//	go test -tags=debug -v
-//
-// Example output:
-//	=== RUN   TestDifference/#34
-//	β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
-//	β”‚ \ Β· Β· Β· Β· Β· Β· Β· Β· Β· Β· Β· Β· Β· Β· β”‚
-//	β”‚ Β· # Β· Β· Β· Β· Β· Β· Β· Β· Β· Β· Β· Β· Β· β”‚
-//	β”‚ Β· \ Β· Β· Β· Β· Β· Β· Β· Β· Β· Β· Β· Β· Β· β”‚
-//	β”‚ Β· Β· \ Β· Β· Β· Β· Β· Β· Β· Β· Β· Β· Β· Β· β”‚
-//	β”‚ Β· Β· Β· X # Β· Β· Β· Β· Β· Β· Β· Β· Β· Β· β”‚
-//	β”‚ Β· Β· Β· # \ Β· Β· Β· Β· Β· Β· Β· Β· Β· Β· β”‚
-//	β”‚ Β· Β· Β· Β· Β· # # Β· Β· Β· Β· Β· Β· Β· Β· β”‚
-//	β”‚ Β· Β· Β· Β· Β· # \ Β· Β· Β· Β· Β· Β· Β· Β· β”‚
-//	β”‚ Β· Β· Β· Β· Β· Β· Β· \ Β· Β· Β· Β· Β· Β· Β· β”‚
-//	β”‚ Β· Β· Β· Β· Β· Β· Β· Β· \ Β· Β· Β· Β· Β· Β· β”‚
-//	β”‚ Β· Β· Β· Β· Β· Β· Β· Β· Β· \ Β· Β· Β· Β· Β· β”‚
-//	β”‚ Β· Β· Β· Β· Β· Β· Β· Β· Β· Β· \ Β· Β· # Β· β”‚
-//	β”‚ Β· Β· Β· Β· Β· Β· Β· Β· Β· Β· Β· \ # # Β· β”‚
-//	β”‚ Β· Β· Β· Β· Β· Β· Β· Β· Β· Β· Β· # # # Β· β”‚
-//	β”‚ Β· Β· Β· Β· Β· Β· Β· Β· Β· Β· # # # # Β· β”‚
-//	β”‚ Β· Β· Β· Β· Β· Β· Β· Β· Β· # # # # # Β· β”‚
-//	β”‚ Β· Β· Β· Β· Β· Β· Β· Β· Β· Β· Β· Β· Β· Β· \ β”‚
-//	β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
-//	[.Y..M.XY......YXYXY.|]
-//
-// The grid represents the edit-graph where the horizontal axis represents
-// list X and the vertical axis represents list Y. The start of the two lists
-// is the top-left, while the ends are the bottom-right. The 'Β·' represents
-// an unexplored node in the graph. The '\' indicates that the two symbols
-// from list X and Y are equal. The 'X' indicates that two symbols are similar
-// (but not exactly equal) to each other. The '#' indicates that the two symbols
-// are different (and not similar). The algorithm traverses this graph trying to
-// make the paths starting in the top-left and the bottom-right connect.
-//
-// The series of '.', 'X', 'Y', and 'M' characters at the bottom represents
-// the currently established path from the forward and reverse searches,
-// separated by a '|' character.
-
-const (
-	updateDelay  = 100 * time.Millisecond
-	finishDelay  = 500 * time.Millisecond
-	ansiTerminal = true // ANSI escape codes used to move terminal cursor
-)
-
-var debug debugger
-
-type debugger struct {
-	sync.Mutex
-	p1, p2           EditScript
-	fwdPath, revPath *EditScript
-	grid             []byte
-	lines            int
-}
-
-func (dbg *debugger) Begin(nx, ny int, f EqualFunc, p1, p2 *EditScript) EqualFunc {
-	dbg.Lock()
-	dbg.fwdPath, dbg.revPath = p1, p2
-	top := "β”Œβ”€" + strings.Repeat("──", nx) + "┐\n"
-	row := "β”‚ " + strings.Repeat("Β· ", nx) + "β”‚\n"
-	btm := "└─" + strings.Repeat("──", nx) + "β”˜\n"
-	dbg.grid = []byte(top + strings.Repeat(row, ny) + btm)
-	dbg.lines = strings.Count(dbg.String(), "\n")
-	fmt.Print(dbg)
-
-	// Wrap the EqualFunc so that we can intercept each result.
-	return func(ix, iy int) (r Result) {
-		cell := dbg.grid[len(top)+iy*len(row):][len("β”‚ ")+len("Β· ")*ix:][:len("Β·")]
-		for i := range cell {
-			cell[i] = 0 // Zero out the multiple bytes of UTF-8 middle-dot
-		}
-		switch r = f(ix, iy); {
-		case r.Equal():
-			cell[0] = '\\'
-		case r.Similar():
-			cell[0] = 'X'
-		default:
-			cell[0] = '#'
-		}
-		return
-	}
-}
-
-func (dbg *debugger) Update() {
-	dbg.print(updateDelay)
-}
-
-func (dbg *debugger) Finish() {
-	dbg.print(finishDelay)
-	dbg.Unlock()
-}
-
-func (dbg *debugger) String() string {
-	dbg.p1, dbg.p2 = *dbg.fwdPath, dbg.p2[:0]
-	for i := len(*dbg.revPath) - 1; i >= 0; i-- {
-		dbg.p2 = append(dbg.p2, (*dbg.revPath)[i])
-	}
-	return fmt.Sprintf("%s[%v|%v]\n\n", dbg.grid, dbg.p1, dbg.p2)
-}
-
-func (dbg *debugger) print(d time.Duration) {
-	if ansiTerminal {
-		fmt.Printf("\x1b[%dA", dbg.lines) // Reset terminal cursor
-	}
-	fmt.Print(dbg)
-	time.Sleep(d)
-}

vendor/github.com/google/go-cmp/cmp/internal/diff/diff.go πŸ”—

@@ -1,363 +0,0 @@
-// Copyright 2017, The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE.md file.
-
-// Package diff implements an algorithm for producing edit-scripts.
-// The edit-script is a sequence of operations needed to transform one list
-// of symbols into another (or vice-versa). The edits allowed are insertions,
-// deletions, and modifications. The summation of all edits is called the
-// Levenshtein distance as this problem is well-known in computer science.
-//
-// This package prioritizes performance over accuracy. That is, the run time
-// is more important than obtaining a minimal Levenshtein distance.
-package diff
-
-// EditType represents a single operation within an edit-script.
-type EditType uint8
-
-const (
-	// Identity indicates that a symbol pair is identical in both list X and Y.
-	Identity EditType = iota
-	// UniqueX indicates that a symbol only exists in X and not Y.
-	UniqueX
-	// UniqueY indicates that a symbol only exists in Y and not X.
-	UniqueY
-	// Modified indicates that a symbol pair is a modification of each other.
-	Modified
-)
-
-// EditScript represents the series of differences between two lists.
-type EditScript []EditType
-
-// String returns a human-readable string representing the edit-script where
-// Identity, UniqueX, UniqueY, and Modified are represented by the
-// '.', 'X', 'Y', and 'M' characters, respectively.
-func (es EditScript) String() string {
-	b := make([]byte, len(es))
-	for i, e := range es {
-		switch e {
-		case Identity:
-			b[i] = '.'
-		case UniqueX:
-			b[i] = 'X'
-		case UniqueY:
-			b[i] = 'Y'
-		case Modified:
-			b[i] = 'M'
-		default:
-			panic("invalid edit-type")
-		}
-	}
-	return string(b)
-}
-
-// stats returns a histogram of the number of each type of edit operation.
-func (es EditScript) stats() (s struct{ NI, NX, NY, NM int }) {
-	for _, e := range es {
-		switch e {
-		case Identity:
-			s.NI++
-		case UniqueX:
-			s.NX++
-		case UniqueY:
-			s.NY++
-		case Modified:
-			s.NM++
-		default:
-			panic("invalid edit-type")
-		}
-	}
-	return
-}
-
-// Dist is the Levenshtein distance and is guaranteed to be 0 if and only if
-// lists X and Y are equal.
-func (es EditScript) Dist() int { return len(es) - es.stats().NI }
-
-// LenX is the length of the X list.
-func (es EditScript) LenX() int { return len(es) - es.stats().NY }
-
-// LenY is the length of the Y list.
-func (es EditScript) LenY() int { return len(es) - es.stats().NX }
-
-// EqualFunc reports whether the symbols at indexes ix and iy are equal.
-// When called by Difference, the index is guaranteed to be within nx and ny.
-type EqualFunc func(ix int, iy int) Result
-
-// Result is the result of comparison.
-// NSame is the number of sub-elements that are equal.
-// NDiff is the number of sub-elements that are not equal.
-type Result struct{ NSame, NDiff int }
-
-// Equal indicates whether the symbols are equal. Two symbols are equal
-// if and only if NDiff == 0. If Equal, then they are also Similar.
-func (r Result) Equal() bool { return r.NDiff == 0 }
-
-// Similar indicates whether two symbols are similar and may be represented
-// by using the Modified type. As a special case, we consider binary comparisons
-// (i.e., those that return Result{1, 0} or Result{0, 1}) to be similar.
-//
-// The exact ratio of NSame to NDiff to determine similarity may change.
-func (r Result) Similar() bool {
-	// Use NSame+1 to offset NSame so that binary comparisons are similar.
-	return r.NSame+1 >= r.NDiff
-}
-
-// Difference reports whether two lists of lengths nx and ny are equal
-// given the definition of equality provided as f.
-//
-// This function returns an edit-script, which is a sequence of operations
-// needed to convert one list into the other. The following invariants for
-// the edit-script are maintained:
-//	β€’ eq == (es.Dist()==0)
-//	β€’ nx == es.LenX()
-//	β€’ ny == es.LenY()
-//
-// This algorithm is not guaranteed to be an optimal solution (i.e., one that
-// produces an edit-script with a minimal Levenshtein distance). This algorithm
-// favors performance over optimality. The exact output is not guaranteed to
-// be stable and may change over time.
-func Difference(nx, ny int, f EqualFunc) (es EditScript) {
-	// This algorithm is based on traversing what is known as an "edit-graph".
-	// See Figure 1 from "An O(ND) Difference Algorithm and Its Variations"
-	// by Eugene W. Myers. Since D can be as large as N itself, this is
-	// effectively O(N^2). Unlike the algorithm from that paper, we are not
-	// interested in the optimal path, but at least some "decent" path.
-	//
-	// For example, let X and Y be lists of symbols:
-	//	X = [A B C A B B A]
-	//	Y = [C B A B A C]
-	//
-	// The edit-graph can be drawn as the following:
-	//	   A B C A B B A
-	//	  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
-	//	C β”‚_|_|\|_|_|_|_β”‚ 0
-	//	B β”‚_|\|_|_|\|\|_β”‚ 1
-	//	A β”‚\|_|_|\|_|_|\β”‚ 2
-	//	B β”‚_|\|_|_|\|\|_β”‚ 3
-	//	A β”‚\|_|_|\|_|_|\β”‚ 4
-	//	C β”‚ | |\| | | | β”‚ 5
-	//	  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ 6
-	//	   0 1 2 3 4 5 6 7
-	//
-	// List X is written along the horizontal axis, while list Y is written
-	// along the vertical axis. At any point on this grid, if the symbol in
-	// list X matches the corresponding symbol in list Y, then a '\' is drawn.
-	// The goal of any minimal edit-script algorithm is to find a path from the
-	// top-left corner to the bottom-right corner, while traveling through the
-	// fewest horizontal or vertical edges.
-	// A horizontal edge is equivalent to inserting a symbol from list X.
-	// A vertical edge is equivalent to inserting a symbol from list Y.
-	// A diagonal edge is equivalent to a matching symbol between both X and Y.
-
-	// Invariants:
-	//	β€’ 0 ≀ fwdPath.X ≀ (fwdFrontier.X, revFrontier.X) ≀ revPath.X ≀ nx
-	//	β€’ 0 ≀ fwdPath.Y ≀ (fwdFrontier.Y, revFrontier.Y) ≀ revPath.Y ≀ ny
-	//
-	// In general:
-	//	β€’ fwdFrontier.X < revFrontier.X
-	//	β€’ fwdFrontier.Y < revFrontier.Y
-	// Unless, it is time for the algorithm to terminate.
-	fwdPath := path{+1, point{0, 0}, make(EditScript, 0, (nx+ny)/2)}
-	revPath := path{-1, point{nx, ny}, make(EditScript, 0)}
-	fwdFrontier := fwdPath.point // Forward search frontier
-	revFrontier := revPath.point // Reverse search frontier
-
-	// Search budget bounds the cost of searching for better paths.
-	// The longest sequence of non-matching symbols that can be tolerated is
-	// approximately the square-root of the search budget.
-	searchBudget := 4 * (nx + ny) // O(n)
-
-	// The algorithm below is a greedy, meet-in-the-middle algorithm for
-	// computing sub-optimal edit-scripts between two lists.
-	//
-	// The algorithm is approximately as follows:
-	//	β€’ Searching for differences switches back-and-forth between
-	//	a search that starts at the beginning (the top-left corner), and
-	//	a search that starts at the end (the bottom-right corner). The goal of
-	//	the search is connect with the search from the opposite corner.
-	//	β€’ As we search, we build a path in a greedy manner, where the first
-	//	match seen is added to the path (this is sub-optimal, but provides a
-	//	decent result in practice). When matches are found, we try the next pair
-	//	of symbols in the lists and follow all matches as far as possible.
-	//	β€’ When searching for matches, we search along a diagonal going through
-	//	through the "frontier" point. If no matches are found, we advance the
-	//	frontier towards the opposite corner.
-	//	β€’ This algorithm terminates when either the X coordinates or the
-	//	Y coordinates of the forward and reverse frontier points ever intersect.
-	//
-	// This algorithm is correct even if searching only in the forward direction
-	// or in the reverse direction. We do both because it is commonly observed
-	// that two lists commonly differ because elements were added to the front
-	// or end of the other list.
-	//
-	// Running the tests with the "debug" build tag prints a visualization of
-	// the algorithm running in real-time. This is educational for understanding
-	// how the algorithm works. See debug_enable.go.
-	f = debug.Begin(nx, ny, f, &fwdPath.es, &revPath.es)
-	for {
-		// Forward search from the beginning.
-		if fwdFrontier.X >= revFrontier.X || fwdFrontier.Y >= revFrontier.Y || searchBudget == 0 {
-			break
-		}
-		for stop1, stop2, i := false, false, 0; !(stop1 && stop2) && searchBudget > 0; i++ {
-			// Search in a diagonal pattern for a match.
-			z := zigzag(i)
-			p := point{fwdFrontier.X + z, fwdFrontier.Y - z}
-			switch {
-			case p.X >= revPath.X || p.Y < fwdPath.Y:
-				stop1 = true // Hit top-right corner
-			case p.Y >= revPath.Y || p.X < fwdPath.X:
-				stop2 = true // Hit bottom-left corner
-			case f(p.X, p.Y).Equal():
-				// Match found, so connect the path to this point.
-				fwdPath.connect(p, f)
-				fwdPath.append(Identity)
-				// Follow sequence of matches as far as possible.
-				for fwdPath.X < revPath.X && fwdPath.Y < revPath.Y {
-					if !f(fwdPath.X, fwdPath.Y).Equal() {
-						break
-					}
-					fwdPath.append(Identity)
-				}
-				fwdFrontier = fwdPath.point
-				stop1, stop2 = true, true
-			default:
-				searchBudget-- // Match not found
-			}
-			debug.Update()
-		}
-		// Advance the frontier towards reverse point.
-		if revPath.X-fwdFrontier.X >= revPath.Y-fwdFrontier.Y {
-			fwdFrontier.X++
-		} else {
-			fwdFrontier.Y++
-		}
-
-		// Reverse search from the end.
-		if fwdFrontier.X >= revFrontier.X || fwdFrontier.Y >= revFrontier.Y || searchBudget == 0 {
-			break
-		}
-		for stop1, stop2, i := false, false, 0; !(stop1 && stop2) && searchBudget > 0; i++ {
-			// Search in a diagonal pattern for a match.
-			z := zigzag(i)
-			p := point{revFrontier.X - z, revFrontier.Y + z}
-			switch {
-			case fwdPath.X >= p.X || revPath.Y < p.Y:
-				stop1 = true // Hit bottom-left corner
-			case fwdPath.Y >= p.Y || revPath.X < p.X:
-				stop2 = true // Hit top-right corner
-			case f(p.X-1, p.Y-1).Equal():
-				// Match found, so connect the path to this point.
-				revPath.connect(p, f)
-				revPath.append(Identity)
-				// Follow sequence of matches as far as possible.
-				for fwdPath.X < revPath.X && fwdPath.Y < revPath.Y {
-					if !f(revPath.X-1, revPath.Y-1).Equal() {
-						break
-					}
-					revPath.append(Identity)
-				}
-				revFrontier = revPath.point
-				stop1, stop2 = true, true
-			default:
-				searchBudget-- // Match not found
-			}
-			debug.Update()
-		}
-		// Advance the frontier towards forward point.
-		if revFrontier.X-fwdPath.X >= revFrontier.Y-fwdPath.Y {
-			revFrontier.X--
-		} else {
-			revFrontier.Y--
-		}
-	}
-
-	// Join the forward and reverse paths and then append the reverse path.
-	fwdPath.connect(revPath.point, f)
-	for i := len(revPath.es) - 1; i >= 0; i-- {
-		t := revPath.es[i]
-		revPath.es = revPath.es[:i]
-		fwdPath.append(t)
-	}
-	debug.Finish()
-	return fwdPath.es
-}
-
-type path struct {
-	dir   int // +1 if forward, -1 if reverse
-	point     // Leading point of the EditScript path
-	es    EditScript
-}
-
-// connect appends any necessary Identity, Modified, UniqueX, or UniqueY types
-// to the edit-script to connect p.point to dst.
-func (p *path) connect(dst point, f EqualFunc) {
-	if p.dir > 0 {
-		// Connect in forward direction.
-		for dst.X > p.X && dst.Y > p.Y {
-			switch r := f(p.X, p.Y); {
-			case r.Equal():
-				p.append(Identity)
-			case r.Similar():
-				p.append(Modified)
-			case dst.X-p.X >= dst.Y-p.Y:
-				p.append(UniqueX)
-			default:
-				p.append(UniqueY)
-			}
-		}
-		for dst.X > p.X {
-			p.append(UniqueX)
-		}
-		for dst.Y > p.Y {
-			p.append(UniqueY)
-		}
-	} else {
-		// Connect in reverse direction.
-		for p.X > dst.X && p.Y > dst.Y {
-			switch r := f(p.X-1, p.Y-1); {
-			case r.Equal():
-				p.append(Identity)
-			case r.Similar():
-				p.append(Modified)
-			case p.Y-dst.Y >= p.X-dst.X:
-				p.append(UniqueY)
-			default:
-				p.append(UniqueX)
-			}
-		}
-		for p.X > dst.X {
-			p.append(UniqueX)
-		}
-		for p.Y > dst.Y {
-			p.append(UniqueY)
-		}
-	}
-}
-
-func (p *path) append(t EditType) {
-	p.es = append(p.es, t)
-	switch t {
-	case Identity, Modified:
-		p.add(p.dir, p.dir)
-	case UniqueX:
-		p.add(p.dir, 0)
-	case UniqueY:
-		p.add(0, p.dir)
-	}
-	debug.Update()
-}
-
-type point struct{ X, Y int }
-
-func (p *point) add(dx, dy int) { p.X += dx; p.Y += dy }
-
-// zigzag maps a consecutive sequence of integers to a zig-zag sequence.
-//	[0 1 2 3 4 5 ...] => [0 -1 +1 -2 +2 ...]
-func zigzag(x int) int {
-	if x&1 != 0 {
-		x = ^x
-	}
-	return x >> 1
-}

vendor/github.com/google/go-cmp/cmp/internal/function/func.go πŸ”—

@@ -1,49 +0,0 @@
-// Copyright 2017, The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE.md file.
-
-// Package function identifies function types.
-package function
-
-import "reflect"
-
-type funcType int
-
-const (
-	_ funcType = iota
-
-	ttbFunc // func(T, T) bool
-	tibFunc // func(T, I) bool
-	trFunc  // func(T) R
-
-	Equal           = ttbFunc // func(T, T) bool
-	EqualAssignable = tibFunc // func(T, I) bool; encapsulates func(T, T) bool
-	Transformer     = trFunc  // func(T) R
-	ValueFilter     = ttbFunc // func(T, T) bool
-	Less            = ttbFunc // func(T, T) bool
-)
-
-var boolType = reflect.TypeOf(true)
-
-// IsType reports whether the reflect.Type is of the specified function type.
-func IsType(t reflect.Type, ft funcType) bool {
-	if t == nil || t.Kind() != reflect.Func || t.IsVariadic() {
-		return false
-	}
-	ni, no := t.NumIn(), t.NumOut()
-	switch ft {
-	case ttbFunc: // func(T, T) bool
-		if ni == 2 && no == 1 && t.In(0) == t.In(1) && t.Out(0) == boolType {
-			return true
-		}
-	case tibFunc: // func(T, I) bool
-		if ni == 2 && no == 1 && t.In(0).AssignableTo(t.In(1)) && t.Out(0) == boolType {
-			return true
-		}
-	case trFunc: // func(T) R
-		if ni == 1 && no == 1 {
-			return true
-		}
-	}
-	return false
-}

vendor/github.com/google/go-cmp/cmp/internal/value/format.go πŸ”—

@@ -1,277 +0,0 @@
-// Copyright 2017, The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE.md file.
-
-// Package value provides functionality for reflect.Value types.
-package value
-
-import (
-	"fmt"
-	"reflect"
-	"strconv"
-	"strings"
-	"unicode"
-)
-
-var stringerIface = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
-
-// Format formats the value v as a string.
-//
-// This is similar to fmt.Sprintf("%+v", v) except this:
-//	* Prints the type unless it can be elided
-//	* Avoids printing struct fields that are zero
-//	* Prints a nil-slice as being nil, not empty
-//	* Prints map entries in deterministic order
-func Format(v reflect.Value, conf FormatConfig) string {
-	conf.printType = true
-	conf.followPointers = true
-	conf.realPointers = true
-	return formatAny(v, conf, nil)
-}
-
-type FormatConfig struct {
-	UseStringer        bool // Should the String method be used if available?
-	printType          bool // Should we print the type before the value?
-	PrintPrimitiveType bool // Should we print the type of primitives?
-	followPointers     bool // Should we recursively follow pointers?
-	realPointers       bool // Should we print the real address of pointers?
-}
-
-func formatAny(v reflect.Value, conf FormatConfig, visited map[uintptr]bool) string {
-	// TODO: Should this be a multi-line printout in certain situations?
-
-	if !v.IsValid() {
-		return "<non-existent>"
-	}
-	if conf.UseStringer && v.Type().Implements(stringerIface) && v.CanInterface() {
-		if (v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface) && v.IsNil() {
-			return "<nil>"
-		}
-
-		const stringerPrefix = "s" // Indicates that the String method was used
-		s := v.Interface().(fmt.Stringer).String()
-		return stringerPrefix + formatString(s)
-	}
-
-	switch v.Kind() {
-	case reflect.Bool:
-		return formatPrimitive(v.Type(), v.Bool(), conf)
-	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
-		return formatPrimitive(v.Type(), v.Int(), conf)
-	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
-		if v.Type().PkgPath() == "" || v.Kind() == reflect.Uintptr {
-			// Unnamed uints are usually bytes or words, so use hexadecimal.
-			return formatPrimitive(v.Type(), formatHex(v.Uint()), conf)
-		}
-		return formatPrimitive(v.Type(), v.Uint(), conf)
-	case reflect.Float32, reflect.Float64:
-		return formatPrimitive(v.Type(), v.Float(), conf)
-	case reflect.Complex64, reflect.Complex128:
-		return formatPrimitive(v.Type(), v.Complex(), conf)
-	case reflect.String:
-		return formatPrimitive(v.Type(), formatString(v.String()), conf)
-	case reflect.UnsafePointer, reflect.Chan, reflect.Func:
-		return formatPointer(v, conf)
-	case reflect.Ptr:
-		if v.IsNil() {
-			if conf.printType {
-				return fmt.Sprintf("(%v)(nil)", v.Type())
-			}
-			return "<nil>"
-		}
-		if visited[v.Pointer()] || !conf.followPointers {
-			return formatPointer(v, conf)
-		}
-		visited = insertPointer(visited, v.Pointer())
-		return "&" + formatAny(v.Elem(), conf, visited)
-	case reflect.Interface:
-		if v.IsNil() {
-			if conf.printType {
-				return fmt.Sprintf("%v(nil)", v.Type())
-			}
-			return "<nil>"
-		}
-		return formatAny(v.Elem(), conf, visited)
-	case reflect.Slice:
-		if v.IsNil() {
-			if conf.printType {
-				return fmt.Sprintf("%v(nil)", v.Type())
-			}
-			return "<nil>"
-		}
-		if visited[v.Pointer()] {
-			return formatPointer(v, conf)
-		}
-		visited = insertPointer(visited, v.Pointer())
-		fallthrough
-	case reflect.Array:
-		var ss []string
-		subConf := conf
-		subConf.printType = v.Type().Elem().Kind() == reflect.Interface
-		for i := 0; i < v.Len(); i++ {
-			s := formatAny(v.Index(i), subConf, visited)
-			ss = append(ss, s)
-		}
-		s := fmt.Sprintf("{%s}", strings.Join(ss, ", "))
-		if conf.printType {
-			return v.Type().String() + s
-		}
-		return s
-	case reflect.Map:
-		if v.IsNil() {
-			if conf.printType {
-				return fmt.Sprintf("%v(nil)", v.Type())
-			}
-			return "<nil>"
-		}
-		if visited[v.Pointer()] {
-			return formatPointer(v, conf)
-		}
-		visited = insertPointer(visited, v.Pointer())
-
-		var ss []string
-		keyConf, valConf := conf, conf
-		keyConf.printType = v.Type().Key().Kind() == reflect.Interface
-		keyConf.followPointers = false
-		valConf.printType = v.Type().Elem().Kind() == reflect.Interface
-		for _, k := range SortKeys(v.MapKeys()) {
-			sk := formatAny(k, keyConf, visited)
-			sv := formatAny(v.MapIndex(k), valConf, visited)
-			ss = append(ss, fmt.Sprintf("%s: %s", sk, sv))
-		}
-		s := fmt.Sprintf("{%s}", strings.Join(ss, ", "))
-		if conf.printType {
-			return v.Type().String() + s
-		}
-		return s
-	case reflect.Struct:
-		var ss []string
-		subConf := conf
-		subConf.printType = true
-		for i := 0; i < v.NumField(); i++ {
-			vv := v.Field(i)
-			if isZero(vv) {
-				continue // Elide zero value fields
-			}
-			name := v.Type().Field(i).Name
-			subConf.UseStringer = conf.UseStringer
-			s := formatAny(vv, subConf, visited)
-			ss = append(ss, fmt.Sprintf("%s: %s", name, s))
-		}
-		s := fmt.Sprintf("{%s}", strings.Join(ss, ", "))
-		if conf.printType {
-			return v.Type().String() + s
-		}
-		return s
-	default:
-		panic(fmt.Sprintf("%v kind not handled", v.Kind()))
-	}
-}
-
-func formatString(s string) string {
-	// Use quoted string if it the same length as a raw string literal.
-	// Otherwise, attempt to use the raw string form.
-	qs := strconv.Quote(s)
-	if len(qs) == 1+len(s)+1 {
-		return qs
-	}
-
-	// Disallow newlines to ensure output is a single line.
-	// Only allow printable runes for readability purposes.
-	rawInvalid := func(r rune) bool {
-		return r == '`' || r == '\n' || !unicode.IsPrint(r)
-	}
-	if strings.IndexFunc(s, rawInvalid) < 0 {
-		return "`" + s + "`"
-	}
-	return qs
-}
-
-func formatPrimitive(t reflect.Type, v interface{}, conf FormatConfig) string {
-	if conf.printType && (conf.PrintPrimitiveType || t.PkgPath() != "") {
-		return fmt.Sprintf("%v(%v)", t, v)
-	}
-	return fmt.Sprintf("%v", v)
-}
-
-func formatPointer(v reflect.Value, conf FormatConfig) string {
-	p := v.Pointer()
-	if !conf.realPointers {
-		p = 0 // For deterministic printing purposes
-	}
-	s := formatHex(uint64(p))
-	if conf.printType {
-		return fmt.Sprintf("(%v)(%s)", v.Type(), s)
-	}
-	return s
-}
-
-func formatHex(u uint64) string {
-	var f string
-	switch {
-	case u <= 0xff:
-		f = "0x%02x"
-	case u <= 0xffff:
-		f = "0x%04x"
-	case u <= 0xffffff:
-		f = "0x%06x"
-	case u <= 0xffffffff:
-		f = "0x%08x"
-	case u <= 0xffffffffff:
-		f = "0x%010x"
-	case u <= 0xffffffffffff:
-		f = "0x%012x"
-	case u <= 0xffffffffffffff:
-		f = "0x%014x"
-	case u <= 0xffffffffffffffff:
-		f = "0x%016x"
-	}
-	return fmt.Sprintf(f, u)
-}
-
-// insertPointer insert p into m, allocating m if necessary.
-func insertPointer(m map[uintptr]bool, p uintptr) map[uintptr]bool {
-	if m == nil {
-		m = make(map[uintptr]bool)
-	}
-	m[p] = true
-	return m
-}
-
-// isZero reports whether v is the zero value.
-// This does not rely on Interface and so can be used on unexported fields.
-func isZero(v reflect.Value) bool {
-	switch v.Kind() {
-	case reflect.Bool:
-		return v.Bool() == false
-	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
-		return v.Int() == 0
-	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
-		return v.Uint() == 0
-	case reflect.Float32, reflect.Float64:
-		return v.Float() == 0
-	case reflect.Complex64, reflect.Complex128:
-		return v.Complex() == 0
-	case reflect.String:
-		return v.String() == ""
-	case reflect.UnsafePointer:
-		return v.Pointer() == 0
-	case reflect.Chan, reflect.Func, reflect.Interface, reflect.Ptr, reflect.Map, reflect.Slice:
-		return v.IsNil()
-	case reflect.Array:
-		for i := 0; i < v.Len(); i++ {
-			if !isZero(v.Index(i)) {
-				return false
-			}
-		}
-		return true
-	case reflect.Struct:
-		for i := 0; i < v.NumField(); i++ {
-			if !isZero(v.Field(i)) {
-				return false
-			}
-		}
-		return true
-	}
-	return false
-}

vendor/github.com/google/go-cmp/cmp/internal/value/sort.go πŸ”—

@@ -1,111 +0,0 @@
-// Copyright 2017, The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE.md file.
-
-package value
-
-import (
-	"fmt"
-	"math"
-	"reflect"
-	"sort"
-)
-
-// SortKeys sorts a list of map keys, deduplicating keys if necessary.
-// The type of each value must be comparable.
-func SortKeys(vs []reflect.Value) []reflect.Value {
-	if len(vs) == 0 {
-		return vs
-	}
-
-	// Sort the map keys.
-	sort.Sort(valueSorter(vs))
-
-	// Deduplicate keys (fails for NaNs).
-	vs2 := vs[:1]
-	for _, v := range vs[1:] {
-		if isLess(vs2[len(vs2)-1], v) {
-			vs2 = append(vs2, v)
-		}
-	}
-	return vs2
-}
-
-// TODO: Use sort.Slice once Google AppEngine is on Go1.8 or above.
-type valueSorter []reflect.Value
-
-func (vs valueSorter) Len() int           { return len(vs) }
-func (vs valueSorter) Less(i, j int) bool { return isLess(vs[i], vs[j]) }
-func (vs valueSorter) Swap(i, j int)      { vs[i], vs[j] = vs[j], vs[i] }
-
-// isLess is a generic function for sorting arbitrary map keys.
-// The inputs must be of the same type and must be comparable.
-func isLess(x, y reflect.Value) bool {
-	switch x.Type().Kind() {
-	case reflect.Bool:
-		return !x.Bool() && y.Bool()
-	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
-		return x.Int() < y.Int()
-	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
-		return x.Uint() < y.Uint()
-	case reflect.Float32, reflect.Float64:
-		fx, fy := x.Float(), y.Float()
-		return fx < fy || math.IsNaN(fx) && !math.IsNaN(fy)
-	case reflect.Complex64, reflect.Complex128:
-		cx, cy := x.Complex(), y.Complex()
-		rx, ix, ry, iy := real(cx), imag(cx), real(cy), imag(cy)
-		if rx == ry || (math.IsNaN(rx) && math.IsNaN(ry)) {
-			return ix < iy || math.IsNaN(ix) && !math.IsNaN(iy)
-		}
-		return rx < ry || math.IsNaN(rx) && !math.IsNaN(ry)
-	case reflect.Ptr, reflect.UnsafePointer, reflect.Chan:
-		return x.Pointer() < y.Pointer()
-	case reflect.String:
-		return x.String() < y.String()
-	case reflect.Array:
-		for i := 0; i < x.Len(); i++ {
-			if isLess(x.Index(i), y.Index(i)) {
-				return true
-			}
-			if isLess(y.Index(i), x.Index(i)) {
-				return false
-			}
-		}
-		return false
-	case reflect.Struct:
-		for i := 0; i < x.NumField(); i++ {
-			if isLess(x.Field(i), y.Field(i)) {
-				return true
-			}
-			if isLess(y.Field(i), x.Field(i)) {
-				return false
-			}
-		}
-		return false
-	case reflect.Interface:
-		vx, vy := x.Elem(), y.Elem()
-		if !vx.IsValid() || !vy.IsValid() {
-			return !vx.IsValid() && vy.IsValid()
-		}
-		tx, ty := vx.Type(), vy.Type()
-		if tx == ty {
-			return isLess(x.Elem(), y.Elem())
-		}
-		if tx.Kind() != ty.Kind() {
-			return vx.Kind() < vy.Kind()
-		}
-		if tx.String() != ty.String() {
-			return tx.String() < ty.String()
-		}
-		if tx.PkgPath() != ty.PkgPath() {
-			return tx.PkgPath() < ty.PkgPath()
-		}
-		// This can happen in rare situations, so we fallback to just comparing
-		// the unique pointer for a reflect.Type. This guarantees deterministic
-		// ordering within a program, but it is obviously not stable.
-		return reflect.ValueOf(vx.Type()).Pointer() < reflect.ValueOf(vy.Type()).Pointer()
-	default:
-		// Must be Func, Map, or Slice; which are not comparable.
-		panic(fmt.Sprintf("%T is not comparable", x.Type()))
-	}
-}

vendor/github.com/google/go-cmp/cmp/options.go πŸ”—

@@ -1,453 +0,0 @@
-// Copyright 2017, The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE.md file.
-
-package cmp
-
-import (
-	"fmt"
-	"reflect"
-	"runtime"
-	"strings"
-
-	"github.com/google/go-cmp/cmp/internal/function"
-)
-
-// Option configures for specific behavior of Equal and Diff. In particular,
-// the fundamental Option functions (Ignore, Transformer, and Comparer),
-// configure how equality is determined.
-//
-// The fundamental options may be composed with filters (FilterPath and
-// FilterValues) to control the scope over which they are applied.
-//
-// The cmp/cmpopts package provides helper functions for creating options that
-// may be used with Equal and Diff.
-type Option interface {
-	// filter applies all filters and returns the option that remains.
-	// Each option may only read s.curPath and call s.callTTBFunc.
-	//
-	// An Options is returned only if multiple comparers or transformers
-	// can apply simultaneously and will only contain values of those types
-	// or sub-Options containing values of those types.
-	filter(s *state, vx, vy reflect.Value, t reflect.Type) applicableOption
-}
-
-// applicableOption represents the following types:
-//	Fundamental: ignore | invalid | *comparer | *transformer
-//	Grouping:    Options
-type applicableOption interface {
-	Option
-
-	// apply executes the option, which may mutate s or panic.
-	apply(s *state, vx, vy reflect.Value)
-}
-
-// coreOption represents the following types:
-//	Fundamental: ignore | invalid | *comparer | *transformer
-//	Filters:     *pathFilter | *valuesFilter
-type coreOption interface {
-	Option
-	isCore()
-}
-
-type core struct{}
-
-func (core) isCore() {}
-
-// Options is a list of Option values that also satisfies the Option interface.
-// Helper comparison packages may return an Options value when packing multiple
-// Option values into a single Option. When this package processes an Options,
-// it will be implicitly expanded into a flat list.
-//
-// Applying a filter on an Options is equivalent to applying that same filter
-// on all individual options held within.
-type Options []Option
-
-func (opts Options) filter(s *state, vx, vy reflect.Value, t reflect.Type) (out applicableOption) {
-	for _, opt := range opts {
-		switch opt := opt.filter(s, vx, vy, t); opt.(type) {
-		case ignore:
-			return ignore{} // Only ignore can short-circuit evaluation
-		case invalid:
-			out = invalid{} // Takes precedence over comparer or transformer
-		case *comparer, *transformer, Options:
-			switch out.(type) {
-			case nil:
-				out = opt
-			case invalid:
-				// Keep invalid
-			case *comparer, *transformer, Options:
-				out = Options{out, opt} // Conflicting comparers or transformers
-			}
-		}
-	}
-	return out
-}
-
-func (opts Options) apply(s *state, _, _ reflect.Value) {
-	const warning = "ambiguous set of applicable options"
-	const help = "consider using filters to ensure at most one Comparer or Transformer may apply"
-	var ss []string
-	for _, opt := range flattenOptions(nil, opts) {
-		ss = append(ss, fmt.Sprint(opt))
-	}
-	set := strings.Join(ss, "\n\t")
-	panic(fmt.Sprintf("%s at %#v:\n\t%s\n%s", warning, s.curPath, set, help))
-}
-
-func (opts Options) String() string {
-	var ss []string
-	for _, opt := range opts {
-		ss = append(ss, fmt.Sprint(opt))
-	}
-	return fmt.Sprintf("Options{%s}", strings.Join(ss, ", "))
-}
-
-// FilterPath returns a new Option where opt is only evaluated if filter f
-// returns true for the current Path in the value tree.
-//
-// The option passed in may be an Ignore, Transformer, Comparer, Options, or
-// a previously filtered Option.
-func FilterPath(f func(Path) bool, opt Option) Option {
-	if f == nil {
-		panic("invalid path filter function")
-	}
-	if opt := normalizeOption(opt); opt != nil {
-		return &pathFilter{fnc: f, opt: opt}
-	}
-	return nil
-}
-
-type pathFilter struct {
-	core
-	fnc func(Path) bool
-	opt Option
-}
-
-func (f pathFilter) filter(s *state, vx, vy reflect.Value, t reflect.Type) applicableOption {
-	if f.fnc(s.curPath) {
-		return f.opt.filter(s, vx, vy, t)
-	}
-	return nil
-}
-
-func (f pathFilter) String() string {
-	fn := getFuncName(reflect.ValueOf(f.fnc).Pointer())
-	return fmt.Sprintf("FilterPath(%s, %v)", fn, f.opt)
-}
-
-// FilterValues returns a new Option where opt is only evaluated if filter f,
-// which is a function of the form "func(T, T) bool", returns true for the
-// current pair of values being compared. If the type of the values is not
-// assignable to T, then this filter implicitly returns false.
-//
-// The filter function must be
-// symmetric (i.e., agnostic to the order of the inputs) and
-// deterministic (i.e., produces the same result when given the same inputs).
-// If T is an interface, it is possible that f is called with two values with
-// different concrete types that both implement T.
-//
-// The option passed in may be an Ignore, Transformer, Comparer, Options, or
-// a previously filtered Option.
-func FilterValues(f interface{}, opt Option) Option {
-	v := reflect.ValueOf(f)
-	if !function.IsType(v.Type(), function.ValueFilter) || v.IsNil() {
-		panic(fmt.Sprintf("invalid values filter function: %T", f))
-	}
-	if opt := normalizeOption(opt); opt != nil {
-		vf := &valuesFilter{fnc: v, opt: opt}
-		if ti := v.Type().In(0); ti.Kind() != reflect.Interface || ti.NumMethod() > 0 {
-			vf.typ = ti
-		}
-		return vf
-	}
-	return nil
-}
-
-type valuesFilter struct {
-	core
-	typ reflect.Type  // T
-	fnc reflect.Value // func(T, T) bool
-	opt Option
-}
-
-func (f valuesFilter) filter(s *state, vx, vy reflect.Value, t reflect.Type) applicableOption {
-	if !vx.IsValid() || !vy.IsValid() {
-		return invalid{}
-	}
-	if (f.typ == nil || t.AssignableTo(f.typ)) && s.callTTBFunc(f.fnc, vx, vy) {
-		return f.opt.filter(s, vx, vy, t)
-	}
-	return nil
-}
-
-func (f valuesFilter) String() string {
-	fn := getFuncName(f.fnc.Pointer())
-	return fmt.Sprintf("FilterValues(%s, %v)", fn, f.opt)
-}
-
-// Ignore is an Option that causes all comparisons to be ignored.
-// This value is intended to be combined with FilterPath or FilterValues.
-// It is an error to pass an unfiltered Ignore option to Equal.
-func Ignore() Option { return ignore{} }
-
-type ignore struct{ core }
-
-func (ignore) isFiltered() bool                                                     { return false }
-func (ignore) filter(_ *state, _, _ reflect.Value, _ reflect.Type) applicableOption { return ignore{} }
-func (ignore) apply(_ *state, _, _ reflect.Value)                                   { return }
-func (ignore) String() string                                                       { return "Ignore()" }
-
-// invalid is a sentinel Option type to indicate that some options could not
-// be evaluated due to unexported fields.
-type invalid struct{ core }
-
-func (invalid) filter(_ *state, _, _ reflect.Value, _ reflect.Type) applicableOption { return invalid{} }
-func (invalid) apply(s *state, _, _ reflect.Value) {
-	const help = "consider using AllowUnexported or cmpopts.IgnoreUnexported"
-	panic(fmt.Sprintf("cannot handle unexported field: %#v\n%s", s.curPath, help))
-}
-
-// Transformer returns an Option that applies a transformation function that
-// converts values of a certain type into that of another.
-//
-// The transformer f must be a function "func(T) R" that converts values of
-// type T to those of type R and is implicitly filtered to input values
-// assignable to T. The transformer must not mutate T in any way.
-//
-// To help prevent some cases of infinite recursive cycles applying the
-// same transform to the output of itself (e.g., in the case where the
-// input and output types are the same), an implicit filter is added such that
-// a transformer is applicable only if that exact transformer is not already
-// in the tail of the Path since the last non-Transform step.
-//
-// The name is a user provided label that is used as the Transform.Name in the
-// transformation PathStep. If empty, an arbitrary name is used.
-func Transformer(name string, f interface{}) Option {
-	v := reflect.ValueOf(f)
-	if !function.IsType(v.Type(), function.Transformer) || v.IsNil() {
-		panic(fmt.Sprintf("invalid transformer function: %T", f))
-	}
-	if name == "" {
-		name = "Ξ»" // Lambda-symbol as place-holder for anonymous transformer
-	}
-	if !isValid(name) {
-		panic(fmt.Sprintf("invalid name: %q", name))
-	}
-	tr := &transformer{name: name, fnc: reflect.ValueOf(f)}
-	if ti := v.Type().In(0); ti.Kind() != reflect.Interface || ti.NumMethod() > 0 {
-		tr.typ = ti
-	}
-	return tr
-}
-
-type transformer struct {
-	core
-	name string
-	typ  reflect.Type  // T
-	fnc  reflect.Value // func(T) R
-}
-
-func (tr *transformer) isFiltered() bool { return tr.typ != nil }
-
-func (tr *transformer) filter(s *state, _, _ reflect.Value, t reflect.Type) applicableOption {
-	for i := len(s.curPath) - 1; i >= 0; i-- {
-		if t, ok := s.curPath[i].(*transform); !ok {
-			break // Hit most recent non-Transform step
-		} else if tr == t.trans {
-			return nil // Cannot directly use same Transform
-		}
-	}
-	if tr.typ == nil || t.AssignableTo(tr.typ) {
-		return tr
-	}
-	return nil
-}
-
-func (tr *transformer) apply(s *state, vx, vy reflect.Value) {
-	// Update path before calling the Transformer so that dynamic checks
-	// will use the updated path.
-	s.curPath.push(&transform{pathStep{tr.fnc.Type().Out(0)}, tr})
-	defer s.curPath.pop()
-
-	vx = s.callTRFunc(tr.fnc, vx)
-	vy = s.callTRFunc(tr.fnc, vy)
-	s.compareAny(vx, vy)
-}
-
-func (tr transformer) String() string {
-	return fmt.Sprintf("Transformer(%s, %s)", tr.name, getFuncName(tr.fnc.Pointer()))
-}
-
-// Comparer returns an Option that determines whether two values are equal
-// to each other.
-//
-// The comparer f must be a function "func(T, T) bool" and is implicitly
-// filtered to input values assignable to T. If T is an interface, it is
-// possible that f is called with two values of different concrete types that
-// both implement T.
-//
-// The equality function must be:
-//	β€’ Symmetric: equal(x, y) == equal(y, x)
-//	β€’ Deterministic: equal(x, y) == equal(x, y)
-//	β€’ Pure: equal(x, y) does not modify x or y
-func Comparer(f interface{}) Option {
-	v := reflect.ValueOf(f)
-	if !function.IsType(v.Type(), function.Equal) || v.IsNil() {
-		panic(fmt.Sprintf("invalid comparer function: %T", f))
-	}
-	cm := &comparer{fnc: v}
-	if ti := v.Type().In(0); ti.Kind() != reflect.Interface || ti.NumMethod() > 0 {
-		cm.typ = ti
-	}
-	return cm
-}
-
-type comparer struct {
-	core
-	typ reflect.Type  // T
-	fnc reflect.Value // func(T, T) bool
-}
-
-func (cm *comparer) isFiltered() bool { return cm.typ != nil }
-
-func (cm *comparer) filter(_ *state, _, _ reflect.Value, t reflect.Type) applicableOption {
-	if cm.typ == nil || t.AssignableTo(cm.typ) {
-		return cm
-	}
-	return nil
-}
-
-func (cm *comparer) apply(s *state, vx, vy reflect.Value) {
-	eq := s.callTTBFunc(cm.fnc, vx, vy)
-	s.report(eq, vx, vy)
-}
-
-func (cm comparer) String() string {
-	return fmt.Sprintf("Comparer(%s)", getFuncName(cm.fnc.Pointer()))
-}
-
-// AllowUnexported returns an Option that forcibly allows operations on
-// unexported fields in certain structs, which are specified by passing in a
-// value of each struct type.
-//
-// Users of this option must understand that comparing on unexported fields
-// from external packages is not safe since changes in the internal
-// implementation of some external package may cause the result of Equal
-// to unexpectedly change. However, it may be valid to use this option on types
-// defined in an internal package where the semantic meaning of an unexported
-// field is in the control of the user.
-//
-// For some cases, a custom Comparer should be used instead that defines
-// equality as a function of the public API of a type rather than the underlying
-// unexported implementation.
-//
-// For example, the reflect.Type documentation defines equality to be determined
-// by the == operator on the interface (essentially performing a shallow pointer
-// comparison) and most attempts to compare *regexp.Regexp types are interested
-// in only checking that the regular expression strings are equal.
-// Both of these are accomplished using Comparers:
-//
-//	Comparer(func(x, y reflect.Type) bool { return x == y })
-//	Comparer(func(x, y *regexp.Regexp) bool { return x.String() == y.String() })
-//
-// In other cases, the cmpopts.IgnoreUnexported option can be used to ignore
-// all unexported fields on specified struct types.
-func AllowUnexported(types ...interface{}) Option {
-	if !supportAllowUnexported {
-		panic("AllowUnexported is not supported on purego builds, Google App Engine Standard, or GopherJS")
-	}
-	m := make(map[reflect.Type]bool)
-	for _, typ := range types {
-		t := reflect.TypeOf(typ)
-		if t.Kind() != reflect.Struct {
-			panic(fmt.Sprintf("invalid struct type: %T", typ))
-		}
-		m[t] = true
-	}
-	return visibleStructs(m)
-}
-
-type visibleStructs map[reflect.Type]bool
-
-func (visibleStructs) filter(_ *state, _, _ reflect.Value, _ reflect.Type) applicableOption {
-	panic("not implemented")
-}
-
-// reporter is an Option that configures how differences are reported.
-type reporter interface {
-	// TODO: Not exported yet.
-	//
-	// Perhaps add PushStep and PopStep and change Report to only accept
-	// a PathStep instead of the full-path? Adding a PushStep and PopStep makes
-	// it clear that we are traversing the value tree in a depth-first-search
-	// manner, which has an effect on how values are printed.
-
-	Option
-
-	// Report is called for every comparison made and will be provided with
-	// the two values being compared, the equality result, and the
-	// current path in the value tree. It is possible for x or y to be an
-	// invalid reflect.Value if one of the values is non-existent;
-	// which is possible with maps and slices.
-	Report(x, y reflect.Value, eq bool, p Path)
-}
-
-// normalizeOption normalizes the input options such that all Options groups
-// are flattened and groups with a single element are reduced to that element.
-// Only coreOptions and Options containing coreOptions are allowed.
-func normalizeOption(src Option) Option {
-	switch opts := flattenOptions(nil, Options{src}); len(opts) {
-	case 0:
-		return nil
-	case 1:
-		return opts[0]
-	default:
-		return opts
-	}
-}
-
-// flattenOptions copies all options in src to dst as a flat list.
-// Only coreOptions and Options containing coreOptions are allowed.
-func flattenOptions(dst, src Options) Options {
-	for _, opt := range src {
-		switch opt := opt.(type) {
-		case nil:
-			continue
-		case Options:
-			dst = flattenOptions(dst, opt)
-		case coreOption:
-			dst = append(dst, opt)
-		default:
-			panic(fmt.Sprintf("invalid option type: %T", opt))
-		}
-	}
-	return dst
-}
-
-// getFuncName returns a short function name from the pointer.
-// The string parsing logic works up until Go1.9.
-func getFuncName(p uintptr) string {
-	fnc := runtime.FuncForPC(p)
-	if fnc == nil {
-		return "<unknown>"
-	}
-	name := fnc.Name() // E.g., "long/path/name/mypkg.(mytype).(long/path/name/mypkg.myfunc)-fm"
-	if strings.HasSuffix(name, ")-fm") || strings.HasSuffix(name, ")Β·fm") {
-		// Strip the package name from method name.
-		name = strings.TrimSuffix(name, ")-fm")
-		name = strings.TrimSuffix(name, ")Β·fm")
-		if i := strings.LastIndexByte(name, '('); i >= 0 {
-			methodName := name[i+1:] // E.g., "long/path/name/mypkg.myfunc"
-			if j := strings.LastIndexByte(methodName, '.'); j >= 0 {
-				methodName = methodName[j+1:] // E.g., "myfunc"
-			}
-			name = name[:i] + methodName // E.g., "long/path/name/mypkg.(mytype)." + "myfunc"
-		}
-	}
-	if i := strings.LastIndexByte(name, '/'); i >= 0 {
-		// Strip the package name.
-		name = name[i+1:] // E.g., "mypkg.(mytype).myfunc"
-	}
-	return name
-}

vendor/github.com/google/go-cmp/cmp/path.go πŸ”—

@@ -1,309 +0,0 @@
-// Copyright 2017, The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE.md file.
-
-package cmp
-
-import (
-	"fmt"
-	"reflect"
-	"strings"
-	"unicode"
-	"unicode/utf8"
-)
-
-type (
-	// Path is a list of PathSteps describing the sequence of operations to get
-	// from some root type to the current position in the value tree.
-	// The first Path element is always an operation-less PathStep that exists
-	// simply to identify the initial type.
-	//
-	// When traversing structs with embedded structs, the embedded struct will
-	// always be accessed as a field before traversing the fields of the
-	// embedded struct themselves. That is, an exported field from the
-	// embedded struct will never be accessed directly from the parent struct.
-	Path []PathStep
-
-	// PathStep is a union-type for specific operations to traverse
-	// a value's tree structure. Users of this package never need to implement
-	// these types as values of this type will be returned by this package.
-	PathStep interface {
-		String() string
-		Type() reflect.Type // Resulting type after performing the path step
-		isPathStep()
-	}
-
-	// SliceIndex is an index operation on a slice or array at some index Key.
-	SliceIndex interface {
-		PathStep
-		Key() int // May return -1 if in a split state
-
-		// SplitKeys returns the indexes for indexing into slices in the
-		// x and y values, respectively. These indexes may differ due to the
-		// insertion or removal of an element in one of the slices, causing
-		// all of the indexes to be shifted. If an index is -1, then that
-		// indicates that the element does not exist in the associated slice.
-		//
-		// Key is guaranteed to return -1 if and only if the indexes returned
-		// by SplitKeys are not the same. SplitKeys will never return -1 for
-		// both indexes.
-		SplitKeys() (x int, y int)
-
-		isSliceIndex()
-	}
-	// MapIndex is an index operation on a map at some index Key.
-	MapIndex interface {
-		PathStep
-		Key() reflect.Value
-		isMapIndex()
-	}
-	// TypeAssertion represents a type assertion on an interface.
-	TypeAssertion interface {
-		PathStep
-		isTypeAssertion()
-	}
-	// StructField represents a struct field access on a field called Name.
-	StructField interface {
-		PathStep
-		Name() string
-		Index() int
-		isStructField()
-	}
-	// Indirect represents pointer indirection on the parent type.
-	Indirect interface {
-		PathStep
-		isIndirect()
-	}
-	// Transform is a transformation from the parent type to the current type.
-	Transform interface {
-		PathStep
-		Name() string
-		Func() reflect.Value
-
-		// Option returns the originally constructed Transformer option.
-		// The == operator can be used to detect the exact option used.
-		Option() Option
-
-		isTransform()
-	}
-)
-
-func (pa *Path) push(s PathStep) {
-	*pa = append(*pa, s)
-}
-
-func (pa *Path) pop() {
-	*pa = (*pa)[:len(*pa)-1]
-}
-
-// Last returns the last PathStep in the Path.
-// If the path is empty, this returns a non-nil PathStep that reports a nil Type.
-func (pa Path) Last() PathStep {
-	return pa.Index(-1)
-}
-
-// Index returns the ith step in the Path and supports negative indexing.
-// A negative index starts counting from the tail of the Path such that -1
-// refers to the last step, -2 refers to the second-to-last step, and so on.
-// If index is invalid, this returns a non-nil PathStep that reports a nil Type.
-func (pa Path) Index(i int) PathStep {
-	if i < 0 {
-		i = len(pa) + i
-	}
-	if i < 0 || i >= len(pa) {
-		return pathStep{}
-	}
-	return pa[i]
-}
-
-// String returns the simplified path to a node.
-// The simplified path only contains struct field accesses.
-//
-// For example:
-//	MyMap.MySlices.MyField
-func (pa Path) String() string {
-	var ss []string
-	for _, s := range pa {
-		if _, ok := s.(*structField); ok {
-			ss = append(ss, s.String())
-		}
-	}
-	return strings.TrimPrefix(strings.Join(ss, ""), ".")
-}
-
-// GoString returns the path to a specific node using Go syntax.
-//
-// For example:
-//	(*root.MyMap["key"].(*mypkg.MyStruct).MySlices)[2][3].MyField
-func (pa Path) GoString() string {
-	var ssPre, ssPost []string
-	var numIndirect int
-	for i, s := range pa {
-		var nextStep PathStep
-		if i+1 < len(pa) {
-			nextStep = pa[i+1]
-		}
-		switch s := s.(type) {
-		case *indirect:
-			numIndirect++
-			pPre, pPost := "(", ")"
-			switch nextStep.(type) {
-			case *indirect:
-				continue // Next step is indirection, so let them batch up
-			case *structField:
-				numIndirect-- // Automatic indirection on struct fields
-			case nil:
-				pPre, pPost = "", "" // Last step; no need for parenthesis
-			}
-			if numIndirect > 0 {
-				ssPre = append(ssPre, pPre+strings.Repeat("*", numIndirect))
-				ssPost = append(ssPost, pPost)
-			}
-			numIndirect = 0
-			continue
-		case *transform:
-			ssPre = append(ssPre, s.trans.name+"(")
-			ssPost = append(ssPost, ")")
-			continue
-		case *typeAssertion:
-			// As a special-case, elide type assertions on anonymous types
-			// since they are typically generated dynamically and can be very
-			// verbose. For example, some transforms return interface{} because
-			// of Go's lack of generics, but typically take in and return the
-			// exact same concrete type.
-			if s.Type().PkgPath() == "" {
-				continue
-			}
-		}
-		ssPost = append(ssPost, s.String())
-	}
-	for i, j := 0, len(ssPre)-1; i < j; i, j = i+1, j-1 {
-		ssPre[i], ssPre[j] = ssPre[j], ssPre[i]
-	}
-	return strings.Join(ssPre, "") + strings.Join(ssPost, "")
-}
-
-type (
-	pathStep struct {
-		typ reflect.Type
-	}
-
-	sliceIndex struct {
-		pathStep
-		xkey, ykey int
-	}
-	mapIndex struct {
-		pathStep
-		key reflect.Value
-	}
-	typeAssertion struct {
-		pathStep
-	}
-	structField struct {
-		pathStep
-		name string
-		idx  int
-
-		// These fields are used for forcibly accessing an unexported field.
-		// pvx, pvy, and field are only valid if unexported is true.
-		unexported bool
-		force      bool                // Forcibly allow visibility
-		pvx, pvy   reflect.Value       // Parent values
-		field      reflect.StructField // Field information
-	}
-	indirect struct {
-		pathStep
-	}
-	transform struct {
-		pathStep
-		trans *transformer
-	}
-)
-
-func (ps pathStep) Type() reflect.Type { return ps.typ }
-func (ps pathStep) String() string {
-	if ps.typ == nil {
-		return "<nil>"
-	}
-	s := ps.typ.String()
-	if s == "" || strings.ContainsAny(s, "{}\n") {
-		return "root" // Type too simple or complex to print
-	}
-	return fmt.Sprintf("{%s}", s)
-}
-
-func (si sliceIndex) String() string {
-	switch {
-	case si.xkey == si.ykey:
-		return fmt.Sprintf("[%d]", si.xkey)
-	case si.ykey == -1:
-		// [5->?] means "I don't know where X[5] went"
-		return fmt.Sprintf("[%d->?]", si.xkey)
-	case si.xkey == -1:
-		// [?->3] means "I don't know where Y[3] came from"
-		return fmt.Sprintf("[?->%d]", si.ykey)
-	default:
-		// [5->3] means "X[5] moved to Y[3]"
-		return fmt.Sprintf("[%d->%d]", si.xkey, si.ykey)
-	}
-}
-func (mi mapIndex) String() string      { return fmt.Sprintf("[%#v]", mi.key) }
-func (ta typeAssertion) String() string { return fmt.Sprintf(".(%v)", ta.typ) }
-func (sf structField) String() string   { return fmt.Sprintf(".%s", sf.name) }
-func (in indirect) String() string      { return "*" }
-func (tf transform) String() string     { return fmt.Sprintf("%s()", tf.trans.name) }
-
-func (si sliceIndex) Key() int {
-	if si.xkey != si.ykey {
-		return -1
-	}
-	return si.xkey
-}
-func (si sliceIndex) SplitKeys() (x, y int) { return si.xkey, si.ykey }
-func (mi mapIndex) Key() reflect.Value      { return mi.key }
-func (sf structField) Name() string         { return sf.name }
-func (sf structField) Index() int           { return sf.idx }
-func (tf transform) Name() string           { return tf.trans.name }
-func (tf transform) Func() reflect.Value    { return tf.trans.fnc }
-func (tf transform) Option() Option         { return tf.trans }
-
-func (pathStep) isPathStep()           {}
-func (sliceIndex) isSliceIndex()       {}
-func (mapIndex) isMapIndex()           {}
-func (typeAssertion) isTypeAssertion() {}
-func (structField) isStructField()     {}
-func (indirect) isIndirect()           {}
-func (transform) isTransform()         {}
-
-var (
-	_ SliceIndex    = sliceIndex{}
-	_ MapIndex      = mapIndex{}
-	_ TypeAssertion = typeAssertion{}
-	_ StructField   = structField{}
-	_ Indirect      = indirect{}
-	_ Transform     = transform{}
-
-	_ PathStep = sliceIndex{}
-	_ PathStep = mapIndex{}
-	_ PathStep = typeAssertion{}
-	_ PathStep = structField{}
-	_ PathStep = indirect{}
-	_ PathStep = transform{}
-)
-
-// isExported reports whether the identifier is exported.
-func isExported(id string) bool {
-	r, _ := utf8.DecodeRuneInString(id)
-	return unicode.IsUpper(r)
-}
-
-// isValid reports whether the identifier is valid.
-// Empty and underscore-only strings are not valid.
-func isValid(id string) bool {
-	ok := id != "" && id != "_"
-	for j, c := range id {
-		ok = ok && (j > 0 || !unicode.IsDigit(c))
-		ok = ok && (c == '_' || unicode.IsLetter(c) || unicode.IsDigit(c))
-	}
-	return ok
-}

vendor/github.com/google/go-cmp/cmp/reporter.go πŸ”—

@@ -1,53 +0,0 @@
-// Copyright 2017, The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE.md file.
-
-package cmp
-
-import (
-	"fmt"
-	"reflect"
-	"strings"
-
-	"github.com/google/go-cmp/cmp/internal/value"
-)
-
-type defaultReporter struct {
-	Option
-	diffs  []string // List of differences, possibly truncated
-	ndiffs int      // Total number of differences
-	nbytes int      // Number of bytes in diffs
-	nlines int      // Number of lines in diffs
-}
-
-var _ reporter = (*defaultReporter)(nil)
-
-func (r *defaultReporter) Report(x, y reflect.Value, eq bool, p Path) {
-	if eq {
-		return // Ignore equal results
-	}
-	const maxBytes = 4096
-	const maxLines = 256
-	r.ndiffs++
-	if r.nbytes < maxBytes && r.nlines < maxLines {
-		sx := value.Format(x, value.FormatConfig{UseStringer: true})
-		sy := value.Format(y, value.FormatConfig{UseStringer: true})
-		if sx == sy {
-			// Unhelpful output, so use more exact formatting.
-			sx = value.Format(x, value.FormatConfig{PrintPrimitiveType: true})
-			sy = value.Format(y, value.FormatConfig{PrintPrimitiveType: true})
-		}
-		s := fmt.Sprintf("%#v:\n\t-: %s\n\t+: %s\n", p, sx, sy)
-		r.diffs = append(r.diffs, s)
-		r.nbytes += len(s)
-		r.nlines += strings.Count(s, "\n")
-	}
-}
-
-func (r *defaultReporter) String() string {
-	s := strings.Join(r.diffs, "")
-	if r.ndiffs == len(r.diffs) {
-		return s
-	}
-	return fmt.Sprintf("%s... %d more differences ...", s, r.ndiffs-len(r.diffs))
-}

vendor/github.com/google/go-cmp/cmp/unsafe_panic.go πŸ”—

@@ -1,15 +0,0 @@
-// Copyright 2017, The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE.md file.
-
-// +build purego appengine js
-
-package cmp
-
-import "reflect"
-
-const supportAllowUnexported = false
-
-func unsafeRetrieveField(reflect.Value, reflect.StructField) reflect.Value {
-	panic("unsafeRetrieveField is not implemented")
-}

vendor/github.com/google/go-cmp/cmp/unsafe_reflect.go πŸ”—

@@ -1,23 +0,0 @@
-// Copyright 2017, The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE.md file.
-
-// +build !purego,!appengine,!js
-
-package cmp
-
-import (
-	"reflect"
-	"unsafe"
-)
-
-const supportAllowUnexported = true
-
-// unsafeRetrieveField uses unsafe to forcibly retrieve any field from a struct
-// such that the value has read-write permissions.
-//
-// The parent struct, v, must be addressable, while f must be a StructField
-// describing the field to retrieve.
-func unsafeRetrieveField(v reflect.Value, f reflect.StructField) reflect.Value {
-	return reflect.NewAt(f.Type, unsafe.Pointer(v.UnsafeAddr()+f.Offset)).Elem()
-}

vendor/gotest.tools/LICENSE πŸ”—

@@ -1,202 +0,0 @@
-
-                                 Apache License
-                           Version 2.0, January 2004
-                        http://www.apache.org/licenses/
-
-   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
-   1. Definitions.
-
-      "License" shall mean the terms and conditions for use, reproduction,
-      and distribution as defined by Sections 1 through 9 of this document.
-
-      "Licensor" shall mean the copyright owner or entity authorized by
-      the copyright owner that is granting the License.
-
-      "Legal Entity" shall mean the union of the acting entity and all
-      other entities that control, are controlled by, or are under common
-      control with that entity. For the purposes of this definition,
-      "control" means (i) the power, direct or indirect, to cause the
-      direction or management of such entity, whether by contract or
-      otherwise, or (ii) ownership of fifty percent (50%) or more of the
-      outstanding shares, or (iii) beneficial ownership of such entity.
-
-      "You" (or "Your") shall mean an individual or Legal Entity
-      exercising permissions granted by this License.
-
-      "Source" form shall mean the preferred form for making modifications,
-      including but not limited to software source code, documentation
-      source, and configuration files.
-
-      "Object" form shall mean any form resulting from mechanical
-      transformation or translation of a Source form, including but
-      not limited to compiled object code, generated documentation,
-      and conversions to other media types.
-
-      "Work" shall mean the work of authorship, whether in Source or
-      Object form, made available under the License, as indicated by a
-      copyright notice that is included in or attached to the work
-      (an example is provided in the Appendix below).
-
-      "Derivative Works" shall mean any work, whether in Source or Object
-      form, that is based on (or derived from) the Work and for which the
-      editorial revisions, annotations, elaborations, or other modifications
-      represent, as a whole, an original work of authorship. For the purposes
-      of this License, Derivative Works shall not include works that remain
-      separable from, or merely link (or bind by name) to the interfaces of,
-      the Work and Derivative Works thereof.
-
-      "Contribution" shall mean any work of authorship, including
-      the original version of the Work and any modifications or additions
-      to that Work or Derivative Works thereof, that is intentionally
-      submitted to Licensor for inclusion in the Work by the copyright owner
-      or by an individual or Legal Entity authorized to submit on behalf of
-      the copyright owner. For the purposes of this definition, "submitted"
-      means any form of electronic, verbal, or written communication sent
-      to the Licensor or its representatives, including but not limited to
-      communication on electronic mailing lists, source code control systems,
-      and issue tracking systems that are managed by, or on behalf of, the
-      Licensor for the purpose of discussing and improving the Work, but
-      excluding communication that is conspicuously marked or otherwise
-      designated in writing by the copyright owner as "Not a Contribution."
-
-      "Contributor" shall mean Licensor and any individual or Legal Entity
-      on behalf of whom a Contribution has been received by Licensor and
-      subsequently incorporated within the Work.
-
-   2. Grant of Copyright License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      copyright license to reproduce, prepare Derivative Works of,
-      publicly display, publicly perform, sublicense, and distribute the
-      Work and such Derivative Works in Source or Object form.
-
-   3. Grant of Patent License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      (except as stated in this section) patent license to make, have made,
-      use, offer to sell, sell, import, and otherwise transfer the Work,
-      where such license applies only to those patent claims licensable
-      by such Contributor that are necessarily infringed by their
-      Contribution(s) alone or by combination of their Contribution(s)
-      with the Work to which such Contribution(s) was submitted. If You
-      institute patent litigation against any entity (including a
-      cross-claim or counterclaim in a lawsuit) alleging that the Work
-      or a Contribution incorporated within the Work constitutes direct
-      or contributory patent infringement, then any patent licenses
-      granted to You under this License for that Work shall terminate
-      as of the date such litigation is filed.
-
-   4. Redistribution. You may reproduce and distribute copies of the
-      Work or Derivative Works thereof in any medium, with or without
-      modifications, and in Source or Object form, provided that You
-      meet the following conditions:
-
-      (a) You must give any other recipients of the Work or
-          Derivative Works a copy of this License; and
-
-      (b) You must cause any modified files to carry prominent notices
-          stating that You changed the files; and
-
-      (c) You must retain, in the Source form of any Derivative Works
-          that You distribute, all copyright, patent, trademark, and
-          attribution notices from the Source form of the Work,
-          excluding those notices that do not pertain to any part of
-          the Derivative Works; and
-
-      (d) If the Work includes a "NOTICE" text file as part of its
-          distribution, then any Derivative Works that You distribute must
-          include a readable copy of the attribution notices contained
-          within such NOTICE file, excluding those notices that do not
-          pertain to any part of the Derivative Works, in at least one
-          of the following places: within a NOTICE text file distributed
-          as part of the Derivative Works; within the Source form or
-          documentation, if provided along with the Derivative Works; or,
-          within a display generated by the Derivative Works, if and
-          wherever such third-party notices normally appear. The contents
-          of the NOTICE file are for informational purposes only and
-          do not modify the License. You may add Your own attribution
-          notices within Derivative Works that You distribute, alongside
-          or as an addendum to the NOTICE text from the Work, provided
-          that such additional attribution notices cannot be construed
-          as modifying the License.
-
-      You may add Your own copyright statement to Your modifications and
-      may provide additional or different license terms and conditions
-      for use, reproduction, or distribution of Your modifications, or
-      for any such Derivative Works as a whole, provided Your use,
-      reproduction, and distribution of the Work otherwise complies with
-      the conditions stated in this License.
-
-   5. Submission of Contributions. Unless You explicitly state otherwise,
-      any Contribution intentionally submitted for inclusion in the Work
-      by You to the Licensor shall be under the terms and conditions of
-      this License, without any additional terms or conditions.
-      Notwithstanding the above, nothing herein shall supersede or modify
-      the terms of any separate license agreement you may have executed
-      with Licensor regarding such Contributions.
-
-   6. Trademarks. This License does not grant permission to use the trade
-      names, trademarks, service marks, or product names of the Licensor,
-      except as required for reasonable and customary use in describing the
-      origin of the Work and reproducing the content of the NOTICE file.
-
-   7. Disclaimer of Warranty. Unless required by applicable law or
-      agreed to in writing, Licensor provides the Work (and each
-      Contributor provides its Contributions) on an "AS IS" BASIS,
-      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-      implied, including, without limitation, any warranties or conditions
-      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
-      PARTICULAR PURPOSE. You are solely responsible for determining the
-      appropriateness of using or redistributing the Work and assume any
-      risks associated with Your exercise of permissions under this License.
-
-   8. Limitation of Liability. In no event and under no legal theory,
-      whether in tort (including negligence), contract, or otherwise,
-      unless required by applicable law (such as deliberate and grossly
-      negligent acts) or agreed to in writing, shall any Contributor be
-      liable to You for damages, including any direct, indirect, special,
-      incidental, or consequential damages of any character arising as a
-      result of this License or out of the use or inability to use the
-      Work (including but not limited to damages for loss of goodwill,
-      work stoppage, computer failure or malfunction, or any and all
-      other commercial damages or losses), even if such Contributor
-      has been advised of the possibility of such damages.
-
-   9. Accepting Warranty or Additional Liability. While redistributing
-      the Work or Derivative Works thereof, You may choose to offer,
-      and charge a fee for, acceptance of support, warranty, indemnity,
-      or other liability obligations and/or rights consistent with this
-      License. However, in accepting such obligations, You may act only
-      on Your own behalf and on Your sole responsibility, not on behalf
-      of any other Contributor, and only if You agree to indemnify,
-      defend, and hold each Contributor harmless for any liability
-      incurred by, or claims asserted against, such Contributor by reason
-      of your accepting any such warranty or additional liability.
-
-   END OF TERMS AND CONDITIONS
-
-   APPENDIX: How to apply the Apache License to your work.
-
-      To apply the Apache License to your work, attach the following
-      boilerplate notice, with the fields enclosed by brackets "[]"
-      replaced with your own identifying information. (Don't include
-      the brackets!)  The text should be enclosed in the appropriate
-      comment syntax for the file format. We also recommend that a
-      file or class name and description of purpose be included on the
-      same "printed page" as the copyright notice for easier
-      identification within third-party archives.
-
-   Copyright [yyyy] [name of copyright owner]
-
-   Licensed under the Apache License, Version 2.0 (the "License");
-   you may not use this file except in compliance with the License.
-   You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-   Unless required by applicable law or agreed to in writing, software
-   distributed under the License is distributed on an "AS IS" BASIS,
-   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-   See the License for the specific language governing permissions and
-   limitations under the License.

vendor/gotest.tools/assert/assert.go πŸ”—

@@ -1,311 +0,0 @@
-/*Package assert provides assertions for comparing expected values to actual
-values. When an assertion fails a helpful error message is printed.
-
-Assert and Check
-
-Assert() and Check() both accept a Comparison, and fail the test when the
-comparison fails. The one difference is that Assert() will end the test execution
-immediately (using t.FailNow()) whereas Check() will fail the test (using t.Fail()),
-return the value of the comparison, then proceed with the rest of the test case.
-
-Example usage
-
-The example below shows assert used with some common types.
-
-
-	import (
-	    "testing"
-
-	    "gotest.tools/assert"
-	    is "gotest.tools/assert/cmp"
-	)
-
-	func TestEverything(t *testing.T) {
-	    // booleans
-	    assert.Assert(t, ok)
-	    assert.Assert(t, !missing)
-
-	    // primitives
-	    assert.Equal(t, count, 1)
-	    assert.Equal(t, msg, "the message")
-	    assert.Assert(t, total != 10) // NotEqual
-
-	    // errors
-	    assert.NilError(t, closer.Close())
-	    assert.Error(t, err, "the exact error message")
-	    assert.ErrorContains(t, err, "includes this")
-	    assert.ErrorType(t, err, os.IsNotExist)
-
-	    // complex types
-	    assert.DeepEqual(t, result, myStruct{Name: "title"})
-	    assert.Assert(t, is.Len(items, 3))
-	    assert.Assert(t, len(sequence) != 0) // NotEmpty
-	    assert.Assert(t, is.Contains(mapping, "key"))
-
-	    // pointers and interface
-	    assert.Assert(t, is.Nil(ref))
-	    assert.Assert(t, ref != nil) // NotNil
-	}
-
-Comparisons
-
-Package https://godoc.org/gotest.tools/assert/cmp provides
-many common comparisons. Additional comparisons can be written to compare
-values in other ways. See the example Assert (CustomComparison).
-
-Automated migration from testify
-
-gty-migrate-from-testify is a binary which can update source code which uses
-testify assertions to use the assertions provided by this package.
-
-See http://bit.do/cmd-gty-migrate-from-testify.
-
-
-*/
-package assert // import "gotest.tools/assert"
-
-import (
-	"fmt"
-	"go/ast"
-	"go/token"
-
-	gocmp "github.com/google/go-cmp/cmp"
-	"gotest.tools/assert/cmp"
-	"gotest.tools/internal/format"
-	"gotest.tools/internal/source"
-)
-
-// BoolOrComparison can be a bool, or cmp.Comparison. See Assert() for usage.
-type BoolOrComparison interface{}
-
-// TestingT is the subset of testing.T used by the assert package.
-type TestingT interface {
-	FailNow()
-	Fail()
-	Log(args ...interface{})
-}
-
-type helperT interface {
-	Helper()
-}
-
-const failureMessage = "assertion failed: "
-
-// nolint: gocyclo
-func assert(
-	t TestingT,
-	failer func(),
-	argSelector argSelector,
-	comparison BoolOrComparison,
-	msgAndArgs ...interface{},
-) bool {
-	if ht, ok := t.(helperT); ok {
-		ht.Helper()
-	}
-	var success bool
-	switch check := comparison.(type) {
-	case bool:
-		if check {
-			return true
-		}
-		logFailureFromBool(t, msgAndArgs...)
-
-	// Undocumented legacy comparison without Result type
-	case func() (success bool, message string):
-		success = runCompareFunc(t, check, msgAndArgs...)
-
-	case nil:
-		return true
-
-	case error:
-		msg := "error is not nil: "
-		t.Log(format.WithCustomMessage(failureMessage+msg+check.Error(), msgAndArgs...))
-
-	case cmp.Comparison:
-		success = runComparison(t, argSelector, check, msgAndArgs...)
-
-	case func() cmp.Result:
-		success = runComparison(t, argSelector, check, msgAndArgs...)
-
-	default:
-		t.Log(fmt.Sprintf("invalid Comparison: %v (%T)", check, check))
-	}
-
-	if success {
-		return true
-	}
-	failer()
-	return false
-}
-
-func runCompareFunc(
-	t TestingT,
-	f func() (success bool, message string),
-	msgAndArgs ...interface{},
-) bool {
-	if ht, ok := t.(helperT); ok {
-		ht.Helper()
-	}
-	if success, message := f(); !success {
-		t.Log(format.WithCustomMessage(failureMessage+message, msgAndArgs...))
-		return false
-	}
-	return true
-}
-
-func logFailureFromBool(t TestingT, msgAndArgs ...interface{}) {
-	if ht, ok := t.(helperT); ok {
-		ht.Helper()
-	}
-	const stackIndex = 3 // Assert()/Check(), assert(), formatFailureFromBool()
-	const comparisonArgPos = 1
-	args, err := source.CallExprArgs(stackIndex)
-	if err != nil {
-		t.Log(err.Error())
-		return
-	}
-
-	msg, err := boolFailureMessage(args[comparisonArgPos])
-	if err != nil {
-		t.Log(err.Error())
-		msg = "expression is false"
-	}
-
-	t.Log(format.WithCustomMessage(failureMessage+msg, msgAndArgs...))
-}
-
-func boolFailureMessage(expr ast.Expr) (string, error) {
-	if binaryExpr, ok := expr.(*ast.BinaryExpr); ok && binaryExpr.Op == token.NEQ {
-		x, err := source.FormatNode(binaryExpr.X)
-		if err != nil {
-			return "", err
-		}
-		y, err := source.FormatNode(binaryExpr.Y)
-		if err != nil {
-			return "", err
-		}
-		return x + " is " + y, nil
-	}
-
-	if unaryExpr, ok := expr.(*ast.UnaryExpr); ok && unaryExpr.Op == token.NOT {
-		x, err := source.FormatNode(unaryExpr.X)
-		if err != nil {
-			return "", err
-		}
-		return x + " is true", nil
-	}
-
-	formatted, err := source.FormatNode(expr)
-	if err != nil {
-		return "", err
-	}
-	return "expression is false: " + formatted, nil
-}
-
-// Assert performs a comparison. If the comparison fails the test is marked as
-// failed, a failure message is logged, and execution is stopped immediately.
-//
-// The comparison argument may be one of three types: bool, cmp.Comparison or
-// error.
-// When called with a bool the failure message will contain the literal source
-// code of the expression.
-// When called with a cmp.Comparison the comparison is responsible for producing
-// a helpful failure message.
-// When called with an error a nil value is considered success. A non-nil error
-// is a failure, and Error() is used as the failure message.
-func Assert(t TestingT, comparison BoolOrComparison, msgAndArgs ...interface{}) {
-	if ht, ok := t.(helperT); ok {
-		ht.Helper()
-	}
-	assert(t, t.FailNow, argsFromComparisonCall, comparison, msgAndArgs...)
-}
-
-// Check performs a comparison. If the comparison fails the test is marked as
-// failed, a failure message is logged, and Check returns false. Otherwise returns
-// true.
-//
-// See Assert for details about the comparison arg and failure messages.
-func Check(t TestingT, comparison BoolOrComparison, msgAndArgs ...interface{}) bool {
-	if ht, ok := t.(helperT); ok {
-		ht.Helper()
-	}
-	return assert(t, t.Fail, argsFromComparisonCall, comparison, msgAndArgs...)
-}
-
-// NilError fails the test immediately if err is not nil.
-// This is equivalent to Assert(t, err)
-func NilError(t TestingT, err error, msgAndArgs ...interface{}) {
-	if ht, ok := t.(helperT); ok {
-		ht.Helper()
-	}
-	assert(t, t.FailNow, argsAfterT, err, msgAndArgs...)
-}
-
-// Equal uses the == operator to assert two values are equal and fails the test
-// if they are not equal.
-//
-// If the comparison fails Equal will use the variable names for x and y as part
-// of the failure message to identify the actual and expected values.
-//
-// If either x or y are a multi-line string the failure message will include a
-// unified diff of the two values. If the values only differ by whitespace
-// the unified diff will be augmented by replacing whitespace characters with
-// visible characters to identify the whitespace difference.
-//
-// This is equivalent to Assert(t, cmp.Equal(x, y)).
-func Equal(t TestingT, x, y interface{}, msgAndArgs ...interface{}) {
-	if ht, ok := t.(helperT); ok {
-		ht.Helper()
-	}
-	assert(t, t.FailNow, argsAfterT, cmp.Equal(x, y), msgAndArgs...)
-}
-
-// DeepEqual uses google/go-cmp (http://bit.do/go-cmp) to assert two values are
-// equal and fails the test if they are not equal.
-//
-// Package https://godoc.org/gotest.tools/assert/opt provides some additional
-// commonly used Options.
-//
-// This is equivalent to Assert(t, cmp.DeepEqual(x, y)).
-func DeepEqual(t TestingT, x, y interface{}, opts ...gocmp.Option) {
-	if ht, ok := t.(helperT); ok {
-		ht.Helper()
-	}
-	assert(t, t.FailNow, argsAfterT, cmp.DeepEqual(x, y, opts...))
-}
-
-// Error fails the test if err is nil, or the error message is not the expected
-// message.
-// Equivalent to Assert(t, cmp.Error(err, message)).
-func Error(t TestingT, err error, message string, msgAndArgs ...interface{}) {
-	if ht, ok := t.(helperT); ok {
-		ht.Helper()
-	}
-	assert(t, t.FailNow, argsAfterT, cmp.Error(err, message), msgAndArgs...)
-}
-
-// ErrorContains fails the test if err is nil, or the error message does not
-// contain the expected substring.
-// Equivalent to Assert(t, cmp.ErrorContains(err, substring)).
-func ErrorContains(t TestingT, err error, substring string, msgAndArgs ...interface{}) {
-	if ht, ok := t.(helperT); ok {
-		ht.Helper()
-	}
-	assert(t, t.FailNow, argsAfterT, cmp.ErrorContains(err, substring), msgAndArgs...)
-}
-
-// ErrorType fails the test if err is nil, or err is not the expected type.
-//
-// Expected can be one of:
-// a func(error) bool which returns true if the error is the expected type,
-// an instance of (or a pointer to) a struct of the expected type,
-// a pointer to an interface the error is expected to implement,
-// a reflect.Type of the expected struct or interface.
-//
-// Equivalent to Assert(t, cmp.ErrorType(err, expected)).
-func ErrorType(t TestingT, err error, expected interface{}, msgAndArgs ...interface{}) {
-	if ht, ok := t.(helperT); ok {
-		ht.Helper()
-	}
-	assert(t, t.FailNow, argsAfterT, cmp.ErrorType(err, expected), msgAndArgs...)
-}

vendor/gotest.tools/assert/cmp/compare.go πŸ”—

@@ -1,312 +0,0 @@
-/*Package cmp provides Comparisons for Assert and Check*/
-package cmp // import "gotest.tools/assert/cmp"
-
-import (
-	"fmt"
-	"reflect"
-	"strings"
-
-	"github.com/google/go-cmp/cmp"
-	"gotest.tools/internal/format"
-)
-
-// Comparison is a function which compares values and returns ResultSuccess if
-// the actual value matches the expected value. If the values do not match the
-// Result will contain a message about why it failed.
-type Comparison func() Result
-
-// DeepEqual compares two values using google/go-cmp (http://bit.do/go-cmp)
-// and succeeds if the values are equal.
-//
-// The comparison can be customized using comparison Options.
-// Package https://godoc.org/gotest.tools/assert/opt provides some additional
-// commonly used Options.
-func DeepEqual(x, y interface{}, opts ...cmp.Option) Comparison {
-	return func() (result Result) {
-		defer func() {
-			if panicmsg, handled := handleCmpPanic(recover()); handled {
-				result = ResultFailure(panicmsg)
-			}
-		}()
-		diff := cmp.Diff(x, y, opts...)
-		if diff == "" {
-			return ResultSuccess
-		}
-		return multiLineDiffResult(diff)
-	}
-}
-
-func handleCmpPanic(r interface{}) (string, bool) {
-	if r == nil {
-		return "", false
-	}
-	panicmsg, ok := r.(string)
-	if !ok {
-		panic(r)
-	}
-	switch {
-	case strings.HasPrefix(panicmsg, "cannot handle unexported field"):
-		return panicmsg, true
-	}
-	panic(r)
-}
-
-func toResult(success bool, msg string) Result {
-	if success {
-		return ResultSuccess
-	}
-	return ResultFailure(msg)
-}
-
-// Equal succeeds if x == y. See assert.Equal for full documentation.
-func Equal(x, y interface{}) Comparison {
-	return func() Result {
-		switch {
-		case x == y:
-			return ResultSuccess
-		case isMultiLineStringCompare(x, y):
-			diff := format.UnifiedDiff(format.DiffConfig{A: x.(string), B: y.(string)})
-			return multiLineDiffResult(diff)
-		}
-		return ResultFailureTemplate(`
-			{{- .Data.x}} (
-				{{- with callArg 0 }}{{ formatNode . }} {{end -}}
-				{{- printf "%T" .Data.x -}}
-			) != {{ .Data.y}} (
-				{{- with callArg 1 }}{{ formatNode . }} {{end -}}
-				{{- printf "%T" .Data.y -}}
-			)`,
-			map[string]interface{}{"x": x, "y": y})
-	}
-}
-
-func isMultiLineStringCompare(x, y interface{}) bool {
-	strX, ok := x.(string)
-	if !ok {
-		return false
-	}
-	strY, ok := y.(string)
-	if !ok {
-		return false
-	}
-	return strings.Contains(strX, "\n") || strings.Contains(strY, "\n")
-}
-
-func multiLineDiffResult(diff string) Result {
-	return ResultFailureTemplate(`
---- {{ with callArg 0 }}{{ formatNode . }}{{else}}←{{end}}
-+++ {{ with callArg 1 }}{{ formatNode . }}{{else}}β†’{{end}}
-{{ .Data.diff }}`,
-		map[string]interface{}{"diff": diff})
-}
-
-// Len succeeds if the sequence has the expected length.
-func Len(seq interface{}, expected int) Comparison {
-	return func() (result Result) {
-		defer func() {
-			if e := recover(); e != nil {
-				result = ResultFailure(fmt.Sprintf("type %T does not have a length", seq))
-			}
-		}()
-		value := reflect.ValueOf(seq)
-		length := value.Len()
-		if length == expected {
-			return ResultSuccess
-		}
-		msg := fmt.Sprintf("expected %s (length %d) to have length %d", seq, length, expected)
-		return ResultFailure(msg)
-	}
-}
-
-// Contains succeeds if item is in collection. Collection may be a string, map,
-// slice, or array.
-//
-// If collection is a string, item must also be a string, and is compared using
-// strings.Contains().
-// If collection is a Map, contains will succeed if item is a key in the map.
-// If collection is a slice or array, item is compared to each item in the
-// sequence using reflect.DeepEqual().
-func Contains(collection interface{}, item interface{}) Comparison {
-	return func() Result {
-		colValue := reflect.ValueOf(collection)
-		if !colValue.IsValid() {
-			return ResultFailure(fmt.Sprintf("nil does not contain items"))
-		}
-		msg := fmt.Sprintf("%v does not contain %v", collection, item)
-
-		itemValue := reflect.ValueOf(item)
-		switch colValue.Type().Kind() {
-		case reflect.String:
-			if itemValue.Type().Kind() != reflect.String {
-				return ResultFailure("string may only contain strings")
-			}
-			return toResult(
-				strings.Contains(colValue.String(), itemValue.String()),
-				fmt.Sprintf("string %q does not contain %q", collection, item))
-
-		case reflect.Map:
-			if itemValue.Type() != colValue.Type().Key() {
-				return ResultFailure(fmt.Sprintf(
-					"%v can not contain a %v key", colValue.Type(), itemValue.Type()))
-			}
-			return toResult(colValue.MapIndex(itemValue).IsValid(), msg)
-
-		case reflect.Slice, reflect.Array:
-			for i := 0; i < colValue.Len(); i++ {
-				if reflect.DeepEqual(colValue.Index(i).Interface(), item) {
-					return ResultSuccess
-				}
-			}
-			return ResultFailure(msg)
-		default:
-			return ResultFailure(fmt.Sprintf("type %T does not contain items", collection))
-		}
-	}
-}
-
-// Panics succeeds if f() panics.
-func Panics(f func()) Comparison {
-	return func() (result Result) {
-		defer func() {
-			if err := recover(); err != nil {
-				result = ResultSuccess
-			}
-		}()
-		f()
-		return ResultFailure("did not panic")
-	}
-}
-
-// Error succeeds if err is a non-nil error, and the error message equals the
-// expected message.
-func Error(err error, message string) Comparison {
-	return func() Result {
-		switch {
-		case err == nil:
-			return ResultFailure("expected an error, got nil")
-		case err.Error() != message:
-			return ResultFailure(fmt.Sprintf(
-				"expected error %q, got %+v", message, err))
-		}
-		return ResultSuccess
-	}
-}
-
-// ErrorContains succeeds if err is a non-nil error, and the error message contains
-// the expected substring.
-func ErrorContains(err error, substring string) Comparison {
-	return func() Result {
-		switch {
-		case err == nil:
-			return ResultFailure("expected an error, got nil")
-		case !strings.Contains(err.Error(), substring):
-			return ResultFailure(fmt.Sprintf(
-				"expected error to contain %q, got %+v", substring, err))
-		}
-		return ResultSuccess
-	}
-}
-
-// Nil succeeds if obj is a nil interface, pointer, or function.
-//
-// Use NilError() for comparing errors. Use Len(obj, 0) for comparing slices,
-// maps, and channels.
-func Nil(obj interface{}) Comparison {
-	msgFunc := func(value reflect.Value) string {
-		return fmt.Sprintf("%v (type %s) is not nil", reflect.Indirect(value), value.Type())
-	}
-	return isNil(obj, msgFunc)
-}
-
-func isNil(obj interface{}, msgFunc func(reflect.Value) string) Comparison {
-	return func() Result {
-		if obj == nil {
-			return ResultSuccess
-		}
-		value := reflect.ValueOf(obj)
-		kind := value.Type().Kind()
-		if kind >= reflect.Chan && kind <= reflect.Slice {
-			if value.IsNil() {
-				return ResultSuccess
-			}
-			return ResultFailure(msgFunc(value))
-		}
-
-		return ResultFailure(fmt.Sprintf("%v (type %s) can not be nil", value, value.Type()))
-	}
-}
-
-// ErrorType succeeds if err is not nil and is of the expected type.
-//
-// Expected can be one of:
-// a func(error) bool which returns true if the error is the expected type,
-// an instance of (or a pointer to) a struct of the expected type,
-// a pointer to an interface the error is expected to implement,
-// a reflect.Type of the expected struct or interface.
-func ErrorType(err error, expected interface{}) Comparison {
-	return func() Result {
-		switch expectedType := expected.(type) {
-		case func(error) bool:
-			return cmpErrorTypeFunc(err, expectedType)
-		case reflect.Type:
-			if expectedType.Kind() == reflect.Interface {
-				return cmpErrorTypeImplementsType(err, expectedType)
-			}
-			return cmpErrorTypeEqualType(err, expectedType)
-		case nil:
-			return ResultFailure(fmt.Sprintf("invalid type for expected: nil"))
-		}
-
-		expectedType := reflect.TypeOf(expected)
-		switch {
-		case expectedType.Kind() == reflect.Struct, isPtrToStruct(expectedType):
-			return cmpErrorTypeEqualType(err, expectedType)
-		case isPtrToInterface(expectedType):
-			return cmpErrorTypeImplementsType(err, expectedType.Elem())
-		}
-		return ResultFailure(fmt.Sprintf("invalid type for expected: %T", expected))
-	}
-}
-
-func cmpErrorTypeFunc(err error, f func(error) bool) Result {
-	if f(err) {
-		return ResultSuccess
-	}
-	actual := "nil"
-	if err != nil {
-		actual = fmt.Sprintf("%s (%T)", err, err)
-	}
-	return ResultFailureTemplate(`error is {{ .Data.actual }}
-		{{- with callArg 1 }}, not {{ formatNode . }}{{end -}}`,
-		map[string]interface{}{"actual": actual})
-}
-
-func cmpErrorTypeEqualType(err error, expectedType reflect.Type) Result {
-	if err == nil {
-		return ResultFailure(fmt.Sprintf("error is nil, not %s", expectedType))
-	}
-	errValue := reflect.ValueOf(err)
-	if errValue.Type() == expectedType {
-		return ResultSuccess
-	}
-	return ResultFailure(fmt.Sprintf("error is %s (%T), not %s", err, err, expectedType))
-}
-
-func cmpErrorTypeImplementsType(err error, expectedType reflect.Type) Result {
-	if err == nil {
-		return ResultFailure(fmt.Sprintf("error is nil, not %s", expectedType))
-	}
-	errValue := reflect.ValueOf(err)
-	if errValue.Type().Implements(expectedType) {
-		return ResultSuccess
-	}
-	return ResultFailure(fmt.Sprintf("error is %s (%T), not %s", err, err, expectedType))
-}
-
-func isPtrToInterface(typ reflect.Type) bool {
-	return typ.Kind() == reflect.Ptr && typ.Elem().Kind() == reflect.Interface
-}
-
-func isPtrToStruct(typ reflect.Type) bool {
-	return typ.Kind() == reflect.Ptr && typ.Elem().Kind() == reflect.Struct
-}

vendor/gotest.tools/assert/cmp/result.go πŸ”—

@@ -1,94 +0,0 @@
-package cmp
-
-import (
-	"bytes"
-	"fmt"
-	"go/ast"
-	"text/template"
-
-	"gotest.tools/internal/source"
-)
-
-// Result of a Comparison.
-type Result interface {
-	Success() bool
-}
-
-type result struct {
-	success bool
-	message string
-}
-
-func (r result) Success() bool {
-	return r.success
-}
-
-func (r result) FailureMessage() string {
-	return r.message
-}
-
-// ResultSuccess is a constant which is returned by a ComparisonWithResult to
-// indicate success.
-var ResultSuccess = result{success: true}
-
-// ResultFailure returns a failed Result with a failure message.
-func ResultFailure(message string) Result {
-	return result{message: message}
-}
-
-// ResultFromError returns ResultSuccess if err is nil. Otherwise ResultFailure
-// is returned with the error message as the failure message.
-func ResultFromError(err error) Result {
-	if err == nil {
-		return ResultSuccess
-	}
-	return ResultFailure(err.Error())
-}
-
-type templatedResult struct {
-	success  bool
-	template string
-	data     map[string]interface{}
-}
-
-func (r templatedResult) Success() bool {
-	return r.success
-}
-
-func (r templatedResult) FailureMessage(args []ast.Expr) string {
-	msg, err := renderMessage(r, args)
-	if err != nil {
-		return fmt.Sprintf("failed to render failure message: %s", err)
-	}
-	return msg
-}
-
-// ResultFailureTemplate returns a Result with a template string and data which
-// can be used to format a failure message. The template may access data from .Data,
-// the comparison args with the callArg function, and the formatNode function may
-// be used to format the call args.
-func ResultFailureTemplate(template string, data map[string]interface{}) Result {
-	return templatedResult{template: template, data: data}
-}
-
-func renderMessage(result templatedResult, args []ast.Expr) (string, error) {
-	tmpl := template.New("failure").Funcs(template.FuncMap{
-		"formatNode": source.FormatNode,
-		"callArg": func(index int) ast.Expr {
-			if index >= len(args) {
-				return nil
-			}
-			return args[index]
-		},
-	})
-	var err error
-	tmpl, err = tmpl.Parse(result.template)
-	if err != nil {
-		return "", err
-	}
-	buf := new(bytes.Buffer)
-	err = tmpl.Execute(buf, map[string]interface{}{
-		"Data": result.data,
-	})
-	return buf.String(), err
-}

vendor/gotest.tools/assert/result.go πŸ”—

@@ -1,107 +0,0 @@
-package assert
-
-import (
-	"fmt"
-	"go/ast"
-
-	"gotest.tools/assert/cmp"
-	"gotest.tools/internal/format"
-	"gotest.tools/internal/source"
-)
-
-func runComparison(
-	t TestingT,
-	argSelector argSelector,
-	f cmp.Comparison,
-	msgAndArgs ...interface{},
-) bool {
-	if ht, ok := t.(helperT); ok {
-		ht.Helper()
-	}
-	result := f()
-	if result.Success() {
-		return true
-	}
-
-	var message string
-	switch typed := result.(type) {
-	case resultWithComparisonArgs:
-		const stackIndex = 3 // Assert/Check, assert, runComparison
-		args, err := source.CallExprArgs(stackIndex)
-		if err != nil {
-			t.Log(err.Error())
-		}
-		message = typed.FailureMessage(filterPrintableExpr(argSelector(args)))
-	case resultBasic:
-		message = typed.FailureMessage()
-	default:
-		message = fmt.Sprintf("comparison returned invalid Result type: %T", result)
-	}
-
-	t.Log(format.WithCustomMessage(failureMessage+message, msgAndArgs...))
-	return false
-}
-
-type resultWithComparisonArgs interface {
-	FailureMessage(args []ast.Expr) string
-}
-
-type resultBasic interface {
-	FailureMessage() string
-}
-
-// filterPrintableExpr filters the ast.Expr slice to only include Expr that are
-// easy to read when printed and contain relevant information to an assertion.
-//
-// Ident and SelectorExpr are included because they print nicely and the variable
-// names may provide additional context to their values.
-// BasicLit and CompositeLit are excluded because their source is equivalent to
-// their value, which is already available.
-// Other types are ignored for now, but could be added if they are relevant.
-func filterPrintableExpr(args []ast.Expr) []ast.Expr {
-	result := make([]ast.Expr, len(args))
-	for i, arg := range args {
-		if isShortPrintableExpr(arg) {
-			result[i] = arg
-			continue
-		}
-
-		if starExpr, ok := arg.(*ast.StarExpr); ok {
-			result[i] = starExpr.X
-			continue
-		}
-		result[i] = nil
-	}
-	return result
-}
-
-func isShortPrintableExpr(expr ast.Expr) bool {
-	switch expr.(type) {
-	case *ast.Ident, *ast.SelectorExpr, *ast.IndexExpr, *ast.SliceExpr:
-		return true
-	case *ast.BinaryExpr, *ast.UnaryExpr:
-		return true
-	default:
-		// CallExpr, ParenExpr, TypeAssertExpr, KeyValueExpr, StarExpr
-		return false
-	}
-}
-
-type argSelector func([]ast.Expr) []ast.Expr
-
-func argsAfterT(args []ast.Expr) []ast.Expr {
-	if len(args) < 1 {
-		return nil
-	}
-	return args[1:]
-}
-
-func argsFromComparisonCall(args []ast.Expr) []ast.Expr {
-	if len(args) < 1 {
-		return nil
-	}
-	if callExpr, ok := args[1].(*ast.CallExpr); ok {
-		return callExpr.Args
-	}
-	return nil
-}

vendor/gotest.tools/internal/difflib/LICENSE πŸ”—

@@ -1,27 +0,0 @@
-Copyright (c) 2013, Patrick Mezard
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are
-met:
-
-    Redistributions of source code must retain the above copyright
-notice, this list of conditions and the following disclaimer.
-    Redistributions in binary form must reproduce the above copyright
-notice, this list of conditions and the following disclaimer in the
-documentation and/or other materials provided with the distribution.
-    The names of its contributors may not be used to endorse or promote
-products derived from this software without specific prior written
-permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
-IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
-TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
-PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
-TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
-PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
-LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
-NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

vendor/gotest.tools/internal/difflib/difflib.go πŸ”—

@@ -1,420 +0,0 @@
-/* Package difflib is a partial port of Python difflib module.
-
-Original source: https://github.com/pmezard/go-difflib
-
-This file is trimmed to only the parts used by this repository.
-*/
-package difflib // import "gotest.tools/internal/difflib"
-
-func min(a, b int) int {
-	if a < b {
-		return a
-	}
-	return b
-}
-
-func max(a, b int) int {
-	if a > b {
-		return a
-	}
-	return b
-}
-
-type Match struct {
-	A    int
-	B    int
-	Size int
-}
-
-type OpCode struct {
-	Tag byte
-	I1  int
-	I2  int
-	J1  int
-	J2  int
-}
-
-// SequenceMatcher compares sequence of strings. The basic
-// algorithm predates, and is a little fancier than, an algorithm
-// published in the late 1980's by Ratcliff and Obershelp under the
-// hyperbolic name "gestalt pattern matching".  The basic idea is to find
-// the longest contiguous matching subsequence that contains no "junk"
-// elements (R-O doesn't address junk).  The same idea is then applied
-// recursively to the pieces of the sequences to the left and to the right
-// of the matching subsequence.  This does not yield minimal edit
-// sequences, but does tend to yield matches that "look right" to people.
-//
-// SequenceMatcher tries to compute a "human-friendly diff" between two
-// sequences.  Unlike e.g. UNIX(tm) diff, the fundamental notion is the
-// longest *contiguous* & junk-free matching subsequence.  That's what
-// catches peoples' eyes.  The Windows(tm) windiff has another interesting
-// notion, pairing up elements that appear uniquely in each sequence.
-// That, and the method here, appear to yield more intuitive difference
-// reports than does diff.  This method appears to be the least vulnerable
-// to synching up on blocks of "junk lines", though (like blank lines in
-// ordinary text files, or maybe "<P>" lines in HTML files).  That may be
-// because this is the only method of the 3 that has a *concept* of
-// "junk" <wink>.
-//
-// Timing:  Basic R-O is cubic time worst case and quadratic time expected
-// case.  SequenceMatcher is quadratic time for the worst case and has
-// expected-case behavior dependent in a complicated way on how many
-// elements the sequences have in common; best case time is linear.
-type SequenceMatcher struct {
-	a              []string
-	b              []string
-	b2j            map[string][]int
-	IsJunk         func(string) bool
-	autoJunk       bool
-	bJunk          map[string]struct{}
-	matchingBlocks []Match
-	fullBCount     map[string]int
-	bPopular       map[string]struct{}
-	opCodes        []OpCode
-}
-
-func NewMatcher(a, b []string) *SequenceMatcher {
-	m := SequenceMatcher{autoJunk: true}
-	m.SetSeqs(a, b)
-	return &m
-}
-
-// Set two sequences to be compared.
-func (m *SequenceMatcher) SetSeqs(a, b []string) {
-	m.SetSeq1(a)
-	m.SetSeq2(b)
-}
-
-// Set the first sequence to be compared. The second sequence to be compared is
-// not changed.
-//
-// SequenceMatcher computes and caches detailed information about the second
-// sequence, so if you want to compare one sequence S against many sequences,
-// use .SetSeq2(s) once and call .SetSeq1(x) repeatedly for each of the other
-// sequences.
-//
-// See also SetSeqs() and SetSeq2().
-func (m *SequenceMatcher) SetSeq1(a []string) {
-	if &a == &m.a {
-		return
-	}
-	m.a = a
-	m.matchingBlocks = nil
-	m.opCodes = nil
-}
-
-// Set the second sequence to be compared. The first sequence to be compared is
-// not changed.
-func (m *SequenceMatcher) SetSeq2(b []string) {
-	if &b == &m.b {
-		return
-	}
-	m.b = b
-	m.matchingBlocks = nil
-	m.opCodes = nil
-	m.fullBCount = nil
-	m.chainB()
-}
-
-func (m *SequenceMatcher) chainB() {
-	// Populate line -> index mapping
-	b2j := map[string][]int{}
-	for i, s := range m.b {
-		indices := b2j[s]
-		indices = append(indices, i)
-		b2j[s] = indices
-	}
-
-	// Purge junk elements
-	m.bJunk = map[string]struct{}{}
-	if m.IsJunk != nil {
-		junk := m.bJunk
-		for s, _ := range b2j {
-			if m.IsJunk(s) {
-				junk[s] = struct{}{}
-			}
-		}
-		for s, _ := range junk {
-			delete(b2j, s)
-		}
-	}
-
-	// Purge remaining popular elements
-	popular := map[string]struct{}{}
-	n := len(m.b)
-	if m.autoJunk && n >= 200 {
-		ntest := n/100 + 1
-		for s, indices := range b2j {
-			if len(indices) > ntest {
-				popular[s] = struct{}{}
-			}
-		}
-		for s, _ := range popular {
-			delete(b2j, s)
-		}
-	}
-	m.bPopular = popular
-	m.b2j = b2j
-}
-
-func (m *SequenceMatcher) isBJunk(s string) bool {
-	_, ok := m.bJunk[s]
-	return ok
-}
-
-// Find longest matching block in a[alo:ahi] and b[blo:bhi].
-//
-// If IsJunk is not defined:
-//
-// Return (i,j,k) such that a[i:i+k] is equal to b[j:j+k], where
-//     alo <= i <= i+k <= ahi
-//     blo <= j <= j+k <= bhi
-// and for all (i',j',k') meeting those conditions,
-//     k >= k'
-//     i <= i'
-//     and if i == i', j <= j'
-//
-// In other words, of all maximal matching blocks, return one that
-// starts earliest in a, and of all those maximal matching blocks that
-// start earliest in a, return the one that starts earliest in b.
-//
-// If IsJunk is defined, first the longest matching block is
-// determined as above, but with the additional restriction that no
-// junk element appears in the block.  Then that block is extended as
-// far as possible by matching (only) junk elements on both sides.  So
-// the resulting block never matches on junk except as identical junk
-// happens to be adjacent to an "interesting" match.
-//
-// If no blocks match, return (alo, blo, 0).
-func (m *SequenceMatcher) findLongestMatch(alo, ahi, blo, bhi int) Match {
-	// CAUTION:  stripping common prefix or suffix would be incorrect.
-	// E.g.,
-	//    ab
-	//    acab
-	// Longest matching block is "ab", but if common prefix is
-	// stripped, it's "a" (tied with "b").  UNIX(tm) diff does so
-	// strip, so ends up claiming that ab is changed to acab by
-	// inserting "ca" in the middle.  That's minimal but unintuitive:
-	// "it's obvious" that someone inserted "ac" at the front.
-	// Windiff ends up at the same place as diff, but by pairing up
-	// the unique 'b's and then matching the first two 'a's.
-	besti, bestj, bestsize := alo, blo, 0
-
-	// find longest junk-free match
-	// during an iteration of the loop, j2len[j] = length of longest
-	// junk-free match ending with a[i-1] and b[j]
-	j2len := map[int]int{}
-	for i := alo; i != ahi; i++ {
-		// look at all instances of a[i] in b; note that because
-		// b2j has no junk keys, the loop is skipped if a[i] is junk
-		newj2len := map[int]int{}
-		for _, j := range m.b2j[m.a[i]] {
-			// a[i] matches b[j]
-			if j < blo {
-				continue
-			}
-			if j >= bhi {
-				break
-			}
-			k := j2len[j-1] + 1
-			newj2len[j] = k
-			if k > bestsize {
-				besti, bestj, bestsize = i-k+1, j-k+1, k
-			}
-		}
-		j2len = newj2len
-	}
-
-	// Extend the best by non-junk elements on each end.  In particular,
-	// "popular" non-junk elements aren't in b2j, which greatly speeds
-	// the inner loop above, but also means "the best" match so far
-	// doesn't contain any junk *or* popular non-junk elements.
-	for besti > alo && bestj > blo && !m.isBJunk(m.b[bestj-1]) &&
-		m.a[besti-1] == m.b[bestj-1] {
-		besti, bestj, bestsize = besti-1, bestj-1, bestsize+1
-	}
-	for besti+bestsize < ahi && bestj+bestsize < bhi &&
-		!m.isBJunk(m.b[bestj+bestsize]) &&
-		m.a[besti+bestsize] == m.b[bestj+bestsize] {
-		bestsize += 1
-	}
-
-	// Now that we have a wholly interesting match (albeit possibly
-	// empty!), we may as well suck up the matching junk on each
-	// side of it too.  Can't think of a good reason not to, and it
-	// saves post-processing the (possibly considerable) expense of
-	// figuring out what to do with it.  In the case of an empty
-	// interesting match, this is clearly the right thing to do,
-	// because no other kind of match is possible in the regions.
-	for besti > alo && bestj > blo && m.isBJunk(m.b[bestj-1]) &&
-		m.a[besti-1] == m.b[bestj-1] {
-		besti, bestj, bestsize = besti-1, bestj-1, bestsize+1
-	}
-	for besti+bestsize < ahi && bestj+bestsize < bhi &&
-		m.isBJunk(m.b[bestj+bestsize]) &&
-		m.a[besti+bestsize] == m.b[bestj+bestsize] {
-		bestsize += 1
-	}
-
-	return Match{A: besti, B: bestj, Size: bestsize}
-}
-
-// Return list of triples describing matching subsequences.
-//
-// Each triple is of the form (i, j, n), and means that
-// a[i:i+n] == b[j:j+n].  The triples are monotonically increasing in
-// i and in j. It's also guaranteed that if (i, j, n) and (i', j', n') are
-// adjacent triples in the list, and the second is not the last triple in the
-// list, then i+n != i' or j+n != j'. IOW, adjacent triples never describe
-// adjacent equal blocks.
-//
-// The last triple is a dummy, (len(a), len(b), 0), and is the only
-// triple with n==0.
-func (m *SequenceMatcher) GetMatchingBlocks() []Match {
-	if m.matchingBlocks != nil {
-		return m.matchingBlocks
-	}
-
-	var matchBlocks func(alo, ahi, blo, bhi int, matched []Match) []Match
-	matchBlocks = func(alo, ahi, blo, bhi int, matched []Match) []Match {
-		match := m.findLongestMatch(alo, ahi, blo, bhi)
-		i, j, k := match.A, match.B, match.Size
-		if match.Size > 0 {
-			if alo < i && blo < j {
-				matched = matchBlocks(alo, i, blo, j, matched)
-			}
-			matched = append(matched, match)
-			if i+k < ahi && j+k < bhi {
-				matched = matchBlocks(i+k, ahi, j+k, bhi, matched)
-			}
-		}
-		return matched
-	}
-	matched := matchBlocks(0, len(m.a), 0, len(m.b), nil)
-
-	// It's possible that we have adjacent equal blocks in the
-	// matching_blocks list now.
-	nonAdjacent := []Match{}
-	i1, j1, k1 := 0, 0, 0
-	for _, b := range matched {
-		// Is this block adjacent to i1, j1, k1?
-		i2, j2, k2 := b.A, b.B, b.Size
-		if i1+k1 == i2 && j1+k1 == j2 {
-			// Yes, so collapse them -- this just increases the length of
-			// the first block by the length of the second, and the first
-			// block so lengthened remains the block to compare against.
-			k1 += k2
-		} else {
-			// Not adjacent.  Remember the first block (k1==0 means it's
-			// the dummy we started with), and make the second block the
-			// new block to compare against.
-			if k1 > 0 {
-				nonAdjacent = append(nonAdjacent, Match{i1, j1, k1})
-			}
-			i1, j1, k1 = i2, j2, k2
-		}
-	}
-	if k1 > 0 {
-		nonAdjacent = append(nonAdjacent, Match{i1, j1, k1})
-	}
-
-	nonAdjacent = append(nonAdjacent, Match{len(m.a), len(m.b), 0})
-	m.matchingBlocks = nonAdjacent
-	return m.matchingBlocks
-}
-
-// Return list of 5-tuples describing how to turn a into b.
-//
-// Each tuple is of the form (tag, i1, i2, j1, j2).  The first tuple
-// has i1 == j1 == 0, and remaining tuples have i1 == the i2 from the
-// tuple preceding it, and likewise for j1 == the previous j2.
-//
-// The tags are characters, with these meanings:
-//
-// 'r' (replace):  a[i1:i2] should be replaced by b[j1:j2]
-//
-// 'd' (delete):   a[i1:i2] should be deleted, j1==j2 in this case.
-//
-// 'i' (insert):   b[j1:j2] should be inserted at a[i1:i1], i1==i2 in this case.
-//
-// 'e' (equal):    a[i1:i2] == b[j1:j2]
-func (m *SequenceMatcher) GetOpCodes() []OpCode {
-	if m.opCodes != nil {
-		return m.opCodes
-	}
-	i, j := 0, 0
-	matching := m.GetMatchingBlocks()
-	opCodes := make([]OpCode, 0, len(matching))
-	for _, m := range matching {
-		//  invariant:  we've pumped out correct diffs to change
-		//  a[:i] into b[:j], and the next matching block is
-		//  a[ai:ai+size] == b[bj:bj+size]. So we need to pump
-		//  out a diff to change a[i:ai] into b[j:bj], pump out
-		//  the matching block, and move (i,j) beyond the match
-		ai, bj, size := m.A, m.B, m.Size
-		tag := byte(0)
-		if i < ai && j < bj {
-			tag = 'r'
-		} else if i < ai {
-			tag = 'd'
-		} else if j < bj {
-			tag = 'i'
-		}
-		if tag > 0 {
-			opCodes = append(opCodes, OpCode{tag, i, ai, j, bj})
-		}
-		i, j = ai+size, bj+size
-		// the list of matching blocks is terminated by a
-		// sentinel with size 0
-		if size > 0 {
-			opCodes = append(opCodes, OpCode{'e', ai, i, bj, j})
-		}
-	}
-	m.opCodes = opCodes
-	return m.opCodes
-}
-
-// Isolate change clusters by eliminating ranges with no changes.
-//
-// Return a generator of groups with up to n lines of context.
-// Each group is in the same format as returned by GetOpCodes().
-func (m *SequenceMatcher) GetGroupedOpCodes(n int) [][]OpCode {
-	if n < 0 {
-		n = 3
-	}
-	codes := m.GetOpCodes()
-	if len(codes) == 0 {
-		codes = []OpCode{OpCode{'e', 0, 1, 0, 1}}
-	}
-	// Fixup leading and trailing groups if they show no changes.
-	if codes[0].Tag == 'e' {
-		c := codes[0]
-		i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2
-		codes[0] = OpCode{c.Tag, max(i1, i2-n), i2, max(j1, j2-n), j2}
-	}
-	if codes[len(codes)-1].Tag == 'e' {
-		c := codes[len(codes)-1]
-		i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2
-		codes[len(codes)-1] = OpCode{c.Tag, i1, min(i2, i1+n), j1, min(j2, j1+n)}
-	}
-	nn := n + n
-	groups := [][]OpCode{}
-	group := []OpCode{}
-	for _, c := range codes {
-		i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2
-		// End the current group and start a new one whenever
-		// there is a large range with no changes.
-		if c.Tag == 'e' && i2-i1 > nn {
-			group = append(group, OpCode{c.Tag, i1, min(i2, i1+n),
-				j1, min(j2, j1+n)})
-			groups = append(groups, group)
-			group = []OpCode{}
-			i1, j1 = max(i1, i2-n), max(j1, j2-n)
-		}
-		group = append(group, OpCode{c.Tag, i1, i2, j1, j2})
-	}
-	if len(group) > 0 && !(len(group) == 1 && group[0].Tag == 'e') {
-		groups = append(groups, group)
-	}
-	return groups
-}

vendor/gotest.tools/internal/format/diff.go πŸ”—

@@ -1,161 +0,0 @@
-package format
-
-import (
-	"bytes"
-	"fmt"
-	"strings"
-	"unicode"
-
-	"gotest.tools/internal/difflib"
-)
-
-const (
-	contextLines = 2
-)
-
-// DiffConfig for a unified diff
-type DiffConfig struct {
-	A    string
-	B    string
-	From string
-	To   string
-}
-
-// UnifiedDiff is a modified version of difflib.WriteUnifiedDiff with better
-// support for showing the whitespace differences.
-func UnifiedDiff(conf DiffConfig) string {
-	a := strings.SplitAfter(conf.A, "\n")
-	b := strings.SplitAfter(conf.B, "\n")
-	groups := difflib.NewMatcher(a, b).GetGroupedOpCodes(contextLines)
-	if len(groups) == 0 {
-		return ""
-	}
-
-	buf := new(bytes.Buffer)
-	writeFormat := func(format string, args ...interface{}) {
-		buf.WriteString(fmt.Sprintf(format, args...))
-	}
-	writeLine := func(prefix string, s string) {
-		buf.WriteString(prefix + s)
-	}
-	if hasWhitespaceDiffLines(groups, a, b) {
-		writeLine = visibleWhitespaceLine(writeLine)
-	}
-	formatHeader(writeFormat, conf)
-	for _, group := range groups {
-		formatRangeLine(writeFormat, group)
-		for _, opCode := range group {
-			in, out := a[opCode.I1:opCode.I2], b[opCode.J1:opCode.J2]
-			switch opCode.Tag {
-			case 'e':
-				formatLines(writeLine, " ", in)
-			case 'r':
-				formatLines(writeLine, "-", in)
-				formatLines(writeLine, "+", out)
-			case 'd':
-				formatLines(writeLine, "-", in)
-			case 'i':
-				formatLines(writeLine, "+", out)
-			}
-		}
-	}
-	return buf.String()
-}
-
-// hasWhitespaceDiffLines returns true if any diff groups is only different
-// because of whitespace characters.
-func hasWhitespaceDiffLines(groups [][]difflib.OpCode, a, b []string) bool {
-	for _, group := range groups {
-		in, out := new(bytes.Buffer), new(bytes.Buffer)
-		for _, opCode := range group {
-			if opCode.Tag == 'e' {
-				continue
-			}
-			for _, line := range a[opCode.I1:opCode.I2] {
-				in.WriteString(line)
-			}
-			for _, line := range b[opCode.J1:opCode.J2] {
-				out.WriteString(line)
-			}
-		}
-		if removeWhitespace(in.String()) == removeWhitespace(out.String()) {
-			return true
-		}
-	}
-	return false
-}
-
-func removeWhitespace(s string) string {
-	var result []rune
-	for _, r := range s {
-		if !unicode.IsSpace(r) {
-			result = append(result, r)
-		}
-	}
-	return string(result)
-}
-
-func visibleWhitespaceLine(ws func(string, string)) func(string, string) {
-	mapToVisibleSpace := func(r rune) rune {
-		switch r {
-		case '\n':
-		case ' ':
-			return 'Β·'
-		case '\t':
-			return 'β–·'
-		case '\v':
-			return 'β–½'
-		case '\r':
-			return '↡'
-		case '\f':
-			return '↓'
-		default:
-			if unicode.IsSpace(r) {
-				return 'οΏ½'
-			}
-		}
-		return r
-	}
-	return func(prefix, s string) {
-		ws(prefix, strings.Map(mapToVisibleSpace, s))
-	}
-}
-
-func formatHeader(wf func(string, ...interface{}), conf DiffConfig) {
-	if conf.From != "" || conf.To != "" {
-		wf("--- %s\n", conf.From)
-		wf("+++ %s\n", conf.To)
-	}
-}
-
-func formatRangeLine(wf func(string, ...interface{}), group []difflib.OpCode) {
-	first, last := group[0], group[len(group)-1]
-	range1 := formatRangeUnified(first.I1, last.I2)
-	range2 := formatRangeUnified(first.J1, last.J2)
-	wf("@@ -%s +%s @@\n", range1, range2)
-}
-
-// Convert range to the "ed" format
-func formatRangeUnified(start, stop int) string {
-	// Per the diff spec at http://www.unix.org/single_unix_specification/
-	beginning := start + 1 // lines start numbering with one
-	length := stop - start
-	if length == 1 {
-		return fmt.Sprintf("%d", beginning)
-	}
-	if length == 0 {
-		beginning-- // empty ranges begin at line just before the range
-	}
-	return fmt.Sprintf("%d,%d", beginning, length)
-}
-
-func formatLines(writeLine func(string, string), prefix string, lines []string) {
-	for _, line := range lines {
-		writeLine(prefix, line)
-	}
-	// Add a newline if the last line is missing one so that the diff displays
-	// properly.
-	if !strings.HasSuffix(lines[len(lines)-1], "\n") {
-		writeLine("", "\n")
-	}
-}

vendor/gotest.tools/internal/format/format.go πŸ”—

@@ -1,27 +0,0 @@
-package format // import "gotest.tools/internal/format"
-
-import "fmt"
-
-// Message accepts a msgAndArgs varargs and formats it using fmt.Sprintf
-func Message(msgAndArgs ...interface{}) string {
-	switch len(msgAndArgs) {
-	case 0:
-		return ""
-	case 1:
-		return fmt.Sprintf("%v", msgAndArgs[0])
-	default:
-		return fmt.Sprintf(msgAndArgs[0].(string), msgAndArgs[1:]...)
-	}
-}
-
-// WithCustomMessage accepts one or two messages and formats them appropriately
-func WithCustomMessage(source string, msgAndArgs ...interface{}) string {
-	custom := Message(msgAndArgs...)
-	switch {
-	case custom == "":
-		return source
-	case source == "":
-		return custom
-	}
-	return fmt.Sprintf("%s: %s", source, custom)
-}

vendor/gotest.tools/internal/source/source.go πŸ”—

@@ -1,163 +0,0 @@
-package source // import "gotest.tools/internal/source"
-
-import (
-	"bytes"
-	"fmt"
-	"go/ast"
-	"go/format"
-	"go/parser"
-	"go/token"
-	"os"
-	"runtime"
-	"strconv"
-	"strings"
-
-	"github.com/pkg/errors"
-)
-
-const baseStackIndex = 1
-
-// FormattedCallExprArg returns the argument from an ast.CallExpr at the
-// index in the call stack. The argument is formatted using FormatNode.
-func FormattedCallExprArg(stackIndex int, argPos int) (string, error) {
-	args, err := CallExprArgs(stackIndex + 1)
-	if err != nil {
-		return "", err
-	}
-	return FormatNode(args[argPos])
-}
-
-func getNodeAtLine(filename string, lineNum int) (ast.Node, error) {
-	fileset := token.NewFileSet()
-	astFile, err := parser.ParseFile(fileset, filename, nil, parser.AllErrors)
-	if err != nil {
-		return nil, errors.Wrapf(err, "failed to parse source file: %s", filename)
-	}
-
-	node := scanToLine(fileset, astFile, lineNum)
-	if node == nil {
-		return nil, errors.Errorf(
-			"failed to find an expression on line %d in %s", lineNum, filename)
-	}
-	return node, nil
-}
-
-func scanToLine(fileset *token.FileSet, node ast.Node, lineNum int) ast.Node {
-	v := &scanToLineVisitor{lineNum: lineNum, fileset: fileset}
-	ast.Walk(v, node)
-	return v.matchedNode
-}
-
-type scanToLineVisitor struct {
-	lineNum     int
-	matchedNode ast.Node
-	fileset     *token.FileSet
-}
-
-func (v *scanToLineVisitor) Visit(node ast.Node) ast.Visitor {
-	if node == nil || v.matchedNode != nil {
-		return nil
-	}
-	if v.nodePosition(node).Line == v.lineNum {
-		v.matchedNode = node
-		return nil
-	}
-	return v
-}
-
-// In golang 1.9 the line number changed from being the line where the statement
-// ended to the line where the statement began.
-func (v *scanToLineVisitor) nodePosition(node ast.Node) token.Position {
-	if goVersionBefore19 {
-		return v.fileset.Position(node.End())
-	}
-	return v.fileset.Position(node.Pos())
-}
-
-var goVersionBefore19 = isGOVersionBefore19()
-
-func isGOVersionBefore19() bool {
-	version := runtime.Version()
-	// not a release version
-	if !strings.HasPrefix(version, "go") {
-		return false
-	}
-	version = strings.TrimPrefix(version, "go")
-	parts := strings.Split(version, ".")
-	if len(parts) < 2 {
-		return false
-	}
-	minor, err := strconv.ParseInt(parts[1], 10, 32)
-	return err == nil && parts[0] == "1" && minor < 9
-}
-
-func getCallExprArgs(node ast.Node) ([]ast.Expr, error) {
-	visitor := &callExprVisitor{}
-	ast.Walk(visitor, node)
-	if visitor.expr == nil {
-		return nil, errors.New("failed to find call expression")
-	}
-	return visitor.expr.Args, nil
-}
-
-type callExprVisitor struct {
-	expr *ast.CallExpr
-}
-
-func (v *callExprVisitor) Visit(node ast.Node) ast.Visitor {
-	if v.expr != nil || node == nil {
-		return nil
-	}
-	debug("visit (%T): %s", node, debugFormatNode{node})
-
-	if callExpr, ok := node.(*ast.CallExpr); ok {
-		v.expr = callExpr
-		return nil
-	}
-	return v
-}
-
-// FormatNode using go/format.Node and return the result as a string
-func FormatNode(node ast.Node) (string, error) {
-	buf := new(bytes.Buffer)
-	err := format.Node(buf, token.NewFileSet(), node)
-	return buf.String(), err
-}
-
-// CallExprArgs returns the ast.Expr slice for the args of an ast.CallExpr at
-// the index in the call stack.
-func CallExprArgs(stackIndex int) ([]ast.Expr, error) {
-	_, filename, lineNum, ok := runtime.Caller(baseStackIndex + stackIndex)
-	if !ok {
-		return nil, errors.New("failed to get call stack")
-	}
-	debug("call stack position: %s:%d", filename, lineNum)
-
-	node, err := getNodeAtLine(filename, lineNum)
-	if err != nil {
-		return nil, err
-	}
-	debug("found node (%T): %s", node, debugFormatNode{node})
-
-	return getCallExprArgs(node)
-}
-
-var debugEnabled = os.Getenv("GOTESTYOURSELF_DEBUG") != ""
-
-func debug(format string, args ...interface{}) {
-	if debugEnabled {
-		fmt.Fprintf(os.Stderr, "DEBUG: "+format+"\n", args...)
-	}
-}
-
-type debugFormatNode struct {
-	ast.Node
-}
-
-func (n debugFormatNode) String() string {
-	out, err := FormatNode(n.Node)
-	if err != nil {
-		return fmt.Sprintf("failed to format %s: %s", n.Node, err)
-	}
-	return out
-}