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