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