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