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