gogit.go

  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, "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}