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