identity: add metadata support

Michael Muré created

Change summary

identity/identity.go      | 86 ++++++++++++++++++++++++++++++++++------
identity/identity_test.go | 86 ++++++++++++++++++++++++++++++++++++++--
identity/interface.go     |  2 
identity/version.go       | 26 +++++++++++
4 files changed, 179 insertions(+), 21 deletions(-)

Detailed changes

identity/identity.go 🔗

@@ -20,19 +20,33 @@ var ErrIdentityNotExist = errors.New("identity doesn't exist")
 
 type Identity struct {
 	id       string
-	Versions []Version
+	Versions []*Version
 }
 
-func NewIdentity(name string, email string) (*Identity, error) {
+func NewIdentity(name string, email string) *Identity {
 	return &Identity{
-		Versions: []Version{
+		Versions: []*Version{
 			{
 				Name:  name,
 				Email: email,
 				Nonce: makeNonce(20),
 			},
 		},
-	}, nil
+	}
+}
+
+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),
+			},
+		},
+	}
 }
 
 type identityJson struct {
@@ -84,7 +98,7 @@ func (i *Identity) Load(repo repository.Repo) error {
 
 	hashes, err := repo.ListCommits(ref)
 
-	var versions []Version
+	var versions []*Version
 
 	// TODO: this is not perfect, it might be a command invoke error
 	if err != nil {
@@ -122,7 +136,7 @@ func (i *Identity) Load(repo repository.Repo) error {
 		// tag the version with the commit hash
 		version.commitHash = hash
 
-		versions = append(versions, version)
+		versions = append(versions, &version)
 	}
 
 	i.Versions = versions
@@ -149,7 +163,7 @@ func NewFromGitUser(repo repository.Repo) (*Identity, error) {
 		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)
+	return NewIdentity(name, email), nil
 }
 
 // BuildFromGit will query the repository for user detail and
@@ -202,7 +216,7 @@ func GetIdentity(repo repository.Repo) (*Identity, error) {
 	return Read(repo, id)
 }
 
-func (i *Identity) AddVersion(version Version) {
+func (i *Identity) AddVersion(version *Version) {
 	i.Versions = append(i.Versions, version)
 }
 
@@ -285,7 +299,15 @@ func (i *Identity) Validate() error {
 	return nil
 }
 
-func (i *Identity) LastVersion() Version {
+func (i *Identity) firstVersion() *Version {
+	if len(i.Versions) <= 0 {
+		panic("no version at all")
+	}
+
+	return i.Versions[0]
+}
+
+func (i *Identity) lastVersion() *Version {
 	if len(i.Versions) <= 0 {
 		panic("no version at all")
 	}
@@ -305,27 +327,27 @@ func (i *Identity) Id() string {
 
 // Name return the last version of the name
 func (i *Identity) Name() string {
-	return i.LastVersion().Name
+	return i.lastVersion().Name
 }
 
 // Email return the last version of the email
 func (i *Identity) Email() string {
-	return i.LastVersion().Email
+	return i.lastVersion().Email
 }
 
 // Login return the last version of the login
 func (i *Identity) Login() string {
-	return i.LastVersion().Login
+	return i.lastVersion().Login
 }
 
 // Login return the last version of the Avatar URL
 func (i *Identity) AvatarUrl() string {
-	return i.LastVersion().AvatarUrl
+	return i.lastVersion().AvatarUrl
 }
 
 // Login return the last version of the valid keys
 func (i *Identity) Keys() []Key {
-	return i.LastVersion().Keys
+	return i.lastVersion().Keys
 }
 
 // IsProtected return true if the chain of git commits started to be signed.
@@ -372,3 +394,39 @@ func (i *Identity) DisplayName() string {
 
 	panic("invalid person data")
 }
+
+// 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_test.go 🔗

@@ -5,15 +5,16 @@ import (
 
 	"github.com/MichaelMure/git-bug/repository"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
-func TestIdentityCommit(t *testing.T) {
+func TestIdentityCommitLoad(t *testing.T) {
 	mockRepo := repository.NewMockRepoForTest()
 
 	// single version
 
 	identity := Identity{
-		Versions: []Version{
+		Versions: []*Version{
 			{
 				Name:  "René Descartes",
 				Email: "rene.descartes@example.com",
@@ -26,10 +27,15 @@ func TestIdentityCommit(t *testing.T) {
 	assert.Nil(t, err)
 	assert.NotEmpty(t, identity.id)
 
+	loaded, err := Read(mockRepo, identity.id)
+	assert.Nil(t, err)
+	commitsAreSet(t, loaded)
+	equivalentIdentity(t, &identity, loaded)
+
 	// multiple version
 
 	identity = Identity{
-		Versions: []Version{
+		Versions: []*Version{
 			{
 				Time:  100,
 				Name:  "René Descartes",
@@ -62,9 +68,14 @@ func TestIdentityCommit(t *testing.T) {
 	assert.Nil(t, err)
 	assert.NotEmpty(t, identity.id)
 
+	loaded, err = Read(mockRepo, identity.id)
+	assert.Nil(t, err)
+	commitsAreSet(t, loaded)
+	equivalentIdentity(t, &identity, loaded)
+
 	// add more version
 
-	identity.AddVersion(Version{
+	identity.AddVersion(&Version{
 		Time:  201,
 		Name:  "René Descartes",
 		Email: "rene.descartes@example.com",
@@ -73,7 +84,7 @@ func TestIdentityCommit(t *testing.T) {
 		},
 	})
 
-	identity.AddVersion(Version{
+	identity.AddVersion(&Version{
 		Time:  300,
 		Name:  "René Descartes",
 		Email: "rene.descartes@example.com",
@@ -86,11 +97,32 @@ func TestIdentityCommit(t *testing.T) {
 
 	assert.Nil(t, err)
 	assert.NotEmpty(t, identity.id)
+
+	loaded, err = Read(mockRepo, identity.id)
+	assert.Nil(t, err)
+	commitsAreSet(t, loaded)
+	equivalentIdentity(t, &identity, loaded)
+}
+
+func commitsAreSet(t *testing.T, identity *Identity) {
+	for _, version := range identity.Versions {
+		assert.NotEmpty(t, version.commitHash)
+	}
+}
+
+func equivalentIdentity(t *testing.T, expected, actual *Identity) {
+	require.Equal(t, len(expected.Versions), len(actual.Versions))
+
+	for i, version := range expected.Versions {
+		actual.Versions[i].commitHash = version.commitHash
+	}
+
+	assert.Equal(t, expected, actual)
 }
 
 func TestIdentity_ValidKeysAtTime(t *testing.T) {
 	identity := Identity{
-		Versions: []Version{
+		Versions: []*Version{
 			{
 				Time:  100,
 				Name:  "René Descartes",
@@ -143,3 +175,45 @@ func TestIdentity_ValidKeysAtTime(t *testing.T) {
 	assert.Equal(t, identity.ValidKeysAtTime(300), []Key{{PubKey: "pubkeyE"}})
 	assert.Equal(t, identity.ValidKeysAtTime(3000), []Key{{PubKey: "pubkeyE"}})
 }
+
+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 := Read(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)
+}

identity/interface.go 🔗

@@ -5,6 +5,8 @@ import (
 )
 
 type Interface interface {
+	Id() string
+
 	Name() string
 	Email() string
 	Login() string

identity/version.go 🔗

@@ -12,7 +12,7 @@ import (
 	"github.com/MichaelMure/git-bug/util/text"
 )
 
-// Version is a complete set of informations about an Identity at a point in time.
+// Version is a complete set of information about an Identity at a point in time.
 type Version struct {
 	// Private field so not serialized
 	commitHash git.Hash
@@ -35,6 +35,9 @@ type Version struct {
 	// 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 `json:"nonce,omitempty"`
+
+	// A set of arbitrary key/value to store metadata about a version or about an Identity in general.
+	Metadata map[string]string `json:"metadata,omitempty"`
 }
 
 func (v *Version) Validate() error {
@@ -103,3 +106,24 @@ func makeNonce(len int) []byte {
 	}
 	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
+}