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