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 "strconv"
11 "strings"
12
13 "github.com/blang/semver"
14 "github.com/pkg/errors"
15
16 "github.com/MichaelMure/git-bug/util/git"
17 "github.com/MichaelMure/git-bug/util/lamport"
18)
19
20const createClockFile = "/git-bug/create-clock"
21const editClockFile = "/git-bug/edit-clock"
22
23// ErrNotARepo is the error returned when the git repo root wan't be found
24var ErrNotARepo = errors.New("not a git repository")
25
26var _ ClockedRepo = &GitRepo{}
27
28// GitRepo represents an instance of a (local) git repository.
29type GitRepo struct {
30 Path string
31 createClock *lamport.Persisted
32 editClock *lamport.Persisted
33}
34
35// Run the given git command with the given I/O reader/writers, returning an error if it fails.
36func (repo *GitRepo) runGitCommandWithIO(stdin io.Reader, stdout, stderr io.Writer, args ...string) error {
37 // fmt.Printf("[%s] Running git %s\n", repo.Path, strings.Join(args, " "))
38
39 cmd := exec.Command("git", args...)
40 cmd.Dir = repo.Path
41 cmd.Stdin = stdin
42 cmd.Stdout = stdout
43 cmd.Stderr = stderr
44
45 return cmd.Run()
46}
47
48// Run the given git command and return its stdout, or an error if the command fails.
49func (repo *GitRepo) runGitCommandRaw(stdin io.Reader, args ...string) (string, string, error) {
50 var stdout bytes.Buffer
51 var stderr bytes.Buffer
52 err := repo.runGitCommandWithIO(stdin, &stdout, &stderr, args...)
53 return strings.TrimSpace(stdout.String()), strings.TrimSpace(stderr.String()), err
54}
55
56// Run the given git command and return its stdout, or an error if the command fails.
57func (repo *GitRepo) runGitCommandWithStdin(stdin io.Reader, args ...string) (string, error) {
58 stdout, stderr, err := repo.runGitCommandRaw(stdin, args...)
59 if err != nil {
60 if stderr == "" {
61 stderr = "Error running git command: " + strings.Join(args, " ")
62 }
63 err = fmt.Errorf(stderr)
64 }
65 return stdout, err
66}
67
68// Run the given git command and return its stdout, or an error if the command fails.
69func (repo *GitRepo) runGitCommand(args ...string) (string, error) {
70 return repo.runGitCommandWithStdin(nil, args...)
71}
72
73// NewGitRepo determines if the given working directory is inside of a git repository,
74// and returns the corresponding GitRepo instance if it is.
75func NewGitRepo(path string, witnesser Witnesser) (*GitRepo, error) {
76 repo := &GitRepo{Path: path}
77
78 // Check the repo and retrieve the root path
79 stdout, err := repo.runGitCommand("rev-parse", "--git-dir")
80
81 // Now dir is fetched with "git rev-parse --git-dir". May be it can
82 // still return nothing in some cases. Then empty stdout check is
83 // kept.
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 versionString := strings.Fields(versionOut)[2]
336 version, err := semver.Make(versionString)
337 if err != nil {
338 return false, err
339 }
340
341 version218string := "2.18.0"
342 gitVersion218, err := semver.Make(version218string)
343 if err != nil {
344 return false, err
345 }
346
347 return version.LT(gitVersion218), nil
348}
349
350// FetchRefs fetch git refs from a remote
351func (repo *GitRepo) FetchRefs(remote, refSpec string) (string, error) {
352 stdout, err := repo.runGitCommand("fetch", remote, refSpec)
353
354 if err != nil {
355 return stdout, fmt.Errorf("failed to fetch from the remote '%s': %v", remote, err)
356 }
357
358 return stdout, err
359}
360
361// PushRefs push git refs to a remote
362func (repo *GitRepo) PushRefs(remote string, refSpec string) (string, error) {
363 stdout, stderr, err := repo.runGitCommandRaw(nil, "push", remote, refSpec)
364
365 if err != nil {
366 return stdout + stderr, fmt.Errorf("failed to push to the remote '%s': %v", remote, stderr)
367 }
368 return stdout + stderr, nil
369}
370
371// StoreData will store arbitrary data and return the corresponding hash
372func (repo *GitRepo) StoreData(data []byte) (git.Hash, error) {
373 var stdin = bytes.NewReader(data)
374
375 stdout, err := repo.runGitCommandWithStdin(stdin, "hash-object", "--stdin", "-w")
376
377 return git.Hash(stdout), err
378}
379
380// ReadData will attempt to read arbitrary data from the given hash
381func (repo *GitRepo) ReadData(hash git.Hash) ([]byte, error) {
382 var stdout bytes.Buffer
383 var stderr bytes.Buffer
384
385 err := repo.runGitCommandWithIO(nil, &stdout, &stderr, "cat-file", "-p", string(hash))
386
387 if err != nil {
388 return []byte{}, err
389 }
390
391 return stdout.Bytes(), nil
392}
393
394// StoreTree will store a mapping key-->Hash as a Git tree
395func (repo *GitRepo) StoreTree(entries []TreeEntry) (git.Hash, error) {
396 buffer := prepareTreeEntries(entries)
397
398 stdout, err := repo.runGitCommandWithStdin(&buffer, "mktree")
399
400 if err != nil {
401 return "", err
402 }
403
404 return git.Hash(stdout), nil
405}
406
407// StoreCommit will store a Git commit with the given Git tree
408func (repo *GitRepo) StoreCommit(treeHash git.Hash) (git.Hash, error) {
409 stdout, err := repo.runGitCommand("commit-tree", string(treeHash))
410
411 if err != nil {
412 return "", err
413 }
414
415 return git.Hash(stdout), nil
416}
417
418// StoreCommitWithParent will store a Git commit with the given Git tree
419func (repo *GitRepo) StoreCommitWithParent(treeHash git.Hash, parent git.Hash) (git.Hash, error) {
420 stdout, err := repo.runGitCommand("commit-tree", string(treeHash),
421 "-p", string(parent))
422
423 if err != nil {
424 return "", err
425 }
426
427 return git.Hash(stdout), nil
428}
429
430// UpdateRef will create or update a Git reference
431func (repo *GitRepo) UpdateRef(ref string, hash git.Hash) error {
432 _, err := repo.runGitCommand("update-ref", ref, string(hash))
433
434 return err
435}
436
437// ListRefs will return a list of Git ref matching the given refspec
438func (repo *GitRepo) ListRefs(refspec string) ([]string, error) {
439 stdout, err := repo.runGitCommand("for-each-ref", "--format=%(refname)", refspec)
440
441 if err != nil {
442 return nil, err
443 }
444
445 split := strings.Split(stdout, "\n")
446
447 if len(split) == 1 && split[0] == "" {
448 return []string{}, nil
449 }
450
451 return split, nil
452}
453
454// RefExist will check if a reference exist in Git
455func (repo *GitRepo) RefExist(ref string) (bool, error) {
456 stdout, err := repo.runGitCommand("for-each-ref", ref)
457
458 if err != nil {
459 return false, err
460 }
461
462 return stdout != "", nil
463}
464
465// CopyRef will create a new reference with the same value as another one
466func (repo *GitRepo) CopyRef(source string, dest string) error {
467 _, err := repo.runGitCommand("update-ref", dest, source)
468
469 return err
470}
471
472// ListCommits will return the list of commit hashes of a ref, in chronological order
473func (repo *GitRepo) ListCommits(ref string) ([]git.Hash, error) {
474 stdout, err := repo.runGitCommand("rev-list", "--first-parent", "--reverse", ref)
475
476 if err != nil {
477 return nil, err
478 }
479
480 split := strings.Split(stdout, "\n")
481
482 casted := make([]git.Hash, len(split))
483 for i, line := range split {
484 casted[i] = git.Hash(line)
485 }
486
487 return casted, nil
488
489}
490
491// ListEntries will return the list of entries in a Git tree
492func (repo *GitRepo) ListEntries(hash git.Hash) ([]TreeEntry, error) {
493 stdout, err := repo.runGitCommand("ls-tree", string(hash))
494
495 if err != nil {
496 return nil, err
497 }
498
499 return readTreeEntries(stdout)
500}
501
502// FindCommonAncestor will return the last common ancestor of two chain of commit
503func (repo *GitRepo) FindCommonAncestor(hash1 git.Hash, hash2 git.Hash) (git.Hash, error) {
504 stdout, err := repo.runGitCommand("merge-base", string(hash1), string(hash2))
505
506 if err != nil {
507 return "", err
508 }
509
510 return git.Hash(stdout), nil
511}
512
513// GetTreeHash return the git tree hash referenced in a commit
514func (repo *GitRepo) GetTreeHash(commit git.Hash) (git.Hash, error) {
515 stdout, err := repo.runGitCommand("rev-parse", string(commit)+"^{tree}")
516
517 if err != nil {
518 return "", err
519 }
520
521 return git.Hash(stdout), nil
522}
523
524// AddRemote add a new remote to the repository
525// Not in the interface because it's only used for testing
526func (repo *GitRepo) AddRemote(name string, url string) error {
527 _, err := repo.runGitCommand("remote", "add", name, url)
528
529 return err
530}
531
532func (repo *GitRepo) createClocks() error {
533 createPath := path.Join(repo.Path, createClockFile)
534 createClock, err := lamport.NewPersisted(createPath)
535 if err != nil {
536 return err
537 }
538
539 editPath := path.Join(repo.Path, editClockFile)
540 editClock, err := lamport.NewPersisted(editPath)
541 if err != nil {
542 return err
543 }
544
545 repo.createClock = createClock
546 repo.editClock = editClock
547
548 return nil
549}
550
551// LoadClocks read the clocks values from the on-disk repo
552func (repo *GitRepo) LoadClocks() error {
553 createClock, err := lamport.LoadPersisted(repo.GetPath() + createClockFile)
554 if err != nil {
555 return err
556 }
557
558 editClock, err := lamport.LoadPersisted(repo.GetPath() + editClockFile)
559 if err != nil {
560 return err
561 }
562
563 repo.createClock = createClock
564 repo.editClock = editClock
565 return nil
566}
567
568// WriteClocks write the clocks values into the repo
569func (repo *GitRepo) WriteClocks() error {
570 err := repo.createClock.Write()
571 if err != nil {
572 return err
573 }
574
575 err = repo.editClock.Write()
576 if err != nil {
577 return err
578 }
579
580 return nil
581}
582
583// CreateTime return the current value of the creation clock
584func (repo *GitRepo) CreateTime() lamport.Time {
585 return repo.createClock.Time()
586}
587
588// CreateTimeIncrement increment the creation clock and return the new value.
589func (repo *GitRepo) CreateTimeIncrement() (lamport.Time, error) {
590 return repo.createClock.Increment()
591}
592
593// EditTime return the current value of the edit clock
594func (repo *GitRepo) EditTime() lamport.Time {
595 return repo.editClock.Time()
596}
597
598// EditTimeIncrement increment the edit clock and return the new value.
599func (repo *GitRepo) EditTimeIncrement() (lamport.Time, error) {
600 return repo.editClock.Increment()
601}
602
603// CreateWitness witness another create time and increment the corresponding clock
604// if needed.
605func (repo *GitRepo) CreateWitness(time lamport.Time) error {
606 return repo.createClock.Witness(time)
607}
608
609// EditWitness witness another edition time and increment the corresponding clock
610// if needed.
611func (repo *GitRepo) EditWitness(time lamport.Time) error {
612 return repo.editClock.Witness(time)
613}