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