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/MichaelMure/git-bug/repository"
10 "github.com/MichaelMure/git-bug/util/git"
11 "github.com/MichaelMure/git-bug/util/lamport"
12 "github.com/pkg/errors"
13)
14
15const identityRefPattern = "refs/identities/"
16const versionEntryName = "version"
17const identityConfigKey = "git-bug.identity"
18
19var ErrIdentityNotExist = errors.New("identity doesn't exist")
20
21type ErrMultipleMatch struct {
22 Matching []string
23}
24
25func (e ErrMultipleMatch) Error() string {
26 return fmt.Sprintf("Multiple matching identities found:\n%s", strings.Join(e.Matching, "\n"))
27}
28
29var _ Interface = &Identity{}
30
31type Identity struct {
32 id string
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
62type identityJson struct {
63 Id string `json:"id"`
64}
65
66// MarshalJSON will only serialize the id
67func (i *Identity) MarshalJSON() ([]byte, error) {
68 return json.Marshal(identityJson{
69 Id: i.Id(),
70 })
71}
72
73// UnmarshalJSON will only read the id
74// Users of this package are expected to run Load() to load
75// the remaining data from the identities data in git.
76func (i *Identity) UnmarshalJSON(data []byte) error {
77 aux := identityJson{}
78
79 if err := json.Unmarshal(data, &aux); err != nil {
80 return err
81 }
82
83 i.id = aux.Id
84
85 return nil
86}
87
88// TODO: load/write from OpBase
89
90// Read load an Identity from the identities data available in git
91func Read(repo repository.Repo, id string) (*Identity, error) {
92 i := &Identity{
93 id: id,
94 }
95
96 err := i.Load(repo)
97 if err != nil {
98 return nil, err
99 }
100
101 return i, nil
102}
103
104// Load will read the corresponding identity data from git and replace any
105// data already loaded if any.
106func (i *Identity) Load(repo repository.Repo) error {
107 ref := fmt.Sprintf("%s%s", identityRefPattern, i.Id())
108
109 hashes, err := repo.ListCommits(ref)
110
111 var versions []*Version
112
113 // TODO: this is not perfect, it might be a command invoke error
114 if err != nil {
115 return ErrIdentityNotExist
116 }
117
118 for _, hash := range hashes {
119 entries, err := repo.ListEntries(hash)
120 if err != nil {
121 return errors.Wrap(err, "can't list git tree entries")
122 }
123
124 if len(entries) != 1 {
125 return fmt.Errorf("invalid identity data at hash %s", hash)
126 }
127
128 entry := entries[0]
129
130 if entry.Name != versionEntryName {
131 return fmt.Errorf("invalid identity data at hash %s", hash)
132 }
133
134 data, err := repo.ReadData(entry.Hash)
135 if err != nil {
136 return errors.Wrap(err, "failed to read git blob data")
137 }
138
139 var version Version
140 err = json.Unmarshal(data, &version)
141
142 if err != nil {
143 return errors.Wrapf(err, "failed to decode Identity version json %s", hash)
144 }
145
146 // tag the version with the commit hash
147 version.commitHash = hash
148
149 versions = append(versions, &version)
150 }
151
152 i.Versions = versions
153
154 return nil
155}
156
157// NewFromGitUser will query the repository for user detail and
158// build the corresponding Identity
159func NewFromGitUser(repo repository.Repo) (*Identity, error) {
160 name, err := repo.GetUserName()
161 if err != nil {
162 return nil, err
163 }
164 if name == "" {
165 return nil, errors.New("user name is not configured in git yet. Please use `git config --global user.name \"John Doe\"`")
166 }
167
168 email, err := repo.GetUserEmail()
169 if err != nil {
170 return nil, err
171 }
172 if email == "" {
173 return nil, errors.New("user name is not configured in git yet. Please use `git config --global user.email johndoe@example.com`")
174 }
175
176 return NewIdentity(name, email), nil
177}
178
179// BuildFromGit will query the repository for user detail and
180// build the corresponding Identity
181/*func BuildFromGit(repo repository.Repo) *Identity {
182 version := Version{}
183
184 name, err := repo.GetUserName()
185 if err == nil {
186 version.Name = name
187 }
188
189 email, err := repo.GetUserEmail()
190 if err == nil {
191 version.Email = email
192 }
193
194 return &Identity{
195 Versions: []Version{
196 version,
197 },
198 }
199}*/
200
201// SetIdentity store the user identity's id in the git config
202func SetIdentity(repo repository.RepoCommon, identity Identity) error {
203 return repo.StoreConfig(identityConfigKey, identity.Id())
204}
205
206// GetIdentity read the current user identity, set with a git config entry
207func GetIdentity(repo repository.Repo) (*Identity, error) {
208 configs, err := repo.ReadConfigs(identityConfigKey)
209 if err != nil {
210 return nil, err
211 }
212
213 if len(configs) == 0 {
214 return nil, fmt.Errorf("no identity set")
215 }
216
217 if len(configs) > 1 {
218 return nil, fmt.Errorf("multiple identity config exist")
219 }
220
221 var id string
222 for _, val := range configs {
223 id = val
224 }
225
226 return Read(repo, id)
227}
228
229func (i *Identity) AddVersion(version *Version) {
230 i.Versions = append(i.Versions, version)
231}
232
233func (i *Identity) Commit(repo repository.ClockedRepo) error {
234 // Todo: check for mismatch between memory and commited data
235
236 var lastCommit git.Hash = ""
237
238 for _, v := range i.Versions {
239 if v.commitHash != "" {
240 lastCommit = v.commitHash
241 // ignore already commited versions
242 continue
243 }
244
245 blobHash, err := v.Write(repo)
246 if err != nil {
247 return err
248 }
249
250 // Make a git tree referencing the blob
251 tree := []repository.TreeEntry{
252 {ObjectType: repository.Blob, Hash: blobHash, Name: versionEntryName},
253 }
254
255 treeHash, err := repo.StoreTree(tree)
256 if err != nil {
257 return err
258 }
259
260 var commitHash git.Hash
261 if lastCommit != "" {
262 commitHash, err = repo.StoreCommitWithParent(treeHash, lastCommit)
263 } else {
264 commitHash, err = repo.StoreCommit(treeHash)
265 }
266
267 if err != nil {
268 return err
269 }
270
271 lastCommit = commitHash
272
273 // if it was the first commit, use the commit hash as the Identity id
274 if i.id == "" {
275 i.id = string(commitHash)
276 }
277 }
278
279 if i.id == "" {
280 panic("identity with no id")
281 }
282
283 ref := fmt.Sprintf("%s%s", identityRefPattern, i.id)
284 err := repo.UpdateRef(ref, lastCommit)
285
286 if err != nil {
287 return err
288 }
289
290 return nil
291}
292
293// Validate check if the Identity data is valid
294func (i *Identity) Validate() error {
295 lastTime := lamport.Time(0)
296
297 for _, v := range i.Versions {
298 if err := v.Validate(); err != nil {
299 return err
300 }
301
302 if v.Time < lastTime {
303 return fmt.Errorf("non-chronological version (%d --> %d)", lastTime, v.Time)
304 }
305
306 lastTime = v.Time
307 }
308
309 return nil
310}
311
312func (i *Identity) firstVersion() *Version {
313 if len(i.Versions) <= 0 {
314 panic("no version at all")
315 }
316
317 return i.Versions[0]
318}
319
320func (i *Identity) lastVersion() *Version {
321 if len(i.Versions) <= 0 {
322 panic("no version at all")
323 }
324
325 return i.Versions[len(i.Versions)-1]
326}
327
328// Id return the Identity identifier
329func (i *Identity) Id() string {
330 if i.id == "" {
331 // simply panic as it would be a coding error
332 // (using an id of an identity not stored yet)
333 panic("no id yet")
334 }
335 return i.id
336}
337
338// Name return the last version of the name
339func (i *Identity) Name() string {
340 return i.lastVersion().Name
341}
342
343// Email return the last version of the email
344func (i *Identity) Email() string {
345 return i.lastVersion().Email
346}
347
348// Login return the last version of the login
349func (i *Identity) Login() string {
350 return i.lastVersion().Login
351}
352
353// Login return the last version of the Avatar URL
354func (i *Identity) AvatarUrl() string {
355 return i.lastVersion().AvatarUrl
356}
357
358// Login return the last version of the valid keys
359func (i *Identity) Keys() []Key {
360 return i.lastVersion().Keys
361}
362
363// IsProtected return true if the chain of git commits started to be signed.
364// If that's the case, only signed commit with a valid key for this identity can be added.
365func (i *Identity) IsProtected() bool {
366 // Todo
367 return false
368}
369
370// ValidKeysAtTime return the set of keys valid at a given lamport time
371func (i *Identity) ValidKeysAtTime(time lamport.Time) []Key {
372 var result []Key
373
374 for _, v := range i.Versions {
375 if v.Time > time {
376 return result
377 }
378
379 result = v.Keys
380 }
381
382 return result
383}
384
385// Match tell is the Identity match the given query string
386func (i *Identity) Match(query string) bool {
387 query = strings.ToLower(query)
388
389 return strings.Contains(strings.ToLower(i.Name()), query) ||
390 strings.Contains(strings.ToLower(i.Login()), query)
391}
392
393// DisplayName return a non-empty string to display, representing the
394// identity, based on the non-empty values.
395func (i *Identity) DisplayName() string {
396 switch {
397 case i.Name() == "" && i.Login() != "":
398 return i.Login()
399 case i.Name() != "" && i.Login() == "":
400 return i.Name()
401 case i.Name() != "" && i.Login() != "":
402 return fmt.Sprintf("%s (%s)", i.Name(), i.Login())
403 }
404
405 panic("invalid person data")
406}
407
408// SetMetadata store arbitrary metadata along the last defined Version.
409// If the Version has been commit to git already, it won't be overwritten.
410func (i *Identity) SetMetadata(key string, value string) {
411 i.lastVersion().SetMetadata(key, value)
412}
413
414// ImmutableMetadata return all metadata for this Identity, accumulated from each Version.
415// If multiple value are found, the first defined takes precedence.
416func (i *Identity) ImmutableMetadata() map[string]string {
417 metadata := make(map[string]string)
418
419 for _, version := range i.Versions {
420 for key, value := range version.Metadata {
421 if _, has := metadata[key]; !has {
422 metadata[key] = value
423 }
424 }
425 }
426
427 return metadata
428}
429
430// MutableMetadata return all metadata for this Identity, accumulated from each Version.
431// If multiple value are found, the last defined takes precedence.
432func (i *Identity) MutableMetadata() map[string]string {
433 metadata := make(map[string]string)
434
435 for _, version := range i.Versions {
436 for key, value := range version.Metadata {
437 metadata[key] = value
438 }
439 }
440
441 return metadata
442}