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