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