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