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