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	"strings"
 11	"sync"
 12
 13	"github.com/MichaelMure/git-bug/util/lamport"
 14)
 15
 16const (
 17	clockPath = "git-bug"
 18)
 19
 20var _ ClockedRepo = &GitRepo{}
 21var _ TestedRepo = &GitRepo{}
 22
 23// GitRepo represents an instance of a (local) git repository.
 24type GitRepo struct {
 25	path string
 26
 27	clocksMutex sync.Mutex
 28	clocks      map[string]lamport.Clock
 29
 30	keyring Keyring
 31}
 32
 33// LocalConfig give access to the repository scoped configuration
 34func (repo *GitRepo) LocalConfig() Config {
 35	return newGitConfig(repo, false)
 36}
 37
 38// GlobalConfig give access to the git global configuration
 39func (repo *GitRepo) GlobalConfig() Config {
 40	return newGitConfig(repo, true)
 41}
 42
 43func (repo *GitRepo) Keyring() Keyring {
 44	return repo.keyring
 45}
 46
 47// Run the given git command with the given I/O reader/writers, returning an error if it fails.
 48func (repo *GitRepo) runGitCommandWithIO(stdin io.Reader, stdout, stderr io.Writer, args ...string) error {
 49	// make sure that the working directory for the command
 50	// always exist, in particular when running "git init".
 51	path := strings.TrimSuffix(repo.path, ".git")
 52
 53	// fmt.Printf("[%s] Running git %s\n", path, strings.Join(args, " "))
 54
 55	cmd := exec.Command("git", args...)
 56	cmd.Dir = path
 57	cmd.Stdin = stdin
 58	cmd.Stdout = stdout
 59	cmd.Stderr = stderr
 60
 61	return cmd.Run()
 62}
 63
 64// Run the given git command and return its stdout, or an error if the command fails.
 65func (repo *GitRepo) runGitCommandRaw(stdin io.Reader, args ...string) (string, string, error) {
 66	var stdout bytes.Buffer
 67	var stderr bytes.Buffer
 68	err := repo.runGitCommandWithIO(stdin, &stdout, &stderr, args...)
 69	return strings.TrimSpace(stdout.String()), strings.TrimSpace(stderr.String()), err
 70}
 71
 72// Run the given git command and return its stdout, or an error if the command fails.
 73func (repo *GitRepo) runGitCommandWithStdin(stdin io.Reader, args ...string) (string, error) {
 74	stdout, stderr, err := repo.runGitCommandRaw(stdin, args...)
 75	if err != nil {
 76		if stderr == "" {
 77			stderr = "Error running git command: " + strings.Join(args, " ")
 78		}
 79		err = fmt.Errorf(stderr)
 80	}
 81	return stdout, err
 82}
 83
 84// Run the given git command and return its stdout, or an error if the command fails.
 85func (repo *GitRepo) runGitCommand(args ...string) (string, error) {
 86	return repo.runGitCommandWithStdin(nil, args...)
 87}
 88
 89// NewGitRepo determines if the given working directory is inside of a git repository,
 90// and returns the corresponding GitRepo instance if it is.
 91func NewGitRepo(path string, clockLoaders []ClockLoader) (*GitRepo, error) {
 92	k, err := defaultKeyring()
 93	if err != nil {
 94		return nil, err
 95	}
 96
 97	repo := &GitRepo{
 98		path:    path,
 99		clocks:  make(map[string]lamport.Clock),
100		keyring: k,
101	}
102
103	// Check the repo and retrieve the root path
104	stdout, err := repo.runGitCommand("rev-parse", "--git-dir")
105
106	// Now dir is fetched with "git rev-parse --git-dir". May be it can
107	// still return nothing in some cases. Then empty stdout check is
108	// kept.
109	if err != nil || stdout == "" {
110		return nil, ErrNotARepo
111	}
112
113	// Fix the path to be sure we are at the root
114	repo.path = stdout
115
116	for _, loader := range clockLoaders {
117		allExist := true
118		for _, name := range loader.Clocks {
119			if _, err := repo.getClock(name); err != nil {
120				allExist = false
121			}
122		}
123
124		if !allExist {
125			err = loader.Witnesser(repo)
126			if err != nil {
127				return nil, err
128			}
129		}
130	}
131
132	return repo, nil
133}
134
135// InitGitRepo create a new empty git repo at the given path
136func InitGitRepo(path string) (*GitRepo, error) {
137	repo := &GitRepo{
138		path:   path + "/.git",
139		clocks: make(map[string]lamport.Clock),
140	}
141
142	_, err := repo.runGitCommand("init", path)
143	if err != nil {
144		return nil, err
145	}
146
147	return repo, nil
148}
149
150// InitBareGitRepo create a new --bare empty git repo at the given path
151func InitBareGitRepo(path string) (*GitRepo, error) {
152	repo := &GitRepo{
153		path:   path,
154		clocks: make(map[string]lamport.Clock),
155	}
156
157	_, err := repo.runGitCommand("init", "--bare", path)
158	if err != nil {
159		return nil, err
160	}
161
162	return repo, nil
163}
164
165// GetPath returns the path to the repo.
166func (repo *GitRepo) GetPath() string {
167	return repo.path
168}
169
170// GetUserName returns the name the the user has used to configure git
171func (repo *GitRepo) GetUserName() (string, error) {
172	return repo.runGitCommand("config", "user.name")
173}
174
175// GetUserEmail returns the email address that the user has used to configure git.
176func (repo *GitRepo) GetUserEmail() (string, error) {
177	return repo.runGitCommand("config", "user.email")
178}
179
180// GetCoreEditor returns the name of the editor that the user has used to configure git.
181func (repo *GitRepo) GetCoreEditor() (string, error) {
182	return repo.runGitCommand("var", "GIT_EDITOR")
183}
184
185// GetRemotes returns the configured remotes repositories.
186func (repo *GitRepo) GetRemotes() (map[string]string, error) {
187	stdout, err := repo.runGitCommand("remote", "--verbose")
188	if err != nil {
189		return nil, err
190	}
191
192	lines := strings.Split(stdout, "\n")
193	remotes := make(map[string]string, len(lines))
194
195	for _, line := range lines {
196		if strings.TrimSpace(line) == "" {
197			continue
198		}
199		elements := strings.Fields(line)
200		if len(elements) != 3 {
201			return nil, fmt.Errorf("git remote: unexpected output format: %s", line)
202		}
203
204		remotes[elements[0]] = elements[1]
205	}
206
207	return remotes, nil
208}
209
210// FetchRefs fetch git refs from a remote
211func (repo *GitRepo) FetchRefs(remote, refSpec string) (string, error) {
212	stdout, err := repo.runGitCommand("fetch", remote, refSpec)
213
214	if err != nil {
215		return stdout, fmt.Errorf("failed to fetch from the remote '%s': %v", remote, err)
216	}
217
218	return stdout, err
219}
220
221// PushRefs push git refs to a remote
222func (repo *GitRepo) PushRefs(remote string, refSpec string) (string, error) {
223	stdout, stderr, err := repo.runGitCommandRaw(nil, "push", remote, refSpec)
224
225	if err != nil {
226		return stdout + stderr, fmt.Errorf("failed to push to the remote '%s': %v", remote, stderr)
227	}
228	return stdout + stderr, nil
229}
230
231// StoreData will store arbitrary data and return the corresponding hash
232func (repo *GitRepo) StoreData(data []byte) (Hash, error) {
233	var stdin = bytes.NewReader(data)
234
235	stdout, err := repo.runGitCommandWithStdin(stdin, "hash-object", "--stdin", "-w")
236
237	return Hash(stdout), err
238}
239
240// ReadData will attempt to read arbitrary data from the given hash
241func (repo *GitRepo) ReadData(hash Hash) ([]byte, error) {
242	var stdout bytes.Buffer
243	var stderr bytes.Buffer
244
245	err := repo.runGitCommandWithIO(nil, &stdout, &stderr, "cat-file", "-p", string(hash))
246
247	if err != nil {
248		return []byte{}, err
249	}
250
251	return stdout.Bytes(), nil
252}
253
254// StoreTree will store a mapping key-->Hash as a Git tree
255func (repo *GitRepo) StoreTree(entries []TreeEntry) (Hash, error) {
256	buffer := prepareTreeEntries(entries)
257
258	stdout, err := repo.runGitCommandWithStdin(&buffer, "mktree")
259
260	if err != nil {
261		return "", err
262	}
263
264	return Hash(stdout), nil
265}
266
267// StoreCommit will store a Git commit with the given Git tree
268func (repo *GitRepo) StoreCommit(treeHash Hash) (Hash, error) {
269	stdout, err := repo.runGitCommand("commit-tree", string(treeHash))
270
271	if err != nil {
272		return "", err
273	}
274
275	return Hash(stdout), nil
276}
277
278// StoreCommitWithParent will store a Git commit with the given Git tree
279func (repo *GitRepo) StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, error) {
280	stdout, err := repo.runGitCommand("commit-tree", string(treeHash),
281		"-p", string(parent))
282
283	if err != nil {
284		return "", err
285	}
286
287	return Hash(stdout), nil
288}
289
290// UpdateRef will create or update a Git reference
291func (repo *GitRepo) UpdateRef(ref string, hash Hash) error {
292	_, err := repo.runGitCommand("update-ref", ref, string(hash))
293
294	return err
295}
296
297// RemoveRef will remove a Git reference
298func (repo *GitRepo) RemoveRef(ref string) error {
299	_, err := repo.runGitCommand("update-ref", "-d", ref)
300
301	return err
302}
303
304// ListRefs will return a list of Git ref matching the given refspec
305func (repo *GitRepo) ListRefs(refPrefix string) ([]string, error) {
306	stdout, err := repo.runGitCommand("for-each-ref", "--format=%(refname)", refPrefix)
307
308	if err != nil {
309		return nil, err
310	}
311
312	split := strings.Split(stdout, "\n")
313
314	if len(split) == 1 && split[0] == "" {
315		return []string{}, nil
316	}
317
318	return split, nil
319}
320
321// RefExist will check if a reference exist in Git
322func (repo *GitRepo) RefExist(ref string) (bool, error) {
323	stdout, err := repo.runGitCommand("for-each-ref", ref)
324
325	if err != nil {
326		return false, err
327	}
328
329	return stdout != "", nil
330}
331
332// CopyRef will create a new reference with the same value as another one
333func (repo *GitRepo) CopyRef(source string, dest string) error {
334	_, err := repo.runGitCommand("update-ref", dest, source)
335
336	return err
337}
338
339// ListCommits will return the list of commit hashes of a ref, in chronological order
340func (repo *GitRepo) ListCommits(ref string) ([]Hash, error) {
341	stdout, err := repo.runGitCommand("rev-list", "--first-parent", "--reverse", ref)
342
343	if err != nil {
344		return nil, err
345	}
346
347	split := strings.Split(stdout, "\n")
348
349	casted := make([]Hash, len(split))
350	for i, line := range split {
351		casted[i] = Hash(line)
352	}
353
354	return casted, nil
355
356}
357
358// ReadTree will return the list of entries in a Git tree
359func (repo *GitRepo) ReadTree(hash Hash) ([]TreeEntry, error) {
360	stdout, err := repo.runGitCommand("ls-tree", string(hash))
361
362	if err != nil {
363		return nil, err
364	}
365
366	return readTreeEntries(stdout)
367}
368
369// FindCommonAncestor will return the last common ancestor of two chain of commit
370func (repo *GitRepo) FindCommonAncestor(hash1 Hash, hash2 Hash) (Hash, error) {
371	stdout, err := repo.runGitCommand("merge-base", string(hash1), string(hash2))
372
373	if err != nil {
374		return "", err
375	}
376
377	return Hash(stdout), nil
378}
379
380// GetTreeHash return the git tree hash referenced in a commit
381func (repo *GitRepo) GetTreeHash(commit Hash) (Hash, error) {
382	stdout, err := repo.runGitCommand("rev-parse", string(commit)+"^{tree}")
383
384	if err != nil {
385		return "", err
386	}
387
388	return Hash(stdout), nil
389}
390
391// GetOrCreateClock return a Lamport clock stored in the Repo.
392// If the clock doesn't exist, it's created.
393func (repo *GitRepo) GetOrCreateClock(name string) (lamport.Clock, error) {
394	c, err := repo.getClock(name)
395	if err == nil {
396		return c, nil
397	}
398	if err != ErrClockNotExist {
399		return nil, err
400	}
401
402	repo.clocksMutex.Lock()
403	defer repo.clocksMutex.Unlock()
404
405	p := path.Join(repo.path, clockPath, name+"-clock")
406
407	c, err = lamport.NewPersistedClock(p)
408	if err != nil {
409		return nil, err
410	}
411
412	repo.clocks[name] = c
413	return c, nil
414}
415
416func (repo *GitRepo) getClock(name string) (lamport.Clock, error) {
417	repo.clocksMutex.Lock()
418	defer repo.clocksMutex.Unlock()
419
420	if c, ok := repo.clocks[name]; ok {
421		return c, nil
422	}
423
424	p := path.Join(repo.path, clockPath, name+"-clock")
425
426	c, err := lamport.LoadPersistedClock(p)
427	if err == nil {
428		repo.clocks[name] = c
429		return c, nil
430	}
431	if err == lamport.ErrClockNotExist {
432		return nil, ErrClockNotExist
433	}
434	return nil, err
435}
436
437// AddRemote add a new remote to the repository
438// Not in the interface because it's only used for testing
439func (repo *GitRepo) AddRemote(name string, url string) error {
440	_, err := repo.runGitCommand("remote", "add", name, url)
441
442	return err
443}