gogit.go

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