gogit.go

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