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