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