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/blevesearch/bleve"
 15	"github.com/go-git/go-billy/v5"
 16	"github.com/go-git/go-billy/v5/osfs"
 17	gogit "github.com/go-git/go-git/v5"
 18	"github.com/go-git/go-git/v5/config"
 19	"github.com/go-git/go-git/v5/plumbing"
 20	"github.com/go-git/go-git/v5/plumbing/filemode"
 21	"github.com/go-git/go-git/v5/plumbing/object"
 22	"golang.org/x/crypto/openpgp"
 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	h := plumbing.NewHash(hash.String())
522
523	// the given hash could be a tree or a commit
524	obj, err := repo.r.Storer.EncodedObject(plumbing.AnyObject, h)
525	if err != nil {
526		return nil, err
527	}
528
529	var tree *object.Tree
530	switch obj.Type() {
531	case plumbing.TreeObject:
532		tree, err = object.DecodeTree(repo.r.Storer, obj)
533	case plumbing.CommitObject:
534		var commit *object.Commit
535		commit, err = object.DecodeCommit(repo.r.Storer, obj)
536		if err != nil {
537			return nil, err
538		}
539		tree, err = commit.Tree()
540	default:
541		return nil, fmt.Errorf("given hash is not a tree")
542	}
543	if err != nil {
544		return nil, err
545	}
546
547	treeEntries := make([]TreeEntry, len(tree.Entries))
548	for i, entry := range tree.Entries {
549		objType := Blob
550		if entry.Mode == filemode.Dir {
551			objType = Tree
552		}
553
554		treeEntries[i] = TreeEntry{
555			ObjectType: objType,
556			Hash:       Hash(entry.Hash.String()),
557			Name:       entry.Name,
558		}
559	}
560
561	return treeEntries, nil
562}
563
564// StoreCommit will store a Git commit with the given Git tree
565func (repo *GoGitRepo) StoreCommit(treeHash Hash, parents ...Hash) (Hash, error) {
566	return repo.StoreSignedCommit(treeHash, nil, parents...)
567}
568
569// StoreCommit will store a Git commit with the given Git tree. If signKey is not nil, the commit
570// will be signed accordingly.
571func (repo *GoGitRepo) StoreSignedCommit(treeHash Hash, signKey *openpgp.Entity, parents ...Hash) (Hash, error) {
572	cfg, err := repo.r.Config()
573	if err != nil {
574		return "", err
575	}
576
577	commit := object.Commit{
578		Author: object.Signature{
579			Name:  cfg.Author.Name,
580			Email: cfg.Author.Email,
581			When:  time.Now(),
582		},
583		Committer: object.Signature{
584			Name:  cfg.Committer.Name,
585			Email: cfg.Committer.Email,
586			When:  time.Now(),
587		},
588		Message:  "",
589		TreeHash: plumbing.NewHash(treeHash.String()),
590	}
591
592	for _, parent := range parents {
593		commit.ParentHashes = append(commit.ParentHashes, plumbing.NewHash(parent.String()))
594	}
595
596	// Compute the signature if needed
597	if signKey != nil {
598		// first get the serialized commit
599		encoded := &plumbing.MemoryObject{}
600		if err := commit.Encode(encoded); err != nil {
601			return "", err
602		}
603		r, err := encoded.Reader()
604		if err != nil {
605			return "", err
606		}
607
608		// sign the data
609		var sig bytes.Buffer
610		if err := openpgp.ArmoredDetachSign(&sig, signKey, r, nil); err != nil {
611			return "", err
612		}
613		commit.PGPSignature = sig.String()
614	}
615
616	obj := repo.r.Storer.NewEncodedObject()
617	obj.SetType(plumbing.CommitObject)
618	err = commit.Encode(obj)
619	if err != nil {
620		return "", err
621	}
622
623	hash, err := repo.r.Storer.SetEncodedObject(obj)
624	if err != nil {
625		return "", err
626	}
627
628	return Hash(hash.String()), nil
629}
630
631// GetTreeHash return the git tree hash referenced in a commit
632func (repo *GoGitRepo) GetTreeHash(commit Hash) (Hash, error) {
633	obj, err := repo.r.CommitObject(plumbing.NewHash(commit.String()))
634	if err != nil {
635		return "", err
636	}
637
638	return Hash(obj.TreeHash.String()), nil
639}
640
641// FindCommonAncestor will return the last common ancestor of two chain of commit
642func (repo *GoGitRepo) FindCommonAncestor(commit1 Hash, commit2 Hash) (Hash, error) {
643	obj1, err := repo.r.CommitObject(plumbing.NewHash(commit1.String()))
644	if err != nil {
645		return "", err
646	}
647	obj2, err := repo.r.CommitObject(plumbing.NewHash(commit2.String()))
648	if err != nil {
649		return "", err
650	}
651
652	commits, err := obj1.MergeBase(obj2)
653	if err != nil {
654		return "", err
655	}
656
657	return Hash(commits[0].Hash.String()), nil
658}
659
660func (repo *GoGitRepo) ResolveRef(ref string) (Hash, error) {
661	r, err := repo.r.Reference(plumbing.ReferenceName(ref), false)
662	if err != nil {
663		return "", err
664	}
665	return Hash(r.Hash().String()), nil
666}
667
668// UpdateRef will create or update a Git reference
669func (repo *GoGitRepo) UpdateRef(ref string, hash Hash) error {
670	return repo.r.Storer.SetReference(plumbing.NewHashReference(plumbing.ReferenceName(ref), plumbing.NewHash(hash.String())))
671}
672
673// RemoveRef will remove a Git reference
674func (repo *GoGitRepo) RemoveRef(ref string) error {
675	return repo.r.Storer.RemoveReference(plumbing.ReferenceName(ref))
676}
677
678// ListRefs will return a list of Git ref matching the given refspec
679func (repo *GoGitRepo) ListRefs(refPrefix string) ([]string, error) {
680	refIter, err := repo.r.References()
681	if err != nil {
682		return nil, err
683	}
684
685	refs := make([]string, 0)
686
687	err = refIter.ForEach(func(ref *plumbing.Reference) error {
688		if strings.HasPrefix(ref.Name().String(), refPrefix) {
689			refs = append(refs, ref.Name().String())
690		}
691		return nil
692	})
693	if err != nil {
694		return nil, err
695	}
696
697	return refs, nil
698}
699
700// RefExist will check if a reference exist in Git
701func (repo *GoGitRepo) RefExist(ref string) (bool, error) {
702	_, err := repo.r.Reference(plumbing.ReferenceName(ref), false)
703	if err == nil {
704		return true, nil
705	} else if err == plumbing.ErrReferenceNotFound {
706		return false, nil
707	}
708	return false, err
709}
710
711// CopyRef will create a new reference with the same value as another one
712func (repo *GoGitRepo) CopyRef(source string, dest string) error {
713	r, err := repo.r.Reference(plumbing.ReferenceName(source), false)
714	if err != nil {
715		return err
716	}
717	return repo.r.Storer.SetReference(plumbing.NewHashReference(plumbing.ReferenceName(dest), r.Hash()))
718}
719
720// ListCommits will return the list of tree hashes of a ref, in chronological order
721func (repo *GoGitRepo) ListCommits(ref string) ([]Hash, error) {
722	return nonNativeListCommits(repo, ref)
723}
724
725func (repo *GoGitRepo) ReadCommit(hash Hash) (Commit, error) {
726	commit, err := repo.r.CommitObject(plumbing.NewHash(hash.String()))
727	if err != nil {
728		return Commit{}, err
729	}
730
731	parents := make([]Hash, len(commit.ParentHashes))
732	for i, parentHash := range commit.ParentHashes {
733		parents[i] = Hash(parentHash.String())
734	}
735
736	result := Commit{
737		Hash:     hash,
738		Parents:  parents,
739		TreeHash: Hash(commit.TreeHash.String()),
740	}
741
742	if commit.PGPSignature != "" {
743		// I can't find a way to just remove the signature when reading the encoded commit so we need to
744		// re-encode the commit without signature.
745
746		encoded := &plumbing.MemoryObject{}
747		err := commit.EncodeWithoutSignature(encoded)
748		if err != nil {
749			return Commit{}, err
750		}
751
752		result.SignedData, err = encoded.Reader()
753		if err != nil {
754			return Commit{}, err
755		}
756
757		result.Signature, err = deArmorSignature(strings.NewReader(commit.PGPSignature))
758		if err != nil {
759			return Commit{}, err
760		}
761	}
762
763	return result, nil
764}
765
766func (repo *GoGitRepo) AllClocks() (map[string]lamport.Clock, error) {
767	repo.clocksMutex.Lock()
768	defer repo.clocksMutex.Unlock()
769
770	result := make(map[string]lamport.Clock)
771
772	files, err := ioutil.ReadDir(filepath.Join(repo.path, "git-bug", clockPath))
773	if os.IsNotExist(err) {
774		return nil, nil
775	}
776	if err != nil {
777		return nil, err
778	}
779
780	for _, file := range files {
781		name := file.Name()
782		if c, ok := repo.clocks[name]; ok {
783			result[name] = c
784		} else {
785			c, err := lamport.LoadPersistedClock(repo.LocalStorage(), filepath.Join(clockPath, name))
786			if err != nil {
787				return nil, err
788			}
789			repo.clocks[name] = c
790			result[name] = c
791		}
792	}
793
794	return result, nil
795}
796
797// GetOrCreateClock return a Lamport clock stored in the Repo.
798// If the clock doesn't exist, it's created.
799func (repo *GoGitRepo) GetOrCreateClock(name string) (lamport.Clock, error) {
800	repo.clocksMutex.Lock()
801	defer repo.clocksMutex.Unlock()
802
803	c, err := repo.getClock(name)
804	if err == nil {
805		return c, nil
806	}
807	if err != ErrClockNotExist {
808		return nil, err
809	}
810
811	c, err = lamport.NewPersistedClock(repo.LocalStorage(), filepath.Join(clockPath, name))
812	if err != nil {
813		return nil, err
814	}
815
816	repo.clocks[name] = c
817	return c, nil
818}
819
820func (repo *GoGitRepo) getClock(name string) (lamport.Clock, error) {
821	if c, ok := repo.clocks[name]; ok {
822		return c, nil
823	}
824
825	c, err := lamport.LoadPersistedClock(repo.LocalStorage(), filepath.Join(clockPath, name))
826	if err == nil {
827		repo.clocks[name] = c
828		return c, nil
829	}
830	if err == lamport.ErrClockNotExist {
831		return nil, ErrClockNotExist
832	}
833	return nil, err
834}
835
836// Increment is equivalent to c = GetOrCreateClock(name) + c.Increment()
837func (repo *GoGitRepo) Increment(name string) (lamport.Time, error) {
838	c, err := repo.GetOrCreateClock(name)
839	if err != nil {
840		return lamport.Time(0), err
841	}
842	return c.Increment()
843}
844
845// Witness is equivalent to c = GetOrCreateClock(name) + c.Witness(time)
846func (repo *GoGitRepo) Witness(name string, time lamport.Time) error {
847	c, err := repo.GetOrCreateClock(name)
848	if err != nil {
849		return err
850	}
851	return c.Witness(time)
852}
853
854// AddRemote add a new remote to the repository
855// Not in the interface because it's only used for testing
856func (repo *GoGitRepo) AddRemote(name string, url string) error {
857	_, err := repo.r.CreateRemote(&config.RemoteConfig{
858		Name: name,
859		URLs: []string{url},
860	})
861
862	return err
863}
864
865// GetLocalRemote return the URL to use to add this repo as a local remote
866func (repo *GoGitRepo) GetLocalRemote() string {
867	return repo.path
868}
869
870// EraseFromDisk delete this repository entirely from the disk
871func (repo *GoGitRepo) EraseFromDisk() error {
872	err := repo.Close()
873	if err != nil {
874		return err
875	}
876
877	path := filepath.Clean(strings.TrimSuffix(repo.path, string(filepath.Separator)+".git"))
878
879	// fmt.Println("Cleaning repo:", path)
880	return os.RemoveAll(path)
881}