mock_repo.go

  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}