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