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