git.go

  1// Package repository contains helper methods for working with the Git repo.
  2package repository
  3
  4import (
  5	"bytes"
  6	"fmt"
  7	"io"
  8	"os/exec"
  9	"path"
 10	"strconv"
 11	"strings"
 12
 13	"github.com/blang/semver"
 14	"github.com/pkg/errors"
 15
 16	"github.com/MichaelMure/git-bug/util/git"
 17	"github.com/MichaelMure/git-bug/util/lamport"
 18)
 19
 20const createClockFile = "/.git/git-bug/create-clock"
 21const editClockFile = "/.git/git-bug/edit-clock"
 22
 23// ErrNotARepo is the error returned when the git repo root wan't be found
 24var ErrNotARepo = errors.New("not a git repository")
 25
 26var _ ClockedRepo = &GitRepo{}
 27
 28// GitRepo represents an instance of a (local) git repository.
 29type GitRepo struct {
 30	Path        string
 31	createClock *lamport.Persisted
 32	editClock   *lamport.Persisted
 33}
 34
 35// Run the given git command with the given I/O reader/writers, returning an error if it fails.
 36func (repo *GitRepo) runGitCommandWithIO(stdin io.Reader, stdout, stderr io.Writer, args ...string) error {
 37	// fmt.Printf("[%s] Running git %s\n", repo.Path, strings.Join(args, " "))
 38
 39	cmd := exec.Command("git", args...)
 40	cmd.Dir = repo.Path
 41	cmd.Stdin = stdin
 42	cmd.Stdout = stdout
 43	cmd.Stderr = stderr
 44
 45	return cmd.Run()
 46}
 47
 48// Run the given git command and return its stdout, or an error if the command fails.
 49func (repo *GitRepo) runGitCommandRaw(stdin io.Reader, args ...string) (string, string, error) {
 50	var stdout bytes.Buffer
 51	var stderr bytes.Buffer
 52	err := repo.runGitCommandWithIO(stdin, &stdout, &stderr, args...)
 53	return strings.TrimSpace(stdout.String()), strings.TrimSpace(stderr.String()), err
 54}
 55
 56// Run the given git command and return its stdout, or an error if the command fails.
 57func (repo *GitRepo) runGitCommandWithStdin(stdin io.Reader, args ...string) (string, error) {
 58	stdout, stderr, err := repo.runGitCommandRaw(stdin, args...)
 59	if err != nil {
 60		if stderr == "" {
 61			stderr = "Error running git command: " + strings.Join(args, " ")
 62		}
 63		err = fmt.Errorf(stderr)
 64	}
 65	return stdout, err
 66}
 67
 68// Run the given git command and return its stdout, or an error if the command fails.
 69func (repo *GitRepo) runGitCommand(args ...string) (string, error) {
 70	return repo.runGitCommandWithStdin(nil, args...)
 71}
 72
 73// NewGitRepo determines if the given working directory is inside of a git repository,
 74// and returns the corresponding GitRepo instance if it is.
 75func NewGitRepo(path string, witnesser Witnesser) (*GitRepo, error) {
 76	repo := &GitRepo{Path: path}
 77
 78	// Check the repo and retrieve the root path
 79	stdout, err := repo.runGitCommand("rev-parse", "--show-toplevel")
 80
 81	// for some reason, "git rev-parse --show-toplevel" return nothing
 82	// and no error when inside a ".git" dir
 83	if err != nil || stdout == "" {
 84		return nil, ErrNotARepo
 85	}
 86
 87	// Fix the path to be sure we are at the root
 88	repo.Path = stdout
 89
 90	err = repo.LoadClocks()
 91
 92	if err != nil {
 93		// No clock yet, trying to initialize them
 94		err = repo.createClocks()
 95		if err != nil {
 96			return nil, err
 97		}
 98
 99		err = witnesser(repo)
100		if err != nil {
101			return nil, err
102		}
103
104		err = repo.WriteClocks()
105		if err != nil {
106			return nil, err
107		}
108
109		return repo, nil
110	}
111
112	return repo, nil
113}
114
115// InitGitRepo create a new empty git repo at the given path
116func InitGitRepo(path string) (*GitRepo, error) {
117	repo := &GitRepo{Path: path}
118	err := repo.createClocks()
119	if err != nil {
120		return nil, err
121	}
122
123	_, err = repo.runGitCommand("init", path)
124	if err != nil {
125		return nil, err
126	}
127
128	return repo, nil
129}
130
131// InitBareGitRepo create a new --bare empty git repo at the given path
132func InitBareGitRepo(path string) (*GitRepo, error) {
133	repo := &GitRepo{Path: path}
134	err := repo.createClocks()
135	if err != nil {
136		return nil, err
137	}
138
139	_, err = repo.runGitCommand("init", "--bare", path)
140	if err != nil {
141		return nil, err
142	}
143
144	return repo, nil
145}
146
147// GetPath returns the path to the repo.
148func (repo *GitRepo) GetPath() string {
149	return repo.Path
150}
151
152// GetUserName returns the name the the user has used to configure git
153func (repo *GitRepo) GetUserName() (string, error) {
154	return repo.runGitCommand("config", "user.name")
155}
156
157// GetUserEmail returns the email address that the user has used to configure git.
158func (repo *GitRepo) GetUserEmail() (string, error) {
159	return repo.runGitCommand("config", "user.email")
160}
161
162// GetCoreEditor returns the name of the editor that the user has used to configure git.
163func (repo *GitRepo) GetCoreEditor() (string, error) {
164	return repo.runGitCommand("var", "GIT_EDITOR")
165}
166
167// GetRemotes returns the configured remotes repositories.
168func (repo *GitRepo) GetRemotes() (map[string]string, error) {
169	stdout, err := repo.runGitCommand("remote", "--verbose")
170	if err != nil {
171		return nil, err
172	}
173
174	lines := strings.Split(stdout, "\n")
175	remotes := make(map[string]string, len(lines))
176
177	for _, line := range lines {
178		elements := strings.Fields(line)
179		if len(elements) != 3 {
180			return nil, fmt.Errorf("unexpected output format: %s", line)
181		}
182
183		remotes[elements[0]] = elements[1]
184	}
185
186	return remotes, nil
187}
188
189// StoreConfig store a single key/value pair in the config of the repo
190func (repo *GitRepo) StoreConfig(key string, value string) error {
191	_, err := repo.runGitCommand("config", "--replace-all", key, value)
192
193	return err
194}
195
196// ReadConfigs read all key/value pair matching the key prefix
197func (repo *GitRepo) ReadConfigs(keyPrefix string) (map[string]string, error) {
198	stdout, err := repo.runGitCommand("config", "--get-regexp", keyPrefix)
199
200	//   / \
201	//  / ! \
202	// -------
203	//
204	// There can be a legitimate error here, but I see no portable way to
205	// distinguish them from the git error that say "no matching value exist"
206	if err != nil {
207		return nil, nil
208	}
209
210	lines := strings.Split(stdout, "\n")
211
212	result := make(map[string]string, len(lines))
213
214	for _, line := range lines {
215		if strings.TrimSpace(line) == "" {
216			continue
217		}
218
219		parts := strings.Fields(line)
220		if len(parts) != 2 {
221			return nil, fmt.Errorf("bad git config: %s", line)
222		}
223
224		result[parts[0]] = parts[1]
225	}
226
227	return result, nil
228}
229
230func (repo *GitRepo) ReadConfigBool(key string) (bool, error) {
231	val, err := repo.ReadConfigString(key)
232	if err != nil {
233		return false, err
234	}
235
236	return strconv.ParseBool(val)
237}
238
239func (repo *GitRepo) ReadConfigString(key string) (string, error) {
240	stdout, err := repo.runGitCommand("config", "--get-all", key)
241
242	//   / \
243	//  / ! \
244	// -------
245	//
246	// There can be a legitimate error here, but I see no portable way to
247	// distinguish them from the git error that say "no matching value exist"
248	if err != nil {
249		return "", ErrNoConfigEntry
250	}
251
252	lines := strings.Split(stdout, "\n")
253
254	if len(lines) == 0 {
255		return "", ErrNoConfigEntry
256	}
257	if len(lines) > 1 {
258		return "", ErrMultipleConfigEntry
259	}
260
261	return lines[0], nil
262}
263
264func (repo *GitRepo) rmSection(keyPrefix string) error {
265	_, err := repo.runGitCommand("config", "--remove-section", keyPrefix)
266	return err
267}
268
269func (repo *GitRepo) unsetAll(keyPrefix string) error {
270	_, err := repo.runGitCommand("config", "--unset-all", keyPrefix)
271	return err
272}
273
274// return keyPrefix section
275// example: sectionFromKey(a.b.c.d) return a.b.c
276func sectionFromKey(keyPrefix string) string {
277	s := strings.Split(keyPrefix, ".")
278	if len(s) == 1 {
279		return keyPrefix
280	}
281
282	return strings.Join(s[:len(s)-1], ".")
283}
284
285// rmConfigs with git version lesser than 2.18
286func (repo *GitRepo) rmConfigsGitVersionLT218(keyPrefix string) error {
287	// try to remove key/value pair by key
288	err := repo.unsetAll(keyPrefix)
289	if err != nil {
290		return repo.rmSection(keyPrefix)
291	}
292
293	m, err := repo.ReadConfigs(sectionFromKey(keyPrefix))
294	if err != nil {
295		return err
296	}
297
298	// if section doesn't have any left key/value remove the section
299	if len(m) == 0 {
300		return repo.rmSection(sectionFromKey(keyPrefix))
301	}
302
303	return nil
304}
305
306// RmConfigs remove all key/value pair matching the key prefix
307func (repo *GitRepo) RmConfigs(keyPrefix string) error {
308	// starting from git 2.18.0 sections are automatically deleted when the last existing
309	// key/value is removed. Before 2.18.0 we should remove the section
310	// see https://github.com/git/git/blob/master/Documentation/RelNotes/2.18.0.txt#L379
311	lt218, err := repo.gitVersionLT218()
312	if err != nil {
313		return errors.Wrap(err, "getting git version")
314	}
315
316	if lt218 {
317		return repo.rmConfigsGitVersionLT218(keyPrefix)
318	}
319
320	err = repo.unsetAll(keyPrefix)
321	if err != nil {
322		return repo.rmSection(keyPrefix)
323	}
324
325	return nil
326}
327
328func (repo *GitRepo) gitVersionLT218() (bool, error) {
329	versionOut, err := repo.runGitCommand("version")
330	if err != nil {
331		return false, err
332	}
333
334	versionString := strings.Fields(versionOut)[2]
335	version, err := semver.Make(versionString)
336	if err != nil {
337		return false, err
338	}
339
340	version218string := "2.18.0"
341	gitVersion218, err := semver.Make(version218string)
342	if err != nil {
343		return false, err
344	}
345
346	return version.LT(gitVersion218), nil
347}
348
349// FetchRefs fetch git refs from a remote
350func (repo *GitRepo) FetchRefs(remote, refSpec string) (string, error) {
351	stdout, err := repo.runGitCommand("fetch", remote, refSpec)
352
353	if err != nil {
354		return stdout, fmt.Errorf("failed to fetch from the remote '%s': %v", remote, err)
355	}
356
357	return stdout, err
358}
359
360// PushRefs push git refs to a remote
361func (repo *GitRepo) PushRefs(remote string, refSpec string) (string, error) {
362	stdout, stderr, err := repo.runGitCommandRaw(nil, "push", remote, refSpec)
363
364	if err != nil {
365		return stdout + stderr, fmt.Errorf("failed to push to the remote '%s': %v", remote, stderr)
366	}
367	return stdout + stderr, nil
368}
369
370// StoreData will store arbitrary data and return the corresponding hash
371func (repo *GitRepo) StoreData(data []byte) (git.Hash, error) {
372	var stdin = bytes.NewReader(data)
373
374	stdout, err := repo.runGitCommandWithStdin(stdin, "hash-object", "--stdin", "-w")
375
376	return git.Hash(stdout), err
377}
378
379// ReadData will attempt to read arbitrary data from the given hash
380func (repo *GitRepo) ReadData(hash git.Hash) ([]byte, error) {
381	var stdout bytes.Buffer
382	var stderr bytes.Buffer
383
384	err := repo.runGitCommandWithIO(nil, &stdout, &stderr, "cat-file", "-p", string(hash))
385
386	if err != nil {
387		return []byte{}, err
388	}
389
390	return stdout.Bytes(), nil
391}
392
393// StoreTree will store a mapping key-->Hash as a Git tree
394func (repo *GitRepo) StoreTree(entries []TreeEntry) (git.Hash, error) {
395	buffer := prepareTreeEntries(entries)
396
397	stdout, err := repo.runGitCommandWithStdin(&buffer, "mktree")
398
399	if err != nil {
400		return "", err
401	}
402
403	return git.Hash(stdout), nil
404}
405
406// StoreCommit will store a Git commit with the given Git tree
407func (repo *GitRepo) StoreCommit(treeHash git.Hash) (git.Hash, error) {
408	stdout, err := repo.runGitCommand("commit-tree", string(treeHash))
409
410	if err != nil {
411		return "", err
412	}
413
414	return git.Hash(stdout), nil
415}
416
417// StoreCommitWithParent will store a Git commit with the given Git tree
418func (repo *GitRepo) StoreCommitWithParent(treeHash git.Hash, parent git.Hash) (git.Hash, error) {
419	stdout, err := repo.runGitCommand("commit-tree", string(treeHash),
420		"-p", string(parent))
421
422	if err != nil {
423		return "", err
424	}
425
426	return git.Hash(stdout), nil
427}
428
429// UpdateRef will create or update a Git reference
430func (repo *GitRepo) UpdateRef(ref string, hash git.Hash) error {
431	_, err := repo.runGitCommand("update-ref", ref, string(hash))
432
433	return err
434}
435
436// ListRefs will return a list of Git ref matching the given refspec
437func (repo *GitRepo) ListRefs(refspec string) ([]string, error) {
438	stdout, err := repo.runGitCommand("for-each-ref", "--format=%(refname)", refspec)
439
440	if err != nil {
441		return nil, err
442	}
443
444	split := strings.Split(stdout, "\n")
445
446	if len(split) == 1 && split[0] == "" {
447		return []string{}, nil
448	}
449
450	return split, nil
451}
452
453// RefExist will check if a reference exist in Git
454func (repo *GitRepo) RefExist(ref string) (bool, error) {
455	stdout, err := repo.runGitCommand("for-each-ref", ref)
456
457	if err != nil {
458		return false, err
459	}
460
461	return stdout != "", nil
462}
463
464// CopyRef will create a new reference with the same value as another one
465func (repo *GitRepo) CopyRef(source string, dest string) error {
466	_, err := repo.runGitCommand("update-ref", dest, source)
467
468	return err
469}
470
471// ListCommits will return the list of commit hashes of a ref, in chronological order
472func (repo *GitRepo) ListCommits(ref string) ([]git.Hash, error) {
473	stdout, err := repo.runGitCommand("rev-list", "--first-parent", "--reverse", ref)
474
475	if err != nil {
476		return nil, err
477	}
478
479	split := strings.Split(stdout, "\n")
480
481	casted := make([]git.Hash, len(split))
482	for i, line := range split {
483		casted[i] = git.Hash(line)
484	}
485
486	return casted, nil
487
488}
489
490// ListEntries will return the list of entries in a Git tree
491func (repo *GitRepo) ListEntries(hash git.Hash) ([]TreeEntry, error) {
492	stdout, err := repo.runGitCommand("ls-tree", string(hash))
493
494	if err != nil {
495		return nil, err
496	}
497
498	return readTreeEntries(stdout)
499}
500
501// FindCommonAncestor will return the last common ancestor of two chain of commit
502func (repo *GitRepo) FindCommonAncestor(hash1 git.Hash, hash2 git.Hash) (git.Hash, error) {
503	stdout, err := repo.runGitCommand("merge-base", string(hash1), string(hash2))
504
505	if err != nil {
506		return "", err
507	}
508
509	return git.Hash(stdout), nil
510}
511
512// GetTreeHash return the git tree hash referenced in a commit
513func (repo *GitRepo) GetTreeHash(commit git.Hash) (git.Hash, error) {
514	stdout, err := repo.runGitCommand("rev-parse", string(commit)+"^{tree}")
515
516	if err != nil {
517		return "", err
518	}
519
520	return git.Hash(stdout), nil
521}
522
523// AddRemote add a new remote to the repository
524// Not in the interface because it's only used for testing
525func (repo *GitRepo) AddRemote(name string, url string) error {
526	_, err := repo.runGitCommand("remote", "add", name, url)
527
528	return err
529}
530
531func (repo *GitRepo) createClocks() error {
532	createPath := path.Join(repo.Path, createClockFile)
533	createClock, err := lamport.NewPersisted(createPath)
534	if err != nil {
535		return err
536	}
537
538	editPath := path.Join(repo.Path, editClockFile)
539	editClock, err := lamport.NewPersisted(editPath)
540	if err != nil {
541		return err
542	}
543
544	repo.createClock = createClock
545	repo.editClock = editClock
546
547	return nil
548}
549
550// LoadClocks read the clocks values from the on-disk repo
551func (repo *GitRepo) LoadClocks() error {
552	createClock, err := lamport.LoadPersisted(repo.GetPath() + createClockFile)
553	if err != nil {
554		return err
555	}
556
557	editClock, err := lamport.LoadPersisted(repo.GetPath() + editClockFile)
558	if err != nil {
559		return err
560	}
561
562	repo.createClock = createClock
563	repo.editClock = editClock
564	return nil
565}
566
567// WriteClocks write the clocks values into the repo
568func (repo *GitRepo) WriteClocks() error {
569	err := repo.createClock.Write()
570	if err != nil {
571		return err
572	}
573
574	err = repo.editClock.Write()
575	if err != nil {
576		return err
577	}
578
579	return nil
580}
581
582// CreateTime return the current value of the creation clock
583func (repo *GitRepo) CreateTime() lamport.Time {
584	return repo.createClock.Time()
585}
586
587// CreateTimeIncrement increment the creation clock and return the new value.
588func (repo *GitRepo) CreateTimeIncrement() (lamport.Time, error) {
589	return repo.createClock.Increment()
590}
591
592// EditTime return the current value of the edit clock
593func (repo *GitRepo) EditTime() lamport.Time {
594	return repo.editClock.Time()
595}
596
597// EditTimeIncrement increment the edit clock and return the new value.
598func (repo *GitRepo) EditTimeIncrement() (lamport.Time, error) {
599	return repo.editClock.Increment()
600}
601
602// CreateWitness witness another create time and increment the corresponding clock
603// if needed.
604func (repo *GitRepo) CreateWitness(time lamport.Time) error {
605	return repo.createClock.Witness(time)
606}
607
608// EditWitness witness another edition time and increment the corresponding clock
609// if needed.
610func (repo *GitRepo) EditWitness(time lamport.Time) error {
611	return repo.editClock.Witness(time)
612}