gogit.go

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