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