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