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	c.excerpts = make(map[string]BugExcerpt)
148
149	allBugs := bug.ReadAllLocalBugs(c.repo)
150
151	for b := range allBugs {
152		snap := b.Bug.Compile()
153		c.excerpts[b.Bug.Id()] = NewBugExcerpt(b.Bug, &snap)
154	}
155}
156
157func (c *RepoCache) ResolveBug(id string) (*BugCache, error) {
158	cached, ok := c.bugs[id]
159	if ok {
160		return cached, nil
161	}
162
163	b, err := bug.ReadLocalBug(c.repo, id)
164	if err != nil {
165		return nil, err
166	}
167
168	cached = NewBugCache(c, b)
169	c.bugs[id] = cached
170
171	return cached, nil
172}
173
174func (c *RepoCache) ResolveBugPrefix(prefix string) (*BugCache, error) {
175	// preallocate but empty
176	matching := make([]string, 0, 5)
177
178	for id := range c.bugs {
179		if strings.HasPrefix(id, prefix) {
180			matching = append(matching, id)
181		}
182	}
183
184	// TODO: should check matching bug in the repo as well
185
186	if len(matching) > 1 {
187		return nil, fmt.Errorf("Multiple matching bug found:\n%s", strings.Join(matching, "\n"))
188	}
189
190	if len(matching) == 1 {
191		b := c.bugs[matching[0]]
192		return b, nil
193	}
194
195	b, err := bug.FindLocalBug(c.repo, prefix)
196
197	if err != nil {
198		return nil, err
199	}
200
201	cached := NewBugCache(c, b)
202	c.bugs[b.Id()] = cached
203
204	return cached, nil
205}
206
207func (c *RepoCache) AllBugOrderById() []string {
208	result := make([]string, len(c.excerpts))
209
210	i := 0
211	for key := range c.excerpts {
212		result[i] = key
213		i++
214	}
215
216	sort.Strings(result)
217
218	return result
219}
220
221func (c *RepoCache) AllBugsOrderByEdit() []string {
222	excerpts := make([]BugExcerpt, len(c.excerpts))
223
224	i := 0
225	for _, val := range c.excerpts {
226		excerpts[i] = val
227		i++
228	}
229
230	sort.Sort(BugsByEditTime(excerpts))
231
232	result := make([]string, len(excerpts))
233
234	for i, val := range excerpts {
235		result[i] = val.Id
236	}
237
238	return result
239}
240
241func (c *RepoCache) AllBugsOrderByCreation() []string {
242	excerpts := make([]BugExcerpt, len(c.excerpts))
243
244	i := 0
245	for _, val := range c.excerpts {
246		excerpts[i] = val
247		i++
248	}
249
250	sort.Sort(BugsByCreationTime(excerpts))
251
252	result := make([]string, len(excerpts))
253
254	for i, val := range excerpts {
255		result[i] = val.Id
256	}
257
258	return result
259}
260
261// ClearAllBugs clear all bugs kept in memory
262func (c *RepoCache) ClearAllBugs() {
263	c.bugs = make(map[string]*BugCache)
264}
265
266// NewBug create a new bug
267// The new bug is written in the repository (commit)
268func (c *RepoCache) NewBug(title string, message string) (*BugCache, error) {
269	return c.NewBugWithFiles(title, message, nil)
270}
271
272// NewBugWithFiles create a new bug with attached files for the message
273// The new bug is written in the repository (commit)
274func (c *RepoCache) NewBugWithFiles(title string, message string, files []util.Hash) (*BugCache, error) {
275	author, err := bug.GetUser(c.repo)
276	if err != nil {
277		return nil, err
278	}
279
280	b, err := operations.CreateWithFiles(author, title, message, files)
281	if err != nil {
282		return nil, err
283	}
284
285	err = b.Commit(c.repo)
286	if err != nil {
287		return nil, err
288	}
289
290	cached := NewBugCache(c, b)
291	c.bugs[b.Id()] = cached
292
293	return cached, nil
294}
295
296// Fetch retrieve update from a remote
297// This does not change the local bugs state
298func (c *RepoCache) Fetch(remote string) (string, error) {
299	return bug.Fetch(c.repo, remote)
300}
301
302func (c *RepoCache) MergeAll(remote string) <-chan bug.MergeResult {
303	return bug.MergeAll(c.repo, remote)
304}
305
306// Pull does a Fetch and merge the updates into the local bug states
307func (c *RepoCache) Pull(remote string, out io.Writer) error {
308	return bug.Pull(c.repo, out, remote)
309}
310
311// Push update a remote with the local changes
312func (c *RepoCache) Push(remote string) (string, error) {
313	return bug.Push(c.repo, remote)
314}
315
316func repoLockFilePath(repo repository.Repo) string {
317	return path.Join(repo.GetPath(), ".git", "git-bug", lockfile)
318}
319
320// repoIsAvailable check is the given repository is locked by a Cache.
321// Note: this is a smart function that will cleanup the lock file if the
322// corresponding process is not there anymore.
323// If no error is returned, the repo is free to edit.
324func repoIsAvailable(repo repository.Repo) error {
325	lockPath := repoLockFilePath(repo)
326
327	// Todo: this leave way for a racey access to the repo between the test
328	// if the file exist and the actual write. It's probably not a problem in
329	// practice because using a repository will be done from user interaction
330	// or in a context where a single instance of git-bug is already guaranteed
331	// (say, a server with the web UI running). But still, that might be nice to
332	// have a mutex or something to guard that.
333
334	// Todo: this will fail if somehow the filesystem is shared with another
335	// computer. Should add a configuration that prevent the cleaning of the
336	// lock file
337
338	f, err := os.Open(lockPath)
339
340	if err != nil && !os.IsNotExist(err) {
341		return err
342	}
343
344	if err == nil {
345		// lock file already exist
346		buf, err := ioutil.ReadAll(io.LimitReader(f, 10))
347		if err != nil {
348			return err
349		}
350		if len(buf) == 10 {
351			return fmt.Errorf("The lock file should be < 10 bytes")
352		}
353
354		pid, err := strconv.Atoi(string(buf))
355		if err != nil {
356			return err
357		}
358
359		if util.ProcessIsRunning(pid) {
360			return fmt.Errorf("The repository you want to access is already locked by the process pid %d", pid)
361		}
362
363		// The lock file is just laying there after a crash, clean it
364
365		fmt.Println("A lock file is present but the corresponding process is not, removing it.")
366		err = f.Close()
367		if err != nil {
368			return err
369		}
370
371		os.Remove(lockPath)
372		if err != nil {
373			return err
374		}
375	}
376
377	return nil
378}