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