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