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
161func (repo *GoGitRepo) LocalConfig() Config {
162	return newGoGitConfig(repo.r)
163}
164
165func (repo *GoGitRepo) GlobalConfig() Config {
166	panic("go-git doesn't support writing global config")
167}
168
169func (repo *GoGitRepo) Keyring() Keyring {
170	return repo.keyring
171}
172
173// GetPath returns the path to the repo.
174func (repo *GoGitRepo) GetPath() string {
175	return repo.path
176}
177
178// GetUserName returns the name the the user has used to configure git
179func (repo *GoGitRepo) GetUserName() (string, error) {
180	cfg, err := repo.r.Config()
181	if err != nil {
182		return "", err
183	}
184
185	return cfg.User.Name, nil
186}
187
188// GetUserEmail returns the email address that the user has used to configure git.
189func (repo *GoGitRepo) GetUserEmail() (string, error) {
190	cfg, err := repo.r.Config()
191	if err != nil {
192		return "", err
193	}
194
195	return cfg.User.Email, nil
196}
197
198// GetCoreEditor returns the name of the editor that the user has used to configure git.
199func (repo *GoGitRepo) GetCoreEditor() (string, error) {
200
201	panic("implement me")
202}
203
204// GetRemotes returns the configured remotes repositories.
205func (repo *GoGitRepo) GetRemotes() (map[string]string, error) {
206	cfg, err := repo.r.Config()
207	if err != nil {
208		return nil, err
209	}
210
211	result := make(map[string]string, len(cfg.Remotes))
212	for name, remote := range cfg.Remotes {
213		if len(remote.URLs) > 0 {
214			result[name] = remote.URLs[0]
215		}
216	}
217
218	return result, nil
219}
220
221// FetchRefs fetch git refs from a remote
222func (repo *GoGitRepo) FetchRefs(remote string, refSpec string) (string, error) {
223	buf := bytes.NewBuffer(nil)
224
225	err := repo.r.Fetch(&gogit.FetchOptions{
226		RemoteName: remote,
227		RefSpecs:   []config.RefSpec{config.RefSpec(refSpec)},
228		Progress:   buf,
229	})
230	if err != nil {
231		return "", err
232	}
233
234	return buf.String(), nil
235}
236
237// PushRefs push git refs to a remote
238func (repo *GoGitRepo) PushRefs(remote string, refSpec string) (string, error) {
239	buf := bytes.NewBuffer(nil)
240
241	err := repo.r.Push(&gogit.PushOptions{
242		RemoteName: remote,
243		RefSpecs:   []config.RefSpec{config.RefSpec(refSpec)},
244		Progress:   buf,
245	})
246	if err != nil {
247		return "", err
248	}
249
250	return buf.String(), nil
251}
252
253// StoreData will store arbitrary data and return the corresponding hash
254func (repo *GoGitRepo) StoreData(data []byte) (Hash, error) {
255	obj := repo.r.Storer.NewEncodedObject()
256	obj.SetType(plumbing.BlobObject)
257
258	w, err := obj.Writer()
259	if err != nil {
260		return "", err
261	}
262
263	_, err = w.Write(data)
264	if err != nil {
265		return "", err
266	}
267
268	h, err := repo.r.Storer.SetEncodedObject(obj)
269	if err != nil {
270		return "", err
271	}
272
273	return Hash(h.String()), nil
274}
275
276// ReadData will attempt to read arbitrary data from the given hash
277func (repo *GoGitRepo) ReadData(hash Hash) ([]byte, error) {
278	obj, err := repo.r.BlobObject(plumbing.NewHash(hash.String()))
279	if err != nil {
280		return nil, err
281	}
282
283	r, err := obj.Reader()
284	if err != nil {
285		return nil, err
286	}
287
288	return ioutil.ReadAll(r)
289}
290
291func (repo *GoGitRepo) StoreTree(mapping []TreeEntry) (Hash, error) {
292	var tree object.Tree
293
294	for _, entry := range mapping {
295		mode := filemode.Regular
296		if entry.ObjectType == Tree {
297			mode = filemode.Dir
298		}
299
300		tree.Entries = append(tree.Entries, object.TreeEntry{
301			Name: entry.Name,
302			Mode: mode,
303			Hash: plumbing.NewHash(entry.Hash.String()),
304		})
305	}
306
307	obj := repo.r.Storer.NewEncodedObject()
308	obj.SetType(plumbing.TreeObject)
309	err := tree.Encode(obj)
310	if err != nil {
311		return "", err
312	}
313
314	hash, err := repo.r.Storer.SetEncodedObject(obj)
315	if err != nil {
316		return "", err
317	}
318
319	return Hash(hash.String()), nil
320}
321
322func (repo *GoGitRepo) ReadTree(hash Hash) ([]TreeEntry, error) {
323	obj, err := repo.r.TreeObject(plumbing.NewHash(hash.String()))
324	if err != nil {
325		return nil, err
326	}
327
328	treeEntries := make([]TreeEntry, len(obj.Entries))
329	for i, entry := range obj.Entries {
330		objType := Blob
331		if entry.Mode == filemode.Dir {
332			objType = Tree
333		}
334
335		treeEntries[i] = TreeEntry{
336			ObjectType: objType,
337			Hash:       Hash(entry.Hash.String()),
338			Name:       entry.Name,
339		}
340	}
341
342	return treeEntries, nil
343}
344
345func (repo *GoGitRepo) StoreCommit(treeHash Hash) (Hash, error) {
346	return repo.StoreCommitWithParent(treeHash, "")
347}
348
349func (repo *GoGitRepo) StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, error) {
350	cfg, err := repo.r.Config()
351	if err != nil {
352		return "", err
353	}
354
355	commit := object.Commit{
356		Author: object.Signature{
357			cfg.Author.Name,
358			cfg.Author.Email,
359			time.Now(),
360		},
361		Committer: object.Signature{
362			cfg.Committer.Name,
363			cfg.Committer.Email,
364			time.Now(),
365		},
366		Message:  "",
367		TreeHash: plumbing.NewHash(treeHash.String()),
368	}
369
370	if parent != "" {
371		commit.ParentHashes = []plumbing.Hash{plumbing.NewHash(parent.String())}
372	}
373
374	obj := repo.r.Storer.NewEncodedObject()
375	obj.SetType(plumbing.CommitObject)
376	err = commit.Encode(obj)
377	if err != nil {
378		return "", err
379	}
380
381	hash, err := repo.r.Storer.SetEncodedObject(obj)
382	if err != nil {
383		return "", err
384	}
385
386	return Hash(hash.String()), nil
387}
388
389func (repo *GoGitRepo) GetTreeHash(commit Hash) (Hash, error) {
390	obj, err := repo.r.CommitObject(plumbing.NewHash(commit.String()))
391	if err != nil {
392		return "", err
393	}
394
395	return Hash(obj.TreeHash.String()), nil
396}
397
398func (repo *GoGitRepo) FindCommonAncestor(commit1 Hash, commit2 Hash) (Hash, error) {
399	obj1, err := repo.r.CommitObject(plumbing.NewHash(commit1.String()))
400	if err != nil {
401		return "", err
402	}
403	obj2, err := repo.r.CommitObject(plumbing.NewHash(commit2.String()))
404	if err != nil {
405		return "", err
406	}
407
408	commits, err := obj1.MergeBase(obj2)
409	if err != nil {
410		return "", err
411	}
412
413	return Hash(commits[0].Hash.String()), nil
414}
415
416func (repo *GoGitRepo) UpdateRef(ref string, hash Hash) error {
417	return repo.r.Storer.SetReference(plumbing.NewHashReference(plumbing.ReferenceName(ref), plumbing.NewHash(hash.String())))
418}
419
420func (repo *GoGitRepo) RemoveRef(ref string) error {
421	return repo.r.Storer.RemoveReference(plumbing.ReferenceName(ref))
422}
423
424func (repo *GoGitRepo) ListRefs(refPrefix string) ([]string, error) {
425	refIter, err := repo.r.References()
426	if err != nil {
427		return nil, err
428	}
429
430	refs := make([]string, 0)
431
432	err = refIter.ForEach(func(ref *plumbing.Reference) error {
433		if strings.HasPrefix(ref.Name().String(), refPrefix) {
434			refs = append(refs, ref.Name().String())
435		}
436		return nil
437	})
438	if err != nil {
439		return nil, err
440	}
441
442	return refs, nil
443}
444
445func (repo *GoGitRepo) RefExist(ref string) (bool, error) {
446	_, err := repo.r.Reference(plumbing.ReferenceName(ref), false)
447	if err == nil {
448		return true, nil
449	} else if err == plumbing.ErrReferenceNotFound {
450		return false, nil
451	}
452	return false, err
453}
454
455func (repo *GoGitRepo) CopyRef(source string, dest string) error {
456	r, err := repo.r.Reference(plumbing.ReferenceName(source), false)
457	if err != nil {
458		return err
459	}
460	return repo.r.Storer.SetReference(plumbing.NewHashReference(plumbing.ReferenceName(dest), r.Hash()))
461}
462
463func (repo *GoGitRepo) ListCommits(ref string) ([]Hash, error) {
464	r, err := repo.r.Reference(plumbing.ReferenceName(ref), false)
465	if err != nil {
466		return nil, err
467	}
468
469	commit, err := repo.r.CommitObject(r.Hash())
470	if err != nil {
471		return nil, err
472	}
473	commits := []Hash{Hash(commit.Hash.String())}
474
475	for {
476		commit, err = commit.Parent(0)
477
478		if err != nil {
479			if err == object.ErrParentNotFound {
480				break
481			}
482
483			return nil, err
484		}
485
486		if commit.NumParents() > 1 {
487			return nil, fmt.Errorf("multiple parents")
488		}
489
490		commits = append(commits, Hash(commit.Hash.String()))
491	}
492
493	return commits, nil
494}
495
496// GetOrCreateClock return a Lamport clock stored in the Repo.
497// If the clock doesn't exist, it's created.
498func (repo *GoGitRepo) GetOrCreateClock(name string) (lamport.Clock, error) {
499	c, err := repo.getClock(name)
500	if err == nil {
501		return c, nil
502	}
503	if err != ErrClockNotExist {
504		return nil, err
505	}
506
507	repo.clocksMutex.Lock()
508	defer repo.clocksMutex.Unlock()
509
510	p := clockPath + name + "-clock"
511
512	c, err = lamport.NewPersistedClock(p)
513	if err != nil {
514		return nil, err
515	}
516
517	repo.clocks[name] = c
518	return c, nil
519}
520
521func (repo *GoGitRepo) getClock(name string) (lamport.Clock, error) {
522	repo.clocksMutex.Lock()
523	defer repo.clocksMutex.Unlock()
524
525	if c, ok := repo.clocks[name]; ok {
526		return c, nil
527	}
528
529	p := clockPath + name + "-clock"
530
531	c, err := lamport.LoadPersistedClock(p)
532	if err == nil {
533		repo.clocks[name] = c
534		return c, nil
535	}
536	if err == lamport.ErrClockNotExist {
537		return nil, ErrClockNotExist
538	}
539	return nil, err
540}
541
542// AddRemote add a new remote to the repository
543// Not in the interface because it's only used for testing
544func (repo *GoGitRepo) AddRemote(name string, url string) error {
545	_, err := repo.r.CreateRemote(&config.RemoteConfig{
546		Name: name,
547		URLs: []string{url},
548	})
549
550	return err
551}