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