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
122var _ RepoStorage = &mockRepoStorage{}
123
124type mockRepoStorage struct {
125 localFs LocalStorage
126}
127
128func NewMockRepoStorage() *mockRepoStorage {
129 return &mockRepoStorage{localFs: billyLocalStorage{Filesystem: memfs.New()}}
130}
131
132func (m *mockRepoStorage) LocalStorage() LocalStorage {
133 return m.localFs
134}
135
136var _ RepoIndex = &mockRepoIndex{}
137
138type mockRepoIndex struct {
139 indexesMutex sync.Mutex
140 indexes map[string]Index
141}
142
143func newMockRepoIndex() *mockRepoIndex {
144 return &mockRepoIndex{
145 indexes: make(map[string]Index),
146 }
147}
148
149func (m *mockRepoIndex) GetIndex(name string) (Index, error) {
150 m.indexesMutex.Lock()
151 defer m.indexesMutex.Unlock()
152
153 if index, ok := m.indexes[name]; ok {
154 return index, nil
155 }
156
157 index := newIndex()
158 m.indexes[name] = index
159 return index, nil
160}
161
162var _ Index = &mockIndex{}
163
164type mockIndex map[string][]string
165
166func newIndex() *mockIndex {
167 m := make(map[string][]string)
168 return (*mockIndex)(&m)
169}
170
171func (m *mockIndex) IndexOne(id string, texts []string) error {
172 (*m)[id] = texts
173 return nil
174}
175
176func (m *mockIndex) IndexBatch() (indexer func(id string, texts []string) error, closer func() error) {
177 indexer = func(id string, texts []string) error {
178 (*m)[id] = texts
179 return nil
180 }
181 closer = func() error { return nil }
182 return indexer, closer
183}
184
185func (m *mockIndex) Search(terms []string) (ids []string, err error) {
186loop:
187 for id, texts := range *m {
188 for _, text := range texts {
189 for _, s := range strings.Fields(text) {
190 for _, term := range terms {
191 if s == term {
192 ids = append(ids, id)
193 continue loop
194 }
195 }
196 }
197 }
198 }
199 return ids, nil
200}
201
202func (m *mockIndex) DocCount() (uint64, error) {
203 return uint64(len(*m)), nil
204}
205
206func (m *mockIndex) Remove(id string) error {
207 delete(*m, id)
208 return nil
209}
210
211func (m *mockIndex) Clear() error {
212 for k, _ := range *m {
213 delete(*m, k)
214 }
215 return nil
216}
217
218func (m *mockIndex) Close() error {
219 return nil
220}
221
222var _ RepoData = &mockRepoData{}
223
224type commit struct {
225 treeHash Hash
226 parents []Hash
227 sig string
228}
229
230type mockRepoData struct {
231 blobs map[Hash][]byte
232 trees map[Hash]string
233 commits map[Hash]commit
234 refs map[string]Hash
235}
236
237func NewMockRepoData() *mockRepoData {
238 return &mockRepoData{
239 blobs: make(map[Hash][]byte),
240 trees: make(map[Hash]string),
241 commits: make(map[Hash]commit),
242 refs: make(map[string]Hash),
243 }
244}
245
246func (r *mockRepoData) FetchRefs(remote string, prefixes ...string) (string, error) {
247 panic("implement me")
248}
249
250// PushRefs push git refs to a remote
251func (r *mockRepoData) PushRefs(remote string, prefixes ...string) (string, error) {
252 panic("implement me")
253}
254
255func (r *mockRepoData) StoreData(data []byte) (Hash, error) {
256 rawHash := sha1.Sum(data)
257 hash := Hash(fmt.Sprintf("%x", rawHash))
258 r.blobs[hash] = data
259 return hash, nil
260}
261
262func (r *mockRepoData) ReadData(hash Hash) ([]byte, error) {
263 data, ok := r.blobs[hash]
264 if !ok {
265 return nil, ErrNotFound
266 }
267
268 return data, nil
269}
270
271func (r *mockRepoData) StoreTree(entries []TreeEntry) (Hash, error) {
272 buffer := prepareTreeEntries(entries)
273 rawHash := sha1.Sum(buffer.Bytes())
274 hash := Hash(fmt.Sprintf("%x", rawHash))
275 r.trees[hash] = buffer.String()
276
277 return hash, nil
278}
279
280func (r *mockRepoData) ReadTree(hash Hash) ([]TreeEntry, error) {
281 var data string
282
283 data, ok := r.trees[hash]
284
285 if !ok {
286 // Git will understand a commit hash to reach a tree
287 commit, ok := r.commits[hash]
288
289 if !ok {
290 return nil, ErrNotFound
291 }
292
293 data, ok = r.trees[commit.treeHash]
294
295 if !ok {
296 return nil, ErrNotFound
297 }
298 }
299
300 return readTreeEntries(data)
301}
302
303func (r *mockRepoData) StoreCommit(treeHash Hash, parents ...Hash) (Hash, error) {
304 return r.StoreSignedCommit(treeHash, nil, parents...)
305}
306
307func (r *mockRepoData) StoreSignedCommit(treeHash Hash, signKey *openpgp.Entity, parents ...Hash) (Hash, error) {
308 hasher := sha1.New()
309 hasher.Write([]byte(treeHash))
310 for _, parent := range parents {
311 hasher.Write([]byte(parent))
312 }
313 rawHash := hasher.Sum(nil)
314 hash := Hash(fmt.Sprintf("%x", rawHash))
315 c := commit{
316 treeHash: treeHash,
317 parents: parents,
318 }
319 if signKey != nil {
320 // unlike go-git, we only sign the tree hash for simplicity instead of all the fields (parents ...)
321 var sig bytes.Buffer
322 if err := openpgp.DetachSign(&sig, signKey, strings.NewReader(string(treeHash)), nil); err != nil {
323 return "", err
324 }
325 c.sig = sig.String()
326 }
327 r.commits[hash] = c
328 return hash, nil
329}
330
331func (r *mockRepoData) ReadCommit(hash Hash) (Commit, error) {
332 c, ok := r.commits[hash]
333 if !ok {
334 return Commit{}, ErrNotFound
335 }
336
337 result := Commit{
338 Hash: hash,
339 Parents: c.parents,
340 TreeHash: c.treeHash,
341 }
342
343 if c.sig != "" {
344 // Note: this is actually incorrect as the signed data should be the full commit (+comment, +date ...)
345 // but only the tree hash work for our purpose here.
346 result.SignedData = strings.NewReader(string(c.treeHash))
347 result.Signature = strings.NewReader(c.sig)
348 }
349
350 return result, nil
351}
352
353func (r *mockRepoData) ResolveRef(ref string) (Hash, error) {
354 h, ok := r.refs[ref]
355 if !ok {
356 return "", ErrNotFound
357 }
358 return h, nil
359}
360
361func (r *mockRepoData) UpdateRef(ref string, hash Hash) error {
362 r.refs[ref] = hash
363 return nil
364}
365
366func (r *mockRepoData) RemoveRef(ref string) error {
367 delete(r.refs, ref)
368 return nil
369}
370
371func (r *mockRepoData) ListRefs(refPrefix string) ([]string, error) {
372 var keys []string
373
374 for k := range r.refs {
375 if strings.HasPrefix(k, refPrefix) {
376 keys = append(keys, k)
377 }
378 }
379
380 return keys, nil
381}
382
383func (r *mockRepoData) RefExist(ref string) (bool, error) {
384 _, exist := r.refs[ref]
385 return exist, nil
386}
387
388func (r *mockRepoData) CopyRef(source string, dest string) error {
389 hash, exist := r.refs[source]
390
391 if !exist {
392 return ErrNotFound
393 }
394
395 r.refs[dest] = hash
396 return nil
397}
398
399func (r *mockRepoData) ListCommits(ref string) ([]Hash, error) {
400 return nonNativeListCommits(r, ref)
401}
402
403var _ RepoClock = &mockRepoClock{}
404
405type mockRepoClock struct {
406 mu sync.Mutex
407 clocks map[string]lamport.Clock
408}
409
410func NewMockRepoClock() *mockRepoClock {
411 return &mockRepoClock{
412 clocks: make(map[string]lamport.Clock),
413 }
414}
415
416func (r *mockRepoClock) AllClocks() (map[string]lamport.Clock, error) {
417 return r.clocks, nil
418}
419
420func (r *mockRepoClock) GetOrCreateClock(name string) (lamport.Clock, error) {
421 r.mu.Lock()
422 defer r.mu.Unlock()
423
424 if c, ok := r.clocks[name]; ok {
425 return c, nil
426 }
427
428 c := lamport.NewMemClock()
429 r.clocks[name] = c
430 return c, nil
431}
432
433func (r *mockRepoClock) Increment(name string) (lamport.Time, error) {
434 c, err := r.GetOrCreateClock(name)
435 if err != nil {
436 return lamport.Time(0), err
437 }
438 return c.Increment()
439}
440
441func (r *mockRepoClock) Witness(name string, time lamport.Time) error {
442 c, err := r.GetOrCreateClock(name)
443 if err != nil {
444 return err
445 }
446 return c.Witness(time)
447}
448
449var _ repoTest = &mockRepoTest{}
450
451type mockRepoTest struct{}
452
453func NewMockRepoTest() *mockRepoTest {
454 return &mockRepoTest{}
455}
456
457func (r *mockRepoTest) AddRemote(name string, url string) error {
458 panic("implement me")
459}
460
461func (r mockRepoTest) GetLocalRemote() string {
462 panic("implement me")
463}
464
465func (r mockRepoTest) EraseFromDisk() error {
466 // nothing to do
467 return nil
468}