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 == gogit.NoErrAlreadyUpToDate {
280		return "already up-to-date", nil
281	}
282	if err != nil {
283		return "", err
284	}
285
286	return buf.String(), nil
287}
288
289// PushRefs push git refs to a remote
290func (repo *GoGitRepo) PushRefs(remote string, refSpec string) (string, error) {
291	buf := bytes.NewBuffer(nil)
292
293	err := repo.r.Push(&gogit.PushOptions{
294		RemoteName: remote,
295		RefSpecs:   []config.RefSpec{config.RefSpec(refSpec)},
296		Progress:   buf,
297	})
298	if err == gogit.NoErrAlreadyUpToDate {
299		return "already up-to-date", nil
300	}
301	if err != nil {
302		return "", err
303	}
304
305	return buf.String(), nil
306}
307
308// StoreData will store arbitrary data and return the corresponding hash
309func (repo *GoGitRepo) StoreData(data []byte) (Hash, error) {
310	obj := repo.r.Storer.NewEncodedObject()
311	obj.SetType(plumbing.BlobObject)
312
313	w, err := obj.Writer()
314	if err != nil {
315		return "", err
316	}
317
318	_, err = w.Write(data)
319	if err != nil {
320		return "", err
321	}
322
323	h, err := repo.r.Storer.SetEncodedObject(obj)
324	if err != nil {
325		return "", err
326	}
327
328	return Hash(h.String()), nil
329}
330
331// ReadData will attempt to read arbitrary data from the given hash
332func (repo *GoGitRepo) ReadData(hash Hash) ([]byte, error) {
333	obj, err := repo.r.BlobObject(plumbing.NewHash(hash.String()))
334	if err != nil {
335		return nil, err
336	}
337
338	r, err := obj.Reader()
339	if err != nil {
340		return nil, err
341	}
342
343	return ioutil.ReadAll(r)
344}
345
346// StoreTree will store a mapping key-->Hash as a Git tree
347func (repo *GoGitRepo) StoreTree(mapping []TreeEntry) (Hash, error) {
348	var tree object.Tree
349
350	for _, entry := range mapping {
351		mode := filemode.Regular
352		if entry.ObjectType == Tree {
353			mode = filemode.Dir
354		}
355
356		tree.Entries = append(tree.Entries, object.TreeEntry{
357			Name: entry.Name,
358			Mode: mode,
359			Hash: plumbing.NewHash(entry.Hash.String()),
360		})
361	}
362
363	obj := repo.r.Storer.NewEncodedObject()
364	obj.SetType(plumbing.TreeObject)
365	err := tree.Encode(obj)
366	if err != nil {
367		return "", err
368	}
369
370	hash, err := repo.r.Storer.SetEncodedObject(obj)
371	if err != nil {
372		return "", err
373	}
374
375	return Hash(hash.String()), nil
376}
377
378// ReadTree will return the list of entries in a Git tree
379func (repo *GoGitRepo) ReadTree(hash Hash) ([]TreeEntry, error) {
380	h := plumbing.NewHash(hash.String())
381
382	// the given hash could be a tree or a commit
383	obj, err := repo.r.Storer.EncodedObject(plumbing.AnyObject, h)
384	if err != nil {
385		return nil, err
386	}
387
388	var tree *object.Tree
389	switch obj.Type() {
390	case plumbing.TreeObject:
391		tree, err = object.DecodeTree(repo.r.Storer, obj)
392	case plumbing.CommitObject:
393		var commit *object.Commit
394		commit, err = object.DecodeCommit(repo.r.Storer, obj)
395		if err != nil {
396			return nil, err
397		}
398		tree, err = commit.Tree()
399	default:
400		return nil, fmt.Errorf("given hash is not a tree")
401	}
402	if err != nil {
403		return nil, err
404	}
405
406	treeEntries := make([]TreeEntry, len(tree.Entries))
407	for i, entry := range tree.Entries {
408		objType := Blob
409		if entry.Mode == filemode.Dir {
410			objType = Tree
411		}
412
413		treeEntries[i] = TreeEntry{
414			ObjectType: objType,
415			Hash:       Hash(entry.Hash.String()),
416			Name:       entry.Name,
417		}
418	}
419
420	return treeEntries, nil
421}
422
423// StoreCommit will store a Git commit with the given Git tree
424func (repo *GoGitRepo) StoreCommit(treeHash Hash) (Hash, error) {
425	return repo.StoreCommitWithParent(treeHash, "")
426}
427
428// StoreCommit will store a Git commit with the given Git tree
429func (repo *GoGitRepo) StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, error) {
430	cfg, err := repo.r.Config()
431	if err != nil {
432		return "", err
433	}
434
435	commit := object.Commit{
436		Author: object.Signature{
437			cfg.Author.Name,
438			cfg.Author.Email,
439			time.Now(),
440		},
441		Committer: object.Signature{
442			cfg.Committer.Name,
443			cfg.Committer.Email,
444			time.Now(),
445		},
446		Message:  "",
447		TreeHash: plumbing.NewHash(treeHash.String()),
448	}
449
450	if parent != "" {
451		commit.ParentHashes = []plumbing.Hash{plumbing.NewHash(parent.String())}
452	}
453
454	obj := repo.r.Storer.NewEncodedObject()
455	obj.SetType(plumbing.CommitObject)
456	err = commit.Encode(obj)
457	if err != nil {
458		return "", err
459	}
460
461	hash, err := repo.r.Storer.SetEncodedObject(obj)
462	if err != nil {
463		return "", err
464	}
465
466	return Hash(hash.String()), nil
467}
468
469// GetTreeHash return the git tree hash referenced in a commit
470func (repo *GoGitRepo) GetTreeHash(commit Hash) (Hash, error) {
471	obj, err := repo.r.CommitObject(plumbing.NewHash(commit.String()))
472	if err != nil {
473		return "", err
474	}
475
476	return Hash(obj.TreeHash.String()), nil
477}
478
479// FindCommonAncestor will return the last common ancestor of two chain of commit
480func (repo *GoGitRepo) FindCommonAncestor(commit1 Hash, commit2 Hash) (Hash, error) {
481	obj1, err := repo.r.CommitObject(plumbing.NewHash(commit1.String()))
482	if err != nil {
483		return "", err
484	}
485	obj2, err := repo.r.CommitObject(plumbing.NewHash(commit2.String()))
486	if err != nil {
487		return "", err
488	}
489
490	commits, err := obj1.MergeBase(obj2)
491	if err != nil {
492		return "", err
493	}
494
495	return Hash(commits[0].Hash.String()), nil
496}
497
498// UpdateRef will create or update a Git reference
499func (repo *GoGitRepo) UpdateRef(ref string, hash Hash) error {
500	return repo.r.Storer.SetReference(plumbing.NewHashReference(plumbing.ReferenceName(ref), plumbing.NewHash(hash.String())))
501}
502
503// RemoveRef will remove a Git reference
504func (repo *GoGitRepo) RemoveRef(ref string) error {
505	return repo.r.Storer.RemoveReference(plumbing.ReferenceName(ref))
506}
507
508// ListRefs will return a list of Git ref matching the given refspec
509func (repo *GoGitRepo) ListRefs(refPrefix string) ([]string, error) {
510	refIter, err := repo.r.References()
511	if err != nil {
512		return nil, err
513	}
514
515	refs := make([]string, 0)
516
517	err = refIter.ForEach(func(ref *plumbing.Reference) error {
518		if strings.HasPrefix(ref.Name().String(), refPrefix) {
519			refs = append(refs, ref.Name().String())
520		}
521		return nil
522	})
523	if err != nil {
524		return nil, err
525	}
526
527	return refs, nil
528}
529
530// RefExist will check if a reference exist in Git
531func (repo *GoGitRepo) RefExist(ref string) (bool, error) {
532	_, err := repo.r.Reference(plumbing.ReferenceName(ref), false)
533	if err == nil {
534		return true, nil
535	} else if err == plumbing.ErrReferenceNotFound {
536		return false, nil
537	}
538	return false, err
539}
540
541// CopyRef will create a new reference with the same value as another one
542func (repo *GoGitRepo) CopyRef(source string, dest string) error {
543	r, err := repo.r.Reference(plumbing.ReferenceName(source), false)
544	if err != nil {
545		return err
546	}
547	return repo.r.Storer.SetReference(plumbing.NewHashReference(plumbing.ReferenceName(dest), r.Hash()))
548}
549
550// ListCommits will return the list of tree hashes of a ref, in chronological order
551func (repo *GoGitRepo) ListCommits(ref string) ([]Hash, error) {
552	r, err := repo.r.Reference(plumbing.ReferenceName(ref), false)
553	if err != nil {
554		return nil, err
555	}
556
557	commit, err := repo.r.CommitObject(r.Hash())
558	if err != nil {
559		return nil, err
560	}
561	hashes := []Hash{Hash(commit.Hash.String())}
562
563	for {
564		commit, err = commit.Parent(0)
565		if err == object.ErrParentNotFound {
566			break
567		}
568		if err != nil {
569			return nil, err
570		}
571
572		if commit.NumParents() > 1 {
573			return nil, fmt.Errorf("multiple parents")
574		}
575
576		hashes = append([]Hash{Hash(commit.Hash.String())}, hashes...)
577	}
578
579	return hashes, nil
580}
581
582// GetOrCreateClock return a Lamport clock stored in the Repo.
583// If the clock doesn't exist, it's created.
584func (repo *GoGitRepo) GetOrCreateClock(name string) (lamport.Clock, error) {
585	c, err := repo.getClock(name)
586	if err == nil {
587		return c, nil
588	}
589	if err != ErrClockNotExist {
590		return nil, err
591	}
592
593	repo.clocksMutex.Lock()
594	defer repo.clocksMutex.Unlock()
595
596	p := stdpath.Join(repo.path, clockPath, name+"-clock")
597
598	c, err = lamport.NewPersistedClock(p)
599	if err != nil {
600		return nil, err
601	}
602
603	repo.clocks[name] = c
604	return c, nil
605}
606
607func (repo *GoGitRepo) getClock(name string) (lamport.Clock, error) {
608	repo.clocksMutex.Lock()
609	defer repo.clocksMutex.Unlock()
610
611	if c, ok := repo.clocks[name]; ok {
612		return c, nil
613	}
614
615	p := stdpath.Join(repo.path, clockPath, name+"-clock")
616
617	c, err := lamport.LoadPersistedClock(p)
618	if err == nil {
619		repo.clocks[name] = c
620		return c, nil
621	}
622	if err == lamport.ErrClockNotExist {
623		return nil, ErrClockNotExist
624	}
625	return nil, err
626}
627
628// AddRemote add a new remote to the repository
629// Not in the interface because it's only used for testing
630func (repo *GoGitRepo) AddRemote(name string, url string) error {
631	_, err := repo.r.CreateRemote(&config.RemoteConfig{
632		Name: name,
633		URLs: []string{url},
634	})
635
636	return err
637}