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, prefixes ...string) (string, error) {
243	panic("implement me")
244}
245
246// PushRefs push git refs to a remote
247func (r *mockRepoData) PushRefs(remote string, prefixes ...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, ErrNotFound
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, ErrNotFound
287		}
288
289		data, ok = r.trees[commit.treeHash]
290
291		if !ok {
292			return nil, ErrNotFound
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{}, ErrNotFound
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) ResolveRef(ref string) (Hash, error) {
350	h, ok := r.refs[ref]
351	if !ok {
352		return "", ErrNotFound
353	}
354	return h, nil
355}
356
357func (r *mockRepoData) UpdateRef(ref string, hash Hash) error {
358	r.refs[ref] = hash
359	return nil
360}
361
362func (r *mockRepoData) RemoveRef(ref string) error {
363	delete(r.refs, ref)
364	return nil
365}
366
367func (r *mockRepoData) ListRefs(refPrefix string) ([]string, error) {
368	var keys []string
369
370	for k := range r.refs {
371		if strings.HasPrefix(k, refPrefix) {
372			keys = append(keys, k)
373		}
374	}
375
376	return keys, nil
377}
378
379func (r *mockRepoData) RefExist(ref string) (bool, error) {
380	_, exist := r.refs[ref]
381	return exist, nil
382}
383
384func (r *mockRepoData) CopyRef(source string, dest string) error {
385	hash, exist := r.refs[source]
386
387	if !exist {
388		return ErrNotFound
389	}
390
391	r.refs[dest] = hash
392	return nil
393}
394
395func (r *mockRepoData) ListCommits(ref string) ([]Hash, error) {
396	return nonNativeListCommits(r, ref)
397}
398
399var _ RepoClock = &mockRepoClock{}
400
401type mockRepoClock struct {
402	mu     sync.Mutex
403	clocks map[string]lamport.Clock
404}
405
406func NewMockRepoClock() *mockRepoClock {
407	return &mockRepoClock{
408		clocks: make(map[string]lamport.Clock),
409	}
410}
411
412func (r *mockRepoClock) AllClocks() (map[string]lamport.Clock, error) {
413	return r.clocks, nil
414}
415
416func (r *mockRepoClock) GetOrCreateClock(name string) (lamport.Clock, error) {
417	r.mu.Lock()
418	defer r.mu.Unlock()
419
420	if c, ok := r.clocks[name]; ok {
421		return c, nil
422	}
423
424	c := lamport.NewMemClock()
425	r.clocks[name] = c
426	return c, nil
427}
428
429func (r *mockRepoClock) Increment(name string) (lamport.Time, error) {
430	c, err := r.GetOrCreateClock(name)
431	if err != nil {
432		return lamport.Time(0), err
433	}
434	return c.Increment()
435}
436
437func (r *mockRepoClock) Witness(name string, time lamport.Time) error {
438	c, err := r.GetOrCreateClock(name)
439	if err != nil {
440		return err
441	}
442	return c.Witness(time)
443}
444
445var _ repoTest = &mockRepoTest{}
446
447type mockRepoTest struct{}
448
449func NewMockRepoTest() *mockRepoTest {
450	return &mockRepoTest{}
451}
452
453func (r *mockRepoTest) AddRemote(name string, url string) error {
454	panic("implement me")
455}
456
457func (r mockRepoTest) GetLocalRemote() string {
458	panic("implement me")
459}
460
461func (r mockRepoTest) EraseFromDisk() error {
462	// nothing to do
463	return nil
464}