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