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