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	"reflect"
  8	"strings"
  9	"time"
 10
 11	"github.com/pkg/errors"
 12
 13	"github.com/MichaelMure/git-bug/entity"
 14	"github.com/MichaelMure/git-bug/repository"
 15	"github.com/MichaelMure/git-bug/util/lamport"
 16	"github.com/MichaelMure/git-bug/util/timestamp"
 17)
 18
 19const identityRefPattern = "refs/identities/"
 20const identityRemoteRefPattern = "refs/remotes/%s/identities/"
 21const versionEntryName = "version"
 22const identityConfigKey = "git-bug.identity"
 23
 24var ErrNonFastForwardMerge = errors.New("non fast-forward identity merge")
 25var ErrNoIdentitySet = errors.New("No identity is set.\n" +
 26	"To interact with bugs, an identity first needs to be created using " +
 27	"\"git bug user create\"")
 28var ErrMultipleIdentitiesSet = errors.New("multiple user identities set")
 29
 30func NewErrMultipleMatchIdentity(matching []entity.Id) *entity.ErrMultipleMatch {
 31	return entity.NewErrMultipleMatch("identity", matching)
 32}
 33
 34var _ Interface = &Identity{}
 35var _ entity.Interface = &Identity{}
 36
 37type Identity struct {
 38	// Id used as unique identifier
 39	id entity.Id
 40
 41	// all the successive version of the identity
 42	versions []*Version
 43
 44	// not serialized
 45	lastCommit repository.Hash
 46}
 47
 48func NewIdentity(name string, email string) *Identity {
 49	return &Identity{
 50		id: entity.UnsetId,
 51		versions: []*Version{
 52			{
 53				name:  name,
 54				email: email,
 55				nonce: makeNonce(20),
 56			},
 57		},
 58	}
 59}
 60
 61func NewIdentityFull(name string, email string, login string, avatarUrl string) *Identity {
 62	return &Identity{
 63		id: entity.UnsetId,
 64		versions: []*Version{
 65			{
 66				name:      name,
 67				email:     email,
 68				login:     login,
 69				avatarURL: avatarUrl,
 70				nonce:     makeNonce(20),
 71			},
 72		},
 73	}
 74}
 75
 76// NewFromGitUser will query the repository for user detail and
 77// build the corresponding Identity
 78func NewFromGitUser(repo repository.Repo) (*Identity, error) {
 79	name, err := repo.GetUserName()
 80	if err != nil {
 81		return nil, err
 82	}
 83	if name == "" {
 84		return nil, errors.New("user name is not configured in git yet. Please use `git config --global user.name \"John Doe\"`")
 85	}
 86
 87	email, err := repo.GetUserEmail()
 88	if err != nil {
 89		return nil, err
 90	}
 91	if email == "" {
 92		return nil, errors.New("user name is not configured in git yet. Please use `git config --global user.email johndoe@example.com`")
 93	}
 94
 95	return NewIdentity(name, email), nil
 96}
 97
 98// MarshalJSON will only serialize the id
 99func (i *Identity) MarshalJSON() ([]byte, error) {
100	return json.Marshal(&IdentityStub{
101		id: i.id,
102	})
103}
104
105// UnmarshalJSON will only read the id
106// Users of this package are expected to run Load() to load
107// the remaining data from the identities data in git.
108func (i *Identity) UnmarshalJSON(data []byte) error {
109	panic("identity should be loaded with identity.UnmarshalJSON")
110}
111
112// ReadLocal load a local Identity from the identities data available in git
113func ReadLocal(repo repository.Repo, id entity.Id) (*Identity, error) {
114	ref := fmt.Sprintf("%s%s", identityRefPattern, id)
115	return read(repo, ref)
116}
117
118// ReadRemote load a remote Identity from the identities data available in git
119func ReadRemote(repo repository.Repo, remote string, id string) (*Identity, error) {
120	ref := fmt.Sprintf(identityRemoteRefPattern, remote) + id
121	return read(repo, ref)
122}
123
124// read will load and parse an identity from git
125func read(repo repository.Repo, ref string) (*Identity, error) {
126	refSplit := strings.Split(ref, "/")
127	id := entity.Id(refSplit[len(refSplit)-1])
128
129	if err := id.Validate(); err != nil {
130		return nil, errors.Wrap(err, "invalid ref")
131	}
132
133	hashes, err := repo.ListCommits(ref)
134
135	// TODO: this is not perfect, it might be a command invoke error
136	if err != nil {
137		return nil, ErrIdentityNotExist
138	}
139
140	i := &Identity{
141		id: id,
142	}
143
144	for _, hash := range hashes {
145		entries, err := repo.ReadTree(hash)
146		if err != nil {
147			return nil, errors.Wrap(err, "can't list git tree entries")
148		}
149
150		if len(entries) != 1 {
151			return nil, fmt.Errorf("invalid identity data at hash %s", hash)
152		}
153
154		entry := entries[0]
155
156		if entry.Name != versionEntryName {
157			return nil, fmt.Errorf("invalid identity data at hash %s", hash)
158		}
159
160		data, err := repo.ReadData(entry.Hash)
161		if err != nil {
162			return nil, errors.Wrap(err, "failed to read git blob data")
163		}
164
165		var version Version
166		err = json.Unmarshal(data, &version)
167
168		if err != nil {
169			return nil, errors.Wrapf(err, "failed to decode Identity version json %s", hash)
170		}
171
172		// tag the version with the commit hash
173		version.commitHash = hash
174		i.lastCommit = hash
175
176		i.versions = append(i.versions, &version)
177	}
178
179	return i, nil
180}
181
182// ListLocalIds list all the available local identity ids
183func ListLocalIds(repo repository.Repo) ([]entity.Id, error) {
184	refs, err := repo.ListRefs(identityRefPattern)
185	if err != nil {
186		return nil, err
187	}
188
189	return entity.RefsToIds(refs), nil
190}
191
192// RemoveIdentity will remove a local identity from its entity.Id
193func RemoveIdentity(repo repository.ClockedRepo, id entity.Id) error {
194	var fullMatches []string
195
196	refs, err := repo.ListRefs(identityRefPattern + id.String())
197	if err != nil {
198		return err
199	}
200	if len(refs) > 1 {
201		return NewErrMultipleMatchIdentity(entity.RefsToIds(refs))
202	}
203	if len(refs) == 1 {
204		// we have the identity locally
205		fullMatches = append(fullMatches, refs[0])
206	}
207
208	remotes, err := repo.GetRemotes()
209	if err != nil {
210		return err
211	}
212
213	for remote := range remotes {
214		remotePrefix := fmt.Sprintf(identityRemoteRefPattern+id.String(), remote)
215		remoteRefs, err := repo.ListRefs(remotePrefix)
216		if err != nil {
217			return err
218		}
219		if len(remoteRefs) > 1 {
220			return NewErrMultipleMatchIdentity(entity.RefsToIds(refs))
221		}
222		if len(remoteRefs) == 1 {
223			// found the identity in a remote
224			fullMatches = append(fullMatches, remoteRefs[0])
225		}
226	}
227
228	if len(fullMatches) == 0 {
229		return ErrIdentityNotExist
230	}
231
232	for _, ref := range fullMatches {
233		err = repo.RemoveRef(ref)
234		if err != nil {
235			return err
236		}
237	}
238
239	return nil
240}
241
242type StreamedIdentity struct {
243	Identity *Identity
244	Err      error
245}
246
247// ReadAllLocal read and parse all local Identity
248func ReadAllLocal(repo repository.ClockedRepo) <-chan StreamedIdentity {
249	return readAll(repo, identityRefPattern)
250}
251
252// ReadAllRemote read and parse all remote Identity for a given remote
253func ReadAllRemote(repo repository.ClockedRepo, remote string) <-chan StreamedIdentity {
254	refPrefix := fmt.Sprintf(identityRemoteRefPattern, remote)
255	return readAll(repo, refPrefix)
256}
257
258// readAll read and parse all available bug with a given ref prefix
259func readAll(repo repository.ClockedRepo, refPrefix string) <-chan StreamedIdentity {
260	out := make(chan StreamedIdentity)
261
262	go func() {
263		defer close(out)
264
265		refs, err := repo.ListRefs(refPrefix)
266		if err != nil {
267			out <- StreamedIdentity{Err: err}
268			return
269		}
270
271		for _, ref := range refs {
272			b, err := read(repo, ref)
273
274			if err != nil {
275				out <- StreamedIdentity{Err: err}
276				return
277			}
278
279			out <- StreamedIdentity{Identity: b}
280		}
281	}()
282
283	return out
284}
285
286type Mutator struct {
287	Name      string
288	Login     string
289	Email     string
290	AvatarUrl string
291	Keys      []*Key
292}
293
294// Mutate allow to create a new version of the Identity in one go
295func (i *Identity) Mutate(f func(orig Mutator) Mutator) {
296	orig := Mutator{
297		Name:      i.Name(),
298		Email:     i.Email(),
299		Login:     i.Login(),
300		AvatarUrl: i.AvatarUrl(),
301		Keys:      i.Keys(),
302	}
303	mutated := f(orig)
304	if reflect.DeepEqual(orig, mutated) {
305		return
306	}
307	i.versions = append(i.versions, &Version{
308		name:      mutated.Name,
309		email:     mutated.Email,
310		login:     mutated.Login,
311		avatarURL: mutated.AvatarUrl,
312		keys:      mutated.Keys,
313	})
314}
315
316// Write the identity into the Repository. In particular, this ensure that
317// the Id is properly set.
318func (i *Identity) Commit(repo repository.ClockedRepo) error {
319	// Todo: check for mismatch between memory and commit data
320
321	if !i.NeedCommit() {
322		return fmt.Errorf("can't commit an identity with no pending version")
323	}
324
325	if err := i.Validate(); err != nil {
326		return errors.Wrap(err, "can't commit an identity with invalid data")
327	}
328
329	for _, v := range i.versions {
330		if v.commitHash != "" {
331			i.lastCommit = v.commitHash
332			// ignore already commit versions
333			continue
334		}
335
336		// get the times where new versions starts to be valid
337		// TODO: instead of this hardcoded clock for bugs only, this need to be
338		// a vector of edit clock, one for each entity (bug, PR, config ..)
339		bugEditClock, err := repo.GetOrCreateClock("bug-edit")
340		if err != nil {
341			return err
342		}
343
344		v.time = bugEditClock.Time()
345		v.unixTime = time.Now().Unix()
346
347		blobHash, err := v.Write(repo)
348		if err != nil {
349			return err
350		}
351
352		// Make a git tree referencing the blob
353		tree := []repository.TreeEntry{
354			{ObjectType: repository.Blob, Hash: blobHash, Name: versionEntryName},
355		}
356
357		treeHash, err := repo.StoreTree(tree)
358		if err != nil {
359			return err
360		}
361
362		var commitHash repository.Hash
363		if i.lastCommit != "" {
364			commitHash, err = repo.StoreCommitWithParent(treeHash, i.lastCommit)
365		} else {
366			commitHash, err = repo.StoreCommit(treeHash)
367		}
368
369		if err != nil {
370			return err
371		}
372
373		i.lastCommit = commitHash
374		v.commitHash = commitHash
375
376		// if it was the first commit, use the commit hash as the Identity id
377		if i.id == "" || i.id == entity.UnsetId {
378			i.id = entity.Id(commitHash)
379		}
380	}
381
382	if i.id == "" {
383		panic("identity with no id")
384	}
385
386	ref := fmt.Sprintf("%s%s", identityRefPattern, i.id)
387	err := repo.UpdateRef(ref, i.lastCommit)
388
389	if err != nil {
390		return err
391	}
392
393	return nil
394}
395
396func (i *Identity) CommitAsNeeded(repo repository.ClockedRepo) error {
397	if !i.NeedCommit() {
398		return nil
399	}
400	return i.Commit(repo)
401}
402
403func (i *Identity) NeedCommit() bool {
404	for _, v := range i.versions {
405		if v.commitHash == "" {
406			return true
407		}
408	}
409
410	return false
411}
412
413// Merge will merge a different version of the same Identity
414//
415// To make sure that an Identity history can't be altered, a strict fast-forward
416// only policy is applied here. As an Identity should be tied to a single user, this
417// should work in practice but it does leave a possibility that a user would edit his
418// Identity from two different repo concurrently and push the changes in a non-centralized
419// network of repositories. In this case, it would result in some of the repo accepting one
420// version and some other accepting another, preventing the network in general to converge
421// to the same result. This would create a sort of partition of the network, and manual
422// cleaning would be required.
423//
424// An alternative approach would be to have a determinist rebase:
425// - any commits present in both local and remote version would be kept, never changed.
426// - newer commits would be merged in a linear chain of commits, ordered based on the
427//   Lamport time
428//
429// However, this approach leave the possibility, in the case of a compromised crypto keys,
430// of forging a new version with a bogus Lamport time to be inserted before a legit version,
431// invalidating the correct version and hijacking the Identity. There would only be a short
432// period of time where this would be possible (before the network converge) but I'm not
433// confident enough to implement that. I choose the strict fast-forward only approach,
434// despite it's potential problem with two different version as mentioned above.
435func (i *Identity) Merge(repo repository.Repo, other *Identity) (bool, error) {
436	if i.id != other.id {
437		return false, errors.New("merging unrelated identities is not supported")
438	}
439
440	if i.lastCommit == "" || other.lastCommit == "" {
441		return false, errors.New("can't merge identities that has never been stored")
442	}
443
444	modified := false
445	for j, otherVersion := range other.versions {
446		// if there is more version in other, take them
447		if len(i.versions) == j {
448			i.versions = append(i.versions, otherVersion)
449			i.lastCommit = otherVersion.commitHash
450			modified = true
451		}
452
453		// we have a non fast-forward merge.
454		// as explained in the doc above, refusing to merge
455		if i.versions[j].commitHash != otherVersion.commitHash {
456			return false, ErrNonFastForwardMerge
457		}
458	}
459
460	if modified {
461		err := repo.UpdateRef(identityRefPattern+i.id.String(), i.lastCommit)
462		if err != nil {
463			return false, err
464		}
465	}
466
467	return false, nil
468}
469
470// Validate check if the Identity data is valid
471func (i *Identity) Validate() error {
472	lastTime := lamport.Time(0)
473
474	if len(i.versions) == 0 {
475		return fmt.Errorf("no version")
476	}
477
478	for _, v := range i.versions {
479		if err := v.Validate(); err != nil {
480			return err
481		}
482
483		if v.commitHash != "" && v.time < lastTime {
484			return fmt.Errorf("non-chronological version (%d --> %d)", lastTime, v.time)
485		}
486
487		lastTime = v.time
488	}
489
490	// The identity Id should be the hash of the first commit
491	if i.versions[0].commitHash != "" && string(i.versions[0].commitHash) != i.id.String() {
492		return fmt.Errorf("identity id should be the first commit hash")
493	}
494
495	return nil
496}
497
498func (i *Identity) lastVersion() *Version {
499	if len(i.versions) <= 0 {
500		panic("no version at all")
501	}
502
503	return i.versions[len(i.versions)-1]
504}
505
506// Id return the Identity identifier
507func (i *Identity) Id() entity.Id {
508	if i.id == "" || i.id == entity.UnsetId {
509		// simply panic as it would be a coding error
510		// (using an id of an identity not stored yet)
511		panic("no id yet")
512	}
513	return i.id
514}
515
516// Name return the last version of the name
517func (i *Identity) Name() string {
518	return i.lastVersion().name
519}
520
521// Email return the last version of the email
522func (i *Identity) Email() string {
523	return i.lastVersion().email
524}
525
526// Login return the last version of the login
527func (i *Identity) Login() string {
528	return i.lastVersion().login
529}
530
531// AvatarUrl return the last version of the Avatar URL
532func (i *Identity) AvatarUrl() string {
533	return i.lastVersion().avatarURL
534}
535
536// Keys return the last version of the valid keys
537func (i *Identity) Keys() []*Key {
538	return i.lastVersion().keys
539}
540
541// ValidKeysAtTime return the set of keys valid at a given lamport time
542func (i *Identity) ValidKeysAtTime(time lamport.Time) []*Key {
543	var result []*Key
544
545	for _, v := range i.versions {
546		if v.time > time {
547			return result
548		}
549
550		result = v.keys
551	}
552
553	return result
554}
555
556// DisplayName return a non-empty string to display, representing the
557// identity, based on the non-empty values.
558func (i *Identity) DisplayName() string {
559	switch {
560	case i.Name() == "" && i.Login() != "":
561		return i.Login()
562	case i.Name() != "" && i.Login() == "":
563		return i.Name()
564	case i.Name() != "" && i.Login() != "":
565		return fmt.Sprintf("%s (%s)", i.Name(), i.Login())
566	}
567
568	panic("invalid person data")
569}
570
571// IsProtected return true if the chain of git commits started to be signed.
572// If that's the case, only signed commit with a valid key for this identity can be added.
573func (i *Identity) IsProtected() bool {
574	// Todo
575	return false
576}
577
578// LastModificationLamportTime return the Lamport time at which the last version of the identity became valid.
579func (i *Identity) LastModificationLamport() lamport.Time {
580	return i.lastVersion().time
581}
582
583// LastModification return the timestamp at which the last version of the identity became valid.
584func (i *Identity) LastModification() timestamp.Timestamp {
585	return timestamp.Timestamp(i.lastVersion().unixTime)
586}
587
588// SetMetadata store arbitrary metadata along the last not-commit Version.
589// If the Version has been commit to git already, a new identical version is added and will need to be
590// commit.
591func (i *Identity) SetMetadata(key string, value string) {
592	if i.lastVersion().commitHash != "" {
593		i.versions = append(i.versions, i.lastVersion().Clone())
594	}
595	i.lastVersion().SetMetadata(key, value)
596}
597
598// ImmutableMetadata return all metadata for this Identity, accumulated from each Version.
599// If multiple value are found, the first defined takes precedence.
600func (i *Identity) ImmutableMetadata() map[string]string {
601	metadata := make(map[string]string)
602
603	for _, version := range i.versions {
604		for key, value := range version.metadata {
605			if _, has := metadata[key]; !has {
606				metadata[key] = value
607			}
608		}
609	}
610
611	return metadata
612}
613
614// MutableMetadata return all metadata for this Identity, accumulated from each Version.
615// If multiple value are found, the last defined takes precedence.
616func (i *Identity) MutableMetadata() map[string]string {
617	metadata := make(map[string]string)
618
619	for _, version := range i.versions {
620		for key, value := range version.metadata {
621			metadata[key] = value
622		}
623	}
624
625	return metadata
626}
627
628// addVersionForTest add a new version to the identity
629// Only for testing !
630func (i *Identity) addVersionForTest(version *Version) {
631	i.versions = append(i.versions, version)
632}