1package repository
2
3import (
4 "bytes"
5 "fmt"
6 "io/ioutil"
7 "os"
8 "os/exec"
9 stdpath "path"
10 "path/filepath"
11 "strings"
12 "sync"
13 "time"
14
15 gogit "github.com/go-git/go-git/v5"
16 "github.com/go-git/go-git/v5/config"
17 "github.com/go-git/go-git/v5/plumbing"
18 "github.com/go-git/go-git/v5/plumbing/filemode"
19 "github.com/go-git/go-git/v5/plumbing/object"
20
21 "github.com/MichaelMure/git-bug/util/lamport"
22)
23
24var _ ClockedRepo = &GoGitRepo{}
25
26type GoGitRepo struct {
27 r *gogit.Repository
28 path string
29
30 clocksMutex sync.Mutex
31 clocks map[string]lamport.Clock
32
33 keyring Keyring
34}
35
36func NewGoGitRepo(path string, clockLoaders []ClockLoader) (*GoGitRepo, error) {
37 path, err := detectGitPath(path)
38 if err != nil {
39 return nil, err
40 }
41
42 r, err := gogit.PlainOpen(path)
43 if err != nil {
44 return nil, err
45 }
46
47 k, err := defaultKeyring()
48 if err != nil {
49 return nil, err
50 }
51
52 repo := &GoGitRepo{
53 r: r,
54 path: path,
55 clocks: make(map[string]lamport.Clock),
56 keyring: k,
57 }
58
59 for _, loader := range clockLoaders {
60 allExist := true
61 for _, name := range loader.Clocks {
62 if _, err := repo.getClock(name); err != nil {
63 allExist = false
64 }
65 }
66
67 if !allExist {
68 err = loader.Witnesser(repo)
69 if err != nil {
70 return nil, err
71 }
72 }
73 }
74
75 return repo, nil
76}
77
78func detectGitPath(path string) (string, error) {
79 // normalize the path
80 path, err := filepath.Abs(path)
81 if err != nil {
82 return "", err
83 }
84
85 for {
86 fi, err := os.Stat(stdpath.Join(path, ".git"))
87 if err == nil {
88 if !fi.IsDir() {
89 return "", fmt.Errorf(".git exist but is not a directory")
90 }
91 return stdpath.Join(path, ".git"), nil
92 }
93 if !os.IsNotExist(err) {
94 // unknown error
95 return "", err
96 }
97
98 // detect bare repo
99 ok, err := isGitDir(path)
100 if err != nil {
101 return "", err
102 }
103 if ok {
104 return path, nil
105 }
106
107 if parent := filepath.Dir(path); parent == path {
108 return "", fmt.Errorf(".git not found")
109 } else {
110 path = parent
111 }
112 }
113}
114
115func isGitDir(path string) (bool, error) {
116 markers := []string{"HEAD", "objects", "refs"}
117
118 for _, marker := range markers {
119 _, err := os.Stat(stdpath.Join(path, marker))
120 if err == nil {
121 continue
122 }
123 if !os.IsNotExist(err) {
124 // unknown error
125 return false, err
126 } else {
127 return false, nil
128 }
129 }
130
131 return true, nil
132}
133
134// InitGoGitRepo create a new empty git repo at the given path
135func InitGoGitRepo(path string) (*GoGitRepo, error) {
136 r, err := gogit.PlainInit(path, false)
137 if err != nil {
138 return nil, err
139 }
140
141 return &GoGitRepo{
142 r: r,
143 path: path + "/.git",
144 clocks: make(map[string]lamport.Clock),
145 }, nil
146}
147
148// InitBareGoGitRepo create a new --bare empty git repo at the given path
149func InitBareGoGitRepo(path string) (*GoGitRepo, error) {
150 r, err := gogit.PlainInit(path, true)
151 if err != nil {
152 return nil, err
153 }
154
155 return &GoGitRepo{
156 r: r,
157 path: path,
158 clocks: make(map[string]lamport.Clock),
159 }, nil
160}
161
162// LocalConfig give access to the repository scoped configuration
163func (repo *GoGitRepo) LocalConfig() Config {
164 return newGoGitLocalConfig(repo.r)
165}
166
167// GlobalConfig give access to the global scoped configuration
168func (repo *GoGitRepo) GlobalConfig() Config {
169 // TODO: replace that with go-git native implementation once it's supported
170 // see: https://github.com/go-git/go-git
171 // see: https://github.com/src-d/go-git/issues/760
172 return newGoGitGlobalConfig(repo.r)
173}
174
175// AnyConfig give access to a merged local/global configuration
176func (repo *GoGitRepo) AnyConfig() ConfigRead {
177 return mergeConfig(repo.LocalConfig(), repo.GlobalConfig())
178}
179
180// Keyring give access to a user-wide storage for secrets
181func (repo *GoGitRepo) Keyring() Keyring {
182 return repo.keyring
183}
184
185// GetPath returns the path to the repo.
186func (repo *GoGitRepo) GetPath() string {
187 return repo.path
188}
189
190// GetUserName returns the name the the user has used to configure git
191func (repo *GoGitRepo) GetUserName() (string, error) {
192 cfg, err := repo.r.Config()
193 if err != nil {
194 return "", err
195 }
196
197 return cfg.User.Name, nil
198}
199
200// GetUserEmail returns the email address that the user has used to configure git.
201func (repo *GoGitRepo) GetUserEmail() (string, error) {
202 cfg, err := repo.r.Config()
203 if err != nil {
204 return "", err
205 }
206
207 return cfg.User.Email, nil
208}
209
210// GetCoreEditor returns the name of the editor that the user has used to configure git.
211func (repo *GoGitRepo) GetCoreEditor() (string, error) {
212 // See https://git-scm.com/docs/git-var
213 // The order of preference is the $GIT_EDITOR environment variable, then core.editor configuration, then $VISUAL, then $EDITOR, and then the default chosen at compile time, which is usually vi.
214
215 if val, ok := os.LookupEnv("GIT_EDITOR"); ok {
216 return val, nil
217 }
218
219 val, err := repo.AnyConfig().ReadString("core.editor")
220 if err == nil && val != "" {
221 return val, nil
222 }
223 if err != nil && err != ErrNoConfigEntry {
224 return "", err
225 }
226
227 if val, ok := os.LookupEnv("VISUAL"); ok {
228 return val, nil
229 }
230
231 if val, ok := os.LookupEnv("EDITOR"); ok {
232 return val, nil
233 }
234
235 priorities := []string{
236 "editor",
237 "nano",
238 "vim",
239 "vi",
240 "emacs",
241 }
242
243 for _, cmd := range priorities {
244 if _, err = exec.LookPath(cmd); err == nil {
245 return cmd, nil
246 }
247
248 }
249
250 return "ed", nil
251}
252
253// GetRemotes returns the configured remotes repositories.
254func (repo *GoGitRepo) GetRemotes() (map[string]string, error) {
255 cfg, err := repo.r.Config()
256 if err != nil {
257 return nil, err
258 }
259
260 result := make(map[string]string, len(cfg.Remotes))
261 for name, remote := range cfg.Remotes {
262 if len(remote.URLs) > 0 {
263 result[name] = remote.URLs[0]
264 }
265 }
266
267 return result, nil
268}
269
270// FetchRefs fetch git refs from a remote
271func (repo *GoGitRepo) FetchRefs(remote string, refSpec string) (string, error) {
272 buf := bytes.NewBuffer(nil)
273
274 err := repo.r.Fetch(&gogit.FetchOptions{
275 RemoteName: remote,
276 RefSpecs: []config.RefSpec{config.RefSpec(refSpec)},
277 Progress: buf,
278 })
279 if err != nil {
280 return "", err
281 }
282
283 return buf.String(), nil
284}
285
286// PushRefs push git refs to a remote
287func (repo *GoGitRepo) PushRefs(remote string, refSpec string) (string, error) {
288 buf := bytes.NewBuffer(nil)
289
290 err := repo.r.Push(&gogit.PushOptions{
291 RemoteName: remote,
292 RefSpecs: []config.RefSpec{config.RefSpec(refSpec)},
293 Progress: buf,
294 })
295 if err != nil {
296 return "", err
297 }
298
299 return buf.String(), nil
300}
301
302// StoreData will store arbitrary data and return the corresponding hash
303func (repo *GoGitRepo) StoreData(data []byte) (Hash, error) {
304 obj := repo.r.Storer.NewEncodedObject()
305 obj.SetType(plumbing.BlobObject)
306
307 w, err := obj.Writer()
308 if err != nil {
309 return "", err
310 }
311
312 _, err = w.Write(data)
313 if err != nil {
314 return "", err
315 }
316
317 h, err := repo.r.Storer.SetEncodedObject(obj)
318 if err != nil {
319 return "", err
320 }
321
322 return Hash(h.String()), nil
323}
324
325// ReadData will attempt to read arbitrary data from the given hash
326func (repo *GoGitRepo) ReadData(hash Hash) ([]byte, error) {
327 obj, err := repo.r.BlobObject(plumbing.NewHash(hash.String()))
328 if err != nil {
329 return nil, err
330 }
331
332 r, err := obj.Reader()
333 if err != nil {
334 return nil, err
335 }
336
337 return ioutil.ReadAll(r)
338}
339
340// StoreTree will store a mapping key-->Hash as a Git tree
341func (repo *GoGitRepo) StoreTree(mapping []TreeEntry) (Hash, error) {
342 var tree object.Tree
343
344 for _, entry := range mapping {
345 mode := filemode.Regular
346 if entry.ObjectType == Tree {
347 mode = filemode.Dir
348 }
349
350 tree.Entries = append(tree.Entries, object.TreeEntry{
351 Name: entry.Name,
352 Mode: mode,
353 Hash: plumbing.NewHash(entry.Hash.String()),
354 })
355 }
356
357 obj := repo.r.Storer.NewEncodedObject()
358 obj.SetType(plumbing.TreeObject)
359 err := tree.Encode(obj)
360 if err != nil {
361 return "", err
362 }
363
364 hash, err := repo.r.Storer.SetEncodedObject(obj)
365 if err != nil {
366 return "", err
367 }
368
369 return Hash(hash.String()), nil
370}
371
372// ReadTree will return the list of entries in a Git tree
373func (repo *GoGitRepo) ReadTree(hash Hash) ([]TreeEntry, error) {
374 h := plumbing.NewHash(hash.String())
375
376 // the given hash could be a tree or a commit
377 obj, err := repo.r.Storer.EncodedObject(plumbing.AnyObject, h)
378 if err != nil {
379 return nil, err
380 }
381
382 var tree *object.Tree
383 switch obj.Type() {
384 case plumbing.TreeObject:
385 tree, err = object.DecodeTree(repo.r.Storer, obj)
386 case plumbing.CommitObject:
387 var commit *object.Commit
388 commit, err = object.DecodeCommit(repo.r.Storer, obj)
389 if err != nil {
390 return nil, err
391 }
392 tree, err = commit.Tree()
393 default:
394 return nil, fmt.Errorf("given hash is not a tree")
395 }
396 if err != nil {
397 return nil, err
398 }
399
400 treeEntries := make([]TreeEntry, len(tree.Entries))
401 for i, entry := range tree.Entries {
402 objType := Blob
403 if entry.Mode == filemode.Dir {
404 objType = Tree
405 }
406
407 treeEntries[i] = TreeEntry{
408 ObjectType: objType,
409 Hash: Hash(entry.Hash.String()),
410 Name: entry.Name,
411 }
412 }
413
414 return treeEntries, nil
415}
416
417// StoreCommit will store a Git commit with the given Git tree
418func (repo *GoGitRepo) StoreCommit(treeHash Hash) (Hash, error) {
419 return repo.StoreCommitWithParent(treeHash, "")
420}
421
422// StoreCommit will store a Git commit with the given Git tree
423func (repo *GoGitRepo) StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, error) {
424 cfg, err := repo.r.Config()
425 if err != nil {
426 return "", err
427 }
428
429 commit := object.Commit{
430 Author: object.Signature{
431 cfg.Author.Name,
432 cfg.Author.Email,
433 time.Now(),
434 },
435 Committer: object.Signature{
436 cfg.Committer.Name,
437 cfg.Committer.Email,
438 time.Now(),
439 },
440 Message: "",
441 TreeHash: plumbing.NewHash(treeHash.String()),
442 }
443
444 if parent != "" {
445 commit.ParentHashes = []plumbing.Hash{plumbing.NewHash(parent.String())}
446 }
447
448 obj := repo.r.Storer.NewEncodedObject()
449 obj.SetType(plumbing.CommitObject)
450 err = commit.Encode(obj)
451 if err != nil {
452 return "", err
453 }
454
455 hash, err := repo.r.Storer.SetEncodedObject(obj)
456 if err != nil {
457 return "", err
458 }
459
460 return Hash(hash.String()), nil
461}
462
463// GetTreeHash return the git tree hash referenced in a commit
464func (repo *GoGitRepo) GetTreeHash(commit Hash) (Hash, error) {
465 obj, err := repo.r.CommitObject(plumbing.NewHash(commit.String()))
466 if err != nil {
467 return "", err
468 }
469
470 return Hash(obj.TreeHash.String()), nil
471}
472
473// FindCommonAncestor will return the last common ancestor of two chain of commit
474func (repo *GoGitRepo) FindCommonAncestor(commit1 Hash, commit2 Hash) (Hash, error) {
475 obj1, err := repo.r.CommitObject(plumbing.NewHash(commit1.String()))
476 if err != nil {
477 return "", err
478 }
479 obj2, err := repo.r.CommitObject(plumbing.NewHash(commit2.String()))
480 if err != nil {
481 return "", err
482 }
483
484 commits, err := obj1.MergeBase(obj2)
485 if err != nil {
486 return "", err
487 }
488
489 return Hash(commits[0].Hash.String()), nil
490}
491
492// UpdateRef will create or update a Git reference
493func (repo *GoGitRepo) UpdateRef(ref string, hash Hash) error {
494 return repo.r.Storer.SetReference(plumbing.NewHashReference(plumbing.ReferenceName(ref), plumbing.NewHash(hash.String())))
495}
496
497// RemoveRef will remove a Git reference
498func (repo *GoGitRepo) RemoveRef(ref string) error {
499 return repo.r.Storer.RemoveReference(plumbing.ReferenceName(ref))
500}
501
502// ListRefs will return a list of Git ref matching the given refspec
503func (repo *GoGitRepo) ListRefs(refPrefix string) ([]string, error) {
504 refIter, err := repo.r.References()
505 if err != nil {
506 return nil, err
507 }
508
509 refs := make([]string, 0)
510
511 err = refIter.ForEach(func(ref *plumbing.Reference) error {
512 if strings.HasPrefix(ref.Name().String(), refPrefix) {
513 refs = append(refs, ref.Name().String())
514 }
515 return nil
516 })
517 if err != nil {
518 return nil, err
519 }
520
521 return refs, nil
522}
523
524// RefExist will check if a reference exist in Git
525func (repo *GoGitRepo) RefExist(ref string) (bool, error) {
526 _, err := repo.r.Reference(plumbing.ReferenceName(ref), false)
527 if err == nil {
528 return true, nil
529 } else if err == plumbing.ErrReferenceNotFound {
530 return false, nil
531 }
532 return false, err
533}
534
535// CopyRef will create a new reference with the same value as another one
536func (repo *GoGitRepo) CopyRef(source string, dest string) error {
537 r, err := repo.r.Reference(plumbing.ReferenceName(source), false)
538 if err != nil {
539 return err
540 }
541 return repo.r.Storer.SetReference(plumbing.NewHashReference(plumbing.ReferenceName(dest), r.Hash()))
542}
543
544// ListCommits will return the list of tree hashes of a ref, in chronological order
545func (repo *GoGitRepo) ListCommits(ref string) ([]Hash, error) {
546 r, err := repo.r.Reference(plumbing.ReferenceName(ref), false)
547 if err != nil {
548 return nil, err
549 }
550
551 commit, err := repo.r.CommitObject(r.Hash())
552 if err != nil {
553 return nil, err
554 }
555 hashes := []Hash{Hash(commit.Hash.String())}
556
557 for {
558 commit, err = commit.Parent(0)
559 if err == object.ErrParentNotFound {
560 break
561 }
562 if err != nil {
563 return nil, err
564 }
565
566 if commit.NumParents() > 1 {
567 return nil, fmt.Errorf("multiple parents")
568 }
569
570 hashes = append([]Hash{Hash(commit.Hash.String())}, hashes...)
571 }
572
573 return hashes, nil
574}
575
576// GetOrCreateClock return a Lamport clock stored in the Repo.
577// If the clock doesn't exist, it's created.
578func (repo *GoGitRepo) GetOrCreateClock(name string) (lamport.Clock, error) {
579 c, err := repo.getClock(name)
580 if err == nil {
581 return c, nil
582 }
583 if err != ErrClockNotExist {
584 return nil, err
585 }
586
587 repo.clocksMutex.Lock()
588 defer repo.clocksMutex.Unlock()
589
590 p := stdpath.Join(repo.path, clockPath, name+"-clock")
591
592 c, err = lamport.NewPersistedClock(p)
593 if err != nil {
594 return nil, err
595 }
596
597 repo.clocks[name] = c
598 return c, nil
599}
600
601func (repo *GoGitRepo) getClock(name string) (lamport.Clock, error) {
602 repo.clocksMutex.Lock()
603 defer repo.clocksMutex.Unlock()
604
605 if c, ok := repo.clocks[name]; ok {
606 return c, nil
607 }
608
609 p := stdpath.Join(repo.path, clockPath, name+"-clock")
610
611 c, err := lamport.LoadPersistedClock(p)
612 if err == nil {
613 repo.clocks[name] = c
614 return c, nil
615 }
616 if err == lamport.ErrClockNotExist {
617 return nil, ErrClockNotExist
618 }
619 return nil, err
620}
621
622// AddRemote add a new remote to the repository
623// Not in the interface because it's only used for testing
624func (repo *GoGitRepo) AddRemote(name string, url string) error {
625 _, err := repo.r.CreateRemote(&config.RemoteConfig{
626 Name: name,
627 URLs: []string{url},
628 })
629
630 return err
631}