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