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