identity.go

  1// Package identity contains the identity data model and low-level related functions
  2package identity
  3
  4import (
  5	"encoding/json"
  6	"fmt"
  7	"strings"
  8
  9	"github.com/MichaelMure/git-bug/repository"
 10	"github.com/MichaelMure/git-bug/util/git"
 11	"github.com/MichaelMure/git-bug/util/lamport"
 12	"github.com/pkg/errors"
 13)
 14
 15const identityRefPattern = "refs/identities/"
 16const versionEntryName = "version"
 17const identityConfigKey = "git-bug.identity"
 18
 19var _ Interface = &Identity{}
 20
 21type Identity struct {
 22	id       string
 23	Versions []*Version
 24}
 25
 26func NewIdentity(name string, email string) *Identity {
 27	return &Identity{
 28		Versions: []*Version{
 29			{
 30				Name:  name,
 31				Email: email,
 32				Nonce: makeNonce(20),
 33			},
 34		},
 35	}
 36}
 37
 38func NewIdentityFull(name string, email string, login string, avatarUrl string) *Identity {
 39	return &Identity{
 40		Versions: []*Version{
 41			{
 42				Name:      name,
 43				Email:     email,
 44				Login:     login,
 45				AvatarUrl: avatarUrl,
 46				Nonce:     makeNonce(20),
 47			},
 48		},
 49	}
 50}
 51
 52type identityJson struct {
 53	Id string `json:"id"`
 54}
 55
 56// MarshalJSON will only serialize the id
 57func (i *Identity) MarshalJSON() ([]byte, error) {
 58	return json.Marshal(identityJson{
 59		Id: i.Id(),
 60	})
 61}
 62
 63// UnmarshalJSON will only read the id
 64// Users of this package are expected to run Load() to load
 65// the remaining data from the identities data in git.
 66func (i *Identity) UnmarshalJSON(data []byte) error {
 67	aux := identityJson{}
 68
 69	if err := json.Unmarshal(data, &aux); err != nil {
 70		return err
 71	}
 72
 73	i.id = aux.Id
 74
 75	return nil
 76}
 77
 78// Read load an Identity from the identities data available in git
 79func Read(repo repository.Repo, id string) (*Identity, error) {
 80	i := &Identity{
 81		id: id,
 82	}
 83
 84	err := i.Load(repo)
 85	if err != nil {
 86		return nil, err
 87	}
 88
 89	return i, nil
 90}
 91
 92// Load will read the corresponding identity data from git and replace any
 93// data already loaded if any.
 94func (i *Identity) Load(repo repository.Repo) error {
 95	ref := fmt.Sprintf("%s%s", identityRefPattern, i.Id())
 96
 97	hashes, err := repo.ListCommits(ref)
 98
 99	var versions []*Version
100
101	// TODO: this is not perfect, it might be a command invoke error
102	if err != nil {
103		return ErrIdentityNotExist
104	}
105
106	for _, hash := range hashes {
107		entries, err := repo.ListEntries(hash)
108		if err != nil {
109			return errors.Wrap(err, "can't list git tree entries")
110		}
111
112		if len(entries) != 1 {
113			return fmt.Errorf("invalid identity data at hash %s", hash)
114		}
115
116		entry := entries[0]
117
118		if entry.Name != versionEntryName {
119			return fmt.Errorf("invalid identity data at hash %s", hash)
120		}
121
122		data, err := repo.ReadData(entry.Hash)
123		if err != nil {
124			return errors.Wrap(err, "failed to read git blob data")
125		}
126
127		var version Version
128		err = json.Unmarshal(data, &version)
129
130		if err != nil {
131			return errors.Wrapf(err, "failed to decode Identity version json %s", hash)
132		}
133
134		// tag the version with the commit hash
135		version.commitHash = hash
136
137		versions = append(versions, &version)
138	}
139
140	i.Versions = versions
141
142	return nil
143}
144
145// NewFromGitUser will query the repository for user detail and
146// build the corresponding Identity
147func NewFromGitUser(repo repository.Repo) (*Identity, error) {
148	name, err := repo.GetUserName()
149	if err != nil {
150		return nil, err
151	}
152	if name == "" {
153		return nil, errors.New("user name is not configured in git yet. Please use `git config --global user.name \"John Doe\"`")
154	}
155
156	email, err := repo.GetUserEmail()
157	if err != nil {
158		return nil, err
159	}
160	if email == "" {
161		return nil, errors.New("user name is not configured in git yet. Please use `git config --global user.email johndoe@example.com`")
162	}
163
164	return NewIdentity(name, email), nil
165}
166
167// BuildFromGit will query the repository for user detail and
168// build the corresponding Identity
169/*func BuildFromGit(repo repository.Repo) *Identity {
170	version := Version{}
171
172	name, err := repo.GetUserName()
173	if err == nil {
174		version.Name = name
175	}
176
177	email, err := repo.GetUserEmail()
178	if err == nil {
179		version.Email = email
180	}
181
182	return &Identity{
183		Versions: []Version{
184			version,
185		},
186	}
187}*/
188
189// SetIdentity store the user identity's id in the git config
190func SetIdentity(repo repository.RepoCommon, identity Identity) error {
191	return repo.StoreConfig(identityConfigKey, identity.Id())
192}
193
194// GetIdentity read the current user identity, set with a git config entry
195func GetIdentity(repo repository.Repo) (*Identity, error) {
196	configs, err := repo.ReadConfigs(identityConfigKey)
197	if err != nil {
198		return nil, err
199	}
200
201	if len(configs) == 0 {
202		return nil, fmt.Errorf("no identity set")
203	}
204
205	if len(configs) > 1 {
206		return nil, fmt.Errorf("multiple identity config exist")
207	}
208
209	var id string
210	for _, val := range configs {
211		id = val
212	}
213
214	return Read(repo, id)
215}
216
217func (i *Identity) AddVersion(version *Version) {
218	i.Versions = append(i.Versions, version)
219}
220
221// Write the identity into the Repository. In particular, this ensure that
222// the Id is properly set.
223func (i *Identity) Commit(repo repository.Repo) error {
224	// Todo: check for mismatch between memory and commited data
225
226	var lastCommit git.Hash = ""
227
228	for _, v := range i.Versions {
229		if v.commitHash != "" {
230			lastCommit = v.commitHash
231			// ignore already commited versions
232			continue
233		}
234
235		blobHash, err := v.Write(repo)
236		if err != nil {
237			return err
238		}
239
240		// Make a git tree referencing the blob
241		tree := []repository.TreeEntry{
242			{ObjectType: repository.Blob, Hash: blobHash, Name: versionEntryName},
243		}
244
245		treeHash, err := repo.StoreTree(tree)
246		if err != nil {
247			return err
248		}
249
250		var commitHash git.Hash
251		if lastCommit != "" {
252			commitHash, err = repo.StoreCommitWithParent(treeHash, lastCommit)
253		} else {
254			commitHash, err = repo.StoreCommit(treeHash)
255		}
256
257		if err != nil {
258			return err
259		}
260
261		lastCommit = commitHash
262
263		// if it was the first commit, use the commit hash as the Identity id
264		if i.id == "" {
265			i.id = string(commitHash)
266		}
267	}
268
269	if i.id == "" {
270		panic("identity with no id")
271	}
272
273	ref := fmt.Sprintf("%s%s", identityRefPattern, i.id)
274	err := repo.UpdateRef(ref, lastCommit)
275
276	if err != nil {
277		return err
278	}
279
280	return nil
281}
282
283// Validate check if the Identity data is valid
284func (i *Identity) Validate() error {
285	lastTime := lamport.Time(0)
286
287	for _, v := range i.Versions {
288		if err := v.Validate(); err != nil {
289			return err
290		}
291
292		if v.Time < lastTime {
293			return fmt.Errorf("non-chronological version (%d --> %d)", lastTime, v.Time)
294		}
295
296		lastTime = v.Time
297	}
298
299	return nil
300}
301
302func (i *Identity) firstVersion() *Version {
303	if len(i.Versions) <= 0 {
304		panic("no version at all")
305	}
306
307	return i.Versions[0]
308}
309
310func (i *Identity) lastVersion() *Version {
311	if len(i.Versions) <= 0 {
312		panic("no version at all")
313	}
314
315	return i.Versions[len(i.Versions)-1]
316}
317
318// Id return the Identity identifier
319func (i *Identity) Id() string {
320	if i.id == "" {
321		// simply panic as it would be a coding error
322		// (using an id of an identity not stored yet)
323		panic("no id yet")
324	}
325	return i.id
326}
327
328// Name return the last version of the name
329func (i *Identity) Name() string {
330	return i.lastVersion().Name
331}
332
333// Email return the last version of the email
334func (i *Identity) Email() string {
335	return i.lastVersion().Email
336}
337
338// Login return the last version of the login
339func (i *Identity) Login() string {
340	return i.lastVersion().Login
341}
342
343// Login return the last version of the Avatar URL
344func (i *Identity) AvatarUrl() string {
345	return i.lastVersion().AvatarUrl
346}
347
348// Login return the last version of the valid keys
349func (i *Identity) Keys() []Key {
350	return i.lastVersion().Keys
351}
352
353// IsProtected return true if the chain of git commits started to be signed.
354// If that's the case, only signed commit with a valid key for this identity can be added.
355func (i *Identity) IsProtected() bool {
356	// Todo
357	return false
358}
359
360// ValidKeysAtTime return the set of keys valid at a given lamport time
361func (i *Identity) ValidKeysAtTime(time lamport.Time) []Key {
362	var result []Key
363
364	for _, v := range i.Versions {
365		if v.Time > time {
366			return result
367		}
368
369		result = v.Keys
370	}
371
372	return result
373}
374
375// Match tell is the Identity match the given query string
376func (i *Identity) Match(query string) bool {
377	query = strings.ToLower(query)
378
379	return strings.Contains(strings.ToLower(i.Name()), query) ||
380		strings.Contains(strings.ToLower(i.Login()), query)
381}
382
383// DisplayName return a non-empty string to display, representing the
384// identity, based on the non-empty values.
385func (i *Identity) DisplayName() string {
386	switch {
387	case i.Name() == "" && i.Login() != "":
388		return i.Login()
389	case i.Name() != "" && i.Login() == "":
390		return i.Name()
391	case i.Name() != "" && i.Login() != "":
392		return fmt.Sprintf("%s (%s)", i.Name(), i.Login())
393	}
394
395	panic("invalid person data")
396}
397
398// SetMetadata store arbitrary metadata along the last defined Version.
399// If the Version has been commit to git already, it won't be overwritten.
400func (i *Identity) SetMetadata(key string, value string) {
401	i.lastVersion().SetMetadata(key, value)
402}
403
404// ImmutableMetadata return all metadata for this Identity, accumulated from each Version.
405// If multiple value are found, the first defined takes precedence.
406func (i *Identity) ImmutableMetadata() map[string]string {
407	metadata := make(map[string]string)
408
409	for _, version := range i.Versions {
410		for key, value := range version.Metadata {
411			if _, has := metadata[key]; !has {
412				metadata[key] = value
413			}
414		}
415	}
416
417	return metadata
418}
419
420// MutableMetadata return all metadata for this Identity, accumulated from each Version.
421// If multiple value are found, the last defined takes precedence.
422func (i *Identity) MutableMetadata() map[string]string {
423	metadata := make(map[string]string)
424
425	for _, version := range i.Versions {
426		for key, value := range version.Metadata {
427			metadata[key] = value
428		}
429	}
430
431	return metadata
432}