gogit.go

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