gogit.go

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