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}