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