1package cache
2
3import (
4 "bytes"
5 "encoding/gob"
6 "fmt"
7 "os"
8 "path"
9
10 "github.com/MichaelMure/git-bug/entity"
11 "github.com/MichaelMure/git-bug/identity"
12 "github.com/MichaelMure/git-bug/repository"
13)
14
15const identityCacheFile = "identity-cache"
16
17func identityCacheFilePath(repo repository.Repo) string {
18 return path.Join(repo.GetPath(), "git-bug", identityCacheFile)
19}
20
21// identityUpdated is a callback to trigger when the excerpt of an identity
22// changed, that is each time an identity is updated
23func (c *RepoCache) identityUpdated(id entity.Id) error {
24 c.muIdentity.Lock()
25
26 i, ok := c.identities[id]
27 if !ok {
28 c.muIdentity.Unlock()
29 panic("missing identity in the cache")
30 }
31
32 c.identitiesExcerpts[id] = NewIdentityExcerpt(i.Identity)
33 c.muIdentity.Unlock()
34
35 // we only need to write the identity cache
36 return c.writeIdentityCache()
37}
38
39// load will try to read from the disk the identity cache file
40func (c *RepoCache) loadIdentityCache() error {
41 c.muIdentity.Lock()
42 defer c.muIdentity.Unlock()
43
44 f, err := os.Open(identityCacheFilePath(c.repo))
45 if err != nil {
46 return err
47 }
48
49 decoder := gob.NewDecoder(f)
50
51 aux := struct {
52 Version uint
53 Excerpts map[entity.Id]*IdentityExcerpt
54 }{}
55
56 err = decoder.Decode(&aux)
57 if err != nil {
58 return err
59 }
60
61 if aux.Version != formatVersion {
62 return fmt.Errorf("unknown cache format version %v", aux.Version)
63 }
64
65 c.identitiesExcerpts = aux.Excerpts
66 return nil
67}
68
69// write will serialize on disk the identity cache file
70func (c *RepoCache) writeIdentityCache() error {
71 c.muIdentity.RLock()
72 defer c.muIdentity.RUnlock()
73
74 var data bytes.Buffer
75
76 aux := struct {
77 Version uint
78 Excerpts map[entity.Id]*IdentityExcerpt
79 }{
80 Version: formatVersion,
81 Excerpts: c.identitiesExcerpts,
82 }
83
84 encoder := gob.NewEncoder(&data)
85
86 err := encoder.Encode(aux)
87 if err != nil {
88 return err
89 }
90
91 f, err := os.Create(identityCacheFilePath(c.repo))
92 if err != nil {
93 return err
94 }
95
96 _, err = f.Write(data.Bytes())
97 if err != nil {
98 return err
99 }
100
101 return f.Close()
102}
103
104// ResolveIdentityExcerpt retrieve a IdentityExcerpt matching the exact given id
105func (c *RepoCache) ResolveIdentityExcerpt(id entity.Id) (*IdentityExcerpt, error) {
106 c.muIdentity.RLock()
107 defer c.muIdentity.RUnlock()
108
109 e, ok := c.identitiesExcerpts[id]
110 if !ok {
111 return nil, identity.ErrIdentityNotExist
112 }
113
114 return e, nil
115}
116
117// ResolveIdentity retrieve an identity matching the exact given id
118func (c *RepoCache) ResolveIdentity(id entity.Id) (*IdentityCache, error) {
119 c.muIdentity.RLock()
120 cached, ok := c.identities[id]
121 c.muIdentity.RUnlock()
122 if ok {
123 return cached, nil
124 }
125
126 i, err := identity.ReadLocal(c.repo, id)
127 if err != nil {
128 return nil, err
129 }
130
131 cached = NewIdentityCache(c, i)
132
133 c.muIdentity.Lock()
134 c.identities[id] = cached
135 c.muIdentity.Unlock()
136
137 return cached, nil
138}
139
140// ResolveIdentityExcerptPrefix retrieve a IdentityExcerpt matching an id prefix.
141// It fails if multiple identities match.
142func (c *RepoCache) ResolveIdentityExcerptPrefix(prefix string) (*IdentityExcerpt, error) {
143 return c.ResolveIdentityExcerptMatcher(func(excerpt *IdentityExcerpt) bool {
144 return excerpt.Id.HasPrefix(prefix)
145 })
146}
147
148// ResolveIdentityPrefix retrieve an Identity matching an id prefix.
149// It fails if multiple identities match.
150func (c *RepoCache) ResolveIdentityPrefix(prefix string) (*IdentityCache, error) {
151 return c.ResolveIdentityMatcher(func(excerpt *IdentityExcerpt) bool {
152 return excerpt.Id.HasPrefix(prefix)
153 })
154}
155
156// ResolveIdentityImmutableMetadata retrieve an Identity that has the exact given metadata on
157// one of it's version. If multiple version have the same key, the first defined take precedence.
158func (c *RepoCache) ResolveIdentityImmutableMetadata(key string, value string) (*IdentityCache, error) {
159 return c.ResolveIdentityMatcher(func(excerpt *IdentityExcerpt) bool {
160 return excerpt.ImmutableMetadata[key] == value
161 })
162}
163
164func (c *RepoCache) ResolveIdentityExcerptMatcher(f func(*IdentityExcerpt) bool) (*IdentityExcerpt, error) {
165 id, err := c.resolveIdentityMatcher(f)
166 if err != nil {
167 return nil, err
168 }
169 return c.ResolveIdentityExcerpt(id)
170}
171
172func (c *RepoCache) ResolveIdentityMatcher(f func(*IdentityExcerpt) bool) (*IdentityCache, error) {
173 id, err := c.resolveIdentityMatcher(f)
174 if err != nil {
175 return nil, err
176 }
177 return c.ResolveIdentity(id)
178}
179
180func (c *RepoCache) resolveIdentityMatcher(f func(*IdentityExcerpt) bool) (entity.Id, error) {
181 c.muIdentity.RLock()
182 defer c.muIdentity.RUnlock()
183
184 // preallocate but empty
185 matching := make([]entity.Id, 0, 5)
186
187 for _, excerpt := range c.identitiesExcerpts {
188 if f(excerpt) {
189 matching = append(matching, excerpt.Id)
190 }
191 }
192
193 if len(matching) > 1 {
194 return entity.UnsetId, identity.NewErrMultipleMatch(matching)
195 }
196
197 if len(matching) == 0 {
198 return entity.UnsetId, identity.ErrIdentityNotExist
199 }
200
201 return matching[0], nil
202}
203
204// AllIdentityIds return all known identity ids
205func (c *RepoCache) AllIdentityIds() []entity.Id {
206 c.muIdentity.RLock()
207 defer c.muIdentity.RUnlock()
208
209 result := make([]entity.Id, len(c.identitiesExcerpts))
210
211 i := 0
212 for _, excerpt := range c.identitiesExcerpts {
213 result[i] = excerpt.Id
214 i++
215 }
216
217 return result
218}
219
220func (c *RepoCache) NewIdentityFromGitUser() (*IdentityCache, error) {
221 return c.NewIdentityFromGitUserRaw(nil)
222}
223
224func (c *RepoCache) NewIdentityFromGitUserRaw(metadata map[string]string) (*IdentityCache, error) {
225 i, err := identity.NewFromGitUser(c.repo)
226 if err != nil {
227 return nil, err
228 }
229 return c.finishIdentity(i, metadata)
230}
231
232// NewIdentity create a new identity
233// The new identity is written in the repository (commit)
234func (c *RepoCache) NewIdentity(name string, email string) (*IdentityCache, error) {
235 return c.NewIdentityRaw(name, email, "", "", nil)
236}
237
238// NewIdentityFull create a new identity
239// The new identity is written in the repository (commit)
240func (c *RepoCache) NewIdentityFull(name string, email string, login string, avatarUrl string) (*IdentityCache, error) {
241 return c.NewIdentityRaw(name, email, login, avatarUrl, nil)
242}
243
244func (c *RepoCache) NewIdentityRaw(name string, email string, login string, avatarUrl string, metadata map[string]string) (*IdentityCache, error) {
245 i := identity.NewIdentityFull(name, email, login, avatarUrl)
246 return c.finishIdentity(i, metadata)
247}
248
249func (c *RepoCache) finishIdentity(i *identity.Identity, metadata map[string]string) (*IdentityCache, error) {
250 for key, value := range metadata {
251 i.SetMetadata(key, value)
252 }
253
254 err := i.Commit(c.repo)
255 if err != nil {
256 return nil, err
257 }
258
259 c.muIdentity.Lock()
260 if _, has := c.identities[i.Id()]; has {
261 return nil, fmt.Errorf("identity %s already exist in the cache", i.Id())
262 }
263
264 cached := NewIdentityCache(c, i)
265 c.identities[i.Id()] = cached
266 c.muIdentity.Unlock()
267
268 // force the write of the excerpt
269 err = c.identityUpdated(i.Id())
270 if err != nil {
271 return nil, err
272 }
273
274 return cached, nil
275}