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