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 bootstrap "github.com/MichaelMure/git-bug/entity/boostrap"
13 "github.com/MichaelMure/git-bug/repository"
14 "github.com/MichaelMure/git-bug/util/lamport"
15 "github.com/MichaelMure/git-bug/util/timestamp"
16)
17
18const identityRefPattern = "refs/identities/"
19const identityRemoteRefPattern = "refs/remotes/%s/identities/"
20const versionEntryName = "version"
21const identityConfigKey = "git-bug.identity"
22
23const Typename = "identity"
24const Namespace = "identities"
25
26var ErrNonFastForwardMerge = errors.New("non fast-forward identity merge")
27var ErrNoIdentitySet = errors.New("No identity is set.\n" +
28 "To interact with bugs, an identity first needs to be created using " +
29 "\"git bug user new\" or adopted with \"git bug user adopt\"")
30var ErrMultipleIdentitiesSet = errors.New("multiple user identities set")
31
32var _ entity.Identity = &Identity{}
33var _ bootstrap.Entity = &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 []bootstrap.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 bootstrap.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 := bootstrap.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, bootstrap.NewErrNotFound(Typename)
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) ([]bootstrap.Id, error) {
160 refs, err := repo.ListRefs(identityRefPattern)
161 if err != nil {
162 return nil, err
163 }
164
165 return bootstrap.RefsToIds(refs), nil
166}
167
168// ReadAllLocal read and parse all local Identity
169func ReadAllLocal(repo repository.ClockedRepo) <-chan bootstrap.StreamedEntity[*Identity] {
170 return readAll(repo, identityRefPattern)
171}
172
173// ReadAllRemote read and parse all remote Identity for a given remote
174func ReadAllRemote(repo repository.ClockedRepo, remote string) <-chan bootstrap.StreamedEntity[*Identity] {
175 refPrefix := fmt.Sprintf(identityRemoteRefPattern, remote)
176 return readAll(repo, refPrefix)
177}
178
179// readAll read and parse all available bug with a given ref prefix
180func readAll(repo repository.ClockedRepo, refPrefix string) <-chan bootstrap.StreamedEntity[*Identity] {
181 out := make(chan bootstrap.StreamedEntity[*Identity])
182
183 go func() {
184 defer close(out)
185
186 refs, err := repo.ListRefs(refPrefix)
187 if err != nil {
188 out <- bootstrap.StreamedEntity[*Identity]{Err: err}
189 return
190 }
191
192 total := int64(len(refs))
193 current := int64(1)
194
195 for _, ref := range refs {
196 i, err := read(repo, ref)
197
198 if err != nil {
199 out <- bootstrap.StreamedEntity[*Identity]{Err: err}
200 return
201 }
202
203 out <- bootstrap.StreamedEntity[*Identity]{
204 Entity: i,
205 CurrentEntity: current,
206 TotalEntities: total,
207 }
208 current++
209 }
210 }()
211
212 return out
213}
214
215type Mutator struct {
216 Name string
217 Login string
218 Email string
219 AvatarUrl string
220 Keys []bootstrap.Key
221}
222
223// Mutate allow to create a new version of the Identity in one go
224func (i *Identity) Mutate(repo repository.RepoClock, f func(orig *Mutator)) error {
225 copyKeys := func(keys []bootstrap.Key) []bootstrap.Key {
226 result := make([]bootstrap.Key, len(keys))
227 for i, key := range keys {
228 result[i] = key.Clone()
229 }
230 return result
231 }
232
233 orig := Mutator{
234 Name: i.Name(),
235 Email: i.Email(),
236 Login: i.Login(),
237 AvatarUrl: i.AvatarUrl(),
238 Keys: copyKeys(i.Keys()),
239 }
240 mutated := orig
241 mutated.Keys = copyKeys(orig.Keys)
242
243 f(&mutated)
244
245 if reflect.DeepEqual(orig, mutated) {
246 return nil
247 }
248
249 v, err := newVersion(repo,
250 mutated.Name,
251 mutated.Email,
252 mutated.Login,
253 mutated.AvatarUrl,
254 mutated.Keys,
255 )
256 if err != nil {
257 return err
258 }
259
260 i.versions = append(i.versions, v)
261 return nil
262}
263
264// Commit write the identity into the Repository. In particular, this ensures that
265// the Id is properly set.
266func (i *Identity) Commit(repo repository.ClockedRepo) error {
267 if !i.NeedCommit() {
268 return fmt.Errorf("can't commit an identity with no pending version")
269 }
270
271 if err := i.Validate(); err != nil {
272 return errors.Wrap(err, "can't commit an identity with invalid data")
273 }
274
275 var lastCommit repository.Hash
276 for _, v := range i.versions {
277 if v.commitHash != "" {
278 lastCommit = v.commitHash
279 // ignore already commit versions
280 continue
281 }
282
283 blobHash, err := v.Write(repo)
284 if err != nil {
285 return err
286 }
287
288 // Make a git tree referencing the blob
289 tree := []repository.TreeEntry{
290 {ObjectType: repository.Blob, Hash: blobHash, Name: versionEntryName},
291 }
292
293 treeHash, err := repo.StoreTree(tree)
294 if err != nil {
295 return err
296 }
297
298 var commitHash repository.Hash
299 if lastCommit != "" {
300 commitHash, err = repo.StoreCommit(treeHash, lastCommit)
301 } else {
302 commitHash, err = repo.StoreCommit(treeHash)
303 }
304 if err != nil {
305 return err
306 }
307
308 lastCommit = commitHash
309 v.commitHash = commitHash
310 }
311
312 ref := fmt.Sprintf("%s%s", identityRefPattern, i.Id().String())
313 return repo.UpdateRef(ref, lastCommit)
314}
315
316func (i *Identity) CommitAsNeeded(repo repository.ClockedRepo) error {
317 if !i.NeedCommit() {
318 return nil
319 }
320 return i.Commit(repo)
321}
322
323func (i *Identity) NeedCommit() bool {
324 for _, v := range i.versions {
325 if v.commitHash == "" {
326 return true
327 }
328 }
329
330 return false
331}
332
333// Merge will merge a different version of the same Identity
334//
335// To make sure that an Identity history can't be altered, a strict fast-forward
336// only policy is applied here. As an Identity should be tied to a single user, this
337// should work in practice, but it does leave a possibility that a user would edit his
338// Identity from two different repo concurrently and push the changes in a non-centralized
339// network of repositories. In this case, it would result in some repo accepting one
340// version and some other accepting another, preventing the network in general to converge
341// to the same result. This would create a sort of partition of the network, and manual
342// cleaning would be required.
343//
344// An alternative approach would be to have a determinist rebase:
345// - any commits present in both local and remote version would be kept, never changed.
346// - newer commits would be merged in a linear chain of commits, ordered based on the
347// Lamport time
348//
349// However, this approach leave the possibility, in the case of a compromised crypto keys,
350// of forging a new version with a bogus Lamport time to be inserted before a legit version,
351// invalidating the correct version and hijacking the Identity. There would only be a short
352// period of time when this would be possible (before the network converge) but I'm not
353// confident enough to implement that. I choose the strict fast-forward only approach,
354// despite its potential problem with two different version as mentioned above.
355func (i *Identity) Merge(repo repository.Repo, other *Identity) (bool, error) {
356 if i.Id() != other.Id() {
357 return false, errors.New("merging unrelated identities is not supported")
358 }
359
360 modified := false
361 var lastCommit repository.Hash
362 for j, otherVersion := range other.versions {
363 // if there is more version in other, take them
364 if len(i.versions) == j {
365 i.versions = append(i.versions, otherVersion)
366 lastCommit = otherVersion.commitHash
367 modified = true
368 }
369
370 // we have a non fast-forward merge.
371 // as explained in the doc above, refusing to merge
372 if i.versions[j].commitHash != otherVersion.commitHash {
373 return false, ErrNonFastForwardMerge
374 }
375 }
376
377 if modified {
378 err := repo.UpdateRef(identityRefPattern+i.Id().String(), lastCommit)
379 if err != nil {
380 return false, err
381 }
382 }
383
384 return false, nil
385}
386
387// Validate check if the Identity data is valid
388func (i *Identity) Validate() error {
389 lastTimes := make(map[string]lamport.Time)
390
391 if len(i.versions) == 0 {
392 return fmt.Errorf("no version")
393 }
394
395 for _, v := range i.versions {
396 if err := v.Validate(); err != nil {
397 return err
398 }
399
400 // check for always increasing lamport time
401 // check that a new version didn't drop a clock
402 for name, previous := range lastTimes {
403 if now, ok := v.times[name]; ok {
404 if now < previous {
405 return fmt.Errorf("non-chronological lamport clock %s (%d --> %d)", name, previous, now)
406 }
407 } else {
408 return fmt.Errorf("version has less lamport clocks than before (missing %s)", name)
409 }
410 }
411
412 for name, now := range v.times {
413 lastTimes[name] = now
414 }
415 }
416
417 return nil
418}
419
420func (i *Identity) lastVersion() *version {
421 if len(i.versions) <= 0 {
422 panic("no version at all")
423 }
424
425 return i.versions[len(i.versions)-1]
426}
427
428// Id return the Identity identifier
429func (i *Identity) Id() bootstrap.Id {
430 // id is the id of the first version
431 return i.versions[0].Id()
432}
433
434// Name return the last version of the name
435func (i *Identity) Name() string {
436 return i.lastVersion().name
437}
438
439// DisplayName return a non-empty string to display, representing the
440// identity, based on the non-empty values.
441func (i *Identity) DisplayName() string {
442 switch {
443 case i.Name() == "" && i.Login() != "":
444 return i.Login()
445 case i.Name() != "" && i.Login() == "":
446 return i.Name()
447 case i.Name() != "" && i.Login() != "":
448 return fmt.Sprintf("%s (%s)", i.Name(), i.Login())
449 }
450
451 panic("invalid person data")
452}
453
454// Email return the last version of the email
455func (i *Identity) Email() string {
456 return i.lastVersion().email
457}
458
459// Login return the last version of the login
460func (i *Identity) Login() string {
461 return i.lastVersion().login
462}
463
464// AvatarUrl return the last version of the Avatar URL
465func (i *Identity) AvatarUrl() string {
466 return i.lastVersion().avatarURL
467}
468
469// Keys return the last version of the valid keys
470func (i *Identity) Keys() []bootstrap.Key {
471 return i.lastVersion().keys
472}
473
474// SigningKey return the key that should be used to sign new messages. If no key is available, return nil.
475func (i *Identity) SigningKey(repo repository.RepoKeyring) (bootstrap.Key, error) {
476 keys := i.Keys()
477 for _, key := range keys {
478 err := key.EnsurePrivateKey(repo)
479 if err == errNoPrivateKey {
480 continue
481 }
482 if err != nil {
483 return nil, err
484 }
485 return key, nil
486 }
487 return nil, nil
488}
489
490// ValidKeysAtTime return the set of keys valid at a given lamport time
491func (i *Identity) ValidKeysAtTime(clockName string, time lamport.Time) []bootstrap.Key {
492 var result []bootstrap.Key
493
494 var lastTime lamport.Time
495 for _, v := range i.versions {
496 refTime, ok := v.times[clockName]
497 if !ok {
498 refTime = lastTime
499 }
500 lastTime = refTime
501
502 if refTime > time {
503 return result
504 }
505
506 result = v.keys
507 }
508
509 return result
510}
511
512// LastModification return the timestamp at which the last version of the identity became valid.
513func (i *Identity) LastModification() timestamp.Timestamp {
514 return timestamp.Timestamp(i.lastVersion().unixTime)
515}
516
517// LastModificationLamports return the lamport times at which the last version of the identity became valid.
518func (i *Identity) LastModificationLamports() map[string]lamport.Time {
519 return i.lastVersion().times
520}
521
522// IsProtected return true if the chain of git commits started to be signed.
523// If that's the case, only signed commit with a valid key for this identity can be added.
524func (i *Identity) IsProtected() bool {
525 // Todo
526 return false
527}
528
529// SetMetadata store arbitrary metadata along the last not-commit version.
530// If the version has been commit to git already, a new identical version is added and will need to be
531// commit.
532func (i *Identity) SetMetadata(key string, value string) {
533 // once commit, data is immutable so we create a new version
534 if i.lastVersion().commitHash != "" {
535 i.versions = append(i.versions, i.lastVersion().Clone())
536 }
537 // if Id() has been called, we can't change the first version anymore, so we create a new version
538 if len(i.versions) == 1 && i.versions[0].id != bootstrap.UnsetId && i.versions[0].id != "" {
539 i.versions = append(i.versions, i.lastVersion().Clone())
540 }
541
542 i.lastVersion().SetMetadata(key, value)
543}
544
545// ImmutableMetadata return all metadata for this Identity, accumulated from each version.
546// If multiple value are found, the first defined takes precedence.
547func (i *Identity) ImmutableMetadata() map[string]string {
548 metadata := make(map[string]string)
549
550 for _, version := range i.versions {
551 for key, value := range version.metadata {
552 if _, has := metadata[key]; !has {
553 metadata[key] = value
554 }
555 }
556 }
557
558 return metadata
559}
560
561// MutableMetadata return all metadata for this Identity, accumulated from each version.
562// If multiple value are found, the last defined takes precedence.
563func (i *Identity) MutableMetadata() map[string]string {
564 metadata := make(map[string]string)
565
566 for _, version := range i.versions {
567 for key, value := range version.metadata {
568 metadata[key] = value
569 }
570 }
571
572 return metadata
573}