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.excerpts {
183		if strings.HasPrefix(id, prefix) {
184			matching = append(matching, id)
185		}
186	}
187
188	if len(matching) > 1 {
189		return nil, fmt.Errorf("Multiple matching bug found:\n%s", strings.Join(matching, "\n"))
190	}
191
192	return c.ResolveBug(matching[0])
193}
194
195func (c *RepoCache) QueryBugs(query *Query) []string {
196	if query == nil {
197		return c.AllBugsIds()
198	}
199
200	var filtered []*BugExcerpt
201
202	for _, excerpt := range c.excerpts {
203		if query.Match(excerpt) {
204			filtered = append(filtered, excerpt)
205		}
206	}
207
208	var sorter sort.Interface
209
210	switch query.OrderBy {
211	case OrderById:
212		sorter = BugsById(filtered)
213	case OrderByCreation:
214		sorter = BugsByCreationTime(filtered)
215	case OrderByEdit:
216		sorter = BugsByEditTime(filtered)
217	default:
218		panic("missing sort type")
219	}
220
221	if query.OrderDirection == OrderDescending {
222		sorter = sort.Reverse(sorter)
223	}
224
225	sort.Sort(sorter)
226
227	result := make([]string, len(filtered))
228
229	for i, val := range filtered {
230		result[i] = val.Id
231	}
232
233	return result
234}
235
236// AllBugsIds return all known bug ids
237func (c *RepoCache) AllBugsIds() []string {
238	result := make([]string, len(c.excerpts))
239
240	i := 0
241	for _, excerpt := range c.excerpts {
242		result[i] = excerpt.Id
243		i++
244	}
245
246	return result
247}
248
249// ClearAllBugs clear all bugs kept in memory
250func (c *RepoCache) ClearAllBugs() {
251	c.bugs = make(map[string]*BugCache)
252}
253
254// NewBug create a new bug
255// The new bug is written in the repository (commit)
256func (c *RepoCache) NewBug(title string, message string) (*BugCache, error) {
257	return c.NewBugWithFiles(title, message, nil)
258}
259
260// NewBugWithFiles create a new bug with attached files for the message
261// The new bug is written in the repository (commit)
262func (c *RepoCache) NewBugWithFiles(title string, message string, files []util.Hash) (*BugCache, error) {
263	author, err := bug.GetUser(c.repo)
264	if err != nil {
265		return nil, err
266	}
267
268	b, err := operations.CreateWithFiles(author, title, message, files)
269	if err != nil {
270		return nil, err
271	}
272
273	err = b.Commit(c.repo)
274	if err != nil {
275		return nil, err
276	}
277
278	cached := NewBugCache(c, b)
279	c.bugs[b.Id()] = cached
280
281	err = c.bugUpdated(b.Id())
282	if err != nil {
283		return nil, err
284	}
285
286	return cached, nil
287}
288
289// Fetch retrieve update from a remote
290// This does not change the local bugs state
291func (c *RepoCache) Fetch(remote string) (string, error) {
292	return bug.Fetch(c.repo, remote)
293}
294
295// MergeAll will merge all the available remote bug
296func (c *RepoCache) MergeAll(remote string) <-chan bug.MergeResult {
297	out := make(chan bug.MergeResult)
298
299	// Intercept merge results to update the cache properly
300	go func() {
301		defer close(out)
302
303		results := bug.MergeAll(c.repo, remote)
304		for result := range results {
305			if result.Err != nil {
306				continue
307			}
308
309			id := result.Id
310
311			switch result.Status {
312			case bug.MsgMergeNew, bug.MsgMergeUpdated:
313				b := result.Bug
314				snap := b.Compile()
315				c.excerpts[id] = NewBugExcerpt(b, &snap)
316
317			default:
318			}
319
320			out <- result
321		}
322
323		err := c.writeExcerpts()
324
325		// No easy way out here ..
326		if err != nil {
327			panic(err)
328		}
329	}()
330
331	return out
332}
333
334// Push update a remote with the local changes
335func (c *RepoCache) Push(remote string) (string, error) {
336	return bug.Push(c.repo, remote)
337}
338
339func repoLockFilePath(repo repository.Repo) string {
340	return path.Join(repo.GetPath(), ".git", "git-bug", lockfile)
341}
342
343// repoIsAvailable check is the given repository is locked by a Cache.
344// Note: this is a smart function that will cleanup the lock file if the
345// corresponding process is not there anymore.
346// If no error is returned, the repo is free to edit.
347func repoIsAvailable(repo repository.Repo) error {
348	lockPath := repoLockFilePath(repo)
349
350	// Todo: this leave way for a racey access to the repo between the test
351	// if the file exist and the actual write. It's probably not a problem in
352	// practice because using a repository will be done from user interaction
353	// or in a context where a single instance of git-bug is already guaranteed
354	// (say, a server with the web UI running). But still, that might be nice to
355	// have a mutex or something to guard that.
356
357	// Todo: this will fail if somehow the filesystem is shared with another
358	// computer. Should add a configuration that prevent the cleaning of the
359	// lock file
360
361	f, err := os.Open(lockPath)
362
363	if err != nil && !os.IsNotExist(err) {
364		return err
365	}
366
367	if err == nil {
368		// lock file already exist
369		buf, err := ioutil.ReadAll(io.LimitReader(f, 10))
370		if err != nil {
371			return err
372		}
373		if len(buf) == 10 {
374			return fmt.Errorf("the lock file should be < 10 bytes")
375		}
376
377		pid, err := strconv.Atoi(string(buf))
378		if err != nil {
379			return err
380		}
381
382		if util.ProcessIsRunning(pid) {
383			return fmt.Errorf("the repository you want to access is already locked by the process pid %d", pid)
384		}
385
386		// The lock file is just laying there after a crash, clean it
387
388		fmt.Println("A lock file is present but the corresponding process is not, removing it.")
389		err = f.Close()
390		if err != nil {
391			return err
392		}
393
394		os.Remove(lockPath)
395		if err != nil {
396			return err
397		}
398	}
399
400	return nil
401}