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