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
 15	"github.com/MichaelMure/git-bug/bug"
 16	"github.com/MichaelMure/git-bug/bug/operations"
 17	"github.com/MichaelMure/git-bug/repository"
 18	"github.com/MichaelMure/git-bug/util"
 19)
 20
 21type RepoCache struct {
 22	// the underlying repo
 23	repo repository.Repo
 24	// excerpt of bugs data for all bugs
 25	excerpts map[string]*BugExcerpt
 26	// bug loaded in memory
 27	bugs map[string]*BugCache
 28}
 29
 30func NewRepoCache(r repository.Repo) (*RepoCache, error) {
 31	c := &RepoCache{
 32		repo: r,
 33		bugs: make(map[string]*BugCache),
 34	}
 35
 36	err := c.lock()
 37	if err != nil {
 38		return &RepoCache{}, err
 39	}
 40
 41	err = c.loadExcerpts()
 42	if err == nil {
 43		return c, nil
 44	}
 45
 46	c.buildAllExcerpt()
 47
 48	return c, c.writeExcerpts()
 49}
 50
 51// Repository return the underlying repository.
 52// If you use this, make sure to never change the repo state.
 53func (c *RepoCache) Repository() repository.Repo {
 54	return c.repo
 55}
 56
 57func (c *RepoCache) lock() error {
 58	lockPath := repoLockFilePath(c.repo)
 59
 60	err := repoIsAvailable(c.repo)
 61	if err != nil {
 62		return err
 63	}
 64
 65	f, err := os.Create(lockPath)
 66	if err != nil {
 67		return err
 68	}
 69
 70	pid := fmt.Sprintf("%d", os.Getpid())
 71	_, err = f.WriteString(pid)
 72	if err != nil {
 73		return err
 74	}
 75
 76	return f.Close()
 77}
 78
 79func (c *RepoCache) Close() error {
 80	lockPath := repoLockFilePath(c.repo)
 81	return os.Remove(lockPath)
 82}
 83
 84// bugUpdated is a callback to trigger when the excerpt of a bug changed,
 85// that is each time a bug is updated
 86func (c *RepoCache) bugUpdated(id string) error {
 87	b, ok := c.bugs[id]
 88	if !ok {
 89		panic("missing bug in the cache")
 90	}
 91
 92	c.excerpts[id] = NewBugExcerpt(b.bug, b.Snapshot())
 93
 94	return c.writeExcerpts()
 95}
 96
 97// loadExcerpts will try to read from the disk the bug excerpt file
 98func (c *RepoCache) loadExcerpts() error {
 99	excerptsPath := repoExcerptsFilePath(c.repo)
100
101	f, err := os.Open(excerptsPath)
102	if err != nil {
103		return err
104	}
105
106	decoder := gob.NewDecoder(f)
107
108	var excerpts map[string]*BugExcerpt
109
110	err = decoder.Decode(&excerpts)
111	if err != nil {
112		return err
113	}
114
115	c.excerpts = excerpts
116	return nil
117}
118
119// writeExcerpts will serialize on disk the BugExcerpt array
120func (c *RepoCache) writeExcerpts() error {
121	var data bytes.Buffer
122
123	encoder := gob.NewEncoder(&data)
124
125	err := encoder.Encode(c.excerpts)
126	if err != nil {
127		return err
128	}
129
130	excerptsPath := repoExcerptsFilePath(c.repo)
131
132	f, err := os.Create(excerptsPath)
133	if err != nil {
134		return err
135	}
136
137	_, err = f.Write(data.Bytes())
138	if err != nil {
139		return err
140	}
141
142	return f.Close()
143}
144
145func repoExcerptsFilePath(repo repository.Repo) string {
146	return path.Join(repo.GetPath(), ".git", "git-bug", excerptsFile)
147}
148
149func (c *RepoCache) buildAllExcerpt() {
150	fmt.Printf("Building bug cache... ")
151
152	c.excerpts = make(map[string]*BugExcerpt)
153
154	allBugs := bug.ReadAllLocalBugs(c.repo)
155
156	for b := range allBugs {
157		snap := b.Bug.Compile()
158		c.excerpts[b.Bug.Id()] = NewBugExcerpt(b.Bug, &snap)
159	}
160
161	fmt.Println("Done.")
162}
163
164func (c *RepoCache) ResolveBug(id string) (*BugCache, error) {
165	cached, ok := c.bugs[id]
166	if ok {
167		return cached, nil
168	}
169
170	b, err := bug.ReadLocalBug(c.repo, id)
171	if err != nil {
172		return nil, err
173	}
174
175	cached = NewBugCache(c, b)
176	c.bugs[id] = cached
177
178	return cached, nil
179}
180
181func (c *RepoCache) ResolveBugPrefix(prefix string) (*BugCache, error) {
182	// preallocate but empty
183	matching := make([]string, 0, 5)
184
185	for id := range c.excerpts {
186		if strings.HasPrefix(id, prefix) {
187			matching = append(matching, id)
188		}
189	}
190
191	if len(matching) > 1 {
192		return nil, fmt.Errorf("Multiple matching bug found:\n%s", strings.Join(matching, "\n"))
193	}
194
195	return c.ResolveBug(matching[0])
196}
197
198func (c *RepoCache) QueryBugs(query *Query) []string {
199	if query == nil {
200		return c.AllBugsIds()
201	}
202
203	var filtered []*BugExcerpt
204
205	for _, excerpt := range c.excerpts {
206		if query.Match(excerpt) {
207			filtered = append(filtered, excerpt)
208		}
209	}
210
211	var sorter sort.Interface
212
213	switch query.OrderBy {
214	case OrderById:
215		sorter = BugsById(filtered)
216	case OrderByCreation:
217		sorter = BugsByCreationTime(filtered)
218	case OrderByEdit:
219		sorter = BugsByEditTime(filtered)
220	default:
221		panic("missing sort type")
222	}
223
224	if query.OrderDirection == OrderDescending {
225		sorter = sort.Reverse(sorter)
226	}
227
228	sort.Sort(sorter)
229
230	result := make([]string, len(filtered))
231
232	for i, val := range filtered {
233		result[i] = val.Id
234	}
235
236	return result
237}
238
239// AllBugsIds return all known bug ids
240func (c *RepoCache) AllBugsIds() []string {
241	result := make([]string, len(c.excerpts))
242
243	i := 0
244	for _, excerpt := range c.excerpts {
245		result[i] = excerpt.Id
246		i++
247	}
248
249	return result
250}
251
252// ClearAllBugs clear all bugs kept in memory
253func (c *RepoCache) ClearAllBugs() {
254	c.bugs = make(map[string]*BugCache)
255}
256
257// NewBug create a new bug
258// The new bug is written in the repository (commit)
259func (c *RepoCache) NewBug(title string, message string) (*BugCache, error) {
260	return c.NewBugWithFiles(title, message, nil)
261}
262
263// NewBugWithFiles create a new bug with attached files for the message
264// The new bug is written in the repository (commit)
265func (c *RepoCache) NewBugWithFiles(title string, message string, files []util.Hash) (*BugCache, error) {
266	author, err := bug.GetUser(c.repo)
267	if err != nil {
268		return nil, err
269	}
270
271	b, err := operations.CreateWithFiles(author, title, message, files)
272	if err != nil {
273		return nil, err
274	}
275
276	err = b.Commit(c.repo)
277	if err != nil {
278		return nil, err
279	}
280
281	cached := NewBugCache(c, b)
282	c.bugs[b.Id()] = cached
283
284	err = c.bugUpdated(b.Id())
285	if err != nil {
286		return nil, err
287	}
288
289	return cached, nil
290}
291
292// Fetch retrieve update from a remote
293// This does not change the local bugs state
294func (c *RepoCache) Fetch(remote string) (string, error) {
295	return bug.Fetch(c.repo, remote)
296}
297
298// MergeAll will merge all the available remote bug
299func (c *RepoCache) MergeAll(remote string) <-chan bug.MergeResult {
300	out := make(chan bug.MergeResult)
301
302	// Intercept merge results to update the cache properly
303	go func() {
304		defer close(out)
305
306		results := bug.MergeAll(c.repo, remote)
307		for result := range results {
308			if result.Err != nil {
309				continue
310			}
311
312			id := result.Id
313
314			switch result.Status {
315			case bug.MsgMergeNew, bug.MsgMergeUpdated:
316				b := result.Bug
317				snap := b.Compile()
318				c.excerpts[id] = NewBugExcerpt(b, &snap)
319
320			default:
321			}
322
323			out <- result
324		}
325
326		err := c.writeExcerpts()
327
328		// No easy way out here ..
329		if err != nil {
330			panic(err)
331		}
332	}()
333
334	return out
335}
336
337// Push update a remote with the local changes
338func (c *RepoCache) Push(remote string) (string, error) {
339	return bug.Push(c.repo, remote)
340}
341
342func repoLockFilePath(repo repository.Repo) string {
343	return path.Join(repo.GetPath(), ".git", "git-bug", lockfile)
344}
345
346// repoIsAvailable check is the given repository is locked by a Cache.
347// Note: this is a smart function that will cleanup the lock file if the
348// corresponding process is not there anymore.
349// If no error is returned, the repo is free to edit.
350func repoIsAvailable(repo repository.Repo) error {
351	lockPath := repoLockFilePath(repo)
352
353	// Todo: this leave way for a racey access to the repo between the test
354	// if the file exist and the actual write. It's probably not a problem in
355	// practice because using a repository will be done from user interaction
356	// or in a context where a single instance of git-bug is already guaranteed
357	// (say, a server with the web UI running). But still, that might be nice to
358	// have a mutex or something to guard that.
359
360	// Todo: this will fail if somehow the filesystem is shared with another
361	// computer. Should add a configuration that prevent the cleaning of the
362	// lock file
363
364	f, err := os.Open(lockPath)
365
366	if err != nil && !os.IsNotExist(err) {
367		return err
368	}
369
370	if err == nil {
371		// lock file already exist
372		buf, err := ioutil.ReadAll(io.LimitReader(f, 10))
373		if err != nil {
374			return err
375		}
376		if len(buf) == 10 {
377			return fmt.Errorf("the lock file should be < 10 bytes")
378		}
379
380		pid, err := strconv.Atoi(string(buf))
381		if err != nil {
382			return err
383		}
384
385		if util.ProcessIsRunning(pid) {
386			return fmt.Errorf("the repository you want to access is already locked by the process pid %d", pid)
387		}
388
389		// The lock file is just laying there after a crash, clean it
390
391		fmt.Println("A lock file is present but the corresponding process is not, removing it.")
392		err = f.Close()
393		if err != nil {
394			return err
395		}
396
397		os.Remove(lockPath)
398		if err != nil {
399			return err
400		}
401	}
402
403	return nil
404}