1package repository
2
3import (
4 "bytes"
5 "crypto/sha1"
6 "fmt"
7 "io"
8 "strings"
9 "sync"
10 "time"
11
12 "github.com/99designs/keyring"
13 "github.com/ProtonMail/go-crypto/openpgp"
14 "github.com/go-git/go-billy/v5/memfs"
15
16 "github.com/git-bug/git-bug/util/lamport"
17)
18
19var _ ClockedRepo = &mockRepo{}
20var _ TestedRepo = &mockRepo{}
21
22// mockRepo defines an instance of Repo that can be used for testing.
23type mockRepo struct {
24 *mockRepoConfig
25 *mockRepoKeyring
26 *mockRepoCommon
27 *mockRepoStorage
28 *mockRepoIndex
29 *mockRepoDataBrowse
30 *mockRepoClock
31 *mockRepoTest
32}
33
34func (m *mockRepo) Close() error { return nil }
35
36func NewMockRepo() *mockRepo {
37 return &mockRepo{
38 mockRepoConfig: NewMockRepoConfig(),
39 mockRepoKeyring: NewMockRepoKeyring(),
40 mockRepoCommon: NewMockRepoCommon(),
41 mockRepoStorage: NewMockRepoStorage(),
42 mockRepoIndex: newMockRepoIndex(),
43 mockRepoDataBrowse: newMockRepoDataBrowse(),
44 mockRepoClock: NewMockRepoClock(),
45 mockRepoTest: NewMockRepoTest(),
46 }
47}
48
49var _ RepoConfig = &mockRepoConfig{}
50
51type mockRepoConfig struct {
52 localConfig *MemConfig
53 globalConfig *MemConfig
54}
55
56func NewMockRepoConfig() *mockRepoConfig {
57 return &mockRepoConfig{
58 localConfig: NewMemConfig(),
59 globalConfig: NewMemConfig(),
60 }
61}
62
63// LocalConfig give access to the repository scoped configuration
64func (r *mockRepoConfig) LocalConfig() Config {
65 return r.localConfig
66}
67
68// GlobalConfig give access to the git global configuration
69func (r *mockRepoConfig) GlobalConfig() Config {
70 return r.globalConfig
71}
72
73// AnyConfig give access to a merged local/global configuration
74func (r *mockRepoConfig) AnyConfig() ConfigRead {
75 return mergeConfig(r.localConfig, r.globalConfig)
76}
77
78var _ RepoKeyring = &mockRepoKeyring{}
79
80type mockRepoKeyring struct {
81 keyring *keyring.ArrayKeyring
82}
83
84func NewMockRepoKeyring() *mockRepoKeyring {
85 return &mockRepoKeyring{
86 keyring: keyring.NewArrayKeyring(nil),
87 }
88}
89
90// Keyring give access to a user-wide storage for secrets
91func (r *mockRepoKeyring) Keyring() Keyring {
92 return r.keyring
93}
94
95var _ RepoCommon = &mockRepoCommon{}
96
97type mockRepoCommon struct{}
98
99func NewMockRepoCommon() *mockRepoCommon {
100 return &mockRepoCommon{}
101}
102
103func (r *mockRepoCommon) GetUserName() (string, error) {
104 return "René Descartes", nil
105}
106
107// GetUserEmail returns the email address that the user has used to configure git.
108func (r *mockRepoCommon) GetUserEmail() (string, error) {
109 return "user@example.com", nil
110}
111
112// GetCoreEditor returns the name of the editor that the user has used to configure git.
113func (r *mockRepoCommon) GetCoreEditor() (string, error) {
114 return "vi", nil
115}
116
117// GetRemotes returns the configured remotes repositories.
118func (r *mockRepoCommon) GetRemotes() (map[string]string, error) {
119 return map[string]string{
120 "origin": "git://github.com/git-bug/git-bug",
121 }, nil
122}
123
124
125var _ RepoStorage = &mockRepoStorage{}
126
127type mockRepoStorage struct {
128 localFs LocalStorage
129}
130
131func NewMockRepoStorage() *mockRepoStorage {
132 return &mockRepoStorage{localFs: billyLocalStorage{Filesystem: memfs.New()}}
133}
134
135func (m *mockRepoStorage) LocalStorage() LocalStorage {
136 return m.localFs
137}
138
139var _ RepoIndex = &mockRepoIndex{}
140
141type mockRepoIndex struct {
142 indexesMutex sync.Mutex
143 indexes map[string]Index
144}
145
146func newMockRepoIndex() *mockRepoIndex {
147 return &mockRepoIndex{
148 indexes: make(map[string]Index),
149 }
150}
151
152func (m *mockRepoIndex) GetIndex(name string) (Index, error) {
153 m.indexesMutex.Lock()
154 defer m.indexesMutex.Unlock()
155
156 if index, ok := m.indexes[name]; ok {
157 return index, nil
158 }
159
160 index := newIndex()
161 m.indexes[name] = index
162 return index, nil
163}
164
165var _ Index = &mockIndex{}
166
167type mockIndex map[string][]string
168
169func newIndex() *mockIndex {
170 m := make(map[string][]string)
171 return (*mockIndex)(&m)
172}
173
174func (m *mockIndex) IndexOne(id string, texts []string) error {
175 (*m)[id] = texts
176 return nil
177}
178
179func (m *mockIndex) IndexBatch() (indexer func(id string, texts []string) error, closer func() error) {
180 indexer = func(id string, texts []string) error {
181 (*m)[id] = texts
182 return nil
183 }
184 closer = func() error { return nil }
185 return indexer, closer
186}
187
188func (m *mockIndex) Search(terms []string) (ids []string, err error) {
189loop:
190 for id, texts := range *m {
191 for _, text := range texts {
192 for _, s := range strings.Fields(text) {
193 for _, term := range terms {
194 if s == term {
195 ids = append(ids, id)
196 continue loop
197 }
198 }
199 }
200 }
201 }
202 return ids, nil
203}
204
205func (m *mockIndex) DocCount() (uint64, error) {
206 return uint64(len(*m)), nil
207}
208
209func (m *mockIndex) Remove(id string) error {
210 delete(*m, id)
211 return nil
212}
213
214func (m *mockIndex) Clear() error {
215 for k, _ := range *m {
216 delete(*m, k)
217 }
218 return nil
219}
220
221func (m *mockIndex) Close() error {
222 return nil
223}
224
225var _ RepoData = &mockRepoDataBrowse{}
226
227type commit struct {
228 treeHash Hash
229 parents []Hash
230 sig string
231 date time.Time
232 message string
233}
234
235type mockRepoDataBrowse struct {
236 blobs map[Hash][]byte
237 trees map[Hash]string
238 commits map[Hash]commit
239 refs map[string]Hash
240 defaultBranch string
241}
242
243func newMockRepoDataBrowse() *mockRepoDataBrowse {
244 return &mockRepoDataBrowse{
245 blobs: make(map[Hash][]byte),
246 trees: make(map[Hash]string),
247 commits: make(map[Hash]commit),
248 refs: make(map[string]Hash),
249 defaultBranch: "main",
250 }
251}
252
253func (r *mockRepoDataBrowse) FetchRefs(remote string, prefixes ...string) (string, error) {
254 panic("implement me")
255}
256
257// PushRefs push git refs to a remote
258func (r *mockRepoDataBrowse) PushRefs(remote string, prefixes ...string) (string, error) {
259 panic("implement me")
260}
261
262func (r *mockRepoDataBrowse) StoreData(data []byte) (Hash, error) {
263 rawHash := sha1.Sum(data)
264 hash := Hash(fmt.Sprintf("%x", rawHash))
265 r.blobs[hash] = data
266 return hash, nil
267}
268
269func (r *mockRepoDataBrowse) ReadData(hash Hash) ([]byte, error) {
270 data, ok := r.blobs[hash]
271 if !ok {
272 return nil, ErrNotFound
273 }
274
275 return data, nil
276}
277
278func (r *mockRepoDataBrowse) StoreTree(entries []TreeEntry) (Hash, error) {
279 buffer := prepareTreeEntries(entries)
280 rawHash := sha1.Sum(buffer.Bytes())
281 hash := Hash(fmt.Sprintf("%x", rawHash))
282 r.trees[hash] = buffer.String()
283
284 return hash, nil
285}
286
287func (r *mockRepoDataBrowse) ReadTree(hash Hash) ([]TreeEntry, error) {
288 var data string
289
290 data, ok := r.trees[hash]
291
292 if !ok {
293 // Git will understand a commit hash to reach a tree
294 commit, ok := r.commits[hash]
295
296 if !ok {
297 return nil, ErrNotFound
298 }
299
300 data, ok = r.trees[commit.treeHash]
301
302 if !ok {
303 return nil, ErrNotFound
304 }
305 }
306
307 return readTreeEntries(data)
308}
309
310func (r *mockRepoDataBrowse) StoreCommit(treeHash Hash, parents ...Hash) (Hash, error) {
311 return r.StoreSignedCommit(treeHash, nil, parents...)
312}
313
314func (r *mockRepoDataBrowse) StoreSignedCommit(treeHash Hash, signKey *openpgp.Entity, parents ...Hash) (Hash, error) {
315 hasher := sha1.New()
316 hasher.Write([]byte(treeHash))
317 for _, parent := range parents {
318 hasher.Write([]byte(parent))
319 }
320 rawHash := hasher.Sum(nil)
321 hash := Hash(fmt.Sprintf("%x", rawHash))
322 c := commit{
323 treeHash: treeHash,
324 parents: parents,
325 date: time.Now(),
326 }
327 if signKey != nil {
328 // unlike go-git, we only sign the tree hash for simplicity instead of all the fields (parents ...)
329 var sig bytes.Buffer
330 if err := openpgp.DetachSign(&sig, signKey, strings.NewReader(string(treeHash)), nil); err != nil {
331 return "", err
332 }
333 c.sig = sig.String()
334 }
335 r.commits[hash] = c
336 return hash, nil
337}
338
339func (r *mockRepoDataBrowse) ReadCommit(hash Hash) (Commit, error) {
340 c, ok := r.commits[hash]
341 if !ok {
342 return Commit{}, ErrNotFound
343 }
344
345 result := Commit{
346 Hash: hash,
347 Parents: c.parents,
348 TreeHash: c.treeHash,
349 }
350
351 if c.sig != "" {
352 // Note: this is actually incorrect as the signed data should be the full commit (+comment, +date ...)
353 // but only the tree hash work for our purpose here.
354 result.SignedData = strings.NewReader(string(c.treeHash))
355 result.Signature = strings.NewReader(c.sig)
356 }
357
358 return result, nil
359}
360
361func (r *mockRepoDataBrowse) ResolveRef(ref string) (Hash, error) {
362 h, ok := r.refs[ref]
363 if !ok {
364 return "", ErrNotFound
365 }
366 return h, nil
367}
368
369func (r *mockRepoDataBrowse) UpdateRef(ref string, hash Hash) error {
370 r.refs[ref] = hash
371 return nil
372}
373
374func (r *mockRepoDataBrowse) RemoveRef(ref string) error {
375 delete(r.refs, ref)
376 return nil
377}
378
379func (r *mockRepoDataBrowse) ListRefs(refPrefix string) ([]string, error) {
380 var keys []string
381
382 for k := range r.refs {
383 if strings.HasPrefix(k, refPrefix) {
384 keys = append(keys, k)
385 }
386 }
387
388 return keys, nil
389}
390
391func (r *mockRepoDataBrowse) RefExist(ref string) (bool, error) {
392 _, exist := r.refs[ref]
393 return exist, nil
394}
395
396func (r *mockRepoDataBrowse) CopyRef(source string, dest string) error {
397 hash, exist := r.refs[source]
398
399 if !exist {
400 return ErrNotFound
401 }
402
403 r.refs[dest] = hash
404 return nil
405}
406
407func (r *mockRepoDataBrowse) ListCommits(ref string) ([]Hash, error) {
408 return nonNativeListCommits(r, ref)
409}
410
411// resolveRef resolves a ref matching the RepoBrowse contract:
412// refs/heads/<ref>, refs/tags/<ref>, full ref name, raw commit hash.
413func (r *mockRepoDataBrowse) resolveRef(ref string) (Hash, error) {
414 for _, candidate := range []string{"refs/heads/" + ref, "refs/tags/" + ref, ref} {
415 if h, ok := r.refs[candidate]; ok {
416 return h, nil
417 }
418 }
419 if _, ok := r.commits[Hash(ref)]; ok {
420 return Hash(ref), nil
421 }
422 return "", ErrNotFound
423}
424
425// treeEntriesAtHash parses the entries of the tree stored under hash.
426func (r *mockRepoDataBrowse) treeEntriesAtHash(hash Hash) ([]TreeEntry, error) {
427 data, ok := r.trees[hash]
428 if !ok {
429 return nil, ErrNotFound
430 }
431 return readTreeEntries(data)
432}
433
434// treeEntriesAt returns the directory entries at path inside the tree rooted at
435// treeHash. path="" returns root entries. Returns ErrNotFound if path doesn't
436// exist or resolves to a blob rather than a tree.
437func (r *mockRepoDataBrowse) treeEntriesAt(treeHash Hash, path string) ([]TreeEntry, error) {
438 path = strings.Trim(path, "/")
439 if path == "" {
440 return r.treeEntriesAtHash(treeHash)
441 }
442 seg, rest, _ := strings.Cut(path, "/")
443 entries, err := r.treeEntriesAtHash(treeHash)
444 if err != nil {
445 return nil, err
446 }
447 for _, e := range entries {
448 if e.Name != seg || e.ObjectType != Tree {
449 continue
450 }
451 if rest == "" {
452 return r.treeEntriesAtHash(e.Hash)
453 }
454 return r.treeEntriesAt(e.Hash, rest)
455 }
456 return nil, ErrNotFound
457}
458
459// blobHashAt walks the tree to find the blob hash for the file at path.
460func (r *mockRepoDataBrowse) blobHashAt(treeHash Hash, path string) (Hash, error) {
461 path = strings.Trim(path, "/")
462 seg, rest, hasRest := strings.Cut(path, "/")
463 entries, err := r.treeEntriesAtHash(treeHash)
464 if err != nil {
465 return "", err
466 }
467 for _, e := range entries {
468 if e.Name != seg {
469 continue
470 }
471 if !hasRest {
472 return e.Hash, nil
473 }
474 if e.ObjectType != Tree {
475 return "", ErrNotFound
476 }
477 return r.blobHashAt(e.Hash, rest)
478 }
479 return "", ErrNotFound
480}
481
482// diffTrees returns the changed files between two trees, recursing into
483// sub-trees. fromHash=="" means an empty (non-existent) tree.
484func (r *mockRepoDataBrowse) diffTrees(fromHash, toHash Hash, prefix string) []ChangedFile {
485 var fromEntries, toEntries []TreeEntry
486 if fromHash != "" {
487 fromEntries, _ = r.treeEntriesAtHash(fromHash)
488 }
489 if toHash != "" {
490 toEntries, _ = r.treeEntriesAtHash(toHash)
491 }
492
493 fromMap := make(map[string]TreeEntry, len(fromEntries))
494 for _, e := range fromEntries {
495 fromMap[e.Name] = e
496 }
497 toMap := make(map[string]TreeEntry, len(toEntries))
498 for _, e := range toEntries {
499 toMap[e.Name] = e
500 }
501
502 var result []ChangedFile
503 for _, e := range toEntries {
504 path := prefix + e.Name
505 f, existed := fromMap[e.Name]
506 if e.ObjectType == Tree {
507 var sub Hash
508 if existed {
509 sub = f.Hash
510 }
511 result = append(result, r.diffTrees(sub, e.Hash, path+"/")...)
512 } else if !existed {
513 result = append(result, ChangedFile{Path: path, Status: ChangeStatusAdded})
514 } else if f.Hash != e.Hash {
515 result = append(result, ChangedFile{Path: path, Status: ChangeStatusModified})
516 }
517 }
518 for _, f := range fromEntries {
519 if _, exists := toMap[f.Name]; exists {
520 continue
521 }
522 path := prefix + f.Name
523 if f.ObjectType == Tree {
524 result = append(result, r.diffTrees(f.Hash, "", path+"/")...)
525 } else {
526 result = append(result, ChangedFile{Path: path, Status: ChangeStatusDeleted})
527 }
528 }
529 return result
530}
531
532func mockCommitMeta(hash Hash, c commit) CommitMeta {
533 return CommitMeta{
534 Hash: hash,
535 Parents: c.parents,
536 Date: c.date,
537 Message: c.message,
538 }
539}
540
541func (r *mockRepoDataBrowse) Branches() ([]BranchInfo, error) {
542 var branches []BranchInfo
543 for ref, hash := range r.refs {
544 name, ok := strings.CutPrefix(ref, "refs/heads/")
545 if !ok {
546 continue
547 }
548 branches = append(branches, BranchInfo{
549 Name: name,
550 Hash: hash,
551 IsDefault: name == r.defaultBranch,
552 })
553 }
554 return branches, nil
555}
556
557func (r *mockRepoDataBrowse) Tags() ([]TagInfo, error) {
558 var tags []TagInfo
559 for ref, hash := range r.refs {
560 name, ok := strings.CutPrefix(ref, "refs/tags/")
561 if !ok {
562 continue
563 }
564 tags = append(tags, TagInfo{Name: name, Hash: hash})
565 }
566 return tags, nil
567}
568
569func (r *mockRepoDataBrowse) TreeAtPath(ref, path string) ([]TreeEntry, error) {
570 startHash, err := r.resolveRef(ref)
571 if err != nil {
572 return nil, ErrNotFound
573 }
574 c, ok := r.commits[startHash]
575 if !ok {
576 return nil, ErrNotFound
577 }
578 return r.treeEntriesAt(c.treeHash, path)
579}
580
581func (r *mockRepoDataBrowse) BlobAtPath(ref, path string) (io.ReadCloser, int64, Hash, error) {
582 startHash, err := r.resolveRef(ref)
583 if err != nil {
584 return nil, 0, "", ErrNotFound
585 }
586 c, ok := r.commits[startHash]
587 if !ok {
588 return nil, 0, "", ErrNotFound
589 }
590 blobHash, err := r.blobHashAt(c.treeHash, path)
591 if err != nil {
592 return nil, 0, "", ErrNotFound
593 }
594 data, ok := r.blobs[blobHash]
595 if !ok {
596 return nil, 0, "", ErrNotFound
597 }
598 return io.NopCloser(bytes.NewReader(data)), int64(len(data)), blobHash, nil
599}
600
601func (r *mockRepoDataBrowse) CommitLog(ref, path string, limit int, after Hash, since, until *time.Time) ([]CommitMeta, error) {
602 startHash, err := r.resolveRef(ref)
603 if err != nil {
604 return nil, ErrNotFound
605 }
606 path = strings.Trim(path, "/")
607 var result []CommitMeta
608 skipping := after != ""
609 current := startHash
610 seen := make(map[Hash]bool)
611 for {
612 if seen[current] {
613 break
614 }
615 seen[current] = true
616 c, ok := r.commits[current]
617 if !ok {
618 break
619 }
620 if skipping {
621 if current == after {
622 skipping = false
623 }
624 if len(c.parents) == 0 {
625 break
626 }
627 current = c.parents[0]
628 continue
629 }
630 meta := mockCommitMeta(current, c)
631 if since != nil && meta.Date.Before(*since) {
632 if len(c.parents) == 0 {
633 break
634 }
635 current = c.parents[0]
636 continue
637 }
638 if until != nil && meta.Date.After(*until) {
639 if len(c.parents) == 0 {
640 break
641 }
642 current = c.parents[0]
643 continue
644 }
645 if path != "" {
646 var fromTreeHash Hash
647 if len(c.parents) > 0 {
648 if parent, ok := r.commits[c.parents[0]]; ok {
649 fromTreeHash = parent.treeHash
650 }
651 }
652 touched := false
653 for _, f := range r.diffTrees(fromTreeHash, c.treeHash, "") {
654 if f.Path == path || strings.HasPrefix(f.Path, path+"/") {
655 touched = true
656 break
657 }
658 }
659 if !touched {
660 if len(c.parents) == 0 {
661 break
662 }
663 current = c.parents[0]
664 continue
665 }
666 }
667 result = append(result, meta)
668 if limit > 0 && len(result) >= limit {
669 break
670 }
671 if len(c.parents) == 0 {
672 break
673 }
674 current = c.parents[0]
675 }
676 return result, nil
677}
678
679func (r *mockRepoDataBrowse) LastCommitForEntries(ref, path string, names []string) (map[string]CommitMeta, error) {
680 startHash, err := r.resolveRef(ref)
681 if err != nil {
682 return nil, ErrNotFound
683 }
684 path = strings.Trim(path, "/")
685 remaining := make(map[string]bool, len(names))
686 for _, n := range names {
687 remaining[n] = true
688 }
689 result := make(map[string]CommitMeta)
690 current := startHash
691 seen := make(map[Hash]bool)
692 for len(remaining) > 0 {
693 if seen[current] {
694 break
695 }
696 seen[current] = true
697 c, ok := r.commits[current]
698 if !ok {
699 break
700 }
701 curEntries, err := r.treeEntriesAt(c.treeHash, path)
702 if err != nil {
703 if len(c.parents) == 0 {
704 break
705 }
706 current = c.parents[0]
707 continue
708 }
709 curMap := make(map[string]Hash, len(curEntries))
710 for _, e := range curEntries {
711 curMap[e.Name] = e.Hash
712 }
713 if len(c.parents) == 0 {
714 for name := range remaining {
715 if _, ok := curMap[name]; ok {
716 result[name] = mockCommitMeta(current, c)
717 delete(remaining, name)
718 }
719 }
720 break
721 }
722 pc, ok := r.commits[c.parents[0]]
723 if !ok {
724 break
725 }
726 parentEntries, _ := r.treeEntriesAt(pc.treeHash, path)
727 parentMap := make(map[string]Hash, len(parentEntries))
728 for _, e := range parentEntries {
729 parentMap[e.Name] = e.Hash
730 }
731 for name := range remaining {
732 cur, curExists := curMap[name]
733 par, parExists := parentMap[name]
734 if curExists && (!parExists || cur != par) {
735 result[name] = mockCommitMeta(current, c)
736 delete(remaining, name)
737 }
738 }
739 current = c.parents[0]
740 }
741 return result, nil
742}
743
744func (r *mockRepoDataBrowse) CommitDetail(hash Hash) (CommitDetail, error) {
745 c, ok := r.commits[hash]
746 if !ok {
747 return CommitDetail{}, ErrNotFound
748 }
749 var fromTreeHash Hash
750 if len(c.parents) > 0 {
751 if parent, ok := r.commits[c.parents[0]]; ok {
752 fromTreeHash = parent.treeHash
753 }
754 }
755 return CommitDetail{
756 CommitMeta: mockCommitMeta(hash, c),
757 Files: r.diffTrees(fromTreeHash, c.treeHash, ""),
758 }, nil
759}
760
761func (r *mockRepoDataBrowse) CommitFileDiff(hash Hash, filePath string) (FileDiff, error) {
762 c, ok := r.commits[hash]
763 if !ok {
764 return FileDiff{}, ErrNotFound
765 }
766 var fromTreeHash Hash
767 if len(c.parents) > 0 {
768 if parent, ok := r.commits[c.parents[0]]; ok {
769 fromTreeHash = parent.treeHash
770 }
771 }
772 files := r.diffTrees(fromTreeHash, c.treeHash, "")
773 var matched *ChangedFile
774 for i := range files {
775 if files[i].Path == filePath {
776 matched = &files[i]
777 break
778 }
779 }
780 if matched == nil {
781 return FileDiff{}, ErrNotFound
782 }
783 fd := FileDiff{
784 Path: filePath,
785 IsNew: matched.Status == ChangeStatusAdded,
786 IsDelete: matched.Status == ChangeStatusDeleted,
787 }
788 var oldContent, newContent []byte
789 if fromTreeHash != "" {
790 if bh, err := r.blobHashAt(fromTreeHash, filePath); err == nil {
791 oldContent = r.blobs[bh]
792 }
793 }
794 if bh, err := r.blobHashAt(c.treeHash, filePath); err == nil {
795 newContent = r.blobs[bh]
796 }
797 fd.Hunks = mockDiffHunks(oldContent, newContent)
798 return fd, nil
799}
800
801// mockDiffHunks produces a single DiffHunk using a prefix/suffix scan.
802func mockDiffHunks(old, new []byte) []DiffHunk {
803 oldLines := splitBlobLines(old)
804 newLines := splitBlobLines(new)
805 i := 0
806 for i < len(oldLines) && i < len(newLines) && oldLines[i] == newLines[i] {
807 i++
808 }
809 j, k := len(oldLines), len(newLines)
810 for j > i && k > i && oldLines[j-1] == newLines[k-1] {
811 j--
812 k--
813 }
814 if j == i && k == i {
815 return nil // no changed region
816 }
817 oldLine, newLine := 1, 1
818 var lines []DiffLine
819 for _, l := range oldLines[:i] {
820 lines = append(lines, DiffLine{Type: DiffLineContext, Content: l, OldLine: oldLine, NewLine: newLine})
821 oldLine++
822 newLine++
823 }
824 for _, l := range oldLines[i:j] {
825 lines = append(lines, DiffLine{Type: DiffLineDeleted, Content: l, OldLine: oldLine})
826 oldLine++
827 }
828 for _, l := range newLines[i:k] {
829 lines = append(lines, DiffLine{Type: DiffLineAdded, Content: l, NewLine: newLine})
830 newLine++
831 }
832 for _, l := range oldLines[j:] {
833 lines = append(lines, DiffLine{Type: DiffLineContext, Content: l, OldLine: oldLine, NewLine: newLine})
834 oldLine++
835 newLine++
836 }
837 return []DiffHunk{{OldStart: 1, OldLines: len(oldLines), NewStart: 1, NewLines: len(newLines), Lines: lines}}
838}
839
840func splitBlobLines(data []byte) []string {
841 if len(data) == 0 {
842 return nil
843 }
844 return strings.Split(strings.TrimRight(string(data), "\n"), "\n")
845}
846
847var _ RepoClock = &mockRepoClock{}
848
849type mockRepoClock struct {
850 mu sync.Mutex
851 clocks map[string]lamport.Clock
852}
853
854func NewMockRepoClock() *mockRepoClock {
855 return &mockRepoClock{
856 clocks: make(map[string]lamport.Clock),
857 }
858}
859
860func (r *mockRepoClock) AllClocks() (map[string]lamport.Clock, error) {
861 return r.clocks, nil
862}
863
864func (r *mockRepoClock) GetOrCreateClock(name string) (lamport.Clock, error) {
865 r.mu.Lock()
866 defer r.mu.Unlock()
867
868 if c, ok := r.clocks[name]; ok {
869 return c, nil
870 }
871
872 c := lamport.NewMemClock()
873 r.clocks[name] = c
874 return c, nil
875}
876
877func (r *mockRepoClock) Increment(name string) (lamport.Time, error) {
878 c, err := r.GetOrCreateClock(name)
879 if err != nil {
880 return lamport.Time(0), err
881 }
882 return c.Increment()
883}
884
885func (r *mockRepoClock) Witness(name string, time lamport.Time) error {
886 c, err := r.GetOrCreateClock(name)
887 if err != nil {
888 return err
889 }
890 return c.Witness(time)
891}
892
893var _ repoTest = &mockRepoTest{}
894
895type mockRepoTest struct{}
896
897func NewMockRepoTest() *mockRepoTest {
898 return &mockRepoTest{}
899}
900
901func (r *mockRepoTest) AddRemote(name string, url string) error {
902 panic("implement me")
903}
904
905func (r mockRepoTest) GetLocalRemote() string {
906 panic("implement me")
907}
908
909func (r mockRepoTest) EraseFromDisk() error {
910 // nothing to do
911 return nil
912}