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 "regexp"
11 "strconv"
12 "strings"
13
14 "github.com/blang/semver"
15 "github.com/pkg/errors"
16
17 "github.com/MichaelMure/git-bug/util/git"
18 "github.com/MichaelMure/git-bug/util/lamport"
19)
20
21const createClockFile = "/.git/git-bug/create-clock"
22const editClockFile = "/.git/git-bug/edit-clock"
23
24// ErrNotARepo is the error returned when the git repo root wan't be found
25var ErrNotARepo = errors.New("not a git repository")
26
27var _ ClockedRepo = &GitRepo{}
28
29// GitRepo represents an instance of a (local) git repository.
30type GitRepo struct {
31 Path string
32 createClock *lamport.Persisted
33 editClock *lamport.Persisted
34}
35
36// Run the given git command with the given I/O reader/writers, returning an error if it fails.
37func (repo *GitRepo) runGitCommandWithIO(stdin io.Reader, stdout, stderr io.Writer, args ...string) error {
38 // fmt.Printf("[%s] Running git %s\n", repo.Path, strings.Join(args, " "))
39
40 cmd := exec.Command("git", args...)
41 cmd.Dir = repo.Path
42 cmd.Stdin = stdin
43 cmd.Stdout = stdout
44 cmd.Stderr = stderr
45
46 return cmd.Run()
47}
48
49// Run the given git command and return its stdout, or an error if the command fails.
50func (repo *GitRepo) runGitCommandRaw(stdin io.Reader, args ...string) (string, string, error) {
51 var stdout bytes.Buffer
52 var stderr bytes.Buffer
53 err := repo.runGitCommandWithIO(stdin, &stdout, &stderr, args...)
54 return strings.TrimSpace(stdout.String()), strings.TrimSpace(stderr.String()), err
55}
56
57// Run the given git command and return its stdout, or an error if the command fails.
58func (repo *GitRepo) runGitCommandWithStdin(stdin io.Reader, args ...string) (string, error) {
59 stdout, stderr, err := repo.runGitCommandRaw(stdin, args...)
60 if err != nil {
61 if stderr == "" {
62 stderr = "Error running git command: " + strings.Join(args, " ")
63 }
64 err = fmt.Errorf(stderr)
65 }
66 return stdout, err
67}
68
69// Run the given git command and return its stdout, or an error if the command fails.
70func (repo *GitRepo) runGitCommand(args ...string) (string, error) {
71 return repo.runGitCommandWithStdin(nil, args...)
72}
73
74// NewGitRepo determines if the given working directory is inside of a git repository,
75// and returns the corresponding GitRepo instance if it is.
76func NewGitRepo(path string, witnesser Witnesser) (*GitRepo, error) {
77 repo := &GitRepo{Path: path}
78
79 // Check the repo and retrieve the root path
80 stdout, err := repo.runGitCommand("rev-parse", "--show-toplevel")
81
82 // for some reason, "git rev-parse --show-toplevel" return nothing
83 // and no error when inside a ".git" dir
84 if err != nil || stdout == "" {
85 return nil, ErrNotARepo
86 }
87
88 // Fix the path to be sure we are at the root
89 repo.Path = stdout
90
91 err = repo.LoadClocks()
92
93 if err != nil {
94 // No clock yet, trying to initialize them
95 err = repo.createClocks()
96 if err != nil {
97 return nil, err
98 }
99
100 err = witnesser(repo)
101 if err != nil {
102 return nil, err
103 }
104
105 err = repo.WriteClocks()
106 if err != nil {
107 return nil, err
108 }
109
110 return repo, nil
111 }
112
113 return repo, nil
114}
115
116// InitGitRepo create a new empty git repo at the given path
117func InitGitRepo(path string) (*GitRepo, error) {
118 repo := &GitRepo{Path: path}
119 err := repo.createClocks()
120 if err != nil {
121 return nil, err
122 }
123
124 _, err = repo.runGitCommand("init", path)
125 if err != nil {
126 return nil, err
127 }
128
129 return repo, nil
130}
131
132// InitBareGitRepo create a new --bare empty git repo at the given path
133func InitBareGitRepo(path string) (*GitRepo, error) {
134 repo := &GitRepo{Path: path}
135 err := repo.createClocks()
136 if err != nil {
137 return nil, err
138 }
139
140 _, err = repo.runGitCommand("init", "--bare", path)
141 if err != nil {
142 return nil, err
143 }
144
145 return repo, nil
146}
147
148// GetPath returns the path to the repo.
149func (repo *GitRepo) GetPath() string {
150 return repo.Path
151}
152
153// GetUserName returns the name the the user has used to configure git
154func (repo *GitRepo) GetUserName() (string, error) {
155 return repo.runGitCommand("config", "user.name")
156}
157
158// GetUserEmail returns the email address that the user has used to configure git.
159func (repo *GitRepo) GetUserEmail() (string, error) {
160 return repo.runGitCommand("config", "user.email")
161}
162
163// GetCoreEditor returns the name of the editor that the user has used to configure git.
164func (repo *GitRepo) GetCoreEditor() (string, error) {
165 return repo.runGitCommand("var", "GIT_EDITOR")
166}
167
168// GetRemotes returns the configured remotes repositories.
169func (repo *GitRepo) GetRemotes() (map[string]string, error) {
170 stdout, err := repo.runGitCommand("remote", "--verbose")
171 if err != nil {
172 return nil, err
173 }
174
175 lines := strings.Split(stdout, "\n")
176 remotes := make(map[string]string, len(lines))
177
178 for _, line := range lines {
179 elements := strings.Fields(line)
180 if len(elements) != 3 {
181 return nil, fmt.Errorf("unexpected output format: %s", line)
182 }
183
184 remotes[elements[0]] = elements[1]
185 }
186
187 return remotes, nil
188}
189
190// StoreConfig store a single key/value pair in the config of the repo
191func (repo *GitRepo) StoreConfig(key string, value string) error {
192 _, err := repo.runGitCommand("config", "--replace-all", key, value)
193
194 return err
195}
196
197// ReadConfigs read all key/value pair matching the key prefix
198func (repo *GitRepo) ReadConfigs(keyPrefix string) (map[string]string, error) {
199 stdout, err := repo.runGitCommand("config", "--get-regexp", keyPrefix)
200
201 // / \
202 // / ! \
203 // -------
204 //
205 // There can be a legitimate error here, but I see no portable way to
206 // distinguish them from the git error that say "no matching value exist"
207 if err != nil {
208 return nil, nil
209 }
210
211 lines := strings.Split(stdout, "\n")
212
213 result := make(map[string]string, len(lines))
214
215 for _, line := range lines {
216 if strings.TrimSpace(line) == "" {
217 continue
218 }
219
220 parts := strings.Fields(line)
221 if len(parts) != 2 {
222 return nil, fmt.Errorf("bad git config: %s", line)
223 }
224
225 result[parts[0]] = parts[1]
226 }
227
228 return result, nil
229}
230
231func (repo *GitRepo) ReadConfigBool(key string) (bool, error) {
232 val, err := repo.ReadConfigString(key)
233 if err != nil {
234 return false, err
235 }
236
237 return strconv.ParseBool(val)
238}
239
240func (repo *GitRepo) ReadConfigString(key string) (string, error) {
241 stdout, err := repo.runGitCommand("config", "--get-all", key)
242
243 // / \
244 // / ! \
245 // -------
246 //
247 // There can be a legitimate error here, but I see no portable way to
248 // distinguish them from the git error that say "no matching value exist"
249 if err != nil {
250 return "", ErrNoConfigEntry
251 }
252
253 lines := strings.Split(stdout, "\n")
254
255 if len(lines) == 0 {
256 return "", ErrNoConfigEntry
257 }
258 if len(lines) > 1 {
259 return "", ErrMultipleConfigEntry
260 }
261
262 return lines[0], nil
263}
264
265func (repo *GitRepo) rmSection(keyPrefix string) error {
266 _, err := repo.runGitCommand("config", "--remove-section", keyPrefix)
267 return err
268}
269
270func (repo *GitRepo) unsetAll(keyPrefix string) error {
271 _, err := repo.runGitCommand("config", "--unset-all", keyPrefix)
272 return err
273}
274
275// return keyPrefix section
276// example: sectionFromKey(a.b.c.d) return a.b.c
277func sectionFromKey(keyPrefix string) string {
278 s := strings.Split(keyPrefix, ".")
279 if len(s) == 1 {
280 return keyPrefix
281 }
282
283 return strings.Join(s[:len(s)-1], ".")
284}
285
286// rmConfigs with git version lesser than 2.18
287func (repo *GitRepo) rmConfigsGitVersionLT218(keyPrefix string) error {
288 // try to remove key/value pair by key
289 err := repo.unsetAll(keyPrefix)
290 if err != nil {
291 return repo.rmSection(keyPrefix)
292 }
293
294 m, err := repo.ReadConfigs(sectionFromKey(keyPrefix))
295 if err != nil {
296 return err
297 }
298
299 // if section doesn't have any left key/value remove the section
300 if len(m) == 0 {
301 return repo.rmSection(sectionFromKey(keyPrefix))
302 }
303
304 return nil
305}
306
307// RmConfigs remove all key/value pair matching the key prefix
308func (repo *GitRepo) RmConfigs(keyPrefix string) error {
309 // starting from git 2.18.0 sections are automatically deleted when the last existing
310 // key/value is removed. Before 2.18.0 we should remove the section
311 // see https://github.com/git/git/blob/master/Documentation/RelNotes/2.18.0.txt#L379
312 lt218, err := repo.gitVersionLT218()
313 if err != nil {
314 return errors.Wrap(err, "getting git version")
315 }
316
317 if lt218 {
318 return repo.rmConfigsGitVersionLT218(keyPrefix)
319 }
320
321 err = repo.unsetAll(keyPrefix)
322 if err != nil {
323 return repo.rmSection(keyPrefix)
324 }
325
326 return nil
327}
328
329func (repo *GitRepo) gitVersionLT218() (bool, error) {
330 versionOut, err := repo.runGitCommand("version")
331 if err != nil {
332 return false, err
333 }
334
335 // extract the version and truncate potential bad parts
336 // ex: 2.23.0.rc1 instead of 2.23.0-rc1
337 r := regexp.MustCompile(`(\d+\.){1,2}\d+`)
338
339 extracted := r.FindString(versionOut)
340 if extracted == "" {
341 return false, fmt.Errorf("unreadable git version %s", versionOut)
342 }
343
344 version, err := semver.Make(extracted)
345 if err != nil {
346 return false, err
347 }
348
349 version218string := "2.18.0"
350 gitVersion218, err := semver.Make(version218string)
351 if err != nil {
352 return false, err
353 }
354
355 return version.LT(gitVersion218), nil
356}
357
358// FetchRefs fetch git refs from a remote
359func (repo *GitRepo) FetchRefs(remote, refSpec string) (string, error) {
360 stdout, err := repo.runGitCommand("fetch", remote, refSpec)
361
362 if err != nil {
363 return stdout, fmt.Errorf("failed to fetch from the remote '%s': %v", remote, err)
364 }
365
366 return stdout, err
367}
368
369// PushRefs push git refs to a remote
370func (repo *GitRepo) PushRefs(remote string, refSpec string) (string, error) {
371 stdout, stderr, err := repo.runGitCommandRaw(nil, "push", remote, refSpec)
372
373 if err != nil {
374 return stdout + stderr, fmt.Errorf("failed to push to the remote '%s': %v", remote, stderr)
375 }
376 return stdout + stderr, nil
377}
378
379// StoreData will store arbitrary data and return the corresponding hash
380func (repo *GitRepo) StoreData(data []byte) (git.Hash, error) {
381 var stdin = bytes.NewReader(data)
382
383 stdout, err := repo.runGitCommandWithStdin(stdin, "hash-object", "--stdin", "-w")
384
385 return git.Hash(stdout), err
386}
387
388// ReadData will attempt to read arbitrary data from the given hash
389func (repo *GitRepo) ReadData(hash git.Hash) ([]byte, error) {
390 var stdout bytes.Buffer
391 var stderr bytes.Buffer
392
393 err := repo.runGitCommandWithIO(nil, &stdout, &stderr, "cat-file", "-p", string(hash))
394
395 if err != nil {
396 return []byte{}, err
397 }
398
399 return stdout.Bytes(), nil
400}
401
402// StoreTree will store a mapping key-->Hash as a Git tree
403func (repo *GitRepo) StoreTree(entries []TreeEntry) (git.Hash, error) {
404 buffer := prepareTreeEntries(entries)
405
406 stdout, err := repo.runGitCommandWithStdin(&buffer, "mktree")
407
408 if err != nil {
409 return "", err
410 }
411
412 return git.Hash(stdout), nil
413}
414
415// StoreCommit will store a Git commit with the given Git tree
416func (repo *GitRepo) StoreCommit(treeHash git.Hash) (git.Hash, error) {
417 stdout, err := repo.runGitCommand("commit-tree", string(treeHash))
418
419 if err != nil {
420 return "", err
421 }
422
423 return git.Hash(stdout), nil
424}
425
426// StoreCommitWithParent will store a Git commit with the given Git tree
427func (repo *GitRepo) StoreCommitWithParent(treeHash git.Hash, parent git.Hash) (git.Hash, error) {
428 stdout, err := repo.runGitCommand("commit-tree", string(treeHash),
429 "-p", string(parent))
430
431 if err != nil {
432 return "", err
433 }
434
435 return git.Hash(stdout), nil
436}
437
438// UpdateRef will create or update a Git reference
439func (repo *GitRepo) UpdateRef(ref string, hash git.Hash) error {
440 _, err := repo.runGitCommand("update-ref", ref, string(hash))
441
442 return err
443}
444
445// ListRefs will return a list of Git ref matching the given refspec
446func (repo *GitRepo) ListRefs(refspec string) ([]string, error) {
447 stdout, err := repo.runGitCommand("for-each-ref", "--format=%(refname)", refspec)
448
449 if err != nil {
450 return nil, err
451 }
452
453 split := strings.Split(stdout, "\n")
454
455 if len(split) == 1 && split[0] == "" {
456 return []string{}, nil
457 }
458
459 return split, nil
460}
461
462// RefExist will check if a reference exist in Git
463func (repo *GitRepo) RefExist(ref string) (bool, error) {
464 stdout, err := repo.runGitCommand("for-each-ref", ref)
465
466 if err != nil {
467 return false, err
468 }
469
470 return stdout != "", nil
471}
472
473// CopyRef will create a new reference with the same value as another one
474func (repo *GitRepo) CopyRef(source string, dest string) error {
475 _, err := repo.runGitCommand("update-ref", dest, source)
476
477 return err
478}
479
480// ListCommits will return the list of commit hashes of a ref, in chronological order
481func (repo *GitRepo) ListCommits(ref string) ([]git.Hash, error) {
482 stdout, err := repo.runGitCommand("rev-list", "--first-parent", "--reverse", ref)
483
484 if err != nil {
485 return nil, err
486 }
487
488 split := strings.Split(stdout, "\n")
489
490 casted := make([]git.Hash, len(split))
491 for i, line := range split {
492 casted[i] = git.Hash(line)
493 }
494
495 return casted, nil
496
497}
498
499// ListEntries will return the list of entries in a Git tree
500func (repo *GitRepo) ListEntries(hash git.Hash) ([]TreeEntry, error) {
501 stdout, err := repo.runGitCommand("ls-tree", string(hash))
502
503 if err != nil {
504 return nil, err
505 }
506
507 return readTreeEntries(stdout)
508}
509
510// FindCommonAncestor will return the last common ancestor of two chain of commit
511func (repo *GitRepo) FindCommonAncestor(hash1 git.Hash, hash2 git.Hash) (git.Hash, error) {
512 stdout, err := repo.runGitCommand("merge-base", string(hash1), string(hash2))
513
514 if err != nil {
515 return "", err
516 }
517
518 return git.Hash(stdout), nil
519}
520
521// GetTreeHash return the git tree hash referenced in a commit
522func (repo *GitRepo) GetTreeHash(commit git.Hash) (git.Hash, error) {
523 stdout, err := repo.runGitCommand("rev-parse", string(commit)+"^{tree}")
524
525 if err != nil {
526 return "", err
527 }
528
529 return git.Hash(stdout), nil
530}
531
532// AddRemote add a new remote to the repository
533// Not in the interface because it's only used for testing
534func (repo *GitRepo) AddRemote(name string, url string) error {
535 _, err := repo.runGitCommand("remote", "add", name, url)
536
537 return err
538}
539
540func (repo *GitRepo) createClocks() error {
541 createPath := path.Join(repo.Path, createClockFile)
542 createClock, err := lamport.NewPersisted(createPath)
543 if err != nil {
544 return err
545 }
546
547 editPath := path.Join(repo.Path, editClockFile)
548 editClock, err := lamport.NewPersisted(editPath)
549 if err != nil {
550 return err
551 }
552
553 repo.createClock = createClock
554 repo.editClock = editClock
555
556 return nil
557}
558
559// LoadClocks read the clocks values from the on-disk repo
560func (repo *GitRepo) LoadClocks() error {
561 createClock, err := lamport.LoadPersisted(repo.GetPath() + createClockFile)
562 if err != nil {
563 return err
564 }
565
566 editClock, err := lamport.LoadPersisted(repo.GetPath() + editClockFile)
567 if err != nil {
568 return err
569 }
570
571 repo.createClock = createClock
572 repo.editClock = editClock
573 return nil
574}
575
576// WriteClocks write the clocks values into the repo
577func (repo *GitRepo) WriteClocks() error {
578 err := repo.createClock.Write()
579 if err != nil {
580 return err
581 }
582
583 err = repo.editClock.Write()
584 if err != nil {
585 return err
586 }
587
588 return nil
589}
590
591// CreateTime return the current value of the creation clock
592func (repo *GitRepo) CreateTime() lamport.Time {
593 return repo.createClock.Time()
594}
595
596// CreateTimeIncrement increment the creation clock and return the new value.
597func (repo *GitRepo) CreateTimeIncrement() (lamport.Time, error) {
598 return repo.createClock.Increment()
599}
600
601// EditTime return the current value of the edit clock
602func (repo *GitRepo) EditTime() lamport.Time {
603 return repo.editClock.Time()
604}
605
606// EditTimeIncrement increment the edit clock and return the new value.
607func (repo *GitRepo) EditTimeIncrement() (lamport.Time, error) {
608 return repo.editClock.Increment()
609}
610
611// CreateWitness witness another create time and increment the corresponding clock
612// if needed.
613func (repo *GitRepo) CreateWitness(time lamport.Time) error {
614 return repo.createClock.Witness(time)
615}
616
617// EditWitness witness another edition time and increment the corresponding clock
618// if needed.
619func (repo *GitRepo) EditWitness(time lamport.Time) error {
620 return repo.editClock.Witness(time)
621}