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