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