identity.go

  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}