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 needCommit := false
245 for _, v := range i.versions {
246 if v.commitHash == "" {
247 needCommit = true
248 break
249 }
250 }
251
252 if !needCommit {
253 return fmt.Errorf("can't commit an identity with no pending version")
254 }
255
256 if err := i.Validate(); err != nil {
257 return errors.Wrap(err, "can't commit an identity with invalid data")
258 }
259
260 for _, v := range i.versions {
261 if v.commitHash != "" {
262 i.lastCommit = v.commitHash
263 // ignore already commited versions
264 continue
265 }
266
267 blobHash, err := v.Write(repo)
268 if err != nil {
269 return err
270 }
271
272 // Make a git tree referencing the blob
273 tree := []repository.TreeEntry{
274 {ObjectType: repository.Blob, Hash: blobHash, Name: versionEntryName},
275 }
276
277 treeHash, err := repo.StoreTree(tree)
278 if err != nil {
279 return err
280 }
281
282 var commitHash git.Hash
283 if i.lastCommit != "" {
284 commitHash, err = repo.StoreCommitWithParent(treeHash, i.lastCommit)
285 } else {
286 commitHash, err = repo.StoreCommit(treeHash)
287 }
288
289 if err != nil {
290 return err
291 }
292
293 i.lastCommit = commitHash
294 v.commitHash = commitHash
295
296 // if it was the first commit, use the commit hash as the Identity id
297 if i.id == "" {
298 i.id = string(commitHash)
299 }
300 }
301
302 if i.id == "" {
303 panic("identity with no id")
304 }
305
306 ref := fmt.Sprintf("%s%s", identityRefPattern, i.id)
307 err := repo.UpdateRef(ref, i.lastCommit)
308
309 if err != nil {
310 return err
311 }
312
313 return nil
314}
315
316// Merge will merge a different version of the same Identity
317//
318// To make sure that an Identity history can't be altered, a strict fast-forward
319// only policy is applied here. As an Identity should be tied to a single user, this
320// should work in practice but it does leave a possibility that a user would edit his
321// Identity from two different repo concurrently and push the changes in a non-centralized
322// network of repositories. In this case, it would result in some of the repo accepting one
323// version and some other accepting another, preventing the network in general to converge
324// to the same result. This would create a sort of partition of the network, and manual
325// cleaning would be required.
326//
327// An alternative approach would be to have a determinist rebase:
328// - any commits present in both local and remote version would be kept, never changed.
329// - newer commits would be merged in a linear chain of commits, ordered based on the
330// Lamport time
331//
332// However, this approach leave the possibility, in the case of a compromised crypto keys,
333// of forging a new version with a bogus Lamport time to be inserted before a legit version,
334// invalidating the correct version and hijacking the Identity. There would only be a short
335// period of time where this would be possible (before the network converge) but I'm not
336// confident enough to implement that. I choose the strict fast-forward only approach,
337// despite it's potential problem with two different version as mentioned above.
338func (i *Identity) Merge(repo repository.Repo, other *Identity) (bool, error) {
339 if i.id != other.id {
340 return false, errors.New("merging unrelated identities is not supported")
341 }
342
343 if i.lastCommit == "" || other.lastCommit == "" {
344 return false, errors.New("can't merge identities that has never been stored")
345 }
346
347 /*ancestor, err := repo.FindCommonAncestor(i.lastCommit, other.lastCommit)
348 if err != nil {
349 return false, errors.Wrap(err, "can't find common ancestor")
350 }*/
351
352 modified := false
353 for j, otherVersion := range other.versions {
354 // if there is more version in other, take them
355 if len(i.versions) == j {
356 i.versions = append(i.versions, otherVersion)
357 i.lastCommit = otherVersion.commitHash
358 modified = true
359 }
360
361 // we have a non fast-forward merge.
362 // as explained in the doc above, refusing to merge
363 if i.versions[j].commitHash != otherVersion.commitHash {
364 return false, ErrNonFastForwardMerge
365 }
366 }
367
368 if modified {
369 err := repo.UpdateRef(identityRefPattern+i.id, i.lastCommit)
370 if err != nil {
371 return false, err
372 }
373 }
374
375 return false, nil
376}
377
378// Validate check if the Identity data is valid
379func (i *Identity) Validate() error {
380 lastTime := lamport.Time(0)
381
382 for _, v := range i.versions {
383 if err := v.Validate(); err != nil {
384 return err
385 }
386
387 if v.time < lastTime {
388 return fmt.Errorf("non-chronological version (%d --> %d)", lastTime, v.time)
389 }
390
391 lastTime = v.time
392 }
393
394 return nil
395}
396
397func (i *Identity) lastVersion() *Version {
398 if len(i.versions) <= 0 {
399 panic("no version at all")
400 }
401
402 return i.versions[len(i.versions)-1]
403}
404
405// Id return the Identity identifier
406func (i *Identity) Id() string {
407 if i.id == "" {
408 // simply panic as it would be a coding error
409 // (using an id of an identity not stored yet)
410 panic("no id yet")
411 }
412 return i.id
413}
414
415// Name return the last version of the name
416func (i *Identity) Name() string {
417 return i.lastVersion().name
418}
419
420// Email return the last version of the email
421func (i *Identity) Email() string {
422 return i.lastVersion().email
423}
424
425// Login return the last version of the login
426func (i *Identity) Login() string {
427 return i.lastVersion().login
428}
429
430// AvatarUrl return the last version of the Avatar URL
431func (i *Identity) AvatarUrl() string {
432 return i.lastVersion().avatarURL
433}
434
435// Keys return the last version of the valid keys
436func (i *Identity) Keys() []Key {
437 return i.lastVersion().keys
438}
439
440// IsProtected return true if the chain of git commits started to be signed.
441// If that's the case, only signed commit with a valid key for this identity can be added.
442func (i *Identity) IsProtected() bool {
443 // Todo
444 return false
445}
446
447// ValidKeysAtTime return the set of keys valid at a given lamport time
448func (i *Identity) ValidKeysAtTime(time lamport.Time) []Key {
449 var result []Key
450
451 for _, v := range i.versions {
452 if v.time > time {
453 return result
454 }
455
456 result = v.keys
457 }
458
459 return result
460}
461
462// DisplayName return a non-empty string to display, representing the
463// identity, based on the non-empty values.
464func (i *Identity) DisplayName() string {
465 switch {
466 case i.Name() == "" && i.Login() != "":
467 return i.Login()
468 case i.Name() != "" && i.Login() == "":
469 return i.Name()
470 case i.Name() != "" && i.Login() != "":
471 return fmt.Sprintf("%s (%s)", i.Name(), i.Login())
472 }
473
474 panic("invalid person data")
475}
476
477// SetMetadata store arbitrary metadata along the last defined Version.
478// If the Version has been commit to git already, it won't be overwritten.
479func (i *Identity) SetMetadata(key string, value string) {
480 i.lastVersion().SetMetadata(key, value)
481}
482
483// ImmutableMetadata return all metadata for this Identity, accumulated from each Version.
484// If multiple value are found, the first defined takes precedence.
485func (i *Identity) ImmutableMetadata() map[string]string {
486 metadata := make(map[string]string)
487
488 for _, version := range i.versions {
489 for key, value := range version.metadata {
490 if _, has := metadata[key]; !has {
491 metadata[key] = value
492 }
493 }
494 }
495
496 return metadata
497}
498
499// MutableMetadata return all metadata for this Identity, accumulated from each Version.
500// If multiple value are found, the last defined takes precedence.
501func (i *Identity) MutableMetadata() map[string]string {
502 metadata := make(map[string]string)
503
504 for _, version := range i.versions {
505 for key, value := range version.metadata {
506 metadata[key] = value
507 }
508 }
509
510 return metadata
511}