From bab9b69f7327adbcfd8fcdd463bf8301bd0aaade Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alexandru=20B=C4=83lu=C8=9B?= <ab@daedalean.ai>
Date: Fri, 1 May 2020 15:46:20 +0200
Subject: [PATCH] Verify signed identities

---
 cache/repo_cache.go        |  50 ++++-
 commands/user_create.go    |  25 ++-
 commands/user_key_add.go   |  13 +-
 commands/user_key_rm.go    |   6 +-
 commands/validate.go       |  61 ++++++
 go.mod                     |   5 +-
 go.sum                     |  90 +++++----
 identity/identity.go       |  18 +-
 identity/identity_test.go  |  32 +---
 identity/key.go            |  10 +-
 identity/key_test.go       |  21 +++
 identity/version.go        |  12 ++
 repository/git_test.go     |   2 +-
 repository/git_testing.go  |  68 +++++--
 validate/validator.go      | 378 +++++++++++++++++++++++++++++++++++++
 validate/validator_test.go | 167 ++++++++++++++++
 16 files changed, 863 insertions(+), 95 deletions(-)
 create mode 100644 commands/validate.go
 create mode 100644 identity/key_test.go
 create mode 100644 validate/validator.go
 create mode 100644 validate/validator_test.go

diff --git a/cache/repo_cache.go b/cache/repo_cache.go
index f2e1c7d0..58072463 100644
--- a/cache/repo_cache.go
+++ b/cache/repo_cache.go
@@ -13,6 +13,8 @@ import (
 	"sync"
 	"time"
 
+	"github.com/go-git/go-git/v5/plumbing"
+	"github.com/go-git/go-git/v5/plumbing/object"
 	"github.com/pkg/errors"
 
 	"github.com/MichaelMure/git-bug/bug"
@@ -22,6 +24,7 @@ import (
 	"github.com/MichaelMure/git-bug/repository"
 	"github.com/MichaelMure/git-bug/util/git"
 	"github.com/MichaelMure/git-bug/util/process"
+	git2 "github.com/go-git/go-git/v5"
 )
 
 const bugCacheFile = "bug-cache"
@@ -58,6 +61,8 @@ var _ repository.RepoCommon = &RepoCache{}
 type RepoCache struct {
 	// the underlying repo
 	repo repository.ClockedRepo
+	// the underlying repo powered by go-git, for reading commits
+	repo2 *git2.Repository
 
 	// the name of the repository, as defined in the MultiRepoCache
 	name string
@@ -76,6 +81,10 @@ type RepoCache struct {
 
 	// the user identity's id, if known
 	userIdentityId entity.Id
+
+	// the cache of commits
+	muCommit sync.RWMutex
+	commits  map[git.Hash]*object.Commit
 }
 
 func NewRepoCache(r repository.ClockedRepo) (*RepoCache, error) {
@@ -83,14 +92,21 @@ func NewRepoCache(r repository.ClockedRepo) (*RepoCache, error) {
 }
 
 func NewNamedRepoCache(r repository.ClockedRepo, name string) (*RepoCache, error) {
+	r2, err := git2.PlainOpen(r.GetPath())
+	if err != nil {
+		return &RepoCache{}, err
+	}
+
 	c := &RepoCache{
 		repo:       r,
+		repo2:      r2,
 		name:       name,
 		bugs:       make(map[entity.Id]*BugCache),
 		identities: make(map[entity.Id]*IdentityCache),
+		commits:    make(map[git.Hash]*object.Commit),
 	}
 
-	err := c.lock()
+	err = c.lock()
 	if err != nil {
 		return &RepoCache{}, err
 	}
@@ -177,6 +193,8 @@ func (c *RepoCache) Close() error {
 	defer c.muBug.Unlock()
 	c.muIdentity.Lock()
 	defer c.muIdentity.Unlock()
+	c.muCommit.Lock()
+	defer c.muCommit.Unlock()
 
 	c.identities = make(map[entity.Id]*IdentityCache)
 	c.identitiesExcerpts = nil
@@ -1055,7 +1073,11 @@ func (c *RepoCache) NewIdentityFull(name string, email string, login string, ava
 }
 
 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)
+	return c.NewIdentityWithKeyRaw(name, email, login, avatarUrl, metadata, nil)
+}
+
+func (c *RepoCache) NewIdentityWithKeyRaw(name string, email string, login string, avatarUrl string, metadata map[string]string, key *identity.Key) (*IdentityCache, error) {
+	i := identity.NewIdentityFull(name, email, login, avatarUrl, key)
 	return c.finishIdentity(i, metadata)
 }
 
@@ -1086,3 +1108,27 @@ func (c *RepoCache) finishIdentity(i *identity.Identity, metadata map[string]str
 
 	return cached, nil
 }
+
+func (c *RepoCache) ResolveCommit(hash git.Hash) (*object.Commit, error) {
+	c.muCommit.Lock()
+	defer c.muCommit.Unlock()
+
+	commit, ok := c.commits[hash]
+	if !ok {
+		var err error
+		commit, err = c.repo2.CommitObject(plumbing.NewHash(string(hash)))
+		if err != nil {
+			return nil, err
+		}
+		c.commits[hash] = commit
+	}
+	return commit, nil
+}
+
+func (c *RepoCache) ResolveRef(ref string) (git.Hash, error) {
+	h, err := c.repo2.ResolveRevision(plumbing.Revision(ref))
+	if err != nil {
+		return "", err
+	}
+	return git.Hash(h.String()), nil
+}
diff --git a/commands/user_create.go b/commands/user_create.go
index df4aa8e9..898b7e4c 100644
--- a/commands/user_create.go
+++ b/commands/user_create.go
@@ -5,11 +5,14 @@ import (
 	"os"
 
 	"github.com/MichaelMure/git-bug/cache"
+	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/input"
 	"github.com/MichaelMure/git-bug/util/interrupt"
 	"github.com/spf13/cobra"
 )
 
+var userCreateArmoredKeyFile string
+
 func runUserCreate(cmd *cobra.Command, args []string) error {
 	backend, err := cache.NewRepoCache(repo)
 	if err != nil {
@@ -43,7 +46,22 @@ func runUserCreate(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	id, err := backend.NewIdentityRaw(name, email, "", avatarURL, nil)
+	var key *identity.Key
+	if userCreateArmoredKeyFile != "" {
+		armoredPubkey, err := input.TextFileInput(userCreateArmoredKeyFile)
+		if err != nil {
+			return err
+		}
+
+		key, err = identity.NewKey(armoredPubkey)
+		if err != nil {
+			return err
+		}
+
+		fmt.Printf("Using key from file `%s`:\n%s\n", userCreateArmoredKeyFile, armoredPubkey)
+	}
+
+	id, err := backend.NewIdentityWithKeyRaw(name, email, "", avatarURL, nil, key)
 	if err != nil {
 		return err
 	}
@@ -81,4 +99,9 @@ var userCreateCmd = &cobra.Command{
 func init() {
 	userCmd.AddCommand(userCreateCmd)
 	userCreateCmd.Flags().SortFlags = false
+
+	userCreateCmd.Flags().StringVar(&userCreateArmoredKeyFile, "key-file","",
+		"Take the armored PGP public key from the given file. Use - to read the message from the standard input",
+	)
+
 }
diff --git a/commands/user_key_add.go b/commands/user_key_add.go
index 0102322d..3699ca33 100644
--- a/commands/user_key_add.go
+++ b/commands/user_key_add.go
@@ -7,6 +7,8 @@ import (
 	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/input"
 	"github.com/MichaelMure/git-bug/util/interrupt"
+	"github.com/MichaelMure/git-bug/validate"
+	"github.com/pkg/errors"
 	"github.com/spf13/cobra"
 )
 
@@ -51,16 +53,23 @@ func runKeyAdd(cmd *cobra.Command, args []string) error {
 	}
 
 	key, err := identity.NewKey(keyAddArmored)
-
 	if err != nil {
 		return err
 	}
 
+	validator, err := validate.NewValidator(backend)
+	if err != nil {
+		return errors.Wrap(err, "failed to create validator")
+	}
+	commitHash := validator.KeyCommitHash(key.PublicKey.KeyId)
+	if commitHash != "" {
+		return fmt.Errorf("key id %d is already used by the key introduced in commit %s", key.PublicKey.KeyId, commitHash)
+	}
+
 	err = id.Mutate(func(mutator identity.Mutator) identity.Mutator {
 		mutator.Keys = append(mutator.Keys, key)
 		return mutator
 	})
-
 	if err != nil {
 		return err
 	}
diff --git a/commands/user_key_rm.go b/commands/user_key_rm.go
index c6e62f73..f35d0a89 100644
--- a/commands/user_key_rm.go
+++ b/commands/user_key_rm.go
@@ -1,12 +1,12 @@
 package commands
 
 import (
-	"errors"
 	"fmt"
 
 	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/util/interrupt"
+	"github.com/pkg/errors"
 	"github.com/spf13/cobra"
 )
 
@@ -19,7 +19,7 @@ func runKeyRm(cmd *cobra.Command, args []string) error {
 	interrupt.RegisterCleaner(backend.Close)
 
 	if len(args) == 0 {
-		return errors.New("missing key ID")
+		return errors.New("missing key fingerprint")
 	}
 
 	keyFingerprint := args[0]
@@ -36,7 +36,7 @@ func runKeyRm(cmd *cobra.Command, args []string) error {
 
 	fingerprint, err := identity.DecodeKeyFingerprint(keyFingerprint)
 	if err != nil {
-		return err
+		return errors.Wrap(err, "invalid key fingerprint")
 	}
 
 	var removedKey *identity.Key
diff --git a/commands/validate.go b/commands/validate.go
new file mode 100644
index 00000000..6e4a43c5
--- /dev/null
+++ b/commands/validate.go
@@ -0,0 +1,61 @@
+package commands
+
+import (
+	"fmt"
+
+	"github.com/MichaelMure/git-bug/cache"
+	"github.com/MichaelMure/git-bug/identity"
+	"github.com/MichaelMure/git-bug/util/interrupt"
+	"github.com/MichaelMure/git-bug/validate"
+	"github.com/pkg/errors"
+	"github.com/spf13/cobra"
+)
+
+func runValidate(cmd *cobra.Command, args []string) error {
+	backend, err := cache.NewRepoCache(repo)
+	if err != nil {
+		return err
+	}
+	defer backend.Close()
+	interrupt.RegisterCleaner(backend.Close)
+
+	validator, err := validate.NewValidator(backend)
+	if err != nil {
+		return err
+	}
+
+	fmt.Printf("first commit signed with key: %s\n", identity.EncodeKeyFingerprint(validator.FirstKey.PublicKey.Fingerprint))
+
+	var refErr error
+	for _, ref := range args {
+		hash, err := backend.ResolveRef(ref)
+		if err != nil {
+			return err
+		}
+
+		_, err = validator.ValidateRef(hash)
+		if err != nil {
+			refErr = errors.Wrapf(refErr, "ref %s check fail", ref)
+			fmt.Printf("ref %s\tFAIL\n", ref)
+		} else {
+			fmt.Printf("ref %s\tOK\n", ref)
+		}
+	}
+	if refErr != nil {
+		return refErr
+	}
+
+	return nil
+}
+
+var validateCmd = &cobra.Command{
+	Use:     "validate",
+	Short:   "Validate identities and commits signatures.",
+	PreRunE: loadRepo,
+	RunE:    runValidate,
+}
+
+func init() {
+	RootCmd.AddCommand(validateCmd)
+	validateCmd.Flags().SortFlags = false
+}
diff --git a/go.mod b/go.mod
index 9535bc72..13957a95 100644
--- a/go.mod
+++ b/go.mod
@@ -13,11 +13,12 @@ require (
 	github.com/dustin/go-humanize v1.0.0
 	github.com/fatih/color v1.9.0
 	github.com/go-errors/errors v1.0.1
+	github.com/go-git/go-git/v5 v5.0.0
 	github.com/gorilla/mux v1.7.4
 	github.com/hashicorp/golang-lru v0.5.4 // indirect
 	github.com/icrowley/fake v0.0.0-20180203215853-4178557ae428
 	github.com/mattn/go-isatty v0.0.12
-	github.com/mattn/go-runewidth v0.0.9
+	github.com/mattn/go-runewidth v0.0.9 // indirect
 	github.com/phayes/freeport v0.0.0-20171002181615-b8543db493a5
 	github.com/pkg/errors v0.9.1
 	github.com/shurcooL/githubv4 v0.0.0-20190601194912-068505affed7
@@ -27,7 +28,7 @@ require (
 	github.com/stretchr/testify v1.5.1
 	github.com/vektah/gqlparser v1.3.1
 	github.com/xanzy/go-gitlab v0.29.0
-	golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550
+	golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073
 	golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288
 	golang.org/x/sync v0.0.0-20190423024810-112230192c58
 	golang.org/x/text v0.3.2
diff --git a/go.sum b/go.sum
index 220bb1ce..8db6e030 100644
--- a/go.sum
+++ b/go.sum
@@ -1,28 +1,28 @@
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-github.com/99designs/gqlgen v0.10.3-0.20200208093655-ab8d62b67dd0 h1:ADy3XJwhOYg6Pb90XeXazWvO+9gpOsgLuaM1buZUZOY=
-github.com/99designs/gqlgen v0.10.3-0.20200208093655-ab8d62b67dd0/go.mod h1:dfBhwZKMcSYiYRMTs8qWF+Oha6782e1xPfgRmVal9I8=
 github.com/99designs/gqlgen v0.10.3-0.20200209012558-b7a58a1c0e4b h1:510xa84qGbDemwTHNio4cLWkdKFxxJgVtsIOH+Ku8bo=
 github.com/99designs/gqlgen v0.10.3-0.20200209012558-b7a58a1c0e4b/go.mod h1:dfBhwZKMcSYiYRMTs8qWF+Oha6782e1xPfgRmVal9I8=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/Masterminds/glide v0.13.2/go.mod h1:STyF5vcenH/rUqTEv+/hBXlSTo7KYwg2oc2f4tzPWic=
 github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
 github.com/Masterminds/vcs v1.13.0/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA=
-github.com/MichaelMure/go-term-text v0.2.6 h1:dSmJSzk2iI5xWymSMrMbdVM1bxYWu3DjDFhdcJvAuqA=
-github.com/MichaelMure/go-term-text v0.2.6/go.mod h1:o2Z5T3b28F4kwAojGvvNdbzjHf9t18vbQ7E2pmTe2Ww=
-github.com/MichaelMure/go-term-text v0.2.7 h1:nSYvYGwXxJoiQu6kdGSErpxZ6ah/4WlJyp/niqQor6g=
-github.com/MichaelMure/go-term-text v0.2.7/go.mod h1:6z+q5b/nP1V8I9KkWQcUi5QpmF8DVrz9vLJ4hdoxHnM=
 github.com/MichaelMure/go-term-text v0.2.8 h1:daXIVPjPkAhcLhA+tfjQBHYjatb1D42/LY1Nw2PXYlU=
 github.com/MichaelMure/go-term-text v0.2.8/go.mod h1:6z+q5b/nP1V8I9KkWQcUi5QpmF8DVrz9vLJ4hdoxHnM=
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
 github.com/agnivade/levenshtein v1.0.1 h1:3oJU7J3FGFmyhn8KHjmVaZCN5hxTr7GxgRue+sxIXdQ=
 github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
+github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
+github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
 github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
+github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
+github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
 github.com/araddon/dateparse v0.0.0-20190622164848-0fb0a474d195 h1:c4mLfegoDw6OhSJXTd2jUEQgZUQuJWtocudb97Qn9EM=
 github.com/araddon/dateparse v0.0.0-20190622164848-0fb0a474d195/go.mod h1:SLqhdZcd+dF3TEVL2RMoob5bBP5R1P1qkox+HtCBgGI=
 github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
 github.com/awesome-gocui/gocui v0.6.1-0.20191115151952-a34ffb055986 h1:QvIfX96O11qjX1Zr3hKkG0dI12JBRBGABWffyZ1GI60=
 github.com/awesome-gocui/gocui v0.6.1-0.20191115151952-a34ffb055986/go.mod h1:1QikxFaPhe2frKeKvEwZEIGia3haiOxOUXKinrv17mA=
 github.com/awesome-gocui/termbox-go v0.0.0-20190427202837-c0aef3d18bcc h1:wGNpKcHU8Aadr9yOzsT3GEsFLS7HQu8HxQIomnekqf0=
@@ -38,16 +38,14 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
 github.com/codegangsta/cli v1.20.0/go.mod h1:/qJNoX69yVSKu5o4jLyXAENLRyk1uhi7zkbQ3slBdOA=
 github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
 github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
-github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
 github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
 github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
 github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
 github.com/corpix/uarand v0.1.1 h1:RMr1TWc9F4n5jiPDzFHtmaUXLKLNUFK0SgCLo4BhX/U=
 github.com/corpix/uarand v0.1.1/go.mod h1:SFKZvkcRoLqVRFZ4u25xPmp6m9ktANfbpXZ7SJ0/FNU=
-github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
-github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
 github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
 github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -55,13 +53,27 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm
 github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
 github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
+github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
 github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
 github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
+github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
+github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
+github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
 github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
 github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
 github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
+github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
+github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
+github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM=
+github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
+github.com/go-git/go-git-fixtures/v4 v4.0.1 h1:q+IFMfLx200Q3scvt2hN79JsEzy4AmBTp/pqnefH+Bc=
+github.com/go-git/go-git-fixtures/v4 v4.0.1/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw=
+github.com/go-git/go-git/v5 v5.0.0 h1:k5RWPm4iJwYtfWoxIJy4wJX9ON7ihPeZZYC1fLYDnpg=
+github.com/go-git/go-git/v5 v5.0.0/go.mod h1:oYD8y9kWsGINPFJoLdaScGCN6dlKg23blmClfZwtUVA=
 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
@@ -78,12 +90,12 @@ github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
 github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
 github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
 github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
-github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
-github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
 github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
 github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
 github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ=
@@ -95,6 +107,7 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf
 github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
 github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
 github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
+github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
 github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
 github.com/hashicorp/go-retryablehttp v0.6.4 h1:BbgctKO892xEyOXnGiaAwIoSq1QZ/SS4AhjoAh9DnfY=
 github.com/hashicorp/go-retryablehttp v0.6.4/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
@@ -107,8 +120,13 @@ github.com/icrowley/fake v0.0.0-20180203215853-4178557ae428 h1:Mo9W14pwbO9VfRe+y
 github.com/icrowley/fake v0.0.0-20180203215853-4178557ae428/go.mod h1:uhpZMVGznybq1itEKXj6RYw9I71qK4kH+OGMjRC4KEo=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
+github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
 github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY=
+github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
 github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -118,6 +136,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
 github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
@@ -129,19 +149,20 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA
 github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
 github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
 github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
-github.com/mattn/go-runewidth v0.0.6 h1:V2iyH+aX9C5fsYCpK60U8BYIvmhqxuOL3JZcqc1NB7k=
-github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0=
 github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
 github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
 github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/ngdinhtoan/glide-cleanup v0.2.0/go.mod h1:UQzsmiDOb8YV3nOsCxK/c9zPpCZVNoHScRE3EO9pVMM=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
 github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
 github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
 github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
@@ -166,12 +187,12 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z
 github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
-github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
-github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
 github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
 github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
+github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
+github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
 github.com/shurcooL/githubv4 v0.0.0-20190601194912-068505affed7 h1:Vk3RiBQpF0Ja+OqbFG7lYTk79+l8Cm2QESLXB0x6u6U=
 github.com/shurcooL/githubv4 v0.0.0-20190601194912-068505affed7/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo=
 github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f h1:tygelZueB1EtXkPI6mQ4o9DQ0+FKW41hTbunoXZCTqk=
@@ -189,16 +210,11 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k
 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
 github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
 github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
-github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
-github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
-github.com/spf13/cobra v0.0.6 h1:breEStsVwemnKh2/s6gMvSdMEkwW0sK8vGStnlVBMCs=
-github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
 github.com/spf13/cobra v0.0.7 h1:FfTH+vuMXOas8jmfb5/M7dzEYx7LpcLb7a0LPe34uOU=
 github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
 github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
 github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
 github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
-github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
 github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
 github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -210,31 +226,18 @@ github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJy
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-github.com/theckman/goconstraint v1.11.0 h1:oBUwN5wpE4dwyPhRGraEgJsFTr+JtLWiDnaJZJeeXI0=
-github.com/theckman/goconstraint v1.11.0/go.mod h1:zkCR/f2kOULTk/h1ujgyB9BlCNLaqlQ6GN2Zl4mg81g=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
-github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
 github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
 github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
-github.com/vektah/gqlparser v1.2.1 h1:C+L7Go/eUbN0w6Y0kaiq2W6p2wN5j8wU82EdDXxDivc=
-github.com/vektah/gqlparser v1.2.1/go.mod h1:bkVf0FX+Stjg/MHnm8mEyubuaArhNEqfQhF+OTiAL74=
 github.com/vektah/gqlparser v1.3.1 h1:8b0IcD3qZKWJQHSzynbDlrtP3IxVydZ2DZepCGofqfU=
 github.com/vektah/gqlparser v1.3.1 h1:8b0IcD3qZKWJQHSzynbDlrtP3IxVydZ2DZepCGofqfU=
 github.com/vektah/gqlparser v1.3.1/go.mod h1:bkVf0FX+Stjg/MHnm8mEyubuaArhNEqfQhF+OTiAL74=
 github.com/vektah/gqlparser v1.3.1/go.mod h1:bkVf0FX+Stjg/MHnm8mEyubuaArhNEqfQhF+OTiAL74=
-github.com/xanzy/go-gitlab v0.22.1 h1:TVxgHmoa35jQL+9FCkG0nwPDxU9dQZXknBTDtGaSFno=
-github.com/xanzy/go-gitlab v0.22.1/go.mod h1:t4Bmvnxj7k37S4Y17lfLx+nLqkf/oQwT2HagfWKv5Og=
-github.com/xanzy/go-gitlab v0.24.0 h1:zP1zC4K76Gha0coN5GhygOLhsHTCvUjrnqGL3kHXkVU=
-github.com/xanzy/go-gitlab v0.24.0/go.mod h1:t4Bmvnxj7k37S4Y17lfLx+nLqkf/oQwT2HagfWKv5Og=
-github.com/xanzy/go-gitlab v0.25.0 h1:G5aTZeqZd66Q6qMVieBfmHBsPpF0jY92zCLAMpULe3I=
-github.com/xanzy/go-gitlab v0.25.0/go.mod h1:t4Bmvnxj7k37S4Y17lfLx+nLqkf/oQwT2HagfWKv5Og=
-github.com/xanzy/go-gitlab v0.26.0 h1:eAnJRBUC+GDJSy8OoGCZBqBMpXsGOOT235TFm/F8C0Q=
-github.com/xanzy/go-gitlab v0.26.0/go.mod h1:t4Bmvnxj7k37S4Y17lfLx+nLqkf/oQwT2HagfWKv5Og=
-github.com/xanzy/go-gitlab v0.27.0 h1:zy7xBB8+PID6izH07ZArtkEisJ192dtQajRaeo4+glg=
-github.com/xanzy/go-gitlab v0.27.0/go.mod h1:t4Bmvnxj7k37S4Y17lfLx+nLqkf/oQwT2HagfWKv5Og=
 github.com/xanzy/go-gitlab v0.29.0 h1:9tMvAkG746eIlzcdpnRgpcKPA1woUDmldMIjR/E5OWM=
 github.com/xanzy/go-gitlab v0.29.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug=
+github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
+github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
 go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
@@ -242,11 +245,13 @@ go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
 go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM=
+golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
@@ -261,6 +266,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
 golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
+golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288 h1:JIqe8uIcRBHXDQVvZtHwp80ai3Lw3IJAeJEs55Dc1W0=
 golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -273,9 +280,9 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8=
 golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
@@ -283,6 +290,8 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
 golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So=
+golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
@@ -309,11 +318,18 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
+gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
+gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
 gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 sourcegraph.com/sourcegraph/appdash v0.0.0-20180110180208-2cc67fd64755/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
 sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k=
diff --git a/identity/identity.go b/identity/identity.go
index 6b71fa35..73796769 100644
--- a/identity/identity.go
+++ b/identity/identity.go
@@ -56,7 +56,12 @@ func NewIdentity(name string, email string) *Identity {
 	}
 }
 
-func NewIdentityFull(name string, email string, login string, avatarUrl string) *Identity {
+func NewIdentityFull(name string, email string, login string, avatarUrl string, key *Key) *Identity {
+	var keys []*Key
+	if key != nil {
+		keys = []*Key{key}
+	}
+
 	return &Identity{
 		id: entity.UnsetId,
 		versions: []*Version{
@@ -66,6 +71,7 @@ func NewIdentityFull(name string, email string, login string, avatarUrl string)
 				login:     login,
 				avatarURL: avatarUrl,
 				nonce:     makeNonce(20),
+				keys:      keys,
 			},
 		},
 	}
@@ -332,7 +338,11 @@ func (i *Identity) Commit(repo repository.ClockedRepo) error {
 		}
 
 		// get the times where new versions starts to be valid
-		v.time = repo.EditTime()
+		var err error
+		v.time, err = repo.EditTimeIncrement()
+		if err != nil {
+			return err
+		}
 		v.unixTime = time.Now().Unix()
 
 		blobHash, err := v.Write(repo)
@@ -616,6 +626,10 @@ func (i *Identity) MutableMetadata() map[string]string {
 	return metadata
 }
 
+func (i *Identity) Versions() []*Version {
+	return i.versions
+}
+
 // addVersionForTest add a new version to the identity
 // Only for testing !
 func (i *Identity) addVersionForTest(version *Version) {
diff --git a/identity/identity_test.go b/identity/identity_test.go
index 791c9b41..05b5de2b 100644
--- a/identity/identity_test.go
+++ b/identity/identity_test.go
@@ -2,35 +2,13 @@ package identity
 
 import (
 	"encoding/json"
-	"strings"
 	"testing"
 
 	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/repository"
 	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-	"golang.org/x/crypto/openpgp"
-	"golang.org/x/crypto/openpgp/armor"
 )
 
-// createPubkey returns an armored public PGP key.
-func createPubkey(t *testing.T) string {
-	// Generate a key pair for signing commits.
-	pgpEntity, err := openpgp.NewEntity("First Last", "", "fl@example.org", nil)
-	require.NoError(t, err)
-
-	// Armor the public part.
-	pubBuilder := &strings.Builder{}
-	w, err := armor.Encode(pubBuilder, openpgp.PublicKeyType, nil)
-	require.NoError(t, err)
-	err = pgpEntity.Serialize(w)
-	require.NoError(t, err)
-	err = w.Close()
-	require.NoError(t, err)
-	armoredPub := pubBuilder.String()
-	return armoredPub
-}
-
 // Test the commit and load of an Identity with multiple versions
 func TestIdentityCommitLoad(t *testing.T) {
 	mockRepo := repository.NewMockRepoForTest()
@@ -67,7 +45,7 @@ func TestIdentityCommitLoad(t *testing.T) {
 				name:  "René Descartes",
 				email: "rene.descartes@example.com",
 				keys: []*Key{
-					{ArmoredPublicKey: createPubkey(t)},
+					{ArmoredPublicKey: repository.CreatePubkey(t)},
 				},
 			},
 			{
@@ -75,7 +53,7 @@ func TestIdentityCommitLoad(t *testing.T) {
 				name:  "René Descartes",
 				email: "rene.descartes@example.com",
 				keys: []*Key{
-					{ArmoredPublicKey: createPubkey(t)},
+					{ArmoredPublicKey: repository.CreatePubkey(t)},
 				},
 			},
 			{
@@ -83,7 +61,7 @@ func TestIdentityCommitLoad(t *testing.T) {
 				name:  "René Descartes",
 				email: "rene.descartes@example.com",
 				keys: []*Key{
-					{ArmoredPublicKey: createPubkey(t)},
+					{ArmoredPublicKey: repository.CreatePubkey(t)},
 				},
 			},
 		},
@@ -112,7 +90,7 @@ func TestIdentityCommitLoad(t *testing.T) {
 		name:  "René Descartes",
 		email: "rene.descartes@example.com",
 		keys: []*Key{
-			{ArmoredPublicKey: createPubkey(t)},
+			{ArmoredPublicKey: repository.CreatePubkey(t)},
 		},
 	})
 
@@ -121,7 +99,7 @@ func TestIdentityCommitLoad(t *testing.T) {
 		name:  "René Descartes",
 		email: "rene.descartes@example.com",
 		keys: []*Key{
-			{ArmoredPublicKey: createPubkey(t)},
+			{ArmoredPublicKey: repository.CreatePubkey(t)},
 		},
 	})
 
diff --git a/identity/key.go b/identity/key.go
index 7431086a..5be1e3bb 100644
--- a/identity/key.go
+++ b/identity/key.go
@@ -14,7 +14,7 @@ type Key struct {
 	// PubKey is the armored PGP public key.
 	ArmoredPublicKey string `json:"pub_key"`
 
-	publicKey *packet.PublicKey `json:"-"`
+	PublicKey *packet.PublicKey `json:"-"`
 }
 
 func NewKey(armoredPGPKey string) (*Key, error) {
@@ -61,7 +61,7 @@ func DecodeKeyFingerprint(keyFingerprint string) ([20]byte, error) {
 }
 
 func EncodeKeyFingerprint(fingerprint [20]byte) string {
-	return hex.EncodeToString(fingerprint[:])
+	return strings.ToUpper(hex.EncodeToString(fingerprint[:]))
 }
 
 func (k *Key) Validate() error {
@@ -76,8 +76,8 @@ func (k *Key) Clone() *Key {
 
 func (k *Key) GetPublicKey() (*packet.PublicKey, error) {
 	var err error
-	if k.publicKey == nil {
-		k.publicKey, err = parsePublicKey(k.ArmoredPublicKey)
+	if k.PublicKey == nil {
+		k.PublicKey, err = parsePublicKey(k.ArmoredPublicKey)
 	}
-	return k.publicKey, err
+	return k.PublicKey, err
 }
diff --git a/identity/key_test.go b/identity/key_test.go
new file mode 100644
index 00000000..7cb75e4c
--- /dev/null
+++ b/identity/key_test.go
@@ -0,0 +1,21 @@
+package identity
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestDecodeKeyFingerprint(t *testing.T) {
+	checkEncodeDecodeKeyFingerprint(t, strings.Repeat("0", 40))
+	checkEncodeDecodeKeyFingerprint(t, strings.Repeat("E", 40))
+	checkEncodeDecodeKeyFingerprint(t, "C77E1D7542889EC0E45BA88899DA3BE167DA2410")
+}
+
+func checkEncodeDecodeKeyFingerprint(t *testing.T, fingerprint string) {
+	decoded, err := DecodeKeyFingerprint(fingerprint)
+	require.NoError(t, err)
+	require.Equal(t, fingerprint, EncodeKeyFingerprint(decoded))
+}
+
diff --git a/identity/version.go b/identity/version.go
index 757b029f..65ea17d2 100644
--- a/identity/version.go
+++ b/identity/version.go
@@ -222,3 +222,15 @@ func (v *Version) GetMetadata(key string) (string, bool) {
 func (v *Version) AllMetadata() map[string]string {
 	return v.metadata
 }
+
+func (v *Version) Keys() []*Key {
+	return v.keys
+}
+
+func (v *Version) CommitHash() git.Hash {
+	return v.commitHash
+}
+
+func (v *Version) Time() lamport.Time {
+	return v.time
+}
diff --git a/repository/git_test.go b/repository/git_test.go
index d1a087f2..d218e40d 100644
--- a/repository/git_test.go
+++ b/repository/git_test.go
@@ -94,6 +94,6 @@ func TestGitRepo_StoreCommit(t *testing.T) {
 	checkStoreCommit(t,repo, "N")
 
 	// Commit and expect a good signature with unknown validity.
-	setupSigningKey(t, repo)
+	SetupSigningKey(t, repo, "a@e.org")
 	checkStoreCommit(t, repo, "U")
 }
diff --git a/repository/git_testing.go b/repository/git_testing.go
index 07e14544..e4d6777a 100644
--- a/repository/git_testing.go
+++ b/repository/git_testing.go
@@ -48,20 +48,67 @@ func CreateTestRepo(bare bool) *GitRepo {
 	return repo
 }
 
-// setupSigningKey creates a GPG key and sets up the local config so it's used.
+// CreatePubkey returns an armored public PGP key.
+func CreatePubkey(t *testing.T) string {
+	// Generate a key pair for signing commits.
+	pgpEntity, err := openpgp.NewEntity("First Last", "", "fl@example.org", nil)
+	require.NoError(t, err)
+
+	// Armor the public part.
+	pubBuilder := &strings.Builder{}
+	w, err := armor.Encode(pubBuilder, openpgp.PublicKeyType, nil)
+	require.NoError(t, err)
+	err = pgpEntity.Serialize(w)
+	require.NoError(t, err)
+	err = w.Close()
+	require.NoError(t, err)
+	armoredPub := pubBuilder.String()
+	return armoredPub
+}
+
+// SetupSigningKey creates a GPG key and sets up the local config so it's used.
 // The key id is set as "user.signingkey". For the key to be found, a `gpg`
 // wrapper which uses only a custom keyring is created and set as "gpg.program".
 // Finally "commit.gpgsign" is set to true so the signing takes place.
-func setupSigningKey(t *testing.T, repo *GitRepo) {
+//
+// Returns the armored public key.
+func SetupSigningKey(t *testing.T, repo *GitRepo, email string) string {
+	keyId, armoredPub, gpgWrapper := CreateKey(t, email)
+
+	SetupKey(t, repo, email, keyId, gpgWrapper)
+
+	return armoredPub
+}
+
+func SetupKey(t *testing.T, repo *GitRepo, email, keyId, gpgWrapper string) {
 	config := repo.LocalConfig()
 
-	// Generate a key pair for signing commits.
-	entity, err := openpgp.NewEntity("First Last", "", "fl@example.org", nil)
+	if email != "" {
+		err := config.StoreString("user.email", email)
+		require.NoError(t, err)
+	}
+
+	if keyId != "" {
+		err := config.StoreString("user.signingkey", keyId)
+		require.NoError(t, err)
+	}
+
+	if gpgWrapper != "" {
+		err := config.StoreString("gpg.program", gpgWrapper)
+		require.NoError(t, err)
+	}
+
+	err := config.StoreString("commit.gpgsign", "true")
 	require.NoError(t, err)
+}
 
-	err = config.StoreString("user.signingkey", entity.PrivateKey.KeyIdString())
+func CreateKey(t *testing.T, email string) (keyId, armoredPub, gpgWrapper string) {
+	// Generate a key pair for signing commits.
+	entity, err := openpgp.NewEntity("First Last", "", email, nil)
 	require.NoError(t, err)
 
+	keyId = entity.PrivateKey.KeyIdString()
+
 	// Armor the private part.
 	privBuilder := &strings.Builder{}
 	w, err := armor.Encode(privBuilder, openpgp.PrivateKeyType, nil)
@@ -80,7 +127,7 @@ func setupSigningKey(t *testing.T, repo *GitRepo) {
 	require.NoError(t, err)
 	err = w.Close()
 	require.NoError(t, err)
-	armoredPub := pubBuilder.String()
+	armoredPub = pubBuilder.String()
 
 	// Create a custom gpg keyring to be used when creating commits with `git`.
 	keyring, err := ioutil.TempFile("", "keyring")
@@ -107,14 +154,9 @@ func setupSigningKey(t *testing.T, repo *GitRepo) {
 	require.NoError(t, err)
 
 	// Use a gpg wrapper to use a custom keyring containing GPGKeyID.
-	gpgWrapper := createGPGWrapper(t, keyring.Name())
-	if err := config.StoreString("gpg.program", gpgWrapper); err != nil {
-		log.Fatal("failed to set gpg.program for test repository: ", err)
-	}
+	gpgWrapper = createGPGWrapper(t, keyring.Name())
 
-	if err := config.StoreString("commit.gpgsign", "true"); err != nil {
-		log.Fatal("failed to set commit.gpgsign for test repository: ", err)
-	}
+	return
 }
 
 // createGPGWrapper creates a shell script running gpg with a specific keyring.
diff --git a/validate/validator.go b/validate/validator.go
new file mode 100644
index 00000000..e3de5da8
--- /dev/null
+++ b/validate/validator.go
@@ -0,0 +1,378 @@
+package validate
+
+// Signatures validation.
+
+import (
+	"crypto"
+	"fmt"
+	"io/ioutil"
+	"sort"
+	"strings"
+	"time"
+
+	"github.com/MichaelMure/git-bug/cache"
+	"github.com/MichaelMure/git-bug/identity"
+	"github.com/MichaelMure/git-bug/util/git"
+	"github.com/go-git/go-git/v5/plumbing"
+	"github.com/go-git/go-git/v5/plumbing/object"
+	"github.com/pkg/errors"
+	"golang.org/x/crypto/openpgp"
+	"golang.org/x/crypto/openpgp/armor"
+	"golang.org/x/crypto/openpgp/packet"
+)
+
+type Validator struct {
+	backend *cache.RepoCache
+
+	// FirstKey is the key used to sign the first commit.
+	FirstKey *identity.Key
+
+	// versions holds all the Identity Versions ordered by lamport time.
+	versions       []*versionInfo
+	// keyring holds all the current and past keys along with their expire time.
+	keyring        openpgp.EntityList
+	// keyCommit maps the key id to the commit which introduced that key.
+	keyCommit      map[uint64]*object.Commit
+	// checkedCommits holds the valid already-checked commits.
+	checkedCommits map[git.Hash]bool
+}
+
+
+// versionInfo contains details about a Version of an Identity, including
+// the added and removed keys, if any.
+type versionInfo struct {
+	Version     *identity.Version
+	Identity    *identity.Identity
+	KeysAdded   []*identity.Key
+	KeysRemoved []*identity.Key
+	Commit      *object.Commit
+}
+
+type ByLamportTime []*versionInfo
+
+func (a ByLamportTime) Len() int           { return len(a) }
+func (a ByLamportTime) Less(i, j int) bool { return a[i].Version.Time() < a[j].Version.Time() }
+func (a ByLamportTime) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
+
+
+// NewValidator creates a validator for the current identities snapshot.
+// If identities are changed a new Validator instance should be used.
+//
+// The returned instance can be used to verify multiple git refs
+// from the main repository against the keychain built from the
+// identities snapshot loaded initially.
+func NewValidator(backend *cache.RepoCache) (*Validator, error) {
+	var err error
+
+	v := &Validator{
+		backend:        backend,
+		keyring:        make(openpgp.EntityList, 0),
+		keyCommit:      make(map[uint64]*object.Commit),
+		checkedCommits: make(map[git.Hash]bool),
+	}
+
+	v.versions, err = v.readVersionsInfo()
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to read identity versions")
+	}
+
+	sort.Sort(ByLamportTime(v.versions))
+
+	if len(v.versions) > 0 {
+		lastInfo := v.versions[0]
+		for _, info := range v.versions[1:] {
+			if len(info.KeysAdded) + len(info.KeysRemoved) > 0 && info.Version.Time() == lastInfo.Version.Time() {
+				return nil, fmt.Errorf("multiple versions with the same lamport time: %d in commits %s %s", lastInfo.Version.Time(), lastInfo.Version.CommitHash(), info.Version.CommitHash())
+			}
+			lastInfo = info
+		}
+	}
+
+	v.FirstKey, err = v.validateIdentities()
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to validate identities")
+	}
+
+	return v, nil
+}
+
+// KeyCommitHash reports the hash of the commit associated with the Identity
+// Version introducing the key using the specified keyId, if any.
+func (v *Validator) KeyCommitHash(keyId uint64) string {
+	commit := v.keyCommit[keyId]
+	if commit == nil {
+		return ""
+	}
+	return commit.Hash.String()
+}
+
+// readVersionsInfo stores all the operations ever done on each identity.
+// Checks the keys introduced by the versions to be unique.
+func (v *Validator) readVersionsInfo() ([]*versionInfo, error) {
+	versions := make([]*versionInfo, 0)
+	for _, id := range v.backend.AllIdentityIds() {
+		identityCache, err := v.backend.ResolveIdentity(id)
+		if err != nil {
+			return nil, errors.Wrapf(err, "failed to resolve identity %s", id)
+		}
+
+		lastVersionKeys := make(map[uint64]*identity.Key)
+		for _, version := range identityCache.Identity.Versions() {
+			// Load the commit.
+			hash := version.CommitHash()
+			commit, err := v.backend.ResolveCommit(hash)
+			if err != nil {
+				return nil, errors.Wrapf(err, "failed to read commit %s for identity %s", hash, identityCache.Id())
+			}
+
+			versionKeys := make(map[uint64]*identity.Key)
+
+			// Iterate the keys to see which one has been added in this version.
+			keysAdded := make([]*identity.Key, 0)
+			for _, key := range version.Keys() {
+				pubkey, err := key.GetPublicKey()
+				if err != nil {
+					return nil, err
+				}
+				if _, present := lastVersionKeys[pubkey.KeyId]; present {
+					// The key was already present in the previous version.
+					delete(lastVersionKeys, pubkey.KeyId)
+				} else {
+					// The key was introduced in this version.
+					keysAdded = append(keysAdded, key)
+					if otherCommit, present := v.keyCommit[key.PublicKey.KeyId]; present {
+						// It's simpler to require keyIds to be unique than
+						// to support non-unique keys.
+						return nil, fmt.Errorf("keys with identical keyId introduced in commits %s and %s", otherCommit.Hash, commit.Hash)
+					}
+					v.keyCommit[key.PublicKey.KeyId] = commit
+				}
+				versionKeys[pubkey.KeyId] = key
+			}
+
+			// The remaining keys have been removed.
+			keysRemoved := make([]*identity.Key, 0, len(lastVersionKeys))
+			for _, key := range lastVersionKeys {
+				keysRemoved = append(keysRemoved, key)
+			}
+
+			versions = append(versions, &versionInfo{version, identityCache.Identity, keysAdded, keysRemoved, commit})
+
+			lastVersionKeys = versionKeys
+		}
+	}
+	return versions, nil
+}
+
+// validateIdentities checks the identity operations have been properly signed.
+// Sets the key used to sign the first commit.
+func (v *Validator) validateIdentities() (*identity.Key, error) {
+	var firstKey *identity.Key
+
+	// Iterate the ordered versions to check each of them.
+	for _, info := range v.versions {
+		if firstKey == nil {
+			// For the first commit we update the keyring beforehand,
+			// as it should be signed with the key it introduces.
+			v.updateKeyring(info)
+		}
+
+		signingKey, err := v.ValidateRef(info.Version.CommitHash())
+		if err != nil {
+			return nil, errors.Wrapf(err, "invalid identity %s (%s)", info.Identity.Id(), info.Identity.Email())
+		}
+
+		if firstKey == nil {
+			for _, key := range info.Version.Keys() {
+				if key.PublicKey.KeyId == signingKey.KeyId {
+					firstKey = key
+				}
+			}
+		} else {
+			v.updateKeyring(info)
+		}
+	}
+
+	return firstKey, nil
+}
+
+func (v *Validator) updateKeyring(info *versionInfo) {
+	for _, key := range info.KeysRemoved {
+		for _, entity := range v.keyring {
+			if entity.PrimaryKey.KeyId == key.PublicKey.KeyId {
+				lifetime := info.Commit.Committer.When.Sub(v.keyCommit[key.PublicKey.KeyId].Committer.When)
+				lifetimeSecs := uint32(lifetime.Seconds())
+				// It's only one.
+				for _, i := range entity.Identities {
+					i.SelfSignature.KeyLifetimeSecs = &lifetimeSecs
+				}
+				break
+			}
+		}
+	}
+	for _, key := range info.KeysAdded {
+		e := &openpgp.Entity{
+			PrimaryKey: key.PublicKey,
+			Identities: make(map[string]*openpgp.Identity),
+		}
+
+		creationTime := info.Commit.Committer.When
+		uid := packet.NewUserId(info.Identity.Name(), "", info.Identity.Email())
+		isPrimaryId := true
+		e.Identities[uid.Id] = &openpgp.Identity{
+			Name:   uid.Id,
+			UserId: uid,
+			SelfSignature: &packet.Signature{
+				CreationTime:    creationTime,
+				SigType:         packet.SigTypePositiveCert,
+				PubKeyAlgo:      packet.PubKeyAlgoRSA,
+				Hash:            crypto.SHA256,
+				KeyLifetimeSecs: nil,
+				IsPrimaryId:     &isPrimaryId,
+				FlagsValid:      true,
+				FlagSign:        true,
+				FlagCertify:     true,
+				IssuerKeyId:     &e.PrimaryKey.KeyId,
+			},
+		}
+
+		v.keyring = append(v.keyring, e)
+	}
+}
+
+func (v *Validator) ValidateRef(hash git.Hash) (*packet.PublicKey, error) {
+	if v.checkedCommits[hash] {
+		return nil, nil
+	}
+
+	commit, err := v.backend.ResolveCommit(hash)
+	if err != nil {
+		return nil, err
+	}
+
+	for _, h := range commit.ParentHashes {
+		_, err = v.ValidateRef(git.Hash(h.String()))
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	signingKey, err := v.verifyCommitSignature(commit)
+	if err != nil {
+		return nil, errors.Wrapf(err, "invalid signature for commit %s", hash)
+	}
+
+	v.checkedCommits[hash] = true
+	return signingKey, nil
+}
+
+// verifyCommitSignature returns which public key was able to verify the commit
+// or an error.
+func (v *Validator) verifyCommitSignature(commit *object.Commit) (*packet.PublicKey, error) {
+	if commit.PGPSignature == "" {
+		return nil, errors.New("commit is not signed")
+	}
+
+	signature, err := dearmorSignature(commit.PGPSignature)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to dearmor PGP signature")
+	}
+
+	if signature.IssuerKeyId == nil {
+		// We require this because otherwise it would be expensive to
+		// iterate the keys to check which one can verify the signature.
+		// openpgp.CheckDetachedSignature has the same expectation.
+		return nil, errors.New("signature doesn't have an issuer")
+	}
+
+	// Encode commit components excluding the signature.
+	// This is the content to be signed.
+	encoded := &plumbing.MemoryObject{}
+	if err := commit.EncodeWithoutSignature(encoded); err != nil {
+		return nil, err
+	}
+	er, err := encoded.Reader()
+	if err != nil {
+		return nil, err
+	}
+	body, err := ioutil.ReadAll(er)
+
+	key, err := v.searchKey(signature, body)
+	if err != nil {
+		return nil, err
+	}
+
+	// Check the committer email of the git commit matches
+	// the email of the git-bug identity.
+	var identity_ *openpgp.Identity
+	emails := make([]string, len(key.Entity.Identities))
+	i := 0
+	for _, ei := range key.Entity.Identities {
+		if ei.UserId.Email == commit.Committer.Email {
+			identity_ = ei
+			break
+		}
+		emails[i] = ei.UserId.Email
+		i++
+	}
+	if identity_ == nil {
+		return nil, fmt.Errorf("git commit committer-email does not match the identity-email: %s vs %s",
+			commit.Committer.Email, strings.Join(emails, ","))
+	}
+
+	start := identity_.SelfSignature.CreationTime
+	if start.After(commit.Committer.When) {
+		return nil, fmt.Errorf("key used to sign commit was created after the commit %s", commit.Hash)
+	}
+	if identity_.SelfSignature.KeyLifetimeSecs != nil {
+		expiry := start.Add(time.Duration(*identity_.SelfSignature.KeyLifetimeSecs))
+		if expiry.Before(commit.Committer.When) {
+			return nil, fmt.Errorf("key used to sign commit %s on %s expired on %s",
+				commit.Hash, commit.Committer.When.Format(time.Stamp), expiry.Format(time.Stamp))
+		}
+	}
+
+	return key.PublicKey, nil
+}
+
+// searchKey searches for a key which can verify the signature.
+// It does not check the expire time.
+func (v *Validator) searchKey(signature *packet.Signature, body []byte) (*openpgp.Key, error) {
+	for _, key := range v.keyring.KeysById(*signature.IssuerKeyId) {
+		signed := signature.Hash.New()
+		_, err := signed.Write(body)
+		if err != nil {
+			return nil, err
+		}
+		err = key.PublicKey.VerifySignature(signed, signature)
+		if err == nil {
+			return &key, nil
+		}
+	}
+
+	return nil, errors.New("no key can verify the signature")
+}
+
+// dearmorSignature decodes an armored signature.
+func dearmorSignature(armoredSignature string) (*packet.Signature, error) {
+	block, err := armor.Decode(strings.NewReader(armoredSignature))
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to dearmor signature")
+	}
+	reader := packet.NewReader(block.Body)
+	p, err := reader.Next()
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to read signature packet")
+	}
+	sig, ok := p.(*packet.Signature)
+	if !ok {
+		// https://tools.ietf.org/html/rfc4880#section-5.2.3
+		return nil, errors.New("failed to parse signature as Version 4 Signature Packet Format")
+	}
+	if sig == nil {
+		// The optional "Issuer" field "(8-octet Key ID)" is missing.
+		// https://tools.ietf.org/html/rfc4880#section-5.2.3.5
+		return nil, fmt.Errorf("missing Issuer Key ID")
+	}
+	return sig, nil
+}
diff --git a/validate/validator_test.go b/validate/validator_test.go
new file mode 100644
index 00000000..14cc535f
--- /dev/null
+++ b/validate/validator_test.go
@@ -0,0 +1,167 @@
+package validate
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/MichaelMure/git-bug/cache"
+	"github.com/MichaelMure/git-bug/identity"
+	"github.com/MichaelMure/git-bug/repository"
+	"github.com/stretchr/testify/require"
+)
+
+func checkAddIdentity(t *testing.T, backend *cache.RepoCache, name, email, armoredPubkey string) *cache.IdentityCache {
+	key, err := identity.NewKey(armoredPubkey)
+	require.NoError(t, err)
+
+	id, err := backend.NewIdentityWithKeyRaw(name, email, "", "", nil, key)
+	require.NoError(t, err)
+
+	return id
+}
+
+func checkValidator(t *testing.T, backend *cache.RepoCache, errorMsg, firstKey string) {
+	validator, err := NewValidator(backend)
+	if errorMsg == "" {
+		require.NoError(t, err)
+		if firstKey == "" {
+			require.Nil(t, validator.FirstKey)
+		} else {
+			require.Equal(t, firstKey, validator.FirstKey.ArmoredPublicKey)
+		}
+	} else {
+		require.EqualError(t, err, errorMsg)
+	}
+}
+
+func checkAddKey(t *testing.T, id *cache.IdentityCache, armoredKey string) {
+	key, err := identity.NewKey(armoredKey)
+	require.NoError(t, err)
+
+	err = id.Mutate(func(m identity.Mutator) identity.Mutator {
+		m.Keys = append(m.Keys, key)
+		return m
+	})
+	require.NoError(t, err)
+
+	err = id.Commit()
+	require.NoError(t, err)
+}
+
+func TestNewValidator_EmptyRepo(t *testing.T) {
+	repo := repository.CreateTestRepo(false)
+	defer repository.CleanupTestRepos(t, repo)
+
+	backend, err := cache.NewRepoCache(repo)
+	require.NoError(t, err)
+
+	checkValidator(t, backend, "", "")
+	validator, err := NewValidator(backend)
+	require.NoError(t, err)
+	require.Nil(t, validator.FirstKey)
+}
+
+func TestNewValidator_OneIdentity(t *testing.T) {
+	repo := repository.CreateTestRepo(false)
+	defer repository.CleanupTestRepos(t, repo)
+
+	backend, err := cache.NewRepoCache(repo)
+	require.NoError(t, err)
+
+	armoredPubkey := repository.SetupSigningKey(t, repo, "a@e.org")
+
+	_ = checkAddIdentity(t, backend, "A", "a@e.org", armoredPubkey)
+	checkValidator(t, backend, "", armoredPubkey)
+}
+
+func TestNewValidator_TwoSeparateIdentities(t *testing.T) {
+	repo := repository.CreateTestRepo(false)
+	defer repository.CleanupTestRepos(t, repo)
+
+	backend, err := cache.NewRepoCache(repo)
+	require.NoError(t, err)
+
+	armoredPubkey := repository.SetupSigningKey(t, repo, "a@e.org")
+	_ = checkAddIdentity(t, backend, "A", "a@e.org", armoredPubkey)
+
+	armoredPubkey2 := repository.SetupSigningKey(t, repo, "b@e.org")
+	id2 := checkAddIdentity(t, backend, "B", "b@e.org", armoredPubkey2)
+
+	msg := fmt.Sprintf("failed to validate identities: invalid identity %s (%s): invalid signature for commit %s: no key can verify the signature",
+		id2.Id(), id2.Email(), id2.Versions()[0].CommitHash())
+	checkValidator(t, backend, msg, "")
+}
+
+func TestNewValidator_IdentityWithSameKeyTwice(t *testing.T) {
+	repo := repository.CreateTestRepo(false)
+	defer repository.CleanupTestRepos(t, repo)
+
+	backend, err := cache.NewRepoCache(repo)
+	require.NoError(t, err)
+
+	armoredPubkey := repository.SetupSigningKey(t, repo, "a@e.org")
+	id1 := checkAddIdentity(t, backend, "A", "a@e.org", armoredPubkey)
+
+	checkAddKey(t, id1, armoredPubkey)
+
+	msg := fmt.Sprintf("failed to read identity versions: keys with identical keyId introduced in commits %s and %s",
+		id1.Versions()[0].CommitHash(), id1.Versions()[1].CommitHash())
+	checkValidator(t, backend, msg, "")
+}
+
+func TestNewValidator_TwoIdentitiesWithSameKey(t *testing.T) {
+	repo := repository.CreateTestRepo(false)
+	defer repository.CleanupTestRepos(t, repo)
+
+	backend, err := cache.NewRepoCache(repo)
+	require.NoError(t, err)
+
+	armoredPubkey := repository.SetupSigningKey(t, repo, "a@e.org")
+	id1 := checkAddIdentity(t, backend, "A", "a@e.org", armoredPubkey)
+
+	id2 := checkAddIdentity(t, backend, "B", "b@e.org", armoredPubkey)
+
+	_, err = NewValidator(backend)
+	require.EqualError(t, err,
+		fmt.Sprintf("failed to read identity versions: keys with identical keyId introduced in commits %s and %s",
+			id1.Versions()[0].CommitHash(), id2.Versions()[0].CommitHash()))
+}
+
+func TestNewValidator_TwoIdentitiesTwoVersions(t *testing.T) {
+	repo := repository.CreateTestRepo(false)
+	defer repository.CleanupTestRepos(t, repo)
+
+	backend, err := cache.NewRepoCache(repo)
+	require.NoError(t, err)
+
+	armoredPubkey := repository.SetupSigningKey(t, repo, "a@e.org")
+	id1 := checkAddIdentity(t, backend, "A", "a@e.org", armoredPubkey)
+	checkValidator(t, backend, "", armoredPubkey)
+
+	armoredPubkey2 := repository.CreatePubkey(t)
+	id2 := checkAddIdentity(t, backend, "B", "b@e.org", armoredPubkey2)
+
+	armoredPubkey3 := repository.CreatePubkey(t)
+	checkAddKey(t, id1, armoredPubkey3)
+
+	armoredPubkey4 := repository.CreatePubkey(t)
+	checkAddKey(t, id2, armoredPubkey4)
+
+	checkValidator(t, backend, "", armoredPubkey)
+}
+
+func TestNewValidator_WrongEmail(t *testing.T) {
+	repo := repository.CreateTestRepo(false)
+	defer repository.CleanupTestRepos(t, repo)
+
+	backend, err := cache.NewRepoCache(repo)
+	require.NoError(t, err)
+
+	armoredPubkey := repository.SetupSigningKey(t, repo, "a@e.org")
+	repository.SetupKey(t, repo, "x@a.org", "", "")
+	id1 := checkAddIdentity(t, backend, "A", "a@e.org", armoredPubkey)
+
+	msg := fmt.Sprintf("failed to validate identities: invalid identity %s (%s): invalid signature for commit %s: git commit committer-email does not match the identity-email: x@a.org vs a@e.org",
+		id1.Id(), id1.Email(), id1.Versions()[0].CommitHash())
+	checkValidator(t, backend, msg, armoredPubkey)
+}
\ No newline at end of file
-- 
GitLab