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 "time"
15
16 "github.com/MichaelMure/git-bug/bug"
17 "github.com/MichaelMure/git-bug/operations"
18 "github.com/MichaelMure/git-bug/repository"
19 "github.com/MichaelMure/git-bug/util/git"
20 "github.com/MichaelMure/git-bug/util/process"
21)
22
23const cacheFile = "cache"
24const formatVersion = 1
25
26type RepoCache struct {
27 // the underlying repo
28 repo repository.ClockedRepo
29 // excerpt of bugs data for all bugs
30 excerpts map[string]*BugExcerpt
31 // bug loaded in memory
32 bugs map[string]*BugCache
33}
34
35func NewRepoCache(r repository.ClockedRepo) (*RepoCache, error) {
36 c := &RepoCache{
37 repo: r,
38 bugs: make(map[string]*BugCache),
39 }
40
41 err := c.lock()
42 if err != nil {
43 return &RepoCache{}, err
44 }
45
46 err = c.load()
47 if err == nil {
48 return c, nil
49 }
50
51 err = c.buildCache()
52 if err != nil {
53 return nil, err
54 }
55
56 return c, c.write()
57}
58
59// GetPath returns the path to the repo.
60func (c *RepoCache) GetPath() string {
61 return c.repo.GetPath()
62}
63
64// GetPath returns the path to the repo.
65func (c *RepoCache) GetCoreEditor() (string, error) {
66 return c.repo.GetCoreEditor()
67}
68
69// GetUserName returns the name the the user has used to configure git
70func (c *RepoCache) GetUserName() (string, error) {
71 return c.repo.GetUserName()
72}
73
74// GetUserEmail returns the email address that the user has used to configure git.
75func (c *RepoCache) GetUserEmail() (string, error) {
76 return c.repo.GetUserEmail()
77}
78
79// StoreConfig store a single key/value pair in the config of the repo
80func (c *RepoCache) StoreConfig(key string, value string) error {
81 return c.repo.StoreConfig(key, value)
82}
83
84// ReadConfigs read all key/value pair matching the key prefix
85func (c *RepoCache) ReadConfigs(keyPrefix string) (map[string]string, error) {
86 return c.repo.ReadConfigs(keyPrefix)
87}
88
89// RmConfigs remove all key/value pair matching the key prefix
90func (c *RepoCache) RmConfigs(keyPrefix string) error {
91 return c.repo.RmConfigs(keyPrefix)
92}
93
94func (c *RepoCache) lock() error {
95 lockPath := repoLockFilePath(c.repo)
96
97 err := repoIsAvailable(c.repo)
98 if err != nil {
99 return err
100 }
101
102 f, err := os.Create(lockPath)
103 if err != nil {
104 return err
105 }
106
107 pid := fmt.Sprintf("%d", os.Getpid())
108 _, err = f.WriteString(pid)
109 if err != nil {
110 return err
111 }
112
113 return f.Close()
114}
115
116func (c *RepoCache) Close() error {
117 lockPath := repoLockFilePath(c.repo)
118 return os.Remove(lockPath)
119}
120
121// bugUpdated is a callback to trigger when the excerpt of a bug changed,
122// that is each time a bug is updated
123func (c *RepoCache) bugUpdated(id string) error {
124 b, ok := c.bugs[id]
125 if !ok {
126 panic("missing bug in the cache")
127 }
128
129 c.excerpts[id] = NewBugExcerpt(b.bug, b.Snapshot())
130
131 return c.write()
132}
133
134// load will try to read from the disk the bug cache file
135func (c *RepoCache) load() error {
136 f, err := os.Open(cacheFilePath(c.repo))
137 if err != nil {
138 return err
139 }
140
141 decoder := gob.NewDecoder(f)
142
143 aux := struct {
144 Version uint
145 Excerpts map[string]*BugExcerpt
146 }{}
147
148 err = decoder.Decode(&aux)
149 if err != nil {
150 return err
151 }
152
153 if aux.Version != 1 {
154 return fmt.Errorf("unknown cache format version %v", aux.Version)
155 }
156
157 c.excerpts = aux.Excerpts
158 return nil
159}
160
161// write will serialize on disk the bug cache file
162func (c *RepoCache) write() error {
163 var data bytes.Buffer
164
165 aux := struct {
166 Version uint
167 Excerpts map[string]*BugExcerpt
168 }{
169 Version: formatVersion,
170 Excerpts: c.excerpts,
171 }
172
173 encoder := gob.NewEncoder(&data)
174
175 err := encoder.Encode(aux)
176 if err != nil {
177 return err
178 }
179
180 f, err := os.Create(cacheFilePath(c.repo))
181 if err != nil {
182 return err
183 }
184
185 _, err = f.Write(data.Bytes())
186 if err != nil {
187 return err
188 }
189
190 return f.Close()
191}
192
193func cacheFilePath(repo repository.Repo) string {
194 return path.Join(repo.GetPath(), ".git", "git-bug", cacheFile)
195}
196
197func (c *RepoCache) buildCache() error {
198 fmt.Printf("Building bug cache... ")
199
200 c.excerpts = make(map[string]*BugExcerpt)
201
202 allBugs := bug.ReadAllLocalBugs(c.repo)
203
204 for b := range allBugs {
205 if b.Err != nil {
206 return b.Err
207 }
208
209 snap := b.Bug.Compile()
210 c.excerpts[b.Bug.Id()] = NewBugExcerpt(b.Bug, &snap)
211 }
212
213 fmt.Println("Done.")
214 return nil
215}
216
217func (c *RepoCache) ResolveBug(id string) (*BugCache, error) {
218 cached, ok := c.bugs[id]
219 if ok {
220 return cached, nil
221 }
222
223 b, err := bug.ReadLocalBug(c.repo, id)
224 if err != nil {
225 return nil, err
226 }
227
228 cached = NewBugCache(c, b)
229 c.bugs[id] = cached
230
231 return cached, nil
232}
233
234func (c *RepoCache) ResolveBugPrefix(prefix string) (*BugCache, error) {
235 // preallocate but empty
236 matching := make([]string, 0, 5)
237
238 for id := range c.excerpts {
239 if strings.HasPrefix(id, prefix) {
240 matching = append(matching, id)
241 }
242 }
243
244 if len(matching) > 1 {
245 return nil, fmt.Errorf("Multiple matching bug found:\n%s", strings.Join(matching, "\n"))
246 }
247
248 if len(matching) == 0 {
249 return nil, bug.ErrBugNotExist
250 }
251
252 return c.ResolveBug(matching[0])
253}
254
255func (c *RepoCache) QueryBugs(query *Query) []string {
256 if query == nil {
257 return c.AllBugsIds()
258 }
259
260 var filtered []*BugExcerpt
261
262 for _, excerpt := range c.excerpts {
263 if query.Match(excerpt) {
264 filtered = append(filtered, excerpt)
265 }
266 }
267
268 var sorter sort.Interface
269
270 switch query.OrderBy {
271 case OrderById:
272 sorter = BugsById(filtered)
273 case OrderByCreation:
274 sorter = BugsByCreationTime(filtered)
275 case OrderByEdit:
276 sorter = BugsByEditTime(filtered)
277 default:
278 panic("missing sort type")
279 }
280
281 if query.OrderDirection == OrderDescending {
282 sorter = sort.Reverse(sorter)
283 }
284
285 sort.Sort(sorter)
286
287 result := make([]string, len(filtered))
288
289 for i, val := range filtered {
290 result[i] = val.Id
291 }
292
293 return result
294}
295
296// AllBugsIds return all known bug ids
297func (c *RepoCache) AllBugsIds() []string {
298 result := make([]string, len(c.excerpts))
299
300 i := 0
301 for _, excerpt := range c.excerpts {
302 result[i] = excerpt.Id
303 i++
304 }
305
306 return result
307}
308
309// ClearAllBugs clear all bugs kept in memory
310func (c *RepoCache) ClearAllBugs() {
311 c.bugs = make(map[string]*BugCache)
312}
313
314// ValidLabels list valid labels
315//
316// Note: in the future, a proper label policy could be implemented where valid
317// labels are defined in a configuration file. Until that, the default behavior
318// is to return the list of labels already used.
319func (c *RepoCache) ValidLabels() []bug.Label {
320 set := map[bug.Label]interface{}{}
321
322 for _, excerpt := range c.excerpts {
323 for _, l := range excerpt.Labels {
324 set[l] = nil
325 }
326 }
327
328 result := make([]bug.Label, len(set))
329
330 i := 0
331 for l := range set {
332 result[i] = l
333 i++
334 }
335
336 // Sort
337 sort.Slice(result, func(i, j int) bool {
338 return string(result[i]) < string(result[j])
339 })
340
341 return result
342}
343
344// NewBug create a new bug
345// The new bug is written in the repository (commit)
346func (c *RepoCache) NewBug(title string, message string) (*BugCache, error) {
347 return c.NewBugWithFiles(title, message, nil)
348}
349
350// NewBugWithFiles create a new bug with attached files for the message
351// The new bug is written in the repository (commit)
352func (c *RepoCache) NewBugWithFiles(title string, message string, files []git.Hash) (*BugCache, error) {
353 author, err := bug.GetUser(c.repo)
354 if err != nil {
355 return nil, err
356 }
357
358 return c.NewBugRaw(author, time.Now().Unix(), title, message, files, nil)
359}
360
361// NewBugWithFilesMeta create a new bug with attached files for the message, as
362// well as metadata for the Create operation.
363// The new bug is written in the repository (commit)
364func (c *RepoCache) NewBugRaw(author bug.Person, unixTime int64, title string, message string, files []git.Hash, metadata map[string]string) (*BugCache, error) {
365 b, err := operations.CreateWithFiles(author, unixTime, title, message, files)
366 if err != nil {
367 return nil, err
368 }
369
370 for key, value := range metadata {
371 b.FirstOp().SetMetadata(key, value)
372 }
373
374 err = b.Commit(c.repo)
375 if err != nil {
376 return nil, err
377 }
378
379 cached := NewBugCache(c, b)
380 c.bugs[b.Id()] = cached
381
382 err = c.bugUpdated(b.Id())
383 if err != nil {
384 return nil, err
385 }
386
387 return cached, nil
388}
389
390// Fetch retrieve update from a remote
391// This does not change the local bugs state
392func (c *RepoCache) Fetch(remote string) (string, error) {
393 return bug.Fetch(c.repo, remote)
394}
395
396// MergeAll will merge all the available remote bug
397func (c *RepoCache) MergeAll(remote string) <-chan bug.MergeResult {
398 out := make(chan bug.MergeResult)
399
400 // Intercept merge results to update the cache properly
401 go func() {
402 defer close(out)
403
404 results := bug.MergeAll(c.repo, remote)
405 for result := range results {
406 out <- result
407
408 if result.Err != nil {
409 continue
410 }
411
412 id := result.Id
413
414 switch result.Status {
415 case bug.MergeStatusNew, bug.MergeStatusUpdated:
416 b := result.Bug
417 snap := b.Compile()
418 c.excerpts[id] = NewBugExcerpt(b, &snap)
419 }
420 }
421
422 err := c.write()
423
424 // No easy way out here ..
425 if err != nil {
426 panic(err)
427 }
428 }()
429
430 return out
431}
432
433// Push update a remote with the local changes
434func (c *RepoCache) Push(remote string) (string, error) {
435 return bug.Push(c.repo, remote)
436}
437
438func repoLockFilePath(repo repository.Repo) string {
439 return path.Join(repo.GetPath(), ".git", "git-bug", lockfile)
440}
441
442// repoIsAvailable check is the given repository is locked by a Cache.
443// Note: this is a smart function that will cleanup the lock file if the
444// corresponding process is not there anymore.
445// If no error is returned, the repo is free to edit.
446func repoIsAvailable(repo repository.Repo) error {
447 lockPath := repoLockFilePath(repo)
448
449 // Todo: this leave way for a racey access to the repo between the test
450 // if the file exist and the actual write. It's probably not a problem in
451 // practice because using a repository will be done from user interaction
452 // or in a context where a single instance of git-bug is already guaranteed
453 // (say, a server with the web UI running). But still, that might be nice to
454 // have a mutex or something to guard that.
455
456 // Todo: this will fail if somehow the filesystem is shared with another
457 // computer. Should add a configuration that prevent the cleaning of the
458 // lock file
459
460 f, err := os.Open(lockPath)
461
462 if err != nil && !os.IsNotExist(err) {
463 return err
464 }
465
466 if err == nil {
467 // lock file already exist
468 buf, err := ioutil.ReadAll(io.LimitReader(f, 10))
469 if err != nil {
470 return err
471 }
472 if len(buf) == 10 {
473 return fmt.Errorf("the lock file should be < 10 bytes")
474 }
475
476 pid, err := strconv.Atoi(string(buf))
477 if err != nil {
478 return err
479 }
480
481 if process.IsRunning(pid) {
482 return fmt.Errorf("the repository you want to access is already locked by the process pid %d", pid)
483 }
484
485 // The lock file is just laying there after a crash, clean it
486
487 fmt.Println("A lock file is present but the corresponding process is not, removing it.")
488 err = f.Close()
489 if err != nil {
490 return err
491 }
492
493 os.Remove(lockPath)
494 if err != nil {
495 return err
496 }
497 }
498
499 return nil
500}