gogit.go

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