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