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