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