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// SetUserIdentity store the user identity's id in the git config
208func SetUserIdentity(repo repository.RepoCommon, identity Identity) error {
209 return repo.StoreConfig(identityConfigKey, identity.Id())
210}
211
212// GetUserIdentity read the current user identity, set with a git config entry
213func GetUserIdentity(repo repository.Repo) (*Identity, error) {
214 configs, err := repo.ReadConfigs(identityConfigKey)
215 if err != nil {
216 return nil, err
217 }
218
219 if len(configs) == 0 {
220 return nil, fmt.Errorf("no identity set")
221 }
222
223 if len(configs) > 1 {
224 return nil, fmt.Errorf("multiple identity config exist")
225 }
226
227 var id string
228 for _, val := range configs {
229 id = val
230 }
231
232 return ReadLocal(repo, id)
233}
234
235func (i *Identity) AddVersion(version *Version) {
236 i.versions = append(i.versions, version)
237}
238
239// Write the identity into the Repository. In particular, this ensure that
240// the Id is properly set.
241func (i *Identity) Commit(repo repository.Repo) error {
242 // Todo: check for mismatch between memory and commited data
243
244 if !i.NeedCommit() {
245 return fmt.Errorf("can't commit an identity with no pending version")
246 }
247
248 if err := i.Validate(); err != nil {
249 return errors.Wrap(err, "can't commit an identity with invalid data")
250 }
251
252 for _, v := range i.versions {
253 if v.commitHash != "" {
254 i.lastCommit = v.commitHash
255 // ignore already commited versions
256 continue
257 }
258
259 blobHash, err := v.Write(repo)
260 if err != nil {
261 return err
262 }
263
264 // Make a git tree referencing the blob
265 tree := []repository.TreeEntry{
266 {ObjectType: repository.Blob, Hash: blobHash, Name: versionEntryName},
267 }
268
269 treeHash, err := repo.StoreTree(tree)
270 if err != nil {
271 return err
272 }
273
274 var commitHash git.Hash
275 if i.lastCommit != "" {
276 commitHash, err = repo.StoreCommitWithParent(treeHash, i.lastCommit)
277 } else {
278 commitHash, err = repo.StoreCommit(treeHash)
279 }
280
281 if err != nil {
282 return err
283 }
284
285 i.lastCommit = commitHash
286 v.commitHash = commitHash
287
288 // if it was the first commit, use the commit hash as the Identity id
289 if i.id == "" {
290 i.id = string(commitHash)
291 }
292 }
293
294 if i.id == "" {
295 panic("identity with no id")
296 }
297
298 ref := fmt.Sprintf("%s%s", identityRefPattern, i.id)
299 err := repo.UpdateRef(ref, i.lastCommit)
300
301 if err != nil {
302 return err
303 }
304
305 return nil
306}
307
308func (i *Identity) CommitAsNeeded(repo repository.Repo) error {
309 if !i.NeedCommit() {
310 return nil
311 }
312 return i.Commit(repo)
313}
314
315func (i *Identity) NeedCommit() bool {
316 for _, v := range i.versions {
317 if v.commitHash == "" {
318 return true
319 }
320 }
321
322 return false
323}
324
325// Merge will merge a different version of the same Identity
326//
327// To make sure that an Identity history can't be altered, a strict fast-forward
328// only policy is applied here. As an Identity should be tied to a single user, this
329// should work in practice but it does leave a possibility that a user would edit his
330// Identity from two different repo concurrently and push the changes in a non-centralized
331// network of repositories. In this case, it would result in some of the repo accepting one
332// version and some other accepting another, preventing the network in general to converge
333// to the same result. This would create a sort of partition of the network, and manual
334// cleaning would be required.
335//
336// An alternative approach would be to have a determinist rebase:
337// - any commits present in both local and remote version would be kept, never changed.
338// - newer commits would be merged in a linear chain of commits, ordered based on the
339// Lamport time
340//
341// However, this approach leave the possibility, in the case of a compromised crypto keys,
342// of forging a new version with a bogus Lamport time to be inserted before a legit version,
343// invalidating the correct version and hijacking the Identity. There would only be a short
344// period of time where this would be possible (before the network converge) but I'm not
345// confident enough to implement that. I choose the strict fast-forward only approach,
346// despite it's potential problem with two different version as mentioned above.
347func (i *Identity) Merge(repo repository.Repo, other *Identity) (bool, error) {
348 if i.id != other.id {
349 return false, errors.New("merging unrelated identities is not supported")
350 }
351
352 if i.lastCommit == "" || other.lastCommit == "" {
353 return false, errors.New("can't merge identities that has never been stored")
354 }
355
356 /*ancestor, err := repo.FindCommonAncestor(i.lastCommit, other.lastCommit)
357 if err != nil {
358 return false, errors.Wrap(err, "can't find common ancestor")
359 }*/
360
361 modified := false
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 i.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, i.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 lastTime := lamport.Time(0)
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 if v.time < lastTime {
401 return fmt.Errorf("non-chronological version (%d --> %d)", lastTime, v.time)
402 }
403
404 lastTime = v.time
405 }
406
407 // The identity ID should be the hash of the first commit
408 if i.versions[0].commitHash != "" && string(i.versions[0].commitHash) != i.id {
409 return fmt.Errorf("identity id should be the first commit hash")
410 }
411
412 return nil
413}
414
415func (i *Identity) lastVersion() *Version {
416 if len(i.versions) <= 0 {
417 panic("no version at all")
418 }
419
420 return i.versions[len(i.versions)-1]
421}
422
423// Id return the Identity identifier
424func (i *Identity) Id() string {
425 if i.id == "" {
426 // simply panic as it would be a coding error
427 // (using an id of an identity not stored yet)
428 panic("no id yet")
429 }
430 return i.id
431}
432
433// Name return the last version of the name
434func (i *Identity) Name() string {
435 return i.lastVersion().name
436}
437
438// Email return the last version of the email
439func (i *Identity) Email() string {
440 return i.lastVersion().email
441}
442
443// Login return the last version of the login
444func (i *Identity) Login() string {
445 return i.lastVersion().login
446}
447
448// AvatarUrl return the last version of the Avatar URL
449func (i *Identity) AvatarUrl() string {
450 return i.lastVersion().avatarURL
451}
452
453// Keys return the last version of the valid keys
454func (i *Identity) Keys() []Key {
455 return i.lastVersion().keys
456}
457
458// IsProtected return true if the chain of git commits started to be signed.
459// If that's the case, only signed commit with a valid key for this identity can be added.
460func (i *Identity) IsProtected() bool {
461 // Todo
462 return false
463}
464
465// ValidKeysAtTime return the set of keys valid at a given lamport time
466func (i *Identity) ValidKeysAtTime(time lamport.Time) []Key {
467 var result []Key
468
469 for _, v := range i.versions {
470 if v.time > time {
471 return result
472 }
473
474 result = v.keys
475 }
476
477 return result
478}
479
480// DisplayName return a non-empty string to display, representing the
481// identity, based on the non-empty values.
482func (i *Identity) DisplayName() string {
483 switch {
484 case i.Name() == "" && i.Login() != "":
485 return i.Login()
486 case i.Name() != "" && i.Login() == "":
487 return i.Name()
488 case i.Name() != "" && i.Login() != "":
489 return fmt.Sprintf("%s (%s)", i.Name(), i.Login())
490 }
491
492 panic("invalid person data")
493}
494
495// SetMetadata store arbitrary metadata along the last defined Version.
496// If the Version has been commit to git already, it won't be overwritten.
497func (i *Identity) SetMetadata(key string, value string) {
498 i.lastVersion().SetMetadata(key, value)
499}
500
501// ImmutableMetadata return all metadata for this Identity, accumulated from each Version.
502// If multiple value are found, the first defined takes precedence.
503func (i *Identity) ImmutableMetadata() map[string]string {
504 metadata := make(map[string]string)
505
506 for _, version := range i.versions {
507 for key, value := range version.metadata {
508 if _, has := metadata[key]; !has {
509 metadata[key] = value
510 }
511 }
512 }
513
514 return metadata
515}
516
517// MutableMetadata return all metadata for this Identity, accumulated from each Version.
518// If multiple value are found, the last defined takes precedence.
519func (i *Identity) MutableMetadata() map[string]string {
520 metadata := make(map[string]string)
521
522 for _, version := range i.versions {
523 for key, value := range version.metadata {
524 metadata[key] = value
525 }
526 }
527
528 return metadata
529}