gogit.go

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