git.go

  1// Package repository contains helper methods for working with the Git repo.
  2package repository
  3
  4import (
  5	"bytes"
  6	"errors"
  7	"fmt"
  8	"io"
  9	"os"
 10	"os/exec"
 11	"path"
 12	"strings"
 13
 14	"github.com/MichaelMure/git-bug/util/git"
 15	"github.com/MichaelMure/git-bug/util/lamport"
 16)
 17
 18const createClockFile = "/.git/git-bug/create-clock"
 19const editClockFile = "/.git/git-bug/edit-clock"
 20
 21// ErrNotARepo is the error returned when the git repo root wan't be found
 22var ErrNotARepo = errors.New("not a git repository")
 23
 24// GitRepo represents an instance of a (local) git repository.
 25type GitRepo struct {
 26	Path        string
 27	createClock *lamport.Persisted
 28	editClock   *lamport.Persisted
 29}
 30
 31// Run the given git command with the given I/O reader/writers, returning an error if it fails.
 32func (repo *GitRepo) runGitCommandWithIO(stdin io.Reader, stdout, stderr io.Writer, args ...string) error {
 33	//fmt.Println("Running git", strings.Join(args, " "))
 34
 35	cmd := exec.Command("git", args...)
 36	cmd.Dir = repo.Path
 37	cmd.Stdin = stdin
 38	cmd.Stdout = stdout
 39	cmd.Stderr = stderr
 40
 41	return cmd.Run()
 42}
 43
 44// Run the given git command and return its stdout, or an error if the command fails.
 45func (repo *GitRepo) runGitCommandRaw(stdin io.Reader, args ...string) (string, string, error) {
 46	var stdout bytes.Buffer
 47	var stderr bytes.Buffer
 48	err := repo.runGitCommandWithIO(stdin, &stdout, &stderr, args...)
 49	return strings.TrimSpace(stdout.String()), strings.TrimSpace(stderr.String()), err
 50}
 51
 52// Run the given git command and return its stdout, or an error if the command fails.
 53func (repo *GitRepo) runGitCommandWithStdin(stdin io.Reader, args ...string) (string, error) {
 54	stdout, stderr, err := repo.runGitCommandRaw(stdin, args...)
 55	if err != nil {
 56		if stderr == "" {
 57			stderr = "Error running git command: " + strings.Join(args, " ")
 58		}
 59		err = fmt.Errorf(stderr)
 60	}
 61	return stdout, err
 62}
 63
 64// Run the given git command and return its stdout, or an error if the command fails.
 65func (repo *GitRepo) runGitCommand(args ...string) (string, error) {
 66	return repo.runGitCommandWithStdin(nil, args...)
 67}
 68
 69// Run the given git command using the same stdin, stdout, and stderr as the review tool.
 70func (repo *GitRepo) runGitCommandInline(args ...string) error {
 71	return repo.runGitCommandWithIO(os.Stdin, os.Stdout, os.Stderr, args...)
 72}
 73
 74// NewGitRepo determines if the given working directory is inside of a git repository,
 75// and returns the corresponding GitRepo instance if it is.
 76func NewGitRepo(path string, witnesser func(repo *GitRepo) error) (*GitRepo, error) {
 77	repo := &GitRepo{Path: path}
 78
 79	// Check the repo and retrieve the root path
 80	stdout, err := repo.runGitCommand("rev-parse", "--show-toplevel")
 81
 82	// for some reason, "git rev-parse --show-toplevel" return nothing
 83	// and no error when inside a ".git" dir
 84	if err != nil || stdout == "" {
 85		return nil, ErrNotARepo
 86	}
 87
 88	// Fix the path to be sure we are at the root
 89	repo.Path = stdout
 90
 91	err = repo.LoadClocks()
 92
 93	if err != nil {
 94		// No clock yet, trying to initialize them
 95		err = repo.createClocks()
 96		if err != nil {
 97			return nil, err
 98		}
 99
100		err = witnesser(repo)
101		if err != nil {
102			return nil, err
103		}
104
105		err = repo.WriteClocks()
106		if err != nil {
107			return nil, err
108		}
109
110		return repo, nil
111	}
112
113	return repo, nil
114}
115
116// InitGitRepo create a new empty git repo at the given path
117func InitGitRepo(path string) (*GitRepo, error) {
118	repo := &GitRepo{Path: path}
119	err := repo.createClocks()
120	if err != nil {
121		return nil, err
122	}
123
124	_, err = repo.runGitCommand("init", path)
125	if err != nil {
126		return nil, err
127	}
128
129	return repo, nil
130}
131
132// InitBareGitRepo create a new --bare empty git repo at the given path
133func InitBareGitRepo(path string) (*GitRepo, error) {
134	repo := &GitRepo{Path: path}
135	err := repo.createClocks()
136	if err != nil {
137		return nil, err
138	}
139
140	_, err = repo.runGitCommand("init", "--bare", path)
141	if err != nil {
142		return nil, err
143	}
144
145	return repo, nil
146}
147
148// GetPath returns the path to the repo.
149func (repo *GitRepo) GetPath() string {
150	return repo.Path
151}
152
153// GetUserName returns the name the the user has used to configure git
154func (repo *GitRepo) GetUserName() (string, error) {
155	return repo.runGitCommand("config", "user.name")
156}
157
158// GetUserEmail returns the email address that the user has used to configure git.
159func (repo *GitRepo) GetUserEmail() (string, error) {
160	return repo.runGitCommand("config", "user.email")
161}
162
163// GetCoreEditor returns the name of the editor that the user has used to configure git.
164func (repo *GitRepo) GetCoreEditor() (string, error) {
165	return repo.runGitCommand("var", "GIT_EDITOR")
166}
167
168// FetchRefs fetch git refs from a remote
169func (repo *GitRepo) FetchRefs(remote, refSpec string) (string, error) {
170	stdout, err := repo.runGitCommand("fetch", remote, refSpec)
171
172	if err != nil {
173		return stdout, fmt.Errorf("failed to fetch from the remote '%s': %v", remote, err)
174	}
175
176	return stdout, err
177}
178
179// PushRefs push git refs to a remote
180func (repo *GitRepo) PushRefs(remote string, refSpec string) (string, error) {
181	stdout, stderr, err := repo.runGitCommandRaw(nil, "push", remote, refSpec)
182
183	if err != nil {
184		return stdout + stderr, fmt.Errorf("failed to push to the remote '%s': %v", remote, stderr)
185	}
186	return stdout + stderr, nil
187}
188
189// StoreData will store arbitrary data and return the corresponding hash
190func (repo *GitRepo) StoreData(data []byte) (git.Hash, error) {
191	var stdin = bytes.NewReader(data)
192
193	stdout, err := repo.runGitCommandWithStdin(stdin, "hash-object", "--stdin", "-w")
194
195	return git.Hash(stdout), err
196}
197
198// ReadData will attempt to read arbitrary data from the given hash
199func (repo *GitRepo) ReadData(hash git.Hash) ([]byte, error) {
200	var stdout bytes.Buffer
201	var stderr bytes.Buffer
202
203	err := repo.runGitCommandWithIO(nil, &stdout, &stderr, "cat-file", "-p", string(hash))
204
205	if err != nil {
206		return []byte{}, err
207	}
208
209	return stdout.Bytes(), nil
210}
211
212// StoreTree will store a mapping key-->Hash as a Git tree
213func (repo *GitRepo) StoreTree(entries []TreeEntry) (git.Hash, error) {
214	buffer := prepareTreeEntries(entries)
215
216	stdout, err := repo.runGitCommandWithStdin(&buffer, "mktree")
217
218	if err != nil {
219		return "", err
220	}
221
222	return git.Hash(stdout), nil
223}
224
225// StoreCommit will store a Git commit with the given Git tree
226func (repo *GitRepo) StoreCommit(treeHash git.Hash) (git.Hash, error) {
227	stdout, err := repo.runGitCommand("commit-tree", string(treeHash))
228
229	if err != nil {
230		return "", err
231	}
232
233	return git.Hash(stdout), nil
234}
235
236// StoreCommitWithParent will store a Git commit with the given Git tree
237func (repo *GitRepo) StoreCommitWithParent(treeHash git.Hash, parent git.Hash) (git.Hash, error) {
238	stdout, err := repo.runGitCommand("commit-tree", string(treeHash),
239		"-p", string(parent))
240
241	if err != nil {
242		return "", err
243	}
244
245	return git.Hash(stdout), nil
246}
247
248// UpdateRef will create or update a Git reference
249func (repo *GitRepo) UpdateRef(ref string, hash git.Hash) error {
250	_, err := repo.runGitCommand("update-ref", ref, string(hash))
251
252	return err
253}
254
255// ListRefs will return a list of Git ref matching the given refspec
256func (repo *GitRepo) ListRefs(refspec string) ([]string, error) {
257	stdout, err := repo.runGitCommand("for-each-ref", "--format=%(refname)", refspec)
258
259	if err != nil {
260		return nil, err
261	}
262
263	split := strings.Split(stdout, "\n")
264
265	if len(split) == 1 && split[0] == "" {
266		return []string{}, nil
267	}
268
269	return split, nil
270}
271
272// RefExist will check if a reference exist in Git
273func (repo *GitRepo) RefExist(ref string) (bool, error) {
274	stdout, err := repo.runGitCommand("for-each-ref", ref)
275
276	if err != nil {
277		return false, err
278	}
279
280	return stdout != "", nil
281}
282
283// CopyRef will create a new reference with the same value as another one
284func (repo *GitRepo) CopyRef(source string, dest string) error {
285	_, err := repo.runGitCommand("update-ref", dest, source)
286
287	return err
288}
289
290// ListCommits will return the list of commit hashes of a ref, in chronological order
291func (repo *GitRepo) ListCommits(ref string) ([]git.Hash, error) {
292	stdout, err := repo.runGitCommand("rev-list", "--first-parent", "--reverse", ref)
293
294	if err != nil {
295		return nil, err
296	}
297
298	split := strings.Split(stdout, "\n")
299
300	casted := make([]git.Hash, len(split))
301	for i, line := range split {
302		casted[i] = git.Hash(line)
303	}
304
305	return casted, nil
306
307}
308
309// ListEntries will return the list of entries in a Git tree
310func (repo *GitRepo) ListEntries(hash git.Hash) ([]TreeEntry, error) {
311	stdout, err := repo.runGitCommand("ls-tree", string(hash))
312
313	if err != nil {
314		return nil, err
315	}
316
317	return readTreeEntries(stdout)
318}
319
320// FindCommonAncestor will return the last common ancestor of two chain of commit
321func (repo *GitRepo) FindCommonAncestor(hash1 git.Hash, hash2 git.Hash) (git.Hash, error) {
322	stdout, err := repo.runGitCommand("merge-base", string(hash1), string(hash2))
323
324	if err != nil {
325		return "", nil
326	}
327
328	return git.Hash(stdout), nil
329}
330
331// GetTreeHash return the git tree hash referenced in a commit
332func (repo *GitRepo) GetTreeHash(commit git.Hash) (git.Hash, error) {
333	stdout, err := repo.runGitCommand("rev-parse", string(commit)+"^{tree}")
334
335	if err != nil {
336		return "", nil
337	}
338
339	return git.Hash(stdout), nil
340}
341
342// AddRemote add a new remote to the repository
343// Not in the interface because it's only used for testing
344func (repo *GitRepo) AddRemote(name string, url string) error {
345	_, err := repo.runGitCommand("remote", "add", name, url)
346
347	return err
348}
349
350func (repo *GitRepo) createClocks() error {
351	createPath := path.Join(repo.Path, createClockFile)
352	createClock, err := lamport.NewPersisted(createPath)
353	if err != nil {
354		return err
355	}
356
357	editPath := path.Join(repo.Path, editClockFile)
358	editClock, err := lamport.NewPersisted(editPath)
359	if err != nil {
360		return err
361	}
362
363	repo.createClock = createClock
364	repo.editClock = editClock
365
366	return nil
367}
368
369// LoadClocks read the clocks values from the on-disk repo
370func (repo *GitRepo) LoadClocks() error {
371	createClock, err := lamport.LoadPersisted(repo.GetPath() + createClockFile)
372	if err != nil {
373		return err
374	}
375
376	editClock, err := lamport.LoadPersisted(repo.GetPath() + editClockFile)
377	if err != nil {
378		return err
379	}
380
381	repo.createClock = createClock
382	repo.editClock = editClock
383	return nil
384}
385
386// WriteClocks write the clocks values into the repo
387func (repo *GitRepo) WriteClocks() error {
388	err := repo.createClock.Write()
389	if err != nil {
390		return err
391	}
392
393	err = repo.editClock.Write()
394	if err != nil {
395		return err
396	}
397
398	return nil
399}
400
401// CreateTimeIncrement increment the creation clock and return the new value.
402func (repo *GitRepo) CreateTimeIncrement() (lamport.Time, error) {
403	return repo.createClock.Increment()
404}
405
406// EditTimeIncrement increment the edit clock and return the new value.
407func (repo *GitRepo) EditTimeIncrement() (lamport.Time, error) {
408	return repo.editClock.Increment()
409}
410
411// CreateWitness witness another create time and increment the corresponding clock
412// if needed.
413func (repo *GitRepo) CreateWitness(time lamport.Time) error {
414	return repo.createClock.Witness(time)
415}
416
417// EditWitness witness another edition time and increment the corresponding clock
418// if needed.
419func (repo *GitRepo) EditWitness(time lamport.Time) error {
420	return repo.editClock.Witness(time)
421}