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