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