add remove identity feature

vince created

also adds:
- listlocalidentities
- refactors refstoids into entity package

Change summary

bug/bug.go                |  21 +------
entity/refs.go            |  18 ++++++
go.sum                    |   3 +
identity/identity.go      |  64 +++++++++++++++++++++
identity/identity_test.go | 123 ++++++++++++++++++++++++++++------------
5 files changed, 173 insertions(+), 56 deletions(-)

Detailed changes

bug/bug.go 🔗

@@ -234,7 +234,7 @@ func RemoveBug(repo repository.ClockedRepo, id entity.Id) error {
 		return err
 	}
 	if len(refs) > 1 {
-		return NewErrMultipleMatchBug(refsToIds(refs))
+		return NewErrMultipleMatchBug(entity.RefsToIds(refs))
 	}
 	if len(refs) == 1 {
 		// we have the bug locally
@@ -253,7 +253,7 @@ func RemoveBug(repo repository.ClockedRepo, id entity.Id) error {
 			return err
 		}
 		if len(remoteRefs) > 1 {
-			return NewErrMultipleMatchBug(refsToIds(refs))
+			return NewErrMultipleMatchBug(entity.RefsToIds(refs))
 		}
 		if len(remoteRefs) == 1 {
 			// found the bug in a remote
@@ -337,22 +337,7 @@ func ListLocalIds(repo repository.Repo) ([]entity.Id, error) {
 		return nil, err
 	}
 
-	return refsToIds(refs), nil
-}
-
-func refsToIds(refs []string) []entity.Id {
-	ids := make([]entity.Id, len(refs))
-
-	for i, ref := range refs {
-		ids[i] = refToId(ref)
-	}
-
-	return ids
-}
-
-func refToId(ref string) entity.Id {
-	split := strings.Split(ref, "/")
-	return entity.Id(split[len(split)-1])
+	return entity.RefsToIds(refs), nil
 }
 
 // Validate check if the Bug data is valid

entity/refs.go 🔗

@@ -0,0 +1,18 @@
+package entity
+
+import "strings"
+
+func RefsToIds(refs []string) []Id {
+	ids := make([]Id, len(refs))
+
+	for i, ref := range refs {
+		ids[i] = refToId(ref)
+	}
+
+	return ids
+}
+
+func refToId(ref string) Id {
+	split := strings.Split(ref, "/")
+	return Id(split[len(split)-1])
+}

go.sum 🔗

@@ -43,6 +43,7 @@ github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF0
 github.com/Masterminds/vcs v1.13.0/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA=
 github.com/MichaelMure/go-term-text v0.2.9 h1:jUxInT3rDhl4WoJgLnmMS3hR79zigyJS1TqKFDTI6xE=
 github.com/MichaelMure/go-term-text v0.2.9/go.mod h1:2QSU/Nn2u41Tqoar+90RlYuhjngJPYgod7evnsYwkWc=
+github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
 github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
 github.com/agnivade/levenshtein v1.0.1 h1:3oJU7J3FGFmyhn8KHjmVaZCN5hxTr7GxgRue+sxIXdQ=
@@ -207,6 +208,7 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
 github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
 github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ=
 github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
 github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
@@ -379,6 +381,7 @@ github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJ
 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/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
 github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=

identity/identity.go 🔗

@@ -27,6 +27,10 @@ var ErrNoIdentitySet = errors.New("No identity is set.\n" +
 	"\"git bug user create\"")
 var ErrMultipleIdentitiesSet = errors.New("multiple user identities set")
 
+func NewErrMultipleMatchIdentity(matching []entity.Id) *entity.ErrMultipleMatch {
+	return entity.NewErrMultipleMatch("identity", matching)
+}
+
 var _ Interface = &Identity{}
 var _ entity.Interface = &Identity{}
 
@@ -175,6 +179,66 @@ func read(repo repository.Repo, ref string) (*Identity, error) {
 	return i, nil
 }
 
+// ListLocalIds list all the available local identity ids
+func ListLocalIds(repo repository.Repo) ([]entity.Id, error) {
+	refs, err := repo.ListRefs(identityRefPattern)
+	if err != nil {
+		return nil, err
+	}
+
+	return entity.RefsToIds(refs), nil
+}
+
+// RemoveIdentity will remove a local identity from its entity.Id
+func RemoveIdentity(repo repository.ClockedRepo, id entity.Id) error {
+	var fullMatches []string
+
+	refs, err := repo.ListRefs(identityRefPattern + id.String())
+	if err != nil {
+		return err
+	}
+	if len(refs) > 1 {
+		return NewErrMultipleMatchIdentity(entity.RefsToIds(refs))
+	}
+	if len(refs) == 1 {
+		// we have the identity locally
+		fullMatches = append(fullMatches, refs[0])
+	}
+
+	remotes, err := repo.GetRemotes()
+	if err != nil {
+		return err
+	}
+
+	for remote := range remotes {
+		remotePrefix := fmt.Sprintf(identityRemoteRefPattern+id.String(), remote)
+		remoteRefs, err := repo.ListRefs(remotePrefix)
+		if err != nil {
+			return err
+		}
+		if len(remoteRefs) > 1 {
+			return NewErrMultipleMatchIdentity(entity.RefsToIds(refs))
+		}
+		if len(remoteRefs) == 1 {
+			// found the identity in a remote
+			fullMatches = append(fullMatches, remoteRefs[0])
+		}
+	}
+
+	if len(fullMatches) == 0 {
+		return ErrIdentityNotExist
+	}
+
+	for _, ref := range fullMatches {
+		err = repo.RemoveRef(ref)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
 type StreamedIdentity struct {
 	Identity *Identity
 	Err      error

identity/identity_test.go 🔗

@@ -4,9 +4,10 @@ import (
 	"encoding/json"
 	"testing"
 
+	"github.com/stretchr/testify/require"
+
 	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/repository"
-	"github.com/stretchr/testify/assert"
 )
 
 // Test the commit and load of an Identity with multiple versions
@@ -27,13 +28,13 @@ func TestIdentityCommitLoad(t *testing.T) {
 
 	err := identity.Commit(mockRepo)
 
-	assert.Nil(t, err)
-	assert.NotEmpty(t, identity.id)
+	require.NoError(t, err)
+	require.NotEmpty(t, identity.id)
 
 	loaded, err := ReadLocal(mockRepo, identity.id)
-	assert.Nil(t, err)
+	require.NoError(t, err)
 	commitsAreSet(t, loaded)
-	assert.Equal(t, identity, loaded)
+	require.Equal(t, identity, loaded)
 
 	// multiple version
 
@@ -69,13 +70,13 @@ func TestIdentityCommitLoad(t *testing.T) {
 
 	err = identity.Commit(mockRepo)
 
-	assert.Nil(t, err)
-	assert.NotEmpty(t, identity.id)
+	require.NoError(t, err)
+	require.NotEmpty(t, identity.id)
 
 	loaded, err = ReadLocal(mockRepo, identity.id)
-	assert.Nil(t, err)
+	require.NoError(t, err)
 	commitsAreSet(t, loaded)
-	assert.Equal(t, identity, loaded)
+	require.Equal(t, identity, loaded)
 
 	// add more version
 
@@ -99,19 +100,19 @@ func TestIdentityCommitLoad(t *testing.T) {
 
 	err = identity.Commit(mockRepo)
 
-	assert.Nil(t, err)
-	assert.NotEmpty(t, identity.id)
+	require.NoError(t, err)
+	require.NotEmpty(t, identity.id)
 
 	loaded, err = ReadLocal(mockRepo, identity.id)
-	assert.Nil(t, err)
+	require.NoError(t, err)
 	commitsAreSet(t, loaded)
-	assert.Equal(t, identity, loaded)
+	require.Equal(t, identity, loaded)
 }
 
 func TestIdentityMutate(t *testing.T) {
 	identity := NewIdentity("René Descartes", "rene.descartes@example.com")
 
-	assert.Len(t, identity.versions, 1)
+	require.Len(t, identity.versions, 1)
 
 	identity.Mutate(func(orig Mutator) Mutator {
 		orig.Email = "rene@descartes.fr"
@@ -120,15 +121,15 @@ func TestIdentityMutate(t *testing.T) {
 		return orig
 	})
 
-	assert.Len(t, identity.versions, 2)
-	assert.Equal(t, identity.Email(), "rene@descartes.fr")
-	assert.Equal(t, identity.Name(), "René")
-	assert.Equal(t, identity.Login(), "rene")
+	require.Len(t, identity.versions, 2)
+	require.Equal(t, identity.Email(), "rene@descartes.fr")
+	require.Equal(t, identity.Name(), "René")
+	require.Equal(t, identity.Login(), "rene")
 }
 
 func commitsAreSet(t *testing.T, identity *Identity) {
 	for _, version := range identity.versions {
-		assert.NotEmpty(t, version.commitHash)
+		require.NotEmpty(t, version.commitHash)
 	}
 }
 
@@ -180,14 +181,14 @@ func TestIdentity_ValidKeysAtTime(t *testing.T) {
 		},
 	}
 
-	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"}})
+	require.Nil(t, identity.ValidKeysAtTime(10))
+	require.Equal(t, identity.ValidKeysAtTime(100), []*Key{{PubKey: "pubkeyA"}})
+	require.Equal(t, identity.ValidKeysAtTime(140), []*Key{{PubKey: "pubkeyA"}})
+	require.Equal(t, identity.ValidKeysAtTime(200), []*Key{{PubKey: "pubkeyB"}})
+	require.Equal(t, identity.ValidKeysAtTime(201), []*Key{{PubKey: "pubkeyD"}})
+	require.Equal(t, identity.ValidKeysAtTime(202), []*Key{{PubKey: "pubkeyD"}})
+	require.Equal(t, identity.ValidKeysAtTime(300), []*Key{{PubKey: "pubkeyE"}})
+	require.Equal(t, identity.ValidKeysAtTime(3000), []*Key{{PubKey: "pubkeyE"}})
 }
 
 // Test the immutable or mutable metadata search
@@ -201,7 +202,7 @@ func TestMetadata(t *testing.T) {
 	assertHasKeyValue(t, identity.MutableMetadata(), "key1", "value1")
 
 	err := identity.Commit(mockRepo)
-	assert.NoError(t, err)
+	require.NoError(t, err)
 
 	assertHasKeyValue(t, identity.ImmutableMetadata(), "key1", "value1")
 	assertHasKeyValue(t, identity.MutableMetadata(), "key1", "value1")
@@ -217,11 +218,11 @@ func TestMetadata(t *testing.T) {
 	assertHasKeyValue(t, identity.MutableMetadata(), "key1", "value2")
 
 	err = identity.Commit(mockRepo)
-	assert.NoError(t, err)
+	require.NoError(t, err)
 
 	// reload
 	loaded, err := ReadLocal(mockRepo, identity.id)
-	assert.Nil(t, err)
+	require.NoError(t, err)
 
 	assertHasKeyValue(t, loaded.ImmutableMetadata(), "key1", "value1")
 	assertHasKeyValue(t, loaded.MutableMetadata(), "key1", "value2")
@@ -229,8 +230,8 @@ func TestMetadata(t *testing.T) {
 
 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)
+	require.True(t, ok)
+	require.Equal(t, val, value)
 }
 
 func TestJSON(t *testing.T) {
@@ -248,20 +249,66 @@ func TestJSON(t *testing.T) {
 
 	// commit to make sure we have an Id
 	err := identity.Commit(mockRepo)
-	assert.Nil(t, err)
-	assert.NotEmpty(t, identity.id)
+	require.NoError(t, err)
+	require.NotEmpty(t, identity.id)
 
 	// serialize
 	data, err := json.Marshal(identity)
-	assert.NoError(t, err)
+	require.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())
+	require.NoError(t, err)
+	require.Equal(t, identity.id, i.Id())
 
 	// make sure we can load the identity properly
 	i, err = ReadLocal(mockRepo, i.Id())
-	assert.NoError(t, err)
+	require.NoError(t, err)
+}
+
+func TestIdentityRemove(t *testing.T) {
+	repo := repository.CreateGoGitTestRepo(false)
+	remoteA := repository.CreateGoGitTestRepo(true)
+	remoteB := repository.CreateGoGitTestRepo(true)
+	defer repository.CleanupTestRepos(repo, remoteA, remoteB)
+
+	err := repo.AddRemote("remoteA", "file://"+remoteA.GetPath())
+	require.NoError(t, err)
+
+	err = repo.AddRemote("remoteB", "file://"+remoteB.GetPath())
+	require.NoError(t, err)
+
+	// generate an identity for testing
+	rene := NewIdentity("René Descartes", "rene@descartes.fr")
+	err = rene.Commit(repo)
+	require.NoError(t, err)
+
+	_, err = Push(repo, "remoteA")
+	require.NoError(t, err)
+
+	_, err = Push(repo, "remoteB")
+	require.NoError(t, err)
+
+	_, err = Fetch(repo, "remoteA")
+	require.NoError(t, err)
+
+	_, err = Fetch(repo, "remoteB")
+	require.NoError(t, err)
+
+	err = RemoveIdentity(repo, rene.Id())
+	require.NoError(t, err)
+
+	_, err = ReadLocal(repo, rene.Id())
+	require.Error(t, ErrIdentityNotExist, err)
+
+	_, err = ReadRemote(repo, "remoteA", string(rene.Id()))
+	require.Error(t, ErrIdentityNotExist, err)
+
+	_, err = ReadRemote(repo, "remoteB", string(rene.Id()))
+	require.Error(t, ErrIdentityNotExist, err)
+
+	ids, err := ListLocalIds(repo)
+	require.NoError(t, err)
+	require.Len(t, ids, 0)
 }