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