1package cache
2
3import (
4 "fmt"
5 "io"
6 "io/ioutil"
7 "os"
8 "path"
9 "strconv"
10
11 "github.com/MichaelMure/git-bug/repository"
12 "github.com/MichaelMure/git-bug/util"
13)
14
15const lockfile = "lock"
16
17type Cacher interface {
18 // RegisterRepository register a named repository. Use this for multi-repo setup
19 RegisterRepository(ref string, repo repository.Repo) error
20 // RegisterDefaultRepository register a unnamed repository. Use this for mono-repo setup
21 RegisterDefaultRepository(repo repository.Repo) error
22
23 // ResolveRepo retrieve a repository by name
24 ResolveRepo(ref string) (RepoCacher, error)
25 // DefaultRepo retrieve the default repository
26 DefaultRepo() (RepoCacher, error)
27
28 // Close will do anything that is needed to close the cache properly
29 Close() error
30}
31
32type RootCache struct {
33 repos map[string]RepoCacher
34}
35
36func NewCache() RootCache {
37 return RootCache{
38 repos: make(map[string]RepoCacher),
39 }
40}
41
42// RegisterRepository register a named repository. Use this for multi-repo setup
43func (c *RootCache) RegisterRepository(ref string, repo repository.Repo) error {
44 err := c.lockRepository(repo)
45 if err != nil {
46 return err
47 }
48
49 c.repos[ref] = NewRepoCache(repo)
50 return nil
51}
52
53// RegisterDefaultRepository register a unnamed repository. Use this for mono-repo setup
54func (c *RootCache) RegisterDefaultRepository(repo repository.Repo) error {
55 err := c.lockRepository(repo)
56 if err != nil {
57 return err
58 }
59
60 c.repos[""] = NewRepoCache(repo)
61 return nil
62}
63
64func (c *RootCache) lockRepository(repo repository.Repo) error {
65 lockPath := repoLockFilePath(repo)
66
67 err := RepoIsAvailable(repo)
68 if err != nil {
69 return err
70 }
71
72 f, err := os.Create(lockPath)
73 if err != nil {
74 return err
75 }
76
77 pid := fmt.Sprintf("%d", os.Getpid())
78 _, err = f.WriteString(pid)
79 if err != nil {
80 return err
81 }
82
83 return f.Close()
84}
85
86// ResolveRepo retrieve a repository by name
87func (c *RootCache) DefaultRepo() (RepoCacher, error) {
88 if len(c.repos) != 1 {
89 return nil, fmt.Errorf("repository is not unique")
90 }
91
92 for _, r := range c.repos {
93 return r, nil
94 }
95
96 panic("unreachable")
97}
98
99// DefaultRepo retrieve the default repository
100func (c *RootCache) ResolveRepo(ref string) (RepoCacher, error) {
101 r, ok := c.repos[ref]
102 if !ok {
103 return nil, fmt.Errorf("unknown repo")
104 }
105 return r, nil
106}
107
108func (c *RootCache) Close() error {
109 for _, cachedRepo := range c.repos {
110 lockPath := repoLockFilePath(cachedRepo.Repository())
111 err := os.Remove(lockPath)
112 if err != nil {
113 return err
114 }
115 }
116 return nil
117}
118
119func RepoIsAvailable(repo repository.Repo) error {
120 lockPath := repoLockFilePath(repo)
121
122 // Todo: this leave way for a racey access to the repo between the test
123 // if the file exist and the actual write. It's probably not a problem in
124 // practice because using a repository will be done from user interaction
125 // or in a context where a single instance of git-bug is already guaranteed
126 // (say, a server with the web UI running). But still, that might be nice to
127 // have a mutex or something to guard that.
128
129 // Todo: this will fail if somehow the filesystem is shared with another
130 // computer. Should add a configuration that prevent the cleaning of the
131 // lock file
132
133 f, err := os.Open(lockPath)
134
135 if err != nil && !os.IsNotExist(err) {
136 return err
137 }
138
139 if err == nil {
140 // lock file already exist
141 buf, err := ioutil.ReadAll(io.LimitReader(f, 10))
142 if err != nil {
143 return err
144 }
145 if len(buf) == 10 {
146 return fmt.Errorf("The lock file should be < 10 bytes")
147 }
148
149 pid, err := strconv.Atoi(string(buf))
150 if err != nil {
151 return err
152 }
153
154 if util.ProcessIsRunning(pid) {
155 return fmt.Errorf("The repository you want to access is already locked by the process pid %d", pid)
156 }
157
158 // The lock file is just laying there after a crash, clean it
159
160 fmt.Println("A lock file is present but the corresponding process is not, removing it.")
161 err = f.Close()
162 if err != nil {
163 return err
164 }
165
166 os.Remove(lockPath)
167 if err != nil {
168 return err
169 }
170 }
171
172 return nil
173}
174
175func repoLockFilePath(repo repository.Repo) string {
176 return path.Join(repo.GetPath(), ".git", "git-bug", lockfile)
177}