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