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