git.go

  1// Package repository contains helper methods for working with the Git repo.
  2package repository
  3
  4import (
  5	"bytes"
  6	"fmt"
  7	"io/ioutil"
  8	"os"
  9	"path/filepath"
 10	"strings"
 11	"sync"
 12
 13	"github.com/blevesearch/bleve"
 14	"github.com/go-git/go-billy/v5"
 15	"github.com/go-git/go-billy/v5/osfs"
 16
 17	"github.com/MichaelMure/git-bug/util/lamport"
 18)
 19
 20var _ ClockedRepo = &GitRepo{}
 21var _ TestedRepo = &GitRepo{}
 22
 23// GitRepo represents an instance of a (local) git repository.
 24type GitRepo struct {
 25	gitCli
 26	path string
 27
 28	clocksMutex sync.Mutex
 29	clocks      map[string]lamport.Clock
 30
 31	indexesMutex sync.Mutex
 32	indexes      map[string]bleve.Index
 33
 34	keyring      Keyring
 35	localStorage billy.Filesystem
 36}
 37
 38func (repo *GitRepo) ReadCommit(hash Hash) (Commit, error) {
 39	panic("implement me")
 40}
 41
 42func (repo *GitRepo) ResolveRef(ref string) (Hash, error) {
 43	panic("implement me")
 44}
 45
 46// OpenGitRepo determines if the given working directory is inside of a git repository,
 47// and returns the corresponding GitRepo instance if it is.
 48func OpenGitRepo(path string, clockLoaders []ClockLoader) (*GitRepo, error) {
 49	k, err := defaultKeyring()
 50	if err != nil {
 51		return nil, err
 52	}
 53
 54	repo := &GitRepo{
 55		gitCli:  gitCli{path: path},
 56		path:    path,
 57		clocks:  make(map[string]lamport.Clock),
 58		indexes: make(map[string]bleve.Index),
 59		keyring: k,
 60	}
 61
 62	// Check the repo and retrieve the root path
 63	stdout, err := repo.runGitCommand("rev-parse", "--absolute-git-dir")
 64
 65	// Now dir is fetched with "git rev-parse --git-dir". May be it can
 66	// still return nothing in some cases. Then empty stdout check is
 67	// kept.
 68	if err != nil || stdout == "" {
 69		return nil, ErrNotARepo
 70	}
 71
 72	// Fix the path to be sure we are at the root
 73	repo.path = stdout
 74	repo.gitCli.path = stdout
 75	repo.localStorage = osfs.New(filepath.Join(path, "git-bug"))
 76
 77	for _, loader := range clockLoaders {
 78		allExist := true
 79		for _, name := range loader.Clocks {
 80			if _, err := repo.getClock(name); err != nil {
 81				allExist = false
 82			}
 83		}
 84
 85		if !allExist {
 86			err = loader.Witnesser(repo)
 87			if err != nil {
 88				return nil, err
 89			}
 90		}
 91	}
 92
 93	return repo, nil
 94}
 95
 96// InitGitRepo create a new empty git repo at the given path
 97func InitGitRepo(path string) (*GitRepo, error) {
 98	k, err := defaultKeyring()
 99	if err != nil {
100		return nil, err
101	}
102
103	repo := &GitRepo{
104		gitCli:       gitCli{path: path},
105		path:         filepath.Join(path, ".git"),
106		clocks:       make(map[string]lamport.Clock),
107		indexes:      make(map[string]bleve.Index),
108		keyring:      k,
109		localStorage: osfs.New(filepath.Join(path, ".git", "git-bug")),
110	}
111
112	_, err = repo.runGitCommand("init", path)
113	if err != nil {
114		return nil, err
115	}
116
117	return repo, nil
118}
119
120// InitBareGitRepo create a new --bare empty git repo at the given path
121func InitBareGitRepo(path string) (*GitRepo, error) {
122	k, err := defaultKeyring()
123	if err != nil {
124		return nil, err
125	}
126
127	repo := &GitRepo{
128		gitCli:       gitCli{path: path},
129		path:         path,
130		clocks:       make(map[string]lamport.Clock),
131		indexes:      make(map[string]bleve.Index),
132		keyring:      k,
133		localStorage: osfs.New(filepath.Join(path, "git-bug")),
134	}
135
136	_, err = repo.runGitCommand("init", "--bare", path)
137	if err != nil {
138		return nil, err
139	}
140
141	return repo, nil
142}
143
144func (repo *GitRepo) Close() error {
145	var firstErr error
146	for _, index := range repo.indexes {
147		err := index.Close()
148		if err != nil && firstErr == nil {
149			firstErr = err
150		}
151	}
152	return firstErr
153}
154
155// LocalConfig give access to the repository scoped configuration
156func (repo *GitRepo) LocalConfig() Config {
157	return newGitConfig(repo.gitCli, false)
158}
159
160// GlobalConfig give access to the global scoped configuration
161func (repo *GitRepo) GlobalConfig() Config {
162	return newGitConfig(repo.gitCli, true)
163}
164
165// AnyConfig give access to a merged local/global configuration
166func (repo *GitRepo) AnyConfig() ConfigRead {
167	return mergeConfig(repo.LocalConfig(), repo.GlobalConfig())
168}
169
170// Keyring give access to a user-wide storage for secrets
171func (repo *GitRepo) Keyring() Keyring {
172	return repo.keyring
173}
174
175// GetPath returns the path to the repo.
176func (repo *GitRepo) GetPath() string {
177	return repo.path
178}
179
180// GetUserName returns the name the the user has used to configure git
181func (repo *GitRepo) GetUserName() (string, error) {
182	return repo.runGitCommand("config", "user.name")
183}
184
185// GetUserEmail returns the email address that the user has used to configure git.
186func (repo *GitRepo) GetUserEmail() (string, error) {
187	return repo.runGitCommand("config", "user.email")
188}
189
190// GetCoreEditor returns the name of the editor that the user has used to configure git.
191func (repo *GitRepo) GetCoreEditor() (string, error) {
192	return repo.runGitCommand("var", "GIT_EDITOR")
193}
194
195// GetRemotes returns the configured remotes repositories.
196func (repo *GitRepo) GetRemotes() (map[string]string, error) {
197	stdout, err := repo.runGitCommand("remote", "--verbose")
198	if err != nil {
199		return nil, err
200	}
201
202	lines := strings.Split(stdout, "\n")
203	remotes := make(map[string]string, len(lines))
204
205	for _, line := range lines {
206		if strings.TrimSpace(line) == "" {
207			continue
208		}
209		elements := strings.Fields(line)
210		if len(elements) != 3 {
211			return nil, fmt.Errorf("git remote: unexpected output format: %s", line)
212		}
213
214		remotes[elements[0]] = elements[1]
215	}
216
217	return remotes, nil
218}
219
220// LocalStorage return a billy.Filesystem giving access to $RepoPath/.git/git-bug
221func (repo *GitRepo) LocalStorage() billy.Filesystem {
222	return repo.localStorage
223}
224
225// GetBleveIndex return a bleve.Index that can be used to index documents
226func (repo *GitRepo) GetBleveIndex(name string) (bleve.Index, error) {
227	repo.indexesMutex.Lock()
228	defer repo.indexesMutex.Unlock()
229
230	if index, ok := repo.indexes[name]; ok {
231		return index, nil
232	}
233
234	path := filepath.Join(repo.path, "indexes", name)
235
236	index, err := bleve.Open(path)
237	if err == nil {
238		repo.indexes[name] = index
239		return index, nil
240	}
241
242	err = os.MkdirAll(path, os.ModeDir)
243	if err != nil {
244		return nil, err
245	}
246
247	mapping := bleve.NewIndexMapping()
248	mapping.DefaultAnalyzer = "en"
249
250	index, err = bleve.New(path, mapping)
251	if err != nil {
252		return nil, err
253	}
254
255	repo.indexes[name] = index
256
257	return index, nil
258}
259
260// ClearBleveIndex will wipe the given index
261func (repo *GitRepo) ClearBleveIndex(name string) error {
262	repo.indexesMutex.Lock()
263	defer repo.indexesMutex.Unlock()
264
265	path := filepath.Join(repo.path, "indexes", name)
266
267	err := os.RemoveAll(path)
268	if err != nil {
269		return err
270	}
271
272	delete(repo.indexes, name)
273
274	return nil
275}
276
277// FetchRefs fetch git refs from a remote
278func (repo *GitRepo) FetchRefs(remote, refSpec string) (string, error) {
279	stdout, err := repo.runGitCommand("fetch", remote, refSpec)
280
281	if err != nil {
282		return stdout, fmt.Errorf("failed to fetch from the remote '%s': %v", remote, err)
283	}
284
285	return stdout, err
286}
287
288// PushRefs push git refs to a remote
289func (repo *GitRepo) PushRefs(remote string, refSpec string) (string, error) {
290	stdout, stderr, err := repo.runGitCommandRaw(nil, "push", remote, refSpec)
291
292	if err != nil {
293		return stdout + stderr, fmt.Errorf("failed to push to the remote '%s': %v", remote, stderr)
294	}
295	return stdout + stderr, nil
296}
297
298// StoreData will store arbitrary data and return the corresponding hash
299func (repo *GitRepo) StoreData(data []byte) (Hash, error) {
300	var stdin = bytes.NewReader(data)
301
302	stdout, err := repo.runGitCommandWithStdin(stdin, "hash-object", "--stdin", "-w")
303
304	return Hash(stdout), err
305}
306
307// ReadData will attempt to read arbitrary data from the given hash
308func (repo *GitRepo) ReadData(hash Hash) ([]byte, error) {
309	var stdout bytes.Buffer
310	var stderr bytes.Buffer
311
312	err := repo.runGitCommandWithIO(nil, &stdout, &stderr, "cat-file", "-p", string(hash))
313
314	if err != nil {
315		return []byte{}, err
316	}
317
318	return stdout.Bytes(), nil
319}
320
321// StoreTree will store a mapping key-->Hash as a Git tree
322func (repo *GitRepo) StoreTree(entries []TreeEntry) (Hash, error) {
323	buffer := prepareTreeEntries(entries)
324
325	stdout, err := repo.runGitCommandWithStdin(&buffer, "mktree")
326
327	if err != nil {
328		return "", err
329	}
330
331	return Hash(stdout), nil
332}
333
334// StoreCommit will store a Git commit with the given Git tree
335func (repo *GitRepo) StoreCommit(treeHash Hash) (Hash, error) {
336	stdout, err := repo.runGitCommand("commit-tree", string(treeHash))
337
338	if err != nil {
339		return "", err
340	}
341
342	return Hash(stdout), nil
343}
344
345// StoreCommitWithParent will store a Git commit with the given Git tree
346func (repo *GitRepo) StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, error) {
347	stdout, err := repo.runGitCommand("commit-tree", string(treeHash),
348		"-p", string(parent))
349
350	if err != nil {
351		return "", err
352	}
353
354	return Hash(stdout), nil
355}
356
357// UpdateRef will create or update a Git reference
358func (repo *GitRepo) UpdateRef(ref string, hash Hash) error {
359	_, err := repo.runGitCommand("update-ref", ref, string(hash))
360
361	return err
362}
363
364// RemoveRef will remove a Git reference
365func (repo *GitRepo) RemoveRef(ref string) error {
366	_, err := repo.runGitCommand("update-ref", "-d", ref)
367
368	return err
369}
370
371// ListRefs will return a list of Git ref matching the given refspec
372func (repo *GitRepo) ListRefs(refPrefix string) ([]string, error) {
373	stdout, err := repo.runGitCommand("for-each-ref", "--format=%(refname)", refPrefix)
374
375	if err != nil {
376		return nil, err
377	}
378
379	split := strings.Split(stdout, "\n")
380
381	if len(split) == 1 && split[0] == "" {
382		return []string{}, nil
383	}
384
385	return split, nil
386}
387
388// RefExist will check if a reference exist in Git
389func (repo *GitRepo) RefExist(ref string) (bool, error) {
390	stdout, err := repo.runGitCommand("for-each-ref", ref)
391
392	if err != nil {
393		return false, err
394	}
395
396	return stdout != "", nil
397}
398
399// CopyRef will create a new reference with the same value as another one
400func (repo *GitRepo) CopyRef(source string, dest string) error {
401	_, err := repo.runGitCommand("update-ref", dest, source)
402
403	return err
404}
405
406// ListCommits will return the list of commit hashes of a ref, in chronological order
407func (repo *GitRepo) ListCommits(ref string) ([]Hash, error) {
408	stdout, err := repo.runGitCommand("rev-list", "--first-parent", "--reverse", ref)
409
410	if err != nil {
411		return nil, err
412	}
413
414	split := strings.Split(stdout, "\n")
415
416	casted := make([]Hash, len(split))
417	for i, line := range split {
418		casted[i] = Hash(line)
419	}
420
421	return casted, nil
422
423}
424
425// ReadTree will return the list of entries in a Git tree
426func (repo *GitRepo) ReadTree(hash Hash) ([]TreeEntry, error) {
427	stdout, err := repo.runGitCommand("ls-tree", string(hash))
428
429	if err != nil {
430		return nil, err
431	}
432
433	return readTreeEntries(stdout)
434}
435
436// FindCommonAncestor will return the last common ancestor of two chain of commit
437func (repo *GitRepo) FindCommonAncestor(hash1 Hash, hash2 Hash) (Hash, error) {
438	stdout, err := repo.runGitCommand("merge-base", string(hash1), string(hash2))
439
440	if err != nil {
441		return "", err
442	}
443
444	return Hash(stdout), nil
445}
446
447// GetTreeHash return the git tree hash referenced in a commit
448func (repo *GitRepo) GetTreeHash(commit Hash) (Hash, error) {
449	stdout, err := repo.runGitCommand("rev-parse", string(commit)+"^{tree}")
450
451	if err != nil {
452		return "", err
453	}
454
455	return Hash(stdout), nil
456}
457
458func (repo *GitRepo) AllClocks() (map[string]lamport.Clock, error) {
459	repo.clocksMutex.Lock()
460	defer repo.clocksMutex.Unlock()
461
462	result := make(map[string]lamport.Clock)
463
464	files, err := ioutil.ReadDir(filepath.Join(repo.path, "git-bug", clockPath))
465	if os.IsNotExist(err) {
466		return nil, nil
467	}
468	if err != nil {
469		return nil, err
470	}
471
472	for _, file := range files {
473		name := file.Name()
474		if c, ok := repo.clocks[name]; ok {
475			result[name] = c
476		} else {
477			c, err := lamport.LoadPersistedClock(repo.LocalStorage(), filepath.Join(clockPath, name))
478			if err != nil {
479				return nil, err
480			}
481			repo.clocks[name] = c
482			result[name] = c
483		}
484	}
485
486	return result, nil
487}
488
489// GetOrCreateClock return a Lamport clock stored in the Repo.
490// If the clock doesn't exist, it's created.
491func (repo *GitRepo) GetOrCreateClock(name string) (lamport.Clock, error) {
492	repo.clocksMutex.Lock()
493	defer repo.clocksMutex.Unlock()
494
495	c, err := repo.getClock(name)
496	if err == nil {
497		return c, nil
498	}
499	if err != ErrClockNotExist {
500		return nil, err
501	}
502
503	c, err = lamport.NewPersistedClock(repo.LocalStorage(), filepath.Join(clockPath, name))
504	if err != nil {
505		return nil, err
506	}
507
508	repo.clocks[name] = c
509	return c, nil
510}
511
512func (repo *GitRepo) getClock(name string) (lamport.Clock, error) {
513	if c, ok := repo.clocks[name]; ok {
514		return c, nil
515	}
516
517	c, err := lamport.LoadPersistedClock(repo.LocalStorage(), filepath.Join(clockPath, name))
518	if err == nil {
519		repo.clocks[name] = c
520		return c, nil
521	}
522	if err == lamport.ErrClockNotExist {
523		return nil, ErrClockNotExist
524	}
525	return nil, err
526}
527
528// Increment is equivalent to c = GetOrCreateClock(name) + c.Increment()
529func (repo *GitRepo) Increment(name string) (lamport.Time, error) {
530	c, err := repo.GetOrCreateClock(name)
531	if err != nil {
532		return lamport.Time(0), err
533	}
534	return c.Increment()
535}
536
537// Witness is equivalent to c = GetOrCreateClock(name) + c.Witness(time)
538func (repo *GitRepo) Witness(name string, time lamport.Time) error {
539	c, err := repo.GetOrCreateClock(name)
540	if err != nil {
541		return err
542	}
543	return c.Witness(time)
544}
545
546// AddRemote add a new remote to the repository
547// Not in the interface because it's only used for testing
548func (repo *GitRepo) AddRemote(name string, url string) error {
549	_, err := repo.runGitCommand("remote", "add", name, url)
550
551	return err
552}
553
554// GetLocalRemote return the URL to use to add this repo as a local remote
555func (repo *GitRepo) GetLocalRemote() string {
556	return repo.path
557}
558
559// EraseFromDisk delete this repository entirely from the disk
560func (repo *GitRepo) EraseFromDisk() error {
561	err := repo.Close()
562	if err != nil {
563		return err
564	}
565
566	path := filepath.Clean(strings.TrimSuffix(repo.path, string(filepath.Separator)+".git"))
567
568	// fmt.Println("Cleaning repo:", path)
569	return os.RemoveAll(path)
570}