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