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