gogit.go

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