1package repository
2
3import (
4 "bytes"
5 "crypto/sha1"
6 "fmt"
7 "strings"
8 "sync"
9
10 "github.com/99designs/keyring"
11 "github.com/ProtonMail/go-crypto/openpgp"
12 "github.com/go-git/go-billy/v5"
13 "github.com/go-git/go-billy/v5/memfs"
14
15 "github.com/MichaelMure/git-bug/util/lamport"
16)
17
18var _ ClockedRepo = &mockRepo{}
19var _ TestedRepo = &mockRepo{}
20
21// mockRepo defines an instance of Repo that can be used for testing.
22type mockRepo struct {
23 *mockRepoConfig
24 *mockRepoKeyring
25 *mockRepoCommon
26 *mockRepoStorage
27 *mockRepoIndex
28 *mockRepoData
29 *mockRepoClock
30 *mockRepoTest
31}
32
33func (m *mockRepo) Close() error { return nil }
34
35func NewMockRepo() *mockRepo {
36 return &mockRepo{
37 mockRepoConfig: NewMockRepoConfig(),
38 mockRepoKeyring: NewMockRepoKeyring(),
39 mockRepoCommon: NewMockRepoCommon(),
40 mockRepoStorage: NewMockRepoStorage(),
41 mockRepoIndex: newMockRepoIndex(),
42 mockRepoData: NewMockRepoData(),
43 mockRepoClock: NewMockRepoClock(),
44 mockRepoTest: NewMockRepoTest(),
45 }
46}
47
48var _ RepoConfig = &mockRepoConfig{}
49
50type mockRepoConfig struct {
51 localConfig *MemConfig
52 globalConfig *MemConfig
53}
54
55func NewMockRepoConfig() *mockRepoConfig {
56 return &mockRepoConfig{
57 localConfig: NewMemConfig(),
58 globalConfig: NewMemConfig(),
59 }
60}
61
62// LocalConfig give access to the repository scoped configuration
63func (r *mockRepoConfig) LocalConfig() Config {
64 return r.localConfig
65}
66
67// GlobalConfig give access to the git global configuration
68func (r *mockRepoConfig) GlobalConfig() Config {
69 return r.globalConfig
70}
71
72// AnyConfig give access to a merged local/global configuration
73func (r *mockRepoConfig) AnyConfig() ConfigRead {
74 return mergeConfig(r.localConfig, r.globalConfig)
75}
76
77var _ RepoKeyring = &mockRepoKeyring{}
78
79type mockRepoKeyring struct {
80 keyring *keyring.ArrayKeyring
81}
82
83func NewMockRepoKeyring() *mockRepoKeyring {
84 return &mockRepoKeyring{
85 keyring: keyring.NewArrayKeyring(nil),
86 }
87}
88
89// Keyring give access to a user-wide storage for secrets
90func (r *mockRepoKeyring) Keyring() Keyring {
91 return r.keyring
92}
93
94var _ RepoCommon = &mockRepoCommon{}
95
96type mockRepoCommon struct{}
97
98func NewMockRepoCommon() *mockRepoCommon {
99 return &mockRepoCommon{}
100}
101
102func (r *mockRepoCommon) GetUserName() (string, error) {
103 return "René Descartes", nil
104}
105
106// GetUserEmail returns the email address that the user has used to configure git.
107func (r *mockRepoCommon) GetUserEmail() (string, error) {
108 return "user@example.com", nil
109}
110
111// GetCoreEditor returns the name of the editor that the user has used to configure git.
112func (r *mockRepoCommon) GetCoreEditor() (string, error) {
113 return "vi", nil
114}
115
116// GetRemotes returns the configured remotes repositories.
117func (r *mockRepoCommon) GetRemotes() (map[string]string, error) {
118 return map[string]string{
119 "origin": "git://github.com/MichaelMure/git-bug",
120 }, nil
121}
122
123var _ RepoStorage = &mockRepoStorage{}
124
125type mockRepoStorage struct {
126 localFs billy.Filesystem
127}
128
129func NewMockRepoStorage() *mockRepoStorage {
130 return &mockRepoStorage{localFs: memfs.New()}
131}
132
133func (m *mockRepoStorage) LocalStorage() billy.Filesystem {
134 return m.localFs
135}
136
137var _ RepoIndex = &mockRepoIndex{}
138
139type mockRepoIndex struct {
140 indexesMutex sync.Mutex
141 indexes map[string]Index
142}
143
144func newMockRepoIndex() *mockRepoIndex {
145 return &mockRepoIndex{
146 indexes: make(map[string]Index),
147 }
148}
149
150func (m *mockRepoIndex) GetIndex(name string) (Index, error) {
151 m.indexesMutex.Lock()
152 defer m.indexesMutex.Unlock()
153
154 if index, ok := m.indexes[name]; ok {
155 return index, nil
156 }
157
158 index := newIndex()
159 m.indexes[name] = index
160 return index, nil
161}
162
163var _ Index = &mockIndex{}
164
165type mockIndex map[string][]string
166
167func newIndex() *mockIndex {
168 m := make(map[string][]string)
169 return (*mockIndex)(&m)
170}
171
172func (m *mockIndex) IndexOne(id string, texts []string) error {
173 (*m)[id] = texts
174 return nil
175}
176
177func (m *mockIndex) IndexBatch() (indexer func(id string, texts []string) error, closer func() error) {
178 indexer = func(id string, texts []string) error {
179 (*m)[id] = texts
180 return nil
181 }
182 closer = func() error { return nil }
183 return indexer, closer
184}
185
186func (m *mockIndex) Search(terms []string) (ids []string, err error) {
187loop:
188 for id, texts := range *m {
189 for _, text := range texts {
190 for _, s := range strings.Fields(text) {
191 for _, term := range terms {
192 if s == term {
193 ids = append(ids, id)
194 continue loop
195 }
196 }
197 }
198 }
199 }
200 return ids, nil
201}
202
203func (m *mockIndex) DocCount() (uint64, error) {
204 return uint64(len(*m)), nil
205}
206
207func (m *mockIndex) Clear() error {
208 for k, _ := range *m {
209 delete(*m, k)
210 }
211 return nil
212}
213
214func (m *mockIndex) Close() error {
215 return nil
216}
217
218var _ RepoData = &mockRepoData{}
219
220type commit struct {
221 treeHash Hash
222 parents []Hash
223 sig string
224}
225
226type mockRepoData struct {
227 blobs map[Hash][]byte
228 trees map[Hash]string
229 commits map[Hash]commit
230 refs map[string]Hash
231}
232
233func NewMockRepoData() *mockRepoData {
234 return &mockRepoData{
235 blobs: make(map[Hash][]byte),
236 trees: make(map[Hash]string),
237 commits: make(map[Hash]commit),
238 refs: make(map[string]Hash),
239 }
240}
241
242func (r *mockRepoData) FetchRefs(remote string, prefixes ...string) (string, error) {
243 panic("implement me")
244}
245
246// PushRefs push git refs to a remote
247func (r *mockRepoData) PushRefs(remote string, prefixes ...string) (string, error) {
248 panic("implement me")
249}
250
251func (r *mockRepoData) StoreData(data []byte) (Hash, error) {
252 rawHash := sha1.Sum(data)
253 hash := Hash(fmt.Sprintf("%x", rawHash))
254 r.blobs[hash] = data
255 return hash, nil
256}
257
258func (r *mockRepoData) ReadData(hash Hash) ([]byte, error) {
259 data, ok := r.blobs[hash]
260 if !ok {
261 return nil, ErrNotFound
262 }
263
264 return data, nil
265}
266
267func (r *mockRepoData) StoreTree(entries []TreeEntry) (Hash, error) {
268 buffer := prepareTreeEntries(entries)
269 rawHash := sha1.Sum(buffer.Bytes())
270 hash := Hash(fmt.Sprintf("%x", rawHash))
271 r.trees[hash] = buffer.String()
272
273 return hash, nil
274}
275
276func (r *mockRepoData) ReadTree(hash Hash) ([]TreeEntry, error) {
277 var data string
278
279 data, ok := r.trees[hash]
280
281 if !ok {
282 // Git will understand a commit hash to reach a tree
283 commit, ok := r.commits[hash]
284
285 if !ok {
286 return nil, ErrNotFound
287 }
288
289 data, ok = r.trees[commit.treeHash]
290
291 if !ok {
292 return nil, ErrNotFound
293 }
294 }
295
296 return readTreeEntries(data)
297}
298
299func (r *mockRepoData) StoreCommit(treeHash Hash, parents ...Hash) (Hash, error) {
300 return r.StoreSignedCommit(treeHash, nil, parents...)
301}
302
303func (r *mockRepoData) StoreSignedCommit(treeHash Hash, signKey *openpgp.Entity, parents ...Hash) (Hash, error) {
304 hasher := sha1.New()
305 hasher.Write([]byte(treeHash))
306 for _, parent := range parents {
307 hasher.Write([]byte(parent))
308 }
309 rawHash := hasher.Sum(nil)
310 hash := Hash(fmt.Sprintf("%x", rawHash))
311 c := commit{
312 treeHash: treeHash,
313 parents: parents,
314 }
315 if signKey != nil {
316 // unlike go-git, we only sign the tree hash for simplicity instead of all the fields (parents ...)
317 var sig bytes.Buffer
318 if err := openpgp.DetachSign(&sig, signKey, strings.NewReader(string(treeHash)), nil); err != nil {
319 return "", err
320 }
321 c.sig = sig.String()
322 }
323 r.commits[hash] = c
324 return hash, nil
325}
326
327func (r *mockRepoData) ReadCommit(hash Hash) (Commit, error) {
328 c, ok := r.commits[hash]
329 if !ok {
330 return Commit{}, ErrNotFound
331 }
332
333 result := Commit{
334 Hash: hash,
335 Parents: c.parents,
336 TreeHash: c.treeHash,
337 }
338
339 if c.sig != "" {
340 // Note: this is actually incorrect as the signed data should be the full commit (+comment, +date ...)
341 // but only the tree hash work for our purpose here.
342 result.SignedData = strings.NewReader(string(c.treeHash))
343 result.Signature = strings.NewReader(c.sig)
344 }
345
346 return result, nil
347}
348
349func (r *mockRepoData) ResolveRef(ref string) (Hash, error) {
350 h, ok := r.refs[ref]
351 if !ok {
352 return "", ErrNotFound
353 }
354 return h, nil
355}
356
357func (r *mockRepoData) UpdateRef(ref string, hash Hash) error {
358 r.refs[ref] = hash
359 return nil
360}
361
362func (r *mockRepoData) RemoveRef(ref string) error {
363 delete(r.refs, ref)
364 return nil
365}
366
367func (r *mockRepoData) ListRefs(refPrefix string) ([]string, error) {
368 var keys []string
369
370 for k := range r.refs {
371 if strings.HasPrefix(k, refPrefix) {
372 keys = append(keys, k)
373 }
374 }
375
376 return keys, nil
377}
378
379func (r *mockRepoData) RefExist(ref string) (bool, error) {
380 _, exist := r.refs[ref]
381 return exist, nil
382}
383
384func (r *mockRepoData) CopyRef(source string, dest string) error {
385 hash, exist := r.refs[source]
386
387 if !exist {
388 return ErrNotFound
389 }
390
391 r.refs[dest] = hash
392 return nil
393}
394
395func (r *mockRepoData) ListCommits(ref string) ([]Hash, error) {
396 return nonNativeListCommits(r, ref)
397}
398
399var _ RepoClock = &mockRepoClock{}
400
401type mockRepoClock struct {
402 mu sync.Mutex
403 clocks map[string]lamport.Clock
404}
405
406func NewMockRepoClock() *mockRepoClock {
407 return &mockRepoClock{
408 clocks: make(map[string]lamport.Clock),
409 }
410}
411
412func (r *mockRepoClock) AllClocks() (map[string]lamport.Clock, error) {
413 return r.clocks, nil
414}
415
416func (r *mockRepoClock) GetOrCreateClock(name string) (lamport.Clock, error) {
417 r.mu.Lock()
418 defer r.mu.Unlock()
419
420 if c, ok := r.clocks[name]; ok {
421 return c, nil
422 }
423
424 c := lamport.NewMemClock()
425 r.clocks[name] = c
426 return c, nil
427}
428
429func (r *mockRepoClock) Increment(name string) (lamport.Time, error) {
430 c, err := r.GetOrCreateClock(name)
431 if err != nil {
432 return lamport.Time(0), err
433 }
434 return c.Increment()
435}
436
437func (r *mockRepoClock) Witness(name string, time lamport.Time) error {
438 c, err := r.GetOrCreateClock(name)
439 if err != nil {
440 return err
441 }
442 return c.Witness(time)
443}
444
445var _ repoTest = &mockRepoTest{}
446
447type mockRepoTest struct{}
448
449func NewMockRepoTest() *mockRepoTest {
450 return &mockRepoTest{}
451}
452
453func (r *mockRepoTest) AddRemote(name string, url string) error {
454 panic("implement me")
455}
456
457func (r mockRepoTest) GetLocalRemote() string {
458 panic("implement me")
459}
460
461func (r mockRepoTest) EraseFromDisk() error {
462 // nothing to do
463 return nil
464}