gogit.go

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