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}