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