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