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}