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