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