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