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