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