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