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