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
54// with the specified LocalStorage namespace. Given a repository path
55// of "~/myrepo" and a namespace of "git-bug", local storage for the
56// GoGitRepo will be configured at "~/myrepo/.git/git-bug".
57func OpenGoGitRepo(path, namespace 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, namespace)),
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
102// with the specified LocalStorage namespace. Given a repository path
103// of "~/myrepo" and a namespace of "git-bug", local storage for the
104// GoGitRepo will be configured at "~/myrepo/.git/git-bug".
105func InitGoGitRepo(path, namespace 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", namespace)),
123 }, nil
124}
125
126// InitBareGoGitRepo creates a new --bare empty git repo at the given
127// path and with the specified LocalStorage namespace. Given a repository
128// path of "~/myrepo" and a namespace of "git-bug", local storage for the
129// GoGitRepo will be configured at "~/myrepo/.git/git-bug".
130func InitBareGoGitRepo(path, namespace 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, namespace)),
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/$Namespace.
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.localStorage.Root(), indexPath, name)
324
325 index, err := bleve.Open(path)
326 if err == nil {
327 repo.indexes[name] = index
328 return index, nil
329 }
330
331 err = os.MkdirAll(path, os.ModePerm)
332 if err != nil {
333 return nil, err
334 }
335
336 mapping := bleve.NewIndexMapping()
337 mapping.DefaultAnalyzer = "en"
338
339 index, err = bleve.New(path, mapping)
340 if err != nil {
341 return nil, err
342 }
343
344 repo.indexes[name] = index
345
346 return index, nil
347}
348
349// ClearBleveIndex will wipe the given index
350func (repo *GoGitRepo) ClearBleveIndex(name string) error {
351 repo.indexesMutex.Lock()
352 defer repo.indexesMutex.Unlock()
353
354 if index, ok := repo.indexes[name]; ok {
355 err := index.Close()
356 if err != nil {
357 return err
358 }
359 delete(repo.indexes, name)
360 }
361
362 path := filepath.Join(repo.localStorage.Root(), indexPath, name)
363 err := os.RemoveAll(path)
364 if err != nil {
365 return err
366 }
367
368 return nil
369}
370
371// FetchRefs fetch git refs matching a directory prefix to a remote
372// Ex: prefix="foo" will fetch any remote refs matching "refs/foo/*" locally.
373// The equivalent git refspec would be "refs/foo/*:refs/remotes/<remote>/foo/*"
374func (repo *GoGitRepo) FetchRefs(remote string, prefix string) (string, error) {
375 refspec := fmt.Sprintf("refs/%s/*:refs/remotes/%s/%s/*", prefix, remote, prefix)
376
377 buf := bytes.NewBuffer(nil)
378
379 err := repo.r.Fetch(&gogit.FetchOptions{
380 RemoteName: remote,
381 RefSpecs: []config.RefSpec{config.RefSpec(refspec)},
382 Progress: buf,
383 })
384 if err == gogit.NoErrAlreadyUpToDate {
385 return "already up-to-date", nil
386 }
387 if err != nil {
388 return "", err
389 }
390
391 return buf.String(), nil
392}
393
394// PushRefs push git refs matching a directory prefix to a remote
395// Ex: prefix="foo" will push any local refs matching "refs/foo/*" to the remote.
396// The equivalent git refspec would be "refs/foo/*:refs/foo/*"
397//
398// Additionally, PushRefs will update the local references in refs/remotes/<remote>/foo to match
399// the remote state.
400func (repo *GoGitRepo) PushRefs(remote string, prefix string) (string, error) {
401 refspec := fmt.Sprintf("refs/%s/*:refs/%s/*", prefix, prefix)
402
403 remo, err := repo.r.Remote(remote)
404 if err != nil {
405 return "", err
406 }
407
408 // to make sure that the push also create the corresponding refs/remotes/<remote>/... references,
409 // we need to have a default fetch refspec configured on the remote, to make our refs "track" the remote ones.
410 // This does not change the config on disk, only on memory.
411 hasCustomFetch := false
412 fetchRefspec := fmt.Sprintf("refs/%s/*:refs/remotes/%s/%s/*", prefix, remote, prefix)
413 for _, r := range remo.Config().Fetch {
414 if string(r) == fetchRefspec {
415 hasCustomFetch = true
416 break
417 }
418 }
419
420 if !hasCustomFetch {
421 remo.Config().Fetch = append(remo.Config().Fetch, config.RefSpec(fetchRefspec))
422 }
423
424 buf := bytes.NewBuffer(nil)
425
426 err = remo.Push(&gogit.PushOptions{
427 RemoteName: remote,
428 RefSpecs: []config.RefSpec{config.RefSpec(refspec)},
429 Progress: buf,
430 })
431 if err == gogit.NoErrAlreadyUpToDate {
432 return "already up-to-date", nil
433 }
434 if err != nil {
435 return "", err
436 }
437
438 return buf.String(), nil
439}
440
441// StoreData will store arbitrary data and return the corresponding hash
442func (repo *GoGitRepo) StoreData(data []byte) (Hash, error) {
443 obj := repo.r.Storer.NewEncodedObject()
444 obj.SetType(plumbing.BlobObject)
445
446 w, err := obj.Writer()
447 if err != nil {
448 return "", err
449 }
450
451 _, err = w.Write(data)
452 if err != nil {
453 return "", err
454 }
455
456 h, err := repo.r.Storer.SetEncodedObject(obj)
457 if err != nil {
458 return "", err
459 }
460
461 return Hash(h.String()), nil
462}
463
464// ReadData will attempt to read arbitrary data from the given hash
465func (repo *GoGitRepo) ReadData(hash Hash) ([]byte, error) {
466 repo.rMutex.Lock()
467 defer repo.rMutex.Unlock()
468
469 obj, err := repo.r.BlobObject(plumbing.NewHash(hash.String()))
470 if err != nil {
471 return nil, err
472 }
473
474 r, err := obj.Reader()
475 if err != nil {
476 return nil, err
477 }
478
479 return ioutil.ReadAll(r)
480}
481
482// StoreTree will store a mapping key-->Hash as a Git tree
483func (repo *GoGitRepo) StoreTree(mapping []TreeEntry) (Hash, error) {
484 var tree object.Tree
485
486 // TODO: can be removed once https://github.com/go-git/go-git/issues/193 is resolved
487 sorted := make([]TreeEntry, len(mapping))
488 copy(sorted, mapping)
489 sort.Slice(sorted, func(i, j int) bool {
490 nameI := sorted[i].Name
491 if sorted[i].ObjectType == Tree {
492 nameI += "/"
493 }
494 nameJ := sorted[j].Name
495 if sorted[j].ObjectType == Tree {
496 nameJ += "/"
497 }
498 return nameI < nameJ
499 })
500
501 for _, entry := range sorted {
502 mode := filemode.Regular
503 if entry.ObjectType == Tree {
504 mode = filemode.Dir
505 }
506
507 tree.Entries = append(tree.Entries, object.TreeEntry{
508 Name: entry.Name,
509 Mode: mode,
510 Hash: plumbing.NewHash(entry.Hash.String()),
511 })
512 }
513
514 obj := repo.r.Storer.NewEncodedObject()
515 obj.SetType(plumbing.TreeObject)
516 err := tree.Encode(obj)
517 if err != nil {
518 return "", err
519 }
520
521 hash, err := repo.r.Storer.SetEncodedObject(obj)
522 if err != nil {
523 return "", err
524 }
525
526 return Hash(hash.String()), nil
527}
528
529// ReadTree will return the list of entries in a Git tree
530func (repo *GoGitRepo) ReadTree(hash Hash) ([]TreeEntry, error) {
531 repo.rMutex.Lock()
532 defer repo.rMutex.Unlock()
533
534 h := plumbing.NewHash(hash.String())
535
536 // the given hash could be a tree or a commit
537 obj, err := repo.r.Storer.EncodedObject(plumbing.AnyObject, h)
538 if err != nil {
539 return nil, err
540 }
541
542 var tree *object.Tree
543 switch obj.Type() {
544 case plumbing.TreeObject:
545 tree, err = object.DecodeTree(repo.r.Storer, obj)
546 case plumbing.CommitObject:
547 var commit *object.Commit
548 commit, err = object.DecodeCommit(repo.r.Storer, obj)
549 if err != nil {
550 return nil, err
551 }
552 tree, err = commit.Tree()
553 default:
554 return nil, fmt.Errorf("given hash is not a tree")
555 }
556 if err != nil {
557 return nil, err
558 }
559
560 treeEntries := make([]TreeEntry, len(tree.Entries))
561 for i, entry := range tree.Entries {
562 objType := Blob
563 if entry.Mode == filemode.Dir {
564 objType = Tree
565 }
566
567 treeEntries[i] = TreeEntry{
568 ObjectType: objType,
569 Hash: Hash(entry.Hash.String()),
570 Name: entry.Name,
571 }
572 }
573
574 return treeEntries, nil
575}
576
577// StoreCommit will store a Git commit with the given Git tree
578func (repo *GoGitRepo) StoreCommit(treeHash Hash, parents ...Hash) (Hash, error) {
579 return repo.StoreSignedCommit(treeHash, nil, parents...)
580}
581
582// StoreSignedCommit will store a Git commit with the given Git tree. If signKey is not nil, the commit
583// will be signed accordingly.
584func (repo *GoGitRepo) StoreSignedCommit(treeHash Hash, signKey *openpgp.Entity, parents ...Hash) (Hash, error) {
585 cfg, err := repo.r.Config()
586 if err != nil {
587 return "", err
588 }
589
590 commit := object.Commit{
591 Author: object.Signature{
592 Name: cfg.Author.Name,
593 Email: cfg.Author.Email,
594 When: time.Now(),
595 },
596 Committer: object.Signature{
597 Name: cfg.Committer.Name,
598 Email: cfg.Committer.Email,
599 When: time.Now(),
600 },
601 Message: "",
602 TreeHash: plumbing.NewHash(treeHash.String()),
603 }
604
605 for _, parent := range parents {
606 commit.ParentHashes = append(commit.ParentHashes, plumbing.NewHash(parent.String()))
607 }
608
609 // Compute the signature if needed
610 if signKey != nil {
611 // first get the serialized commit
612 encoded := &plumbing.MemoryObject{}
613 if err := commit.Encode(encoded); err != nil {
614 return "", err
615 }
616 r, err := encoded.Reader()
617 if err != nil {
618 return "", err
619 }
620
621 // sign the data
622 var sig bytes.Buffer
623 if err := openpgp.ArmoredDetachSign(&sig, signKey, r, nil); err != nil {
624 return "", err
625 }
626 commit.PGPSignature = sig.String()
627 }
628
629 obj := repo.r.Storer.NewEncodedObject()
630 obj.SetType(plumbing.CommitObject)
631 err = commit.Encode(obj)
632 if err != nil {
633 return "", err
634 }
635
636 hash, err := repo.r.Storer.SetEncodedObject(obj)
637 if err != nil {
638 return "", err
639 }
640
641 return Hash(hash.String()), nil
642}
643
644// GetTreeHash return the git tree hash referenced in a commit
645func (repo *GoGitRepo) GetTreeHash(commit Hash) (Hash, error) {
646 repo.rMutex.Lock()
647 defer repo.rMutex.Unlock()
648
649 obj, err := repo.r.CommitObject(plumbing.NewHash(commit.String()))
650 if err != nil {
651 return "", err
652 }
653
654 return Hash(obj.TreeHash.String()), nil
655}
656
657// FindCommonAncestor will return the last common ancestor of two chain of commit
658func (repo *GoGitRepo) FindCommonAncestor(commit1 Hash, commit2 Hash) (Hash, error) {
659 repo.rMutex.Lock()
660 defer repo.rMutex.Unlock()
661
662 obj1, err := repo.r.CommitObject(plumbing.NewHash(commit1.String()))
663 if err != nil {
664 return "", err
665 }
666 obj2, err := repo.r.CommitObject(plumbing.NewHash(commit2.String()))
667 if err != nil {
668 return "", err
669 }
670
671 commits, err := obj1.MergeBase(obj2)
672 if err != nil {
673 return "", err
674 }
675
676 return Hash(commits[0].Hash.String()), nil
677}
678
679func (repo *GoGitRepo) ResolveRef(ref string) (Hash, error) {
680 r, err := repo.r.Reference(plumbing.ReferenceName(ref), false)
681 if err != nil {
682 return "", err
683 }
684 return Hash(r.Hash().String()), nil
685}
686
687// UpdateRef will create or update a Git reference
688func (repo *GoGitRepo) UpdateRef(ref string, hash Hash) error {
689 return repo.r.Storer.SetReference(plumbing.NewHashReference(plumbing.ReferenceName(ref), plumbing.NewHash(hash.String())))
690}
691
692// RemoveRef will remove a Git reference
693func (repo *GoGitRepo) RemoveRef(ref string) error {
694 return repo.r.Storer.RemoveReference(plumbing.ReferenceName(ref))
695}
696
697// ListRefs will return a list of Git ref matching the given refspec
698func (repo *GoGitRepo) ListRefs(refPrefix string) ([]string, error) {
699 refIter, err := repo.r.References()
700 if err != nil {
701 return nil, err
702 }
703
704 refs := make([]string, 0)
705
706 err = refIter.ForEach(func(ref *plumbing.Reference) error {
707 if strings.HasPrefix(ref.Name().String(), refPrefix) {
708 refs = append(refs, ref.Name().String())
709 }
710 return nil
711 })
712 if err != nil {
713 return nil, err
714 }
715
716 return refs, nil
717}
718
719// RefExist will check if a reference exist in Git
720func (repo *GoGitRepo) RefExist(ref string) (bool, error) {
721 _, err := repo.r.Reference(plumbing.ReferenceName(ref), false)
722 if err == nil {
723 return true, nil
724 } else if err == plumbing.ErrReferenceNotFound {
725 return false, nil
726 }
727 return false, err
728}
729
730// CopyRef will create a new reference with the same value as another one
731func (repo *GoGitRepo) CopyRef(source string, dest string) error {
732 r, err := repo.r.Reference(plumbing.ReferenceName(source), false)
733 if err != nil {
734 return err
735 }
736 return repo.r.Storer.SetReference(plumbing.NewHashReference(plumbing.ReferenceName(dest), r.Hash()))
737}
738
739// ListCommits will return the list of tree hashes of a ref, in chronological order
740func (repo *GoGitRepo) ListCommits(ref string) ([]Hash, error) {
741 return nonNativeListCommits(repo, ref)
742}
743
744func (repo *GoGitRepo) ReadCommit(hash Hash) (Commit, error) {
745 repo.rMutex.Lock()
746 defer repo.rMutex.Unlock()
747
748 commit, err := repo.r.CommitObject(plumbing.NewHash(hash.String()))
749 if err != nil {
750 return Commit{}, err
751 }
752
753 parents := make([]Hash, len(commit.ParentHashes))
754 for i, parentHash := range commit.ParentHashes {
755 parents[i] = Hash(parentHash.String())
756 }
757
758 result := Commit{
759 Hash: hash,
760 Parents: parents,
761 TreeHash: Hash(commit.TreeHash.String()),
762 }
763
764 if commit.PGPSignature != "" {
765 // I can't find a way to just remove the signature when reading the encoded commit so we need to
766 // re-encode the commit without signature.
767
768 encoded := &plumbing.MemoryObject{}
769 err := commit.EncodeWithoutSignature(encoded)
770 if err != nil {
771 return Commit{}, err
772 }
773
774 result.SignedData, err = encoded.Reader()
775 if err != nil {
776 return Commit{}, err
777 }
778
779 result.Signature, err = deArmorSignature(strings.NewReader(commit.PGPSignature))
780 if err != nil {
781 return Commit{}, err
782 }
783 }
784
785 return result, nil
786}
787
788func (repo *GoGitRepo) AllClocks() (map[string]lamport.Clock, error) {
789 repo.clocksMutex.Lock()
790 defer repo.clocksMutex.Unlock()
791
792 result := make(map[string]lamport.Clock)
793
794 files, err := ioutil.ReadDir(filepath.Join(repo.localStorage.Root(), clockPath))
795 if os.IsNotExist(err) {
796 return nil, nil
797 }
798 if err != nil {
799 return nil, err
800 }
801
802 for _, file := range files {
803 name := file.Name()
804 if c, ok := repo.clocks[name]; ok {
805 result[name] = c
806 } else {
807 c, err := lamport.LoadPersistedClock(repo.LocalStorage(), filepath.Join(clockPath, name))
808 if err != nil {
809 return nil, err
810 }
811 repo.clocks[name] = c
812 result[name] = c
813 }
814 }
815
816 return result, nil
817}
818
819// GetOrCreateClock return a Lamport clock stored in the Repo.
820// If the clock doesn't exist, it's created.
821func (repo *GoGitRepo) GetOrCreateClock(name string) (lamport.Clock, error) {
822 repo.clocksMutex.Lock()
823 defer repo.clocksMutex.Unlock()
824
825 c, err := repo.getClock(name)
826 if err == nil {
827 return c, nil
828 }
829 if err != ErrClockNotExist {
830 return nil, err
831 }
832
833 c, err = lamport.NewPersistedClock(repo.LocalStorage(), filepath.Join(clockPath, name))
834 if err != nil {
835 return nil, err
836 }
837
838 repo.clocks[name] = c
839 return c, nil
840}
841
842func (repo *GoGitRepo) getClock(name string) (lamport.Clock, error) {
843 if c, ok := repo.clocks[name]; ok {
844 return c, nil
845 }
846
847 c, err := lamport.LoadPersistedClock(repo.LocalStorage(), filepath.Join(clockPath, name))
848 if err == nil {
849 repo.clocks[name] = c
850 return c, nil
851 }
852 if err == lamport.ErrClockNotExist {
853 return nil, ErrClockNotExist
854 }
855 return nil, err
856}
857
858// Increment is equivalent to c = GetOrCreateClock(name) + c.Increment()
859func (repo *GoGitRepo) Increment(name string) (lamport.Time, error) {
860 c, err := repo.GetOrCreateClock(name)
861 if err != nil {
862 return lamport.Time(0), err
863 }
864 return c.Increment()
865}
866
867// Witness is equivalent to c = GetOrCreateClock(name) + c.Witness(time)
868func (repo *GoGitRepo) Witness(name string, time lamport.Time) error {
869 c, err := repo.GetOrCreateClock(name)
870 if err != nil {
871 return err
872 }
873 return c.Witness(time)
874}
875
876// AddRemote add a new remote to the repository
877// Not in the interface because it's only used for testing
878func (repo *GoGitRepo) AddRemote(name string, url string) error {
879 _, err := repo.r.CreateRemote(&config.RemoteConfig{
880 Name: name,
881 URLs: []string{url},
882 })
883
884 return err
885}
886
887// GetLocalRemote return the URL to use to add this repo as a local remote
888func (repo *GoGitRepo) GetLocalRemote() string {
889 return repo.path
890}
891
892// EraseFromDisk delete this repository entirely from the disk
893func (repo *GoGitRepo) EraseFromDisk() error {
894 err := repo.Close()
895 if err != nil {
896 return err
897 }
898
899 path := filepath.Clean(strings.TrimSuffix(repo.path, string(filepath.Separator)+".git"))
900
901 // fmt.Println("Cleaning repo:", path)
902 return os.RemoveAll(path)
903}