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