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/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]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]Index),
79 keyring: k,
80 localStorage: osfs.New(filepath.Join(path, namespace)),
81 }
82
83 loaderToRun := make([]ClockLoader, 0, 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]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]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 name, index := range repo.indexes {
222 err := index.Close()
223 if err != nil && firstErr == nil {
224 firstErr = err
225 }
226 delete(repo.indexes, name)
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
327func (repo *GoGitRepo) GetIndex(name string) (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 := openBleveIndex(path)
338 if err == nil {
339 repo.indexes[name] = index
340 }
341 return index, err
342}
343
344// FetchRefs fetch git refs matching a directory prefix to a remote
345// Ex: prefix="foo" will fetch any remote refs matching "refs/foo/*" locally.
346// The equivalent git refspec would be "refs/foo/*:refs/remotes/<remote>/foo/*"
347func (repo *GoGitRepo) FetchRefs(remote string, prefixes ...string) (string, error) {
348 refSpecs := make([]config.RefSpec, len(prefixes))
349
350 for i, prefix := range prefixes {
351 refSpecs[i] = config.RefSpec(fmt.Sprintf("refs/%s/*:refs/remotes/%s/%s/*", prefix, remote, prefix))
352 }
353
354 buf := bytes.NewBuffer(nil)
355
356 err := repo.r.Fetch(&gogit.FetchOptions{
357 RemoteName: remote,
358 RefSpecs: refSpecs,
359 Progress: buf,
360 })
361 if err == gogit.NoErrAlreadyUpToDate {
362 return "already up-to-date", nil
363 }
364 if err != nil {
365 return "", err
366 }
367
368 return buf.String(), nil
369}
370
371// PushRefs push git refs matching a directory prefix to a remote
372// Ex: prefix="foo" will push any local refs matching "refs/foo/*" to the remote.
373// The equivalent git refspec would be "refs/foo/*:refs/foo/*"
374//
375// Additionally, PushRefs will update the local references in refs/remotes/<remote>/foo to match
376// the remote state.
377func (repo *GoGitRepo) PushRefs(remote string, prefixes ...string) (string, error) {
378 remo, err := repo.r.Remote(remote)
379 if err != nil {
380 return "", err
381 }
382
383 refSpecs := make([]config.RefSpec, len(prefixes))
384
385 for i, prefix := range prefixes {
386 refspec := fmt.Sprintf("refs/%s/*:refs/%s/*", prefix, prefix)
387
388 // to make sure that the push also create the corresponding refs/remotes/<remote>/... references,
389 // we need to have a default fetch refspec configured on the remote, to make our refs "track" the remote ones.
390 // This does not change the config on disk, only on memory.
391 hasCustomFetch := false
392 fetchRefspec := fmt.Sprintf("refs/%s/*:refs/remotes/%s/%s/*", prefix, remote, prefix)
393 for _, r := range remo.Config().Fetch {
394 if string(r) == fetchRefspec {
395 hasCustomFetch = true
396 break
397 }
398 }
399
400 if !hasCustomFetch {
401 remo.Config().Fetch = append(remo.Config().Fetch, config.RefSpec(fetchRefspec))
402 }
403
404 refSpecs[i] = config.RefSpec(refspec)
405 }
406
407 buf := bytes.NewBuffer(nil)
408
409 err = remo.Push(&gogit.PushOptions{
410 RemoteName: remote,
411 RefSpecs: refSpecs,
412 Progress: buf,
413 })
414 if err == gogit.NoErrAlreadyUpToDate {
415 return "already up-to-date", nil
416 }
417 if err != nil {
418 return "", err
419 }
420
421 return buf.String(), nil
422}
423
424// StoreData will store arbitrary data and return the corresponding hash
425func (repo *GoGitRepo) StoreData(data []byte) (Hash, error) {
426 obj := repo.r.Storer.NewEncodedObject()
427 obj.SetType(plumbing.BlobObject)
428
429 w, err := obj.Writer()
430 if err != nil {
431 return "", err
432 }
433
434 _, err = w.Write(data)
435 if err != nil {
436 return "", err
437 }
438
439 h, err := repo.r.Storer.SetEncodedObject(obj)
440 if err != nil {
441 return "", err
442 }
443
444 return Hash(h.String()), nil
445}
446
447// ReadData will attempt to read arbitrary data from the given hash
448func (repo *GoGitRepo) ReadData(hash Hash) ([]byte, error) {
449 repo.rMutex.Lock()
450 defer repo.rMutex.Unlock()
451
452 obj, err := repo.r.BlobObject(plumbing.NewHash(hash.String()))
453 if err == plumbing.ErrObjectNotFound {
454 return nil, ErrNotFound
455 }
456 if err != nil {
457 return nil, err
458 }
459
460 r, err := obj.Reader()
461 if err != nil {
462 return nil, err
463 }
464
465 // TODO: return a io.Reader instead
466 return ioutil.ReadAll(r)
467}
468
469// StoreTree will store a mapping key-->Hash as a Git tree
470func (repo *GoGitRepo) StoreTree(mapping []TreeEntry) (Hash, error) {
471 var tree object.Tree
472
473 // TODO: can be removed once https://github.com/go-git/go-git/issues/193 is resolved
474 sorted := make([]TreeEntry, len(mapping))
475 copy(sorted, mapping)
476 sort.Slice(sorted, func(i, j int) bool {
477 nameI := sorted[i].Name
478 if sorted[i].ObjectType == Tree {
479 nameI += "/"
480 }
481 nameJ := sorted[j].Name
482 if sorted[j].ObjectType == Tree {
483 nameJ += "/"
484 }
485 return nameI < nameJ
486 })
487
488 for _, entry := range sorted {
489 mode := filemode.Regular
490 if entry.ObjectType == Tree {
491 mode = filemode.Dir
492 }
493
494 tree.Entries = append(tree.Entries, object.TreeEntry{
495 Name: entry.Name,
496 Mode: mode,
497 Hash: plumbing.NewHash(entry.Hash.String()),
498 })
499 }
500
501 obj := repo.r.Storer.NewEncodedObject()
502 obj.SetType(plumbing.TreeObject)
503 err := tree.Encode(obj)
504 if err != nil {
505 return "", err
506 }
507
508 hash, err := repo.r.Storer.SetEncodedObject(obj)
509 if err != nil {
510 return "", err
511 }
512
513 return Hash(hash.String()), nil
514}
515
516// ReadTree will return the list of entries in a Git tree
517func (repo *GoGitRepo) ReadTree(hash Hash) ([]TreeEntry, error) {
518 repo.rMutex.Lock()
519 defer repo.rMutex.Unlock()
520
521 h := plumbing.NewHash(hash.String())
522
523 // the given hash could be a tree or a commit
524 obj, err := repo.r.Storer.EncodedObject(plumbing.AnyObject, h)
525 if err == plumbing.ErrObjectNotFound {
526 return nil, ErrNotFound
527 }
528 if err != nil {
529 return nil, err
530 }
531
532 var tree *object.Tree
533 switch obj.Type() {
534 case plumbing.TreeObject:
535 tree, err = object.DecodeTree(repo.r.Storer, obj)
536 case plumbing.CommitObject:
537 var commit *object.Commit
538 commit, err = object.DecodeCommit(repo.r.Storer, obj)
539 if err != nil {
540 return nil, err
541 }
542 tree, err = commit.Tree()
543 default:
544 return nil, fmt.Errorf("given hash is not a tree")
545 }
546 if err != nil {
547 return nil, err
548 }
549
550 treeEntries := make([]TreeEntry, len(tree.Entries))
551 for i, entry := range tree.Entries {
552 objType := Blob
553 if entry.Mode == filemode.Dir {
554 objType = Tree
555 }
556
557 treeEntries[i] = TreeEntry{
558 ObjectType: objType,
559 Hash: Hash(entry.Hash.String()),
560 Name: entry.Name,
561 }
562 }
563
564 return treeEntries, nil
565}
566
567// StoreCommit will store a Git commit with the given Git tree
568func (repo *GoGitRepo) StoreCommit(treeHash Hash, parents ...Hash) (Hash, error) {
569 return repo.StoreSignedCommit(treeHash, nil, parents...)
570}
571
572// StoreSignedCommit will store a Git commit with the given Git tree. If signKey is not nil, the commit
573// will be signed accordingly.
574func (repo *GoGitRepo) StoreSignedCommit(treeHash Hash, signKey *openpgp.Entity, parents ...Hash) (Hash, error) {
575 cfg, err := repo.r.Config()
576 if err != nil {
577 return "", err
578 }
579
580 commit := object.Commit{
581 Author: object.Signature{
582 Name: cfg.Author.Name,
583 Email: cfg.Author.Email,
584 When: time.Now(),
585 },
586 Committer: object.Signature{
587 Name: cfg.Committer.Name,
588 Email: cfg.Committer.Email,
589 When: time.Now(),
590 },
591 Message: "",
592 TreeHash: plumbing.NewHash(treeHash.String()),
593 }
594
595 for _, parent := range parents {
596 commit.ParentHashes = append(commit.ParentHashes, plumbing.NewHash(parent.String()))
597 }
598
599 // Compute the signature if needed
600 if signKey != nil {
601 // first get the serialized commit
602 encoded := &plumbing.MemoryObject{}
603 if err := commit.Encode(encoded); err != nil {
604 return "", err
605 }
606 r, err := encoded.Reader()
607 if err != nil {
608 return "", err
609 }
610
611 // sign the data
612 var sig bytes.Buffer
613 if err := openpgp.ArmoredDetachSign(&sig, signKey, r, nil); err != nil {
614 return "", err
615 }
616 commit.PGPSignature = sig.String()
617 }
618
619 obj := repo.r.Storer.NewEncodedObject()
620 obj.SetType(plumbing.CommitObject)
621 err = commit.Encode(obj)
622 if err != nil {
623 return "", err
624 }
625
626 hash, err := repo.r.Storer.SetEncodedObject(obj)
627 if err != nil {
628 return "", err
629 }
630
631 return Hash(hash.String()), nil
632}
633
634func (repo *GoGitRepo) ResolveRef(ref string) (Hash, error) {
635 r, err := repo.r.Reference(plumbing.ReferenceName(ref), false)
636 if err == plumbing.ErrReferenceNotFound {
637 return "", ErrNotFound
638 }
639 if err != nil {
640 return "", err
641 }
642 return Hash(r.Hash().String()), nil
643}
644
645// UpdateRef will create or update a Git reference
646func (repo *GoGitRepo) UpdateRef(ref string, hash Hash) error {
647 return repo.r.Storer.SetReference(plumbing.NewHashReference(plumbing.ReferenceName(ref), plumbing.NewHash(hash.String())))
648}
649
650// RemoveRef will remove a Git reference
651func (repo *GoGitRepo) RemoveRef(ref string) error {
652 return repo.r.Storer.RemoveReference(plumbing.ReferenceName(ref))
653}
654
655// ListRefs will return a list of Git ref matching the given refspec
656func (repo *GoGitRepo) ListRefs(refPrefix string) ([]string, error) {
657 refIter, err := repo.r.References()
658 if err != nil {
659 return nil, err
660 }
661
662 refs := make([]string, 0)
663
664 err = refIter.ForEach(func(ref *plumbing.Reference) error {
665 if strings.HasPrefix(ref.Name().String(), refPrefix) {
666 refs = append(refs, ref.Name().String())
667 }
668 return nil
669 })
670 if err != nil {
671 return nil, err
672 }
673
674 return refs, nil
675}
676
677// RefExist will check if a reference exist in Git
678func (repo *GoGitRepo) RefExist(ref string) (bool, error) {
679 _, err := repo.r.Reference(plumbing.ReferenceName(ref), false)
680 if err == nil {
681 return true, nil
682 } else if err == plumbing.ErrReferenceNotFound {
683 return false, nil
684 }
685 return false, err
686}
687
688// CopyRef will create a new reference with the same value as another one
689func (repo *GoGitRepo) CopyRef(source string, dest string) error {
690 r, err := repo.r.Reference(plumbing.ReferenceName(source), false)
691 if err == plumbing.ErrReferenceNotFound {
692 return ErrNotFound
693 }
694 if err != nil {
695 return err
696 }
697 return repo.r.Storer.SetReference(plumbing.NewHashReference(plumbing.ReferenceName(dest), r.Hash()))
698}
699
700// ListCommits will return the list of tree hashes of a ref, in chronological order
701func (repo *GoGitRepo) ListCommits(ref string) ([]Hash, error) {
702 return nonNativeListCommits(repo, ref)
703}
704
705func (repo *GoGitRepo) ReadCommit(hash Hash) (Commit, error) {
706 repo.rMutex.Lock()
707 defer repo.rMutex.Unlock()
708
709 commit, err := repo.r.CommitObject(plumbing.NewHash(hash.String()))
710 if err == plumbing.ErrObjectNotFound {
711 return Commit{}, ErrNotFound
712 }
713 if err != nil {
714 return Commit{}, err
715 }
716
717 parents := make([]Hash, len(commit.ParentHashes))
718 for i, parentHash := range commit.ParentHashes {
719 parents[i] = Hash(parentHash.String())
720 }
721
722 result := Commit{
723 Hash: hash,
724 Parents: parents,
725 TreeHash: Hash(commit.TreeHash.String()),
726 }
727
728 if commit.PGPSignature != "" {
729 // I can't find a way to just remove the signature when reading the encoded commit so we need to
730 // re-encode the commit without signature.
731
732 encoded := &plumbing.MemoryObject{}
733 err := commit.EncodeWithoutSignature(encoded)
734 if err != nil {
735 return Commit{}, err
736 }
737
738 result.SignedData, err = encoded.Reader()
739 if err != nil {
740 return Commit{}, err
741 }
742
743 result.Signature, err = deArmorSignature(strings.NewReader(commit.PGPSignature))
744 if err != nil {
745 return Commit{}, err
746 }
747 }
748
749 return result, nil
750}
751
752func (repo *GoGitRepo) AllClocks() (map[string]lamport.Clock, error) {
753 repo.clocksMutex.Lock()
754 defer repo.clocksMutex.Unlock()
755
756 result := make(map[string]lamport.Clock)
757
758 files, err := ioutil.ReadDir(filepath.Join(repo.localStorage.Root(), clockPath))
759 if os.IsNotExist(err) {
760 return nil, nil
761 }
762 if err != nil {
763 return nil, err
764 }
765
766 for _, file := range files {
767 name := file.Name()
768 if c, ok := repo.clocks[name]; ok {
769 result[name] = c
770 } else {
771 c, err := lamport.LoadPersistedClock(repo.LocalStorage(), filepath.Join(clockPath, name))
772 if err != nil {
773 return nil, err
774 }
775 repo.clocks[name] = c
776 result[name] = c
777 }
778 }
779
780 return result, nil
781}
782
783// GetOrCreateClock return a Lamport clock stored in the Repo.
784// If the clock doesn't exist, it's created.
785func (repo *GoGitRepo) GetOrCreateClock(name string) (lamport.Clock, error) {
786 repo.clocksMutex.Lock()
787 defer repo.clocksMutex.Unlock()
788
789 c, err := repo.getClock(name)
790 if err == nil {
791 return c, nil
792 }
793 if err != ErrClockNotExist {
794 return nil, err
795 }
796
797 c, err = lamport.NewPersistedClock(repo.LocalStorage(), filepath.Join(clockPath, name))
798 if err != nil {
799 return nil, err
800 }
801
802 repo.clocks[name] = c
803 return c, nil
804}
805
806func (repo *GoGitRepo) getClock(name string) (lamport.Clock, error) {
807 if c, ok := repo.clocks[name]; ok {
808 return c, nil
809 }
810
811 c, err := lamport.LoadPersistedClock(repo.LocalStorage(), filepath.Join(clockPath, name))
812 if err == nil {
813 repo.clocks[name] = c
814 return c, nil
815 }
816 if err == lamport.ErrClockNotExist {
817 return nil, ErrClockNotExist
818 }
819 return nil, err
820}
821
822// Increment is equivalent to c = GetOrCreateClock(name) + c.Increment()
823func (repo *GoGitRepo) Increment(name string) (lamport.Time, error) {
824 c, err := repo.GetOrCreateClock(name)
825 if err != nil {
826 return lamport.Time(0), err
827 }
828 return c.Increment()
829}
830
831// Witness is equivalent to c = GetOrCreateClock(name) + c.Witness(time)
832func (repo *GoGitRepo) Witness(name string, time lamport.Time) error {
833 c, err := repo.GetOrCreateClock(name)
834 if err != nil {
835 return err
836 }
837 return c.Witness(time)
838}
839
840// AddRemote add a new remote to the repository
841// Not in the interface because it's only used for testing
842func (repo *GoGitRepo) AddRemote(name string, url string) error {
843 _, err := repo.r.CreateRemote(&config.RemoteConfig{
844 Name: name,
845 URLs: []string{url},
846 })
847
848 return err
849}
850
851// GetLocalRemote return the URL to use to add this repo as a local remote
852func (repo *GoGitRepo) GetLocalRemote() string {
853 return repo.path
854}
855
856// EraseFromDisk delete this repository entirely from the disk
857func (repo *GoGitRepo) EraseFromDisk() error {
858 err := repo.Close()
859 if err != nil {
860 return err
861 }
862
863 path := filepath.Clean(strings.TrimSuffix(repo.path, string(filepath.Separator)+".git"))
864
865 // fmt.Println("Cleaning repo:", path)
866 return os.RemoveAll(path)
867}