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