1// Package repository contains helper methods for working with the Git repo.
2package repository
3
4import (
5 "bytes"
6 "fmt"
7 "io/ioutil"
8 "os"
9 "path/filepath"
10 "strings"
11 "sync"
12
13 "github.com/blevesearch/bleve"
14 "github.com/go-git/go-billy/v5"
15 "github.com/go-git/go-billy/v5/osfs"
16
17 "github.com/MichaelMure/git-bug/util/lamport"
18)
19
20var _ ClockedRepo = &GitRepo{}
21var _ TestedRepo = &GitRepo{}
22
23// GitRepo represents an instance of a (local) git repository.
24type GitRepo struct {
25 gitCli
26 path string
27
28 clocksMutex sync.Mutex
29 clocks map[string]lamport.Clock
30
31 indexesMutex sync.Mutex
32 indexes map[string]bleve.Index
33
34 keyring Keyring
35 localStorage billy.Filesystem
36}
37
38// OpenGitRepo determines if the given working directory is inside of a git repository,
39// and returns the corresponding GitRepo instance if it is.
40func OpenGitRepo(path string, clockLoaders []ClockLoader) (*GitRepo, error) {
41 k, err := defaultKeyring()
42 if err != nil {
43 return nil, err
44 }
45
46 repo := &GitRepo{
47 gitCli: gitCli{path: path},
48 path: path,
49 clocks: make(map[string]lamport.Clock),
50 indexes: make(map[string]bleve.Index),
51 keyring: k,
52 }
53
54 // Check the repo and retrieve the root path
55 stdout, err := repo.runGitCommand("rev-parse", "--absolute-git-dir")
56
57 // Now dir is fetched with "git rev-parse --git-dir". May be it can
58 // still return nothing in some cases. Then empty stdout check is
59 // kept.
60 if err != nil || stdout == "" {
61 return nil, ErrNotARepo
62 }
63
64 // Fix the path to be sure we are at the root
65 repo.path = stdout
66 repo.gitCli.path = stdout
67 repo.localStorage = osfs.New(filepath.Join(path, "git-bug"))
68
69 for _, loader := range clockLoaders {
70 allExist := true
71 for _, name := range loader.Clocks {
72 if _, err := repo.getClock(name); err != nil {
73 allExist = false
74 }
75 }
76
77 if !allExist {
78 err = loader.Witnesser(repo)
79 if err != nil {
80 return nil, err
81 }
82 }
83 }
84
85 return repo, nil
86}
87
88// InitGitRepo create a new empty git repo at the given path
89func InitGitRepo(path string) (*GitRepo, error) {
90 k, err := defaultKeyring()
91 if err != nil {
92 return nil, err
93 }
94
95 repo := &GitRepo{
96 gitCli: gitCli{path: path},
97 path: filepath.Join(path, ".git"),
98 clocks: make(map[string]lamport.Clock),
99 indexes: make(map[string]bleve.Index),
100 keyring: k,
101 localStorage: osfs.New(filepath.Join(path, ".git", "git-bug")),
102 }
103
104 _, err = repo.runGitCommand("init", path)
105 if err != nil {
106 return nil, err
107 }
108
109 return repo, nil
110}
111
112// InitBareGitRepo create a new --bare empty git repo at the given path
113func InitBareGitRepo(path string) (*GitRepo, error) {
114 k, err := defaultKeyring()
115 if err != nil {
116 return nil, err
117 }
118
119 repo := &GitRepo{
120 gitCli: gitCli{path: path},
121 path: path,
122 clocks: make(map[string]lamport.Clock),
123 indexes: make(map[string]bleve.Index),
124 keyring: k,
125 localStorage: osfs.New(filepath.Join(path, "git-bug")),
126 }
127
128 _, err = repo.runGitCommand("init", "--bare", path)
129 if err != nil {
130 return nil, err
131 }
132
133 return repo, nil
134}
135
136func (repo *GitRepo) Close() error {
137 var firstErr error
138 for _, index := range repo.indexes {
139 err := index.Close()
140 if err != nil && firstErr == nil {
141 firstErr = err
142 }
143 }
144 return firstErr
145}
146
147// LocalConfig give access to the repository scoped configuration
148func (repo *GitRepo) LocalConfig() Config {
149 return newGitConfig(repo.gitCli, false)
150}
151
152// GlobalConfig give access to the global scoped configuration
153func (repo *GitRepo) GlobalConfig() Config {
154 return newGitConfig(repo.gitCli, true)
155}
156
157// AnyConfig give access to a merged local/global configuration
158func (repo *GitRepo) AnyConfig() ConfigRead {
159 return mergeConfig(repo.LocalConfig(), repo.GlobalConfig())
160}
161
162// Keyring give access to a user-wide storage for secrets
163func (repo *GitRepo) Keyring() Keyring {
164 return repo.keyring
165}
166
167// GetPath returns the path to the repo.
168func (repo *GitRepo) GetPath() string {
169 return repo.path
170}
171
172// GetUserName returns the name the the user has used to configure git
173func (repo *GitRepo) GetUserName() (string, error) {
174 return repo.runGitCommand("config", "user.name")
175}
176
177// GetUserEmail returns the email address that the user has used to configure git.
178func (repo *GitRepo) GetUserEmail() (string, error) {
179 return repo.runGitCommand("config", "user.email")
180}
181
182// GetCoreEditor returns the name of the editor that the user has used to configure git.
183func (repo *GitRepo) GetCoreEditor() (string, error) {
184 return repo.runGitCommand("var", "GIT_EDITOR")
185}
186
187// GetRemotes returns the configured remotes repositories.
188func (repo *GitRepo) GetRemotes() (map[string]string, error) {
189 stdout, err := repo.runGitCommand("remote", "--verbose")
190 if err != nil {
191 return nil, err
192 }
193
194 lines := strings.Split(stdout, "\n")
195 remotes := make(map[string]string, len(lines))
196
197 for _, line := range lines {
198 if strings.TrimSpace(line) == "" {
199 continue
200 }
201 elements := strings.Fields(line)
202 if len(elements) != 3 {
203 return nil, fmt.Errorf("git remote: unexpected output format: %s", line)
204 }
205
206 remotes[elements[0]] = elements[1]
207 }
208
209 return remotes, nil
210}
211
212// LocalStorage return a billy.Filesystem giving access to $RepoPath/.git/git-bug
213func (repo *GitRepo) LocalStorage() billy.Filesystem {
214 return repo.localStorage
215}
216
217// GetBleveIndex return a bleve.Index that can be used to index documents
218func (repo *GitRepo) GetBleveIndex(name string) (bleve.Index, error) {
219 repo.indexesMutex.Lock()
220 defer repo.indexesMutex.Unlock()
221
222 if index, ok := repo.indexes[name]; ok {
223 return index, nil
224 }
225
226 path := filepath.Join(repo.path, "indexes", name)
227
228 index, err := bleve.Open(path)
229 if err == nil {
230 repo.indexes[name] = index
231 return index, nil
232 }
233
234 err = os.MkdirAll(path, os.ModeDir)
235 if err != nil {
236 return nil, err
237 }
238
239 mapping := bleve.NewIndexMapping()
240 mapping.DefaultAnalyzer = "en"
241
242 index, err = bleve.New(path, mapping)
243 if err != nil {
244 return nil, err
245 }
246
247 repo.indexes[name] = index
248
249 return index, nil
250}
251
252// ClearBleveIndex will wipe the given index
253func (repo *GitRepo) ClearBleveIndex(name string) error {
254 repo.indexesMutex.Lock()
255 defer repo.indexesMutex.Unlock()
256
257 path := filepath.Join(repo.path, "indexes", name)
258
259 err := os.RemoveAll(path)
260 if err != nil {
261 return err
262 }
263
264 delete(repo.indexes, name)
265
266 return nil
267}
268
269// FetchRefs fetch git refs from a remote
270func (repo *GitRepo) FetchRefs(remote, refSpec string) (string, error) {
271 stdout, err := repo.runGitCommand("fetch", remote, refSpec)
272
273 if err != nil {
274 return stdout, fmt.Errorf("failed to fetch from the remote '%s': %v", remote, err)
275 }
276
277 return stdout, err
278}
279
280// PushRefs push git refs to a remote
281func (repo *GitRepo) PushRefs(remote string, refSpec string) (string, error) {
282 stdout, stderr, err := repo.runGitCommandRaw(nil, "push", remote, refSpec)
283
284 if err != nil {
285 return stdout + stderr, fmt.Errorf("failed to push to the remote '%s': %v", remote, stderr)
286 }
287 return stdout + stderr, nil
288}
289
290// StoreData will store arbitrary data and return the corresponding hash
291func (repo *GitRepo) StoreData(data []byte) (Hash, error) {
292 var stdin = bytes.NewReader(data)
293
294 stdout, err := repo.runGitCommandWithStdin(stdin, "hash-object", "--stdin", "-w")
295
296 return Hash(stdout), err
297}
298
299// ReadData will attempt to read arbitrary data from the given hash
300func (repo *GitRepo) ReadData(hash Hash) ([]byte, error) {
301 var stdout bytes.Buffer
302 var stderr bytes.Buffer
303
304 err := repo.runGitCommandWithIO(nil, &stdout, &stderr, "cat-file", "-p", string(hash))
305
306 if err != nil {
307 return []byte{}, err
308 }
309
310 return stdout.Bytes(), nil
311}
312
313// StoreTree will store a mapping key-->Hash as a Git tree
314func (repo *GitRepo) StoreTree(entries []TreeEntry) (Hash, error) {
315 buffer := prepareTreeEntries(entries)
316
317 stdout, err := repo.runGitCommandWithStdin(&buffer, "mktree")
318
319 if err != nil {
320 return "", err
321 }
322
323 return Hash(stdout), nil
324}
325
326// StoreCommit will store a Git commit with the given Git tree
327func (repo *GitRepo) StoreCommit(treeHash Hash) (Hash, error) {
328 stdout, err := repo.runGitCommand("commit-tree", string(treeHash))
329
330 if err != nil {
331 return "", err
332 }
333
334 return Hash(stdout), nil
335}
336
337// StoreCommitWithParent will store a Git commit with the given Git tree
338func (repo *GitRepo) StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, error) {
339 stdout, err := repo.runGitCommand("commit-tree", string(treeHash),
340 "-p", string(parent))
341
342 if err != nil {
343 return "", err
344 }
345
346 return Hash(stdout), nil
347}
348
349// UpdateRef will create or update a Git reference
350func (repo *GitRepo) UpdateRef(ref string, hash Hash) error {
351 _, err := repo.runGitCommand("update-ref", ref, string(hash))
352
353 return err
354}
355
356// RemoveRef will remove a Git reference
357func (repo *GitRepo) RemoveRef(ref string) error {
358 _, err := repo.runGitCommand("update-ref", "-d", ref)
359
360 return err
361}
362
363// ListRefs will return a list of Git ref matching the given refspec
364func (repo *GitRepo) ListRefs(refPrefix string) ([]string, error) {
365 stdout, err := repo.runGitCommand("for-each-ref", "--format=%(refname)", refPrefix)
366
367 if err != nil {
368 return nil, err
369 }
370
371 split := strings.Split(stdout, "\n")
372
373 if len(split) == 1 && split[0] == "" {
374 return []string{}, nil
375 }
376
377 return split, nil
378}
379
380// RefExist will check if a reference exist in Git
381func (repo *GitRepo) RefExist(ref string) (bool, error) {
382 stdout, err := repo.runGitCommand("for-each-ref", ref)
383
384 if err != nil {
385 return false, err
386 }
387
388 return stdout != "", nil
389}
390
391// CopyRef will create a new reference with the same value as another one
392func (repo *GitRepo) CopyRef(source string, dest string) error {
393 _, err := repo.runGitCommand("update-ref", dest, source)
394
395 return err
396}
397
398// ListCommits will return the list of commit hashes of a ref, in chronological order
399func (repo *GitRepo) ListCommits(ref string) ([]Hash, error) {
400 stdout, err := repo.runGitCommand("rev-list", "--first-parent", "--reverse", ref)
401
402 if err != nil {
403 return nil, err
404 }
405
406 split := strings.Split(stdout, "\n")
407
408 casted := make([]Hash, len(split))
409 for i, line := range split {
410 casted[i] = Hash(line)
411 }
412
413 return casted, nil
414
415}
416
417// ReadTree will return the list of entries in a Git tree
418func (repo *GitRepo) ReadTree(hash Hash) ([]TreeEntry, error) {
419 stdout, err := repo.runGitCommand("ls-tree", string(hash))
420
421 if err != nil {
422 return nil, err
423 }
424
425 return readTreeEntries(stdout)
426}
427
428// FindCommonAncestor will return the last common ancestor of two chain of commit
429func (repo *GitRepo) FindCommonAncestor(hash1 Hash, hash2 Hash) (Hash, error) {
430 stdout, err := repo.runGitCommand("merge-base", string(hash1), string(hash2))
431
432 if err != nil {
433 return "", err
434 }
435
436 return Hash(stdout), nil
437}
438
439// GetTreeHash return the git tree hash referenced in a commit
440func (repo *GitRepo) GetTreeHash(commit Hash) (Hash, error) {
441 stdout, err := repo.runGitCommand("rev-parse", string(commit)+"^{tree}")
442
443 if err != nil {
444 return "", err
445 }
446
447 return Hash(stdout), nil
448}
449
450func (repo *GitRepo) AllClocks() (map[string]lamport.Clock, error) {
451 repo.clocksMutex.Lock()
452 defer repo.clocksMutex.Unlock()
453
454 result := make(map[string]lamport.Clock)
455
456 files, err := ioutil.ReadDir(filepath.Join(repo.path, "git-bug", clockPath))
457 if os.IsNotExist(err) {
458 return nil, nil
459 }
460 if err != nil {
461 return nil, err
462 }
463
464 for _, file := range files {
465 name := file.Name()
466 if c, ok := repo.clocks[name]; ok {
467 result[name] = c
468 } else {
469 c, err := lamport.LoadPersistedClock(repo.LocalStorage(), filepath.Join(clockPath, name))
470 if err != nil {
471 return nil, err
472 }
473 repo.clocks[name] = c
474 result[name] = c
475 }
476 }
477
478 return result, nil
479}
480
481// GetOrCreateClock return a Lamport clock stored in the Repo.
482// If the clock doesn't exist, it's created.
483func (repo *GitRepo) GetOrCreateClock(name string) (lamport.Clock, error) {
484 repo.clocksMutex.Lock()
485 defer repo.clocksMutex.Unlock()
486
487 c, err := repo.getClock(name)
488 if err == nil {
489 return c, nil
490 }
491 if err != ErrClockNotExist {
492 return nil, err
493 }
494
495 c, err = lamport.NewPersistedClock(repo.LocalStorage(), filepath.Join(clockPath, name))
496 if err != nil {
497 return nil, err
498 }
499
500 repo.clocks[name] = c
501 return c, nil
502}
503
504func (repo *GitRepo) getClock(name string) (lamport.Clock, error) {
505 if c, ok := repo.clocks[name]; ok {
506 return c, nil
507 }
508
509 c, err := lamport.LoadPersistedClock(repo.LocalStorage(), filepath.Join(clockPath, name))
510 if err == nil {
511 repo.clocks[name] = c
512 return c, nil
513 }
514 if err == lamport.ErrClockNotExist {
515 return nil, ErrClockNotExist
516 }
517 return nil, err
518}
519
520// Increment is equivalent to c = GetOrCreateClock(name) + c.Increment()
521func (repo *GitRepo) Increment(name string) (lamport.Time, error) {
522 c, err := repo.GetOrCreateClock(name)
523 if err != nil {
524 return lamport.Time(0), err
525 }
526 return c.Increment()
527}
528
529// Witness is equivalent to c = GetOrCreateClock(name) + c.Witness(time)
530func (repo *GitRepo) Witness(name string, time lamport.Time) error {
531 c, err := repo.GetOrCreateClock(name)
532 if err != nil {
533 return err
534 }
535 return c.Witness(time)
536}
537
538// AddRemote add a new remote to the repository
539// Not in the interface because it's only used for testing
540func (repo *GitRepo) AddRemote(name string, url string) error {
541 _, err := repo.runGitCommand("remote", "add", name, url)
542
543 return err
544}
545
546// GetLocalRemote return the URL to use to add this repo as a local remote
547func (repo *GitRepo) GetLocalRemote() string {
548 return repo.path
549}
550
551// EraseFromDisk delete this repository entirely from the disk
552func (repo *GitRepo) EraseFromDisk() error {
553 err := repo.Close()
554 if err != nil {
555 return err
556 }
557
558 path := filepath.Clean(strings.TrimSuffix(repo.path, string(filepath.Separator)+".git"))
559
560 // fmt.Println("Cleaning repo:", path)
561 return os.RemoveAll(path)
562}