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