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