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