1package repository
2
3import (
4 "bytes"
5 "errors"
6 "fmt"
7 "io/ioutil"
8 "os"
9 "path/filepath"
10 "sort"
11 "strings"
12 "sync"
13 "time"
14
15 "github.com/ProtonMail/go-crypto/openpgp"
16 "github.com/blevesearch/bleve"
17 "github.com/go-git/go-billy/v5"
18 "github.com/go-git/go-billy/v5/osfs"
19 gogit "github.com/go-git/go-git/v5"
20 "github.com/go-git/go-git/v5/config"
21 "github.com/go-git/go-git/v5/plumbing"
22 "github.com/go-git/go-git/v5/plumbing/filemode"
23 "github.com/go-git/go-git/v5/plumbing/object"
24 "golang.org/x/sync/errgroup"
25 "golang.org/x/sys/execabs"
26
27 "github.com/MichaelMure/git-bug/util/lamport"
28)
29
30const clockPath = "clocks"
31const indexPath = "indexes"
32
33var _ ClockedRepo = &GoGitRepo{}
34var _ TestedRepo = &GoGitRepo{}
35
36type GoGitRepo struct {
37 // Unfortunately, some parts of go-git are not thread-safe so we have to cover them with a big fat mutex here.
38 // See https://github.com/go-git/go-git/issues/48
39 // See https://github.com/go-git/go-git/issues/208
40 // See https://github.com/go-git/go-git/pull/186
41 rMutex sync.Mutex
42 r *gogit.Repository
43 path string
44
45 clocksMutex sync.Mutex
46 clocks map[string]lamport.Clock
47
48 indexesMutex sync.Mutex
49 indexes map[string]bleve.Index
50
51 keyring Keyring
52 localStorage billy.Filesystem
53}
54
55// OpenGoGitRepo opens an already existing repo at the given path and
56// with the specified LocalStorage namespace. Given a repository path
57// of "~/myrepo" and a namespace of "git-bug", local storage for the
58// GoGitRepo will be configured at "~/myrepo/.git/git-bug".
59func OpenGoGitRepo(path, namespace string, clockLoaders []ClockLoader) (*GoGitRepo, error) {
60 path, err := detectGitPath(path)
61 if err != nil {
62 return nil, err
63 }
64
65 r, err := gogit.PlainOpen(path)
66 if err != nil {
67 return nil, err
68 }
69
70 k, err := defaultKeyring()
71 if err != nil {
72 return nil, err
73 }
74
75 repo := &GoGitRepo{
76 r: r,
77 path: path,
78 clocks: make(map[string]lamport.Clock),
79 indexes: make(map[string]bleve.Index),
80 keyring: k,
81 localStorage: osfs.New(filepath.Join(path, namespace)),
82 }
83
84 loaderToRun := make([]ClockLoader, 0, len(clockLoaders))
85 for _, loader := range clockLoaders {
86 loader := loader
87 allExist := true
88 for _, name := range loader.Clocks {
89 if _, err := repo.getClock(name); err != nil {
90 allExist = false
91 }
92 }
93
94 if !allExist {
95 loaderToRun = append(loaderToRun, loader)
96 }
97 }
98
99 var errG errgroup.Group
100 for _, loader := range loaderToRun {
101 loader := loader
102 errG.Go(func() error {
103 return loader.Witnesser(repo)
104 })
105 }
106 err = errG.Wait()
107 if err != nil {
108 return nil, err
109 }
110
111 return repo, nil
112}
113
114// InitGoGitRepo creates a new empty git repo at the given path and
115// with the specified LocalStorage namespace. Given a repository path
116// of "~/myrepo" and a namespace of "git-bug", local storage for the
117// GoGitRepo will be configured at "~/myrepo/.git/git-bug".
118func InitGoGitRepo(path, namespace string) (*GoGitRepo, error) {
119 r, err := gogit.PlainInit(path, false)
120 if err != nil {
121 return nil, err
122 }
123
124 k, err := defaultKeyring()
125 if err != nil {
126 return nil, err
127 }
128
129 return &GoGitRepo{
130 r: r,
131 path: filepath.Join(path, ".git"),
132 clocks: make(map[string]lamport.Clock),
133 indexes: make(map[string]bleve.Index),
134 keyring: k,
135 localStorage: osfs.New(filepath.Join(path, ".git", namespace)),
136 }, nil
137}
138
139// InitBareGoGitRepo creates a new --bare empty git repo at the given
140// path and with the specified LocalStorage namespace. Given a repository
141// path of "~/myrepo" and a namespace of "git-bug", local storage for the
142// GoGitRepo will be configured at "~/myrepo/.git/git-bug".
143func InitBareGoGitRepo(path, namespace string) (*GoGitRepo, error) {
144 r, err := gogit.PlainInit(path, true)
145 if err != nil {
146 return nil, err
147 }
148
149 k, err := defaultKeyring()
150 if err != nil {
151 return nil, err
152 }
153
154 return &GoGitRepo{
155 r: r,
156 path: path,
157 clocks: make(map[string]lamport.Clock),
158 indexes: make(map[string]bleve.Index),
159 keyring: k,
160 localStorage: osfs.New(filepath.Join(path, namespace)),
161 }, nil
162}
163
164func detectGitPath(path string) (string, error) {
165 // normalize the path
166 path, err := filepath.Abs(path)
167 if err != nil {
168 return "", err
169 }
170
171 for {
172 fi, err := os.Stat(filepath.Join(path, ".git"))
173 if err == nil {
174 if !fi.IsDir() {
175 return "", fmt.Errorf(".git exist but is not a directory")
176 }
177 return filepath.Join(path, ".git"), nil
178 }
179 if !os.IsNotExist(err) {
180 // unknown error
181 return "", err
182 }
183
184 // detect bare repo
185 ok, err := isGitDir(path)
186 if err != nil {
187 return "", err
188 }
189 if ok {
190 return path, nil
191 }
192
193 if parent := filepath.Dir(path); parent == path {
194 return "", fmt.Errorf(".git not found")
195 } else {
196 path = parent
197 }
198 }
199}
200
201func isGitDir(path string) (bool, error) {
202 markers := []string{"HEAD", "objects", "refs"}
203
204 for _, marker := range markers {
205 _, err := os.Stat(filepath.Join(path, marker))
206 if err == nil {
207 continue
208 }
209 if !os.IsNotExist(err) {
210 // unknown error
211 return false, err
212 } else {
213 return false, nil
214 }
215 }
216
217 return true, nil
218}
219
220func (repo *GoGitRepo) Close() error {
221 var firstErr error
222 for _, index := range repo.indexes {
223 err := index.Close()
224 if err != nil && firstErr == nil {
225 firstErr = err
226 }
227 }
228 return firstErr
229}
230
231// LocalConfig give access to the repository scoped configuration
232func (repo *GoGitRepo) LocalConfig() Config {
233 return newGoGitLocalConfig(repo.r)
234}
235
236// GlobalConfig give access to the global scoped configuration
237func (repo *GoGitRepo) GlobalConfig() Config {
238 return newGoGitGlobalConfig()
239}
240
241// AnyConfig give access to a merged local/global configuration
242func (repo *GoGitRepo) AnyConfig() ConfigRead {
243 return mergeConfig(repo.LocalConfig(), repo.GlobalConfig())
244}
245
246// Keyring give access to a user-wide storage for secrets
247func (repo *GoGitRepo) Keyring() Keyring {
248 return repo.keyring
249}
250
251// GetUserName returns the name the user has used to configure git
252func (repo *GoGitRepo) GetUserName() (string, error) {
253 return repo.AnyConfig().ReadString("user.name")
254}
255
256// GetUserEmail returns the email address that the user has used to configure git.
257func (repo *GoGitRepo) GetUserEmail() (string, error) {
258 return repo.AnyConfig().ReadString("user.email")
259}
260
261// GetCoreEditor returns the name of the editor that the user has used to configure git.
262func (repo *GoGitRepo) GetCoreEditor() (string, error) {
263 // See https://git-scm.com/docs/git-var
264 // The order of preference is the $GIT_EDITOR environment variable, then core.editor configuration, then $VISUAL, then $EDITOR, and then the default chosen at compile time, which is usually vi.
265
266 if val, ok := os.LookupEnv("GIT_EDITOR"); ok {
267 return val, nil
268 }
269
270 val, err := repo.AnyConfig().ReadString("core.editor")
271 if err == nil && val != "" {
272 return val, nil
273 }
274 if err != nil && !errors.Is(err, ErrNoConfigEntry) {
275 return "", err
276 }
277
278 if val, ok := os.LookupEnv("VISUAL"); ok {
279 return val, nil
280 }
281
282 if val, ok := os.LookupEnv("EDITOR"); ok {
283 return val, nil
284 }
285
286 priorities := []string{
287 "editor",
288 "nano",
289 "vim",
290 "vi",
291 "emacs",
292 }
293
294 for _, cmd := range priorities {
295 if _, err = execabs.LookPath(cmd); err == nil {
296 return cmd, nil
297 }
298
299 }
300
301 return "ed", nil
302}
303
304// GetRemotes returns the configured remotes repositories.
305func (repo *GoGitRepo) GetRemotes() (map[string]string, error) {
306 cfg, err := repo.r.Config()
307 if err != nil {
308 return nil, err
309 }
310
311 result := make(map[string]string, len(cfg.Remotes))
312 for name, remote := range cfg.Remotes {
313 if len(remote.URLs) > 0 {
314 result[name] = remote.URLs[0]
315 }
316 }
317
318 return result, nil
319}
320
321// LocalStorage returns a billy.Filesystem giving access to
322// $RepoPath/.git/$Namespace.
323func (repo *GoGitRepo) LocalStorage() billy.Filesystem {
324 return repo.localStorage
325}
326
327// GetBleveIndex return a bleve.Index that can be used to index documents
328func (repo *GoGitRepo) GetBleveIndex(name string) (bleve.Index, error) {
329 repo.indexesMutex.Lock()
330 defer repo.indexesMutex.Unlock()
331
332 if index, ok := repo.indexes[name]; ok {
333 return index, nil
334 }
335
336 path := filepath.Join(repo.localStorage.Root(), indexPath, name)
337
338 index, err := bleve.Open(path)
339 if err == nil {
340 repo.indexes[name] = index
341 return index, nil
342 }
343
344 err = os.MkdirAll(path, os.ModePerm)
345 if err != nil {
346 return nil, err
347 }
348
349 mapping := bleve.NewIndexMapping()
350 mapping.DefaultAnalyzer = "en"
351
352 index, err = bleve.New(path, mapping)
353 if err != nil {
354 return nil, err
355 }
356
357 repo.indexes[name] = index
358
359 return index, nil
360}
361
362// ClearBleveIndex will wipe the given index
363func (repo *GoGitRepo) ClearBleveIndex(name string) error {
364 repo.indexesMutex.Lock()
365 defer repo.indexesMutex.Unlock()
366
367 if index, ok := repo.indexes[name]; ok {
368 err := index.Close()
369 if err != nil {
370 return err
371 }
372 delete(repo.indexes, name)
373 }
374
375 path := filepath.Join(repo.localStorage.Root(), indexPath, name)
376 err := os.RemoveAll(path)
377 if err != nil {
378 return err
379 }
380
381 return nil
382}
383
384// FetchRefs fetch git refs matching a directory prefix to a remote
385// Ex: prefix="foo" will fetch any remote refs matching "refs/foo/*" locally.
386// The equivalent git refspec would be "refs/foo/*:refs/remotes/<remote>/foo/*"
387func (repo *GoGitRepo) FetchRefs(remote string, prefix string) (string, error) {
388 refspec := fmt.Sprintf("refs/%s/*:refs/remotes/%s/%s/*", prefix, remote, prefix)
389
390 buf := bytes.NewBuffer(nil)
391
392 err := repo.r.Fetch(&gogit.FetchOptions{
393 RemoteName: remote,
394 RefSpecs: []config.RefSpec{config.RefSpec(refspec)},
395 Progress: buf,
396 })
397 if err == gogit.NoErrAlreadyUpToDate {
398 return "already up-to-date", nil
399 }
400 if err != nil {
401 return "", err
402 }
403
404 return buf.String(), nil
405}
406
407// PushRefs push git refs matching a directory prefix to a remote
408// Ex: prefix="foo" will push any local refs matching "refs/foo/*" to the remote.
409// The equivalent git refspec would be "refs/foo/*:refs/foo/*"
410//
411// Additionally, PushRefs will update the local references in refs/remotes/<remote>/foo to match
412// the remote state.
413func (repo *GoGitRepo) PushRefs(remote string, prefix string) (string, error) {
414 refspec := fmt.Sprintf("refs/%s/*:refs/%s/*", prefix, prefix)
415
416 remo, err := repo.r.Remote(remote)
417 if err != nil {
418 return "", err
419 }
420
421 // to make sure that the push also create the corresponding refs/remotes/<remote>/... references,
422 // we need to have a default fetch refspec configured on the remote, to make our refs "track" the remote ones.
423 // This does not change the config on disk, only on memory.
424 hasCustomFetch := false
425 fetchRefspec := fmt.Sprintf("refs/%s/*:refs/remotes/%s/%s/*", prefix, remote, prefix)
426 for _, r := range remo.Config().Fetch {
427 if string(r) == fetchRefspec {
428 hasCustomFetch = true
429 break
430 }
431 }
432
433 if !hasCustomFetch {
434 remo.Config().Fetch = append(remo.Config().Fetch, config.RefSpec(fetchRefspec))
435 }
436
437 buf := bytes.NewBuffer(nil)
438
439 err = remo.Push(&gogit.PushOptions{
440 RemoteName: remote,
441 RefSpecs: []config.RefSpec{config.RefSpec(refspec)},
442 Progress: buf,
443 })
444 if err == gogit.NoErrAlreadyUpToDate {
445 return "already up-to-date", nil
446 }
447 if err != nil {
448 return "", err
449 }
450
451 return buf.String(), nil
452}
453
454// StoreData will store arbitrary data and return the corresponding hash
455func (repo *GoGitRepo) StoreData(data []byte) (Hash, error) {
456 obj := repo.r.Storer.NewEncodedObject()
457 obj.SetType(plumbing.BlobObject)
458
459 w, err := obj.Writer()
460 if err != nil {
461 return "", err
462 }
463
464 _, err = w.Write(data)
465 if err != nil {
466 return "", err
467 }
468
469 h, err := repo.r.Storer.SetEncodedObject(obj)
470 if err != nil {
471 return "", err
472 }
473
474 return Hash(h.String()), nil
475}
476
477// ReadData will attempt to read arbitrary data from the given hash
478func (repo *GoGitRepo) ReadData(hash Hash) ([]byte, error) {
479 repo.rMutex.Lock()
480 defer repo.rMutex.Unlock()
481
482 obj, err := repo.r.BlobObject(plumbing.NewHash(hash.String()))
483 if err != nil {
484 return nil, err
485 }
486
487 r, err := obj.Reader()
488 if err != nil {
489 return nil, err
490 }
491
492 // TODO: return a io.Reader instead
493 return ioutil.ReadAll(r)
494}
495
496// StoreTree will store a mapping key-->Hash as a Git tree
497func (repo *GoGitRepo) StoreTree(mapping []TreeEntry) (Hash, error) {
498 var tree object.Tree
499
500 // TODO: can be removed once https://github.com/go-git/go-git/issues/193 is resolved
501 sorted := make([]TreeEntry, len(mapping))
502 copy(sorted, mapping)
503 sort.Slice(sorted, func(i, j int) bool {
504 nameI := sorted[i].Name
505 if sorted[i].ObjectType == Tree {
506 nameI += "/"
507 }
508 nameJ := sorted[j].Name
509 if sorted[j].ObjectType == Tree {
510 nameJ += "/"
511 }
512 return nameI < nameJ
513 })
514
515 for _, entry := range sorted {
516 mode := filemode.Regular
517 if entry.ObjectType == Tree {
518 mode = filemode.Dir
519 }
520
521 tree.Entries = append(tree.Entries, object.TreeEntry{
522 Name: entry.Name,
523 Mode: mode,
524 Hash: plumbing.NewHash(entry.Hash.String()),
525 })
526 }
527
528 obj := repo.r.Storer.NewEncodedObject()
529 obj.SetType(plumbing.TreeObject)
530 err := tree.Encode(obj)
531 if err != nil {
532 return "", err
533 }
534
535 hash, err := repo.r.Storer.SetEncodedObject(obj)
536 if err != nil {
537 return "", err
538 }
539
540 return Hash(hash.String()), nil
541}
542
543// ReadTree will return the list of entries in a Git tree
544func (repo *GoGitRepo) ReadTree(hash Hash) ([]TreeEntry, error) {
545 repo.rMutex.Lock()
546 defer repo.rMutex.Unlock()
547
548 h := plumbing.NewHash(hash.String())
549
550 // the given hash could be a tree or a commit
551 obj, err := repo.r.Storer.EncodedObject(plumbing.AnyObject, h)
552 if err != nil {
553 return nil, err
554 }
555
556 var tree *object.Tree
557 switch obj.Type() {
558 case plumbing.TreeObject:
559 tree, err = object.DecodeTree(repo.r.Storer, obj)
560 case plumbing.CommitObject:
561 var commit *object.Commit
562 commit, err = object.DecodeCommit(repo.r.Storer, obj)
563 if err != nil {
564 return nil, err
565 }
566 tree, err = commit.Tree()
567 default:
568 return nil, fmt.Errorf("given hash is not a tree")
569 }
570 if err != nil {
571 return nil, err
572 }
573
574 treeEntries := make([]TreeEntry, len(tree.Entries))
575 for i, entry := range tree.Entries {
576 objType := Blob
577 if entry.Mode == filemode.Dir {
578 objType = Tree
579 }
580
581 treeEntries[i] = TreeEntry{
582 ObjectType: objType,
583 Hash: Hash(entry.Hash.String()),
584 Name: entry.Name,
585 }
586 }
587
588 return treeEntries, nil
589}
590
591// StoreCommit will store a Git commit with the given Git tree
592func (repo *GoGitRepo) StoreCommit(treeHash Hash, parents ...Hash) (Hash, error) {
593 return repo.StoreSignedCommit(treeHash, nil, parents...)
594}
595
596// StoreSignedCommit will store a Git commit with the given Git tree. If signKey is not nil, the commit
597// will be signed accordingly.
598func (repo *GoGitRepo) StoreSignedCommit(treeHash Hash, signKey *openpgp.Entity, parents ...Hash) (Hash, error) {
599 cfg, err := repo.r.Config()
600 if err != nil {
601 return "", err
602 }
603
604 commit := object.Commit{
605 Author: object.Signature{
606 Name: cfg.Author.Name,
607 Email: cfg.Author.Email,
608 When: time.Now(),
609 },
610 Committer: object.Signature{
611 Name: cfg.Committer.Name,
612 Email: cfg.Committer.Email,
613 When: time.Now(),
614 },
615 Message: "",
616 TreeHash: plumbing.NewHash(treeHash.String()),
617 }
618
619 for _, parent := range parents {
620 commit.ParentHashes = append(commit.ParentHashes, plumbing.NewHash(parent.String()))
621 }
622
623 // Compute the signature if needed
624 if signKey != nil {
625 // first get the serialized commit
626 encoded := &plumbing.MemoryObject{}
627 if err := commit.Encode(encoded); err != nil {
628 return "", err
629 }
630 r, err := encoded.Reader()
631 if err != nil {
632 return "", err
633 }
634
635 // sign the data
636 var sig bytes.Buffer
637 if err := openpgp.ArmoredDetachSign(&sig, signKey, r, nil); err != nil {
638 return "", err
639 }
640 commit.PGPSignature = sig.String()
641 }
642
643 obj := repo.r.Storer.NewEncodedObject()
644 obj.SetType(plumbing.CommitObject)
645 err = commit.Encode(obj)
646 if err != nil {
647 return "", err
648 }
649
650 hash, err := repo.r.Storer.SetEncodedObject(obj)
651 if err != nil {
652 return "", err
653 }
654
655 return Hash(hash.String()), nil
656}
657
658// GetTreeHash return the git tree hash referenced in a commit
659func (repo *GoGitRepo) GetTreeHash(commit Hash) (Hash, error) {
660 repo.rMutex.Lock()
661 defer repo.rMutex.Unlock()
662
663 obj, err := repo.r.CommitObject(plumbing.NewHash(commit.String()))
664 if err != nil {
665 return "", err
666 }
667
668 return Hash(obj.TreeHash.String()), nil
669}
670
671// FindCommonAncestor will return the last common ancestor of two chain of commit
672func (repo *GoGitRepo) FindCommonAncestor(commit1 Hash, commit2 Hash) (Hash, error) {
673 repo.rMutex.Lock()
674 defer repo.rMutex.Unlock()
675
676 obj1, err := repo.r.CommitObject(plumbing.NewHash(commit1.String()))
677 if err != nil {
678 return "", err
679 }
680 obj2, err := repo.r.CommitObject(plumbing.NewHash(commit2.String()))
681 if err != nil {
682 return "", err
683 }
684
685 commits, err := obj1.MergeBase(obj2)
686 if err != nil {
687 return "", err
688 }
689
690 return Hash(commits[0].Hash.String()), nil
691}
692
693func (repo *GoGitRepo) ResolveRef(ref string) (Hash, error) {
694 r, err := repo.r.Reference(plumbing.ReferenceName(ref), false)
695 if err != nil {
696 return "", err
697 }
698 return Hash(r.Hash().String()), nil
699}
700
701// UpdateRef will create or update a Git reference
702func (repo *GoGitRepo) UpdateRef(ref string, hash Hash) error {
703 return repo.r.Storer.SetReference(plumbing.NewHashReference(plumbing.ReferenceName(ref), plumbing.NewHash(hash.String())))
704}
705
706// RemoveRef will remove a Git reference
707func (repo *GoGitRepo) RemoveRef(ref string) error {
708 return repo.r.Storer.RemoveReference(plumbing.ReferenceName(ref))
709}
710
711// ListRefs will return a list of Git ref matching the given refspec
712func (repo *GoGitRepo) ListRefs(refPrefix string) ([]string, error) {
713 refIter, err := repo.r.References()
714 if err != nil {
715 return nil, err
716 }
717
718 refs := make([]string, 0)
719
720 err = refIter.ForEach(func(ref *plumbing.Reference) error {
721 if strings.HasPrefix(ref.Name().String(), refPrefix) {
722 refs = append(refs, ref.Name().String())
723 }
724 return nil
725 })
726 if err != nil {
727 return nil, err
728 }
729
730 return refs, nil
731}
732
733// RefExist will check if a reference exist in Git
734func (repo *GoGitRepo) RefExist(ref string) (bool, error) {
735 _, err := repo.r.Reference(plumbing.ReferenceName(ref), false)
736 if err == nil {
737 return true, nil
738 } else if err == plumbing.ErrReferenceNotFound {
739 return false, nil
740 }
741 return false, err
742}
743
744// CopyRef will create a new reference with the same value as another one
745func (repo *GoGitRepo) CopyRef(source string, dest string) error {
746 r, err := repo.r.Reference(plumbing.ReferenceName(source), false)
747 if err != nil {
748 return err
749 }
750 return repo.r.Storer.SetReference(plumbing.NewHashReference(plumbing.ReferenceName(dest), r.Hash()))
751}
752
753// ListCommits will return the list of tree hashes of a ref, in chronological order
754func (repo *GoGitRepo) ListCommits(ref string) ([]Hash, error) {
755 return nonNativeListCommits(repo, ref)
756}
757
758func (repo *GoGitRepo) ReadCommit(hash Hash) (Commit, error) {
759 repo.rMutex.Lock()
760 defer repo.rMutex.Unlock()
761
762 commit, err := repo.r.CommitObject(plumbing.NewHash(hash.String()))
763 if err != nil {
764 return Commit{}, err
765 }
766
767 parents := make([]Hash, len(commit.ParentHashes))
768 for i, parentHash := range commit.ParentHashes {
769 parents[i] = Hash(parentHash.String())
770 }
771
772 result := Commit{
773 Hash: hash,
774 Parents: parents,
775 TreeHash: Hash(commit.TreeHash.String()),
776 }
777
778 if commit.PGPSignature != "" {
779 // I can't find a way to just remove the signature when reading the encoded commit so we need to
780 // re-encode the commit without signature.
781
782 encoded := &plumbing.MemoryObject{}
783 err := commit.EncodeWithoutSignature(encoded)
784 if err != nil {
785 return Commit{}, err
786 }
787
788 result.SignedData, err = encoded.Reader()
789 if err != nil {
790 return Commit{}, err
791 }
792
793 result.Signature, err = deArmorSignature(strings.NewReader(commit.PGPSignature))
794 if err != nil {
795 return Commit{}, err
796 }
797 }
798
799 return result, nil
800}
801
802func (repo *GoGitRepo) AllClocks() (map[string]lamport.Clock, error) {
803 repo.clocksMutex.Lock()
804 defer repo.clocksMutex.Unlock()
805
806 result := make(map[string]lamport.Clock)
807
808 files, err := ioutil.ReadDir(filepath.Join(repo.localStorage.Root(), clockPath))
809 if os.IsNotExist(err) {
810 return nil, nil
811 }
812 if err != nil {
813 return nil, err
814 }
815
816 for _, file := range files {
817 name := file.Name()
818 if c, ok := repo.clocks[name]; ok {
819 result[name] = c
820 } else {
821 c, err := lamport.LoadPersistedClock(repo.LocalStorage(), filepath.Join(clockPath, name))
822 if err != nil {
823 return nil, err
824 }
825 repo.clocks[name] = c
826 result[name] = c
827 }
828 }
829
830 return result, nil
831}
832
833// GetOrCreateClock return a Lamport clock stored in the Repo.
834// If the clock doesn't exist, it's created.
835func (repo *GoGitRepo) GetOrCreateClock(name string) (lamport.Clock, error) {
836 repo.clocksMutex.Lock()
837 defer repo.clocksMutex.Unlock()
838
839 c, err := repo.getClock(name)
840 if err == nil {
841 return c, nil
842 }
843 if err != ErrClockNotExist {
844 return nil, err
845 }
846
847 c, err = lamport.NewPersistedClock(repo.LocalStorage(), filepath.Join(clockPath, name))
848 if err != nil {
849 return nil, err
850 }
851
852 repo.clocks[name] = c
853 return c, nil
854}
855
856func (repo *GoGitRepo) getClock(name string) (lamport.Clock, error) {
857 if c, ok := repo.clocks[name]; ok {
858 return c, nil
859 }
860
861 c, err := lamport.LoadPersistedClock(repo.LocalStorage(), filepath.Join(clockPath, name))
862 if err == nil {
863 repo.clocks[name] = c
864 return c, nil
865 }
866 if err == lamport.ErrClockNotExist {
867 return nil, ErrClockNotExist
868 }
869 return nil, err
870}
871
872// Increment is equivalent to c = GetOrCreateClock(name) + c.Increment()
873func (repo *GoGitRepo) Increment(name string) (lamport.Time, error) {
874 c, err := repo.GetOrCreateClock(name)
875 if err != nil {
876 return lamport.Time(0), err
877 }
878 return c.Increment()
879}
880
881// Witness is equivalent to c = GetOrCreateClock(name) + c.Witness(time)
882func (repo *GoGitRepo) Witness(name string, time lamport.Time) error {
883 c, err := repo.GetOrCreateClock(name)
884 if err != nil {
885 return err
886 }
887 return c.Witness(time)
888}
889
890// AddRemote add a new remote to the repository
891// Not in the interface because it's only used for testing
892func (repo *GoGitRepo) AddRemote(name string, url string) error {
893 _, err := repo.r.CreateRemote(&config.RemoteConfig{
894 Name: name,
895 URLs: []string{url},
896 })
897
898 return err
899}
900
901// GetLocalRemote return the URL to use to add this repo as a local remote
902func (repo *GoGitRepo) GetLocalRemote() string {
903 return repo.path
904}
905
906// EraseFromDisk delete this repository entirely from the disk
907func (repo *GoGitRepo) EraseFromDisk() error {
908 err := repo.Close()
909 if err != nil {
910 return err
911 }
912
913 path := filepath.Clean(strings.TrimSuffix(repo.path, string(filepath.Separator)+".git"))
914
915 // fmt.Println("Cleaning repo:", path)
916 return os.RemoveAll(path)
917}