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, prefix string) (string, error) {
243 panic("implement me")
244}
245
246// PushRefs push git refs to a remote
247func (r *mockRepoData) PushRefs(remote string, prefix 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, fmt.Errorf("unknown hash")
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, fmt.Errorf("unknown hash")
287 }
288
289 data, ok = r.trees[commit.treeHash]
290
291 if !ok {
292 return nil, fmt.Errorf("unknown hash")
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{}, fmt.Errorf("unknown commit")
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) GetTreeHash(commit Hash) (Hash, error) {
350 c, ok := r.commits[commit]
351 if !ok {
352 return "", fmt.Errorf("unknown commit")
353 }
354
355 return c.treeHash, nil
356}
357
358func (r *mockRepoData) ResolveRef(ref string) (Hash, error) {
359 h, ok := r.refs[ref]
360 if !ok {
361 return "", fmt.Errorf("unknown ref")
362 }
363 return h, nil
364}
365
366func (r *mockRepoData) UpdateRef(ref string, hash Hash) error {
367 r.refs[ref] = hash
368 return nil
369}
370
371func (r *mockRepoData) RemoveRef(ref string) error {
372 delete(r.refs, ref)
373 return nil
374}
375
376func (r *mockRepoData) ListRefs(refPrefix string) ([]string, error) {
377 var keys []string
378
379 for k := range r.refs {
380 if strings.HasPrefix(k, refPrefix) {
381 keys = append(keys, k)
382 }
383 }
384
385 return keys, nil
386}
387
388func (r *mockRepoData) RefExist(ref string) (bool, error) {
389 _, exist := r.refs[ref]
390 return exist, nil
391}
392
393func (r *mockRepoData) CopyRef(source string, dest string) error {
394 hash, exist := r.refs[source]
395
396 if !exist {
397 return fmt.Errorf("Unknown ref")
398 }
399
400 r.refs[dest] = hash
401 return nil
402}
403
404func (r *mockRepoData) FindCommonAncestor(hash1 Hash, hash2 Hash) (Hash, error) {
405 ancestor1 := []Hash{hash1}
406
407 for hash1 != "" {
408 c, ok := r.commits[hash1]
409 if !ok {
410 return "", fmt.Errorf("unknown commit %v", hash1)
411 }
412 if len(c.parents) == 0 {
413 break
414 }
415 ancestor1 = append(ancestor1, c.parents[0])
416 hash1 = c.parents[0]
417 }
418
419 for {
420 for _, ancestor := range ancestor1 {
421 if ancestor == hash2 {
422 return ancestor, nil
423 }
424 }
425
426 c, ok := r.commits[hash2]
427 if !ok {
428 return "", fmt.Errorf("unknown commit %v", hash1)
429 }
430
431 if c.parents[0] == "" {
432 return "", fmt.Errorf("no ancestor found")
433 }
434
435 hash2 = c.parents[0]
436 }
437}
438
439func (r *mockRepoData) ListCommits(ref string) ([]Hash, error) {
440 return nonNativeListCommits(r, ref)
441}
442
443var _ RepoClock = &mockRepoClock{}
444
445type mockRepoClock struct {
446 mu sync.Mutex
447 clocks map[string]lamport.Clock
448}
449
450func NewMockRepoClock() *mockRepoClock {
451 return &mockRepoClock{
452 clocks: make(map[string]lamport.Clock),
453 }
454}
455
456func (r *mockRepoClock) AllClocks() (map[string]lamport.Clock, error) {
457 return r.clocks, nil
458}
459
460func (r *mockRepoClock) GetOrCreateClock(name string) (lamport.Clock, error) {
461 r.mu.Lock()
462 defer r.mu.Unlock()
463
464 if c, ok := r.clocks[name]; ok {
465 return c, nil
466 }
467
468 c := lamport.NewMemClock()
469 r.clocks[name] = c
470 return c, nil
471}
472
473func (r *mockRepoClock) Increment(name string) (lamport.Time, error) {
474 c, err := r.GetOrCreateClock(name)
475 if err != nil {
476 return lamport.Time(0), err
477 }
478 return c.Increment()
479}
480
481func (r *mockRepoClock) Witness(name string, time lamport.Time) error {
482 c, err := r.GetOrCreateClock(name)
483 if err != nil {
484 return err
485 }
486 return c.Witness(time)
487}
488
489var _ repoTest = &mockRepoTest{}
490
491type mockRepoTest struct{}
492
493func NewMockRepoTest() *mockRepoTest {
494 return &mockRepoTest{}
495}
496
497func (r *mockRepoTest) AddRemote(name string, url string) error {
498 panic("implement me")
499}
500
501func (r mockRepoTest) GetLocalRemote() string {
502 panic("implement me")
503}
504
505func (r mockRepoTest) EraseFromDisk() error {
506 // nothing to do
507 return nil
508}