1package cache
2
3import (
4 "fmt"
5 "io"
6 "io/ioutil"
7 "os"
8 "path"
9 "path/filepath"
10 "strconv"
11 "sync"
12
13 "github.com/MichaelMure/git-bug/bug"
14 "github.com/MichaelMure/git-bug/entity"
15 "github.com/MichaelMure/git-bug/identity"
16 "github.com/MichaelMure/git-bug/repository"
17 "github.com/MichaelMure/git-bug/util/process"
18)
19
20// 1: original format
21// 2: added cache for identities with a reference in the bug cache
22// 3: CreateUnixTime --> createUnixTime, EditUnixTime --> editUnixTime
23const formatVersion = 3
24
25var _ repository.RepoCommon = &RepoCache{}
26
27// RepoCache is a cache for a Repository. This cache has multiple functions:
28//
29// 1. After being loaded, a Bug is kept in memory in the cache, allowing for fast
30// access later.
31// 2. The cache maintain in memory and on disk a pre-digested excerpt for each bug,
32// allowing for fast querying the whole set of bugs without having to load
33// them individually.
34// 3. The cache guarantee that a single instance of a Bug is loaded at once, avoiding
35// loss of data that we could have with multiple copies in the same process.
36// 4. The same way, the cache maintain in memory a single copy of the loaded identities.
37//
38// The cache also protect the on-disk data by locking the git repository for its
39// own usage, by writing a lock file. Of course, normal git operations are not
40// affected, only git-bug related one.
41type RepoCache struct {
42 // the underlying repo
43 repo repository.ClockedRepo
44
45 // the name of the repository, as defined in the MultiRepoCache
46 name string
47
48 muBug sync.RWMutex
49 // excerpt of bugs data for all bugs
50 bugExcerpts map[entity.Id]*BugExcerpt
51 // bug loaded in memory
52 bugs map[entity.Id]*BugCache
53
54 muIdentity sync.RWMutex
55 // excerpt of identities data for all identities
56 identitiesExcerpts map[entity.Id]*IdentityExcerpt
57 // identities loaded in memory
58 identities map[entity.Id]*IdentityCache
59
60 // the user identity's id, if known
61 userIdentityId entity.Id
62}
63
64func NewRepoCache(r repository.ClockedRepo) (*RepoCache, error) {
65 return NewNamedRepoCache(r, "")
66}
67
68func NewNamedRepoCache(r repository.ClockedRepo, name string) (*RepoCache, error) {
69 c := &RepoCache{
70 repo: r,
71 name: name,
72 bugs: make(map[entity.Id]*BugCache),
73 identities: make(map[entity.Id]*IdentityCache),
74 }
75
76 err := c.lock()
77 if err != nil {
78 return &RepoCache{}, err
79 }
80
81 err = c.load()
82 if err == nil {
83 return c, nil
84 }
85
86 // Cache is either missing, broken or outdated. Rebuilding.
87 err = c.buildCache()
88 if err != nil {
89 return nil, err
90 }
91
92 return c, c.write()
93}
94
95// load will try to read from the disk all the cache files
96func (c *RepoCache) load() error {
97 err := c.loadBugCache()
98 if err != nil {
99 return err
100 }
101 return c.loadIdentityCache()
102}
103
104// write will serialize on disk all the cache files
105func (c *RepoCache) write() error {
106 err := c.writeBugCache()
107 if err != nil {
108 return err
109 }
110 return c.writeIdentityCache()
111}
112
113func (c *RepoCache) lock() error {
114 lockPath := repoLockFilePath(c.repo)
115
116 err := repoIsAvailable(c.repo)
117 if err != nil {
118 return err
119 }
120
121 err = os.MkdirAll(filepath.Dir(lockPath), 0777)
122 if err != nil {
123 return err
124 }
125
126 f, err := os.Create(lockPath)
127 if err != nil {
128 return err
129 }
130
131 pid := fmt.Sprintf("%d", os.Getpid())
132 _, err = f.WriteString(pid)
133 if err != nil {
134 return err
135 }
136
137 return f.Close()
138}
139
140func (c *RepoCache) Close() error {
141 c.muBug.Lock()
142 defer c.muBug.Unlock()
143 c.muIdentity.Lock()
144 defer c.muIdentity.Unlock()
145
146 c.identities = make(map[entity.Id]*IdentityCache)
147 c.identitiesExcerpts = nil
148 c.bugs = make(map[entity.Id]*BugCache)
149 c.bugExcerpts = nil
150
151 lockPath := repoLockFilePath(c.repo)
152 return os.Remove(lockPath)
153}
154
155func (c *RepoCache) buildCache() error {
156 c.muBug.Lock()
157 defer c.muBug.Unlock()
158 c.muIdentity.Lock()
159 defer c.muIdentity.Unlock()
160
161 _, _ = fmt.Fprintf(os.Stderr, "Building identity cache... ")
162
163 c.identitiesExcerpts = make(map[entity.Id]*IdentityExcerpt)
164
165 allIdentities := identity.ReadAllLocalIdentities(c.repo)
166
167 for i := range allIdentities {
168 if i.Err != nil {
169 return i.Err
170 }
171
172 c.identitiesExcerpts[i.Identity.Id()] = NewIdentityExcerpt(i.Identity)
173 }
174
175 _, _ = fmt.Fprintln(os.Stderr, "Done.")
176
177 _, _ = fmt.Fprintf(os.Stderr, "Building bug cache... ")
178
179 c.bugExcerpts = make(map[entity.Id]*BugExcerpt)
180
181 allBugs := bug.ReadAllLocalBugs(c.repo)
182
183 for b := range allBugs {
184 if b.Err != nil {
185 return b.Err
186 }
187
188 snap := b.Bug.Compile()
189 c.bugExcerpts[b.Bug.Id()] = NewBugExcerpt(b.Bug, &snap)
190 }
191
192 _, _ = fmt.Fprintln(os.Stderr, "Done.")
193 return nil
194}
195
196func repoLockFilePath(repo repository.Repo) string {
197 return path.Join(repo.GetPath(), "git-bug", lockfile)
198}
199
200// repoIsAvailable check is the given repository is locked by a Cache.
201// Note: this is a smart function that will cleanup the lock file if the
202// corresponding process is not there anymore.
203// If no error is returned, the repo is free to edit.
204func repoIsAvailable(repo repository.Repo) error {
205 lockPath := repoLockFilePath(repo)
206
207 // Todo: this leave way for a racey access to the repo between the test
208 // if the file exist and the actual write. It's probably not a problem in
209 // practice because using a repository will be done from user interaction
210 // or in a context where a single instance of git-bug is already guaranteed
211 // (say, a server with the web UI running). But still, that might be nice to
212 // have a mutex or something to guard that.
213
214 // Todo: this will fail if somehow the filesystem is shared with another
215 // computer. Should add a configuration that prevent the cleaning of the
216 // lock file
217
218 f, err := os.Open(lockPath)
219
220 if err != nil && !os.IsNotExist(err) {
221 return err
222 }
223
224 if err == nil {
225 // lock file already exist
226 buf, err := ioutil.ReadAll(io.LimitReader(f, 10))
227 if err != nil {
228 return err
229 }
230 if len(buf) == 10 {
231 return fmt.Errorf("the lock file should be < 10 bytes")
232 }
233
234 pid, err := strconv.Atoi(string(buf))
235 if err != nil {
236 return err
237 }
238
239 if process.IsRunning(pid) {
240 return fmt.Errorf("the repository you want to access is already locked by the process pid %d", pid)
241 }
242
243 // The lock file is just laying there after a crash, clean it
244
245 fmt.Println("A lock file is present but the corresponding process is not, removing it.")
246 err = f.Close()
247 if err != nil {
248 return err
249 }
250
251 err = os.Remove(lockPath)
252 if err != nil {
253 return err
254 }
255 }
256
257 return nil
258}