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}