repo_cache.go

  1package cache
  2
  3import (
  4	"bytes"
  5	"encoding/gob"
  6	"fmt"
  7	"io"
  8	"io/ioutil"
  9	"os"
 10	"path"
 11	"sort"
 12	"strconv"
 13	"strings"
 14	"time"
 15
 16	"github.com/MichaelMure/git-bug/bug"
 17	"github.com/MichaelMure/git-bug/identity"
 18	"github.com/MichaelMure/git-bug/repository"
 19	"github.com/MichaelMure/git-bug/util/git"
 20	"github.com/MichaelMure/git-bug/util/process"
 21)
 22
 23const cacheFile = "cache"
 24const formatVersion = 1
 25
 26type RepoCache struct {
 27	// the underlying repo
 28	repo repository.ClockedRepo
 29	// excerpt of bugs data for all bugs
 30	excerpts map[string]*BugExcerpt
 31	// bug loaded in memory
 32	bugs map[string]*BugCache
 33	// identities loaded in memory
 34	identities map[string]*identity.Identity
 35}
 36
 37func NewRepoCache(r repository.ClockedRepo) (*RepoCache, error) {
 38	c := &RepoCache{
 39		repo: r,
 40		bugs: make(map[string]*BugCache),
 41	}
 42
 43	err := c.lock()
 44	if err != nil {
 45		return &RepoCache{}, err
 46	}
 47
 48	err = c.load()
 49	if err == nil {
 50		return c, nil
 51	}
 52
 53	err = c.buildCache()
 54	if err != nil {
 55		return nil, err
 56	}
 57
 58	return c, c.write()
 59}
 60
 61// GetPath returns the path to the repo.
 62func (c *RepoCache) GetPath() string {
 63	return c.repo.GetPath()
 64}
 65
 66// GetPath returns the path to the repo.
 67func (c *RepoCache) GetCoreEditor() (string, error) {
 68	return c.repo.GetCoreEditor()
 69}
 70
 71// GetUserName returns the name the the user has used to configure git
 72func (c *RepoCache) GetUserName() (string, error) {
 73	return c.repo.GetUserName()
 74}
 75
 76// GetUserEmail returns the email address that the user has used to configure git.
 77func (c *RepoCache) GetUserEmail() (string, error) {
 78	return c.repo.GetUserEmail()
 79}
 80
 81// StoreConfig store a single key/value pair in the config of the repo
 82func (c *RepoCache) StoreConfig(key string, value string) error {
 83	return c.repo.StoreConfig(key, value)
 84}
 85
 86// ReadConfigs read all key/value pair matching the key prefix
 87func (c *RepoCache) ReadConfigs(keyPrefix string) (map[string]string, error) {
 88	return c.repo.ReadConfigs(keyPrefix)
 89}
 90
 91// RmConfigs remove all key/value pair matching the key prefix
 92func (c *RepoCache) RmConfigs(keyPrefix string) error {
 93	return c.repo.RmConfigs(keyPrefix)
 94}
 95
 96func (c *RepoCache) lock() error {
 97	lockPath := repoLockFilePath(c.repo)
 98
 99	err := repoIsAvailable(c.repo)
100	if err != nil {
101		return err
102	}
103
104	f, err := os.Create(lockPath)
105	if err != nil {
106		return err
107	}
108
109	pid := fmt.Sprintf("%d", os.Getpid())
110	_, err = f.WriteString(pid)
111	if err != nil {
112		return err
113	}
114
115	return f.Close()
116}
117
118func (c *RepoCache) Close() error {
119	lockPath := repoLockFilePath(c.repo)
120	return os.Remove(lockPath)
121}
122
123// bugUpdated is a callback to trigger when the excerpt of a bug changed,
124// that is each time a bug is updated
125func (c *RepoCache) bugUpdated(id string) error {
126	b, ok := c.bugs[id]
127	if !ok {
128		panic("missing bug in the cache")
129	}
130
131	c.excerpts[id] = NewBugExcerpt(b.bug, b.Snapshot())
132
133	return c.write()
134}
135
136// load will try to read from the disk the bug cache file
137func (c *RepoCache) load() error {
138	f, err := os.Open(cacheFilePath(c.repo))
139	if err != nil {
140		return err
141	}
142
143	decoder := gob.NewDecoder(f)
144
145	aux := struct {
146		Version  uint
147		Excerpts map[string]*BugExcerpt
148	}{}
149
150	err = decoder.Decode(&aux)
151	if err != nil {
152		return err
153	}
154
155	if aux.Version != 1 {
156		return fmt.Errorf("unknown cache format version %v", aux.Version)
157	}
158
159	c.excerpts = aux.Excerpts
160	return nil
161}
162
163// write will serialize on disk the bug cache file
164func (c *RepoCache) write() error {
165	var data bytes.Buffer
166
167	aux := struct {
168		Version  uint
169		Excerpts map[string]*BugExcerpt
170	}{
171		Version:  formatVersion,
172		Excerpts: c.excerpts,
173	}
174
175	encoder := gob.NewEncoder(&data)
176
177	err := encoder.Encode(aux)
178	if err != nil {
179		return err
180	}
181
182	f, err := os.Create(cacheFilePath(c.repo))
183	if err != nil {
184		return err
185	}
186
187	_, err = f.Write(data.Bytes())
188	if err != nil {
189		return err
190	}
191
192	return f.Close()
193}
194
195func cacheFilePath(repo repository.Repo) string {
196	return path.Join(repo.GetPath(), ".git", "git-bug", cacheFile)
197}
198
199func (c *RepoCache) buildCache() error {
200	_, _ = fmt.Fprintf(os.Stderr, "Building bug cache... ")
201
202	c.excerpts = make(map[string]*BugExcerpt)
203
204	allBugs := bug.ReadAllLocalBugs(c.repo)
205
206	for b := range allBugs {
207		if b.Err != nil {
208			return b.Err
209		}
210
211		snap := b.Bug.Compile()
212		c.excerpts[b.Bug.Id()] = NewBugExcerpt(b.Bug, &snap)
213	}
214
215	_, _ = fmt.Fprintln(os.Stderr, "Done.")
216	return nil
217}
218
219// ResolveBug retrieve a bug matching the exact given id
220func (c *RepoCache) ResolveBug(id string) (*BugCache, error) {
221	cached, ok := c.bugs[id]
222	if ok {
223		return cached, nil
224	}
225
226	b, err := bug.ReadLocalBug(c.repo, id)
227	if err != nil {
228		return nil, err
229	}
230
231	cached = NewBugCache(c, b)
232	c.bugs[id] = cached
233
234	return cached, nil
235}
236
237// ResolveBugPrefix retrieve a bug matching an id prefix. It fails if multiple
238// bugs match.
239func (c *RepoCache) ResolveBugPrefix(prefix string) (*BugCache, error) {
240	// preallocate but empty
241	matching := make([]string, 0, 5)
242
243	for id := range c.excerpts {
244		if strings.HasPrefix(id, prefix) {
245			matching = append(matching, id)
246		}
247	}
248
249	if len(matching) > 1 {
250		return nil, bug.ErrMultipleMatch{Matching: matching}
251	}
252
253	if len(matching) == 0 {
254		return nil, bug.ErrBugNotExist
255	}
256
257	return c.ResolveBug(matching[0])
258}
259
260// ResolveBugCreateMetadata retrieve a bug that has the exact given metadata on
261// its Create operation, that is, the first operation. It fails if multiple bugs
262// match.
263func (c *RepoCache) ResolveBugCreateMetadata(key string, value string) (*BugCache, error) {
264	// preallocate but empty
265	matching := make([]string, 0, 5)
266
267	for id, excerpt := range c.excerpts {
268		if excerpt.CreateMetadata[key] == value {
269			matching = append(matching, id)
270		}
271	}
272
273	if len(matching) > 1 {
274		return nil, bug.ErrMultipleMatch{Matching: matching}
275	}
276
277	if len(matching) == 0 {
278		return nil, bug.ErrBugNotExist
279	}
280
281	return c.ResolveBug(matching[0])
282}
283
284// QueryBugs return the id of all Bug matching the given Query
285func (c *RepoCache) QueryBugs(query *Query) []string {
286	if query == nil {
287		return c.AllBugsIds()
288	}
289
290	var filtered []*BugExcerpt
291
292	for _, excerpt := range c.excerpts {
293		if query.Match(excerpt) {
294			filtered = append(filtered, excerpt)
295		}
296	}
297
298	var sorter sort.Interface
299
300	switch query.OrderBy {
301	case OrderById:
302		sorter = BugsById(filtered)
303	case OrderByCreation:
304		sorter = BugsByCreationTime(filtered)
305	case OrderByEdit:
306		sorter = BugsByEditTime(filtered)
307	default:
308		panic("missing sort type")
309	}
310
311	if query.OrderDirection == OrderDescending {
312		sorter = sort.Reverse(sorter)
313	}
314
315	sort.Sort(sorter)
316
317	result := make([]string, len(filtered))
318
319	for i, val := range filtered {
320		result[i] = val.Id
321	}
322
323	return result
324}
325
326// AllBugsIds return all known bug ids
327func (c *RepoCache) AllBugsIds() []string {
328	result := make([]string, len(c.excerpts))
329
330	i := 0
331	for _, excerpt := range c.excerpts {
332		result[i] = excerpt.Id
333		i++
334	}
335
336	return result
337}
338
339// ClearAllBugs clear all bugs kept in memory
340func (c *RepoCache) ClearAllBugs() {
341	c.bugs = make(map[string]*BugCache)
342}
343
344// ValidLabels list valid labels
345//
346// Note: in the future, a proper label policy could be implemented where valid
347// labels are defined in a configuration file. Until that, the default behavior
348// is to return the list of labels already used.
349func (c *RepoCache) ValidLabels() []bug.Label {
350	set := map[bug.Label]interface{}{}
351
352	for _, excerpt := range c.excerpts {
353		for _, l := range excerpt.Labels {
354			set[l] = nil
355		}
356	}
357
358	result := make([]bug.Label, len(set))
359
360	i := 0
361	for l := range set {
362		result[i] = l
363		i++
364	}
365
366	// Sort
367	sort.Slice(result, func(i, j int) bool {
368		return string(result[i]) < string(result[j])
369	})
370
371	return result
372}
373
374// NewBug create a new bug
375// The new bug is written in the repository (commit)
376func (c *RepoCache) NewBug(title string, message string) (*BugCache, error) {
377	return c.NewBugWithFiles(title, message, nil)
378}
379
380// NewBugWithFiles create a new bug with attached files for the message
381// The new bug is written in the repository (commit)
382func (c *RepoCache) NewBugWithFiles(title string, message string, files []git.Hash) (*BugCache, error) {
383	author, err := identity.GetIdentity(c.repo)
384	if err != nil {
385		return nil, err
386	}
387
388	return c.NewBugRaw(author, time.Now().Unix(), title, message, files, nil)
389}
390
391// NewBugWithFilesMeta create a new bug with attached files for the message, as
392// well as metadata for the Create operation.
393// The new bug is written in the repository (commit)
394func (c *RepoCache) NewBugRaw(author *identity.Identity, unixTime int64, title string, message string, files []git.Hash, metadata map[string]string) (*BugCache, error) {
395	b, op, err := bug.CreateWithFiles(author, unixTime, title, message, files)
396	if err != nil {
397		return nil, err
398	}
399
400	for key, value := range metadata {
401		op.SetMetadata(key, value)
402	}
403
404	err = b.Commit(c.repo)
405	if err != nil {
406		return nil, err
407	}
408
409	cached := NewBugCache(c, b)
410	c.bugs[b.Id()] = cached
411
412	err = c.bugUpdated(b.Id())
413	if err != nil {
414		return nil, err
415	}
416
417	return cached, nil
418}
419
420// Fetch retrieve update from a remote
421// This does not change the local bugs state
422func (c *RepoCache) Fetch(remote string) (string, error) {
423	return bug.Fetch(c.repo, remote)
424}
425
426// MergeAll will merge all the available remote bug
427func (c *RepoCache) MergeAll(remote string) <-chan bug.MergeResult {
428	out := make(chan bug.MergeResult)
429
430	// Intercept merge results to update the cache properly
431	go func() {
432		defer close(out)
433
434		results := bug.MergeAll(c.repo, remote)
435		for result := range results {
436			out <- result
437
438			if result.Err != nil {
439				continue
440			}
441
442			id := result.Id
443
444			switch result.Status {
445			case bug.MergeStatusNew, bug.MergeStatusUpdated:
446				b := result.Bug
447				snap := b.Compile()
448				c.excerpts[id] = NewBugExcerpt(b, &snap)
449			}
450		}
451
452		err := c.write()
453
454		// No easy way out here ..
455		if err != nil {
456			panic(err)
457		}
458	}()
459
460	return out
461}
462
463// Push update a remote with the local changes
464func (c *RepoCache) Push(remote string) (string, error) {
465	return bug.Push(c.repo, remote)
466}
467
468func repoLockFilePath(repo repository.Repo) string {
469	return path.Join(repo.GetPath(), ".git", "git-bug", lockfile)
470}
471
472// repoIsAvailable check is the given repository is locked by a Cache.
473// Note: this is a smart function that will cleanup the lock file if the
474// corresponding process is not there anymore.
475// If no error is returned, the repo is free to edit.
476func repoIsAvailable(repo repository.Repo) error {
477	lockPath := repoLockFilePath(repo)
478
479	// Todo: this leave way for a racey access to the repo between the test
480	// if the file exist and the actual write. It's probably not a problem in
481	// practice because using a repository will be done from user interaction
482	// or in a context where a single instance of git-bug is already guaranteed
483	// (say, a server with the web UI running). But still, that might be nice to
484	// have a mutex or something to guard that.
485
486	// Todo: this will fail if somehow the filesystem is shared with another
487	// computer. Should add a configuration that prevent the cleaning of the
488	// lock file
489
490	f, err := os.Open(lockPath)
491
492	if err != nil && !os.IsNotExist(err) {
493		return err
494	}
495
496	if err == nil {
497		// lock file already exist
498		buf, err := ioutil.ReadAll(io.LimitReader(f, 10))
499		if err != nil {
500			return err
501		}
502		if len(buf) == 10 {
503			return fmt.Errorf("the lock file should be < 10 bytes")
504		}
505
506		pid, err := strconv.Atoi(string(buf))
507		if err != nil {
508			return err
509		}
510
511		if process.IsRunning(pid) {
512			return fmt.Errorf("the repository you want to access is already locked by the process pid %d", pid)
513		}
514
515		// The lock file is just laying there after a crash, clean it
516
517		fmt.Println("A lock file is present but the corresponding process is not, removing it.")
518		err = f.Close()
519		if err != nil {
520			return err
521		}
522
523		err = os.Remove(lockPath)
524		if err != nil {
525			return err
526		}
527	}
528
529	return nil
530}
531
532// ResolveIdentity retrieve an identity matching the exact given id
533func (c *RepoCache) ResolveIdentity(id string) (*identity.Identity, error) {
534	cached, ok := c.identities[id]
535	if ok {
536		return cached, nil
537	}
538
539	i, err := identity.Read(c.repo, id)
540	if err != nil {
541		return nil, err
542	}
543
544	c.identities[id] = i
545
546	return i, nil
547}
548
549// ResolveIdentityPrefix retrieve an Identity matching an id prefix. It fails if multiple
550// bugs match.
551// func (c *RepoCache) ResolveIdentityPrefix(prefix string) (*BugCache, error) {
552// 	// preallocate but empty
553// 	matching := make([]string, 0, 5)
554//
555// 	for id := range c.excerpts {
556// 		if strings.HasPrefix(id, prefix) {
557// 			matching = append(matching, id)
558// 		}
559// 	}
560//
561// 	if len(matching) > 1 {
562// 		return nil, bug.ErrMultipleMatch{Matching: matching}
563// 	}
564//
565// 	if len(matching) == 0 {
566// 		return nil, bug.ErrBugNotExist
567// 	}
568//
569// 	return c.ResolveBug(matching[0])
570// }
571
572// ResolveIdentityImmutableMetadata retrieve an Identity that has the exact given metadata on
573// one of it's version. If multiple version have the same key, the first defined take precedence.
574func (c *RepoCache) ResolveIdentityImmutableMetadata(key string, value string) (*BugCache, error) {
575	// // preallocate but empty
576	// matching := make([]string, 0, 5)
577	//
578	// for id, excerpt := range c.excerpts {
579	// 	if excerpt.CreateMetadata[key] == value {
580	// 		matching = append(matching, id)
581	// 	}
582	// }
583	//
584	// if len(matching) > 1 {
585	// 	return nil, bug.ErrMultipleMatch{Matching: matching}
586	// }
587	//
588	// if len(matching) == 0 {
589	// 	return nil, bug.ErrBugNotExist
590	// }
591	//
592	// return c.ResolveBug(matching[0])
593
594	// TODO
595
596	return nil, nil
597}