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