gogit.go

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