gogit.go

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