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 return repo.AnyConfig().ReadString("user.name")
206}
207
208// GetUserEmail returns the email address that the user has used to configure git.
209func (repo *GoGitRepo) GetUserEmail() (string, error) {
210 return repo.AnyConfig().ReadString("user.email")
211}
212
213// GetCoreEditor returns the name of the editor that the user has used to configure git.
214func (repo *GoGitRepo) GetCoreEditor() (string, error) {
215 // See https://git-scm.com/docs/git-var
216 // 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.
217
218 if val, ok := os.LookupEnv("GIT_EDITOR"); ok {
219 return val, nil
220 }
221
222 val, err := repo.AnyConfig().ReadString("core.editor")
223 if err == nil && val != "" {
224 return val, nil
225 }
226 if err != nil && err != ErrNoConfigEntry {
227 return "", err
228 }
229
230 if val, ok := os.LookupEnv("VISUAL"); ok {
231 return val, nil
232 }
233
234 if val, ok := os.LookupEnv("EDITOR"); ok {
235 return val, nil
236 }
237
238 priorities := []string{
239 "editor",
240 "nano",
241 "vim",
242 "vi",
243 "emacs",
244 }
245
246 for _, cmd := range priorities {
247 if _, err = exec.LookPath(cmd); err == nil {
248 return cmd, nil
249 }
250
251 }
252
253 return "ed", nil
254}
255
256// GetRemotes returns the configured remotes repositories.
257func (repo *GoGitRepo) GetRemotes() (map[string]string, error) {
258 cfg, err := repo.r.Config()
259 if err != nil {
260 return nil, err
261 }
262
263 result := make(map[string]string, len(cfg.Remotes))
264 for name, remote := range cfg.Remotes {
265 if len(remote.URLs) > 0 {
266 result[name] = remote.URLs[0]
267 }
268 }
269
270 return result, nil
271}
272
273// FetchRefs fetch git refs from a remote
274func (repo *GoGitRepo) FetchRefs(remote string, refSpec string) (string, error) {
275 buf := bytes.NewBuffer(nil)
276
277 err := repo.r.Fetch(&gogit.FetchOptions{
278 RemoteName: remote,
279 RefSpecs: []config.RefSpec{config.RefSpec(refSpec)},
280 Progress: buf,
281 })
282 if err == gogit.NoErrAlreadyUpToDate {
283 return "already up-to-date", nil
284 }
285 if err != nil {
286 return "", err
287 }
288
289 return buf.String(), nil
290}
291
292// PushRefs push git refs to a remote
293func (repo *GoGitRepo) PushRefs(remote string, refSpec string) (string, error) {
294 buf := bytes.NewBuffer(nil)
295
296 err := repo.r.Push(&gogit.PushOptions{
297 RemoteName: remote,
298 RefSpecs: []config.RefSpec{config.RefSpec(refSpec)},
299 Progress: buf,
300 })
301 if err == gogit.NoErrAlreadyUpToDate {
302 return "already up-to-date", nil
303 }
304 if err != nil {
305 return "", err
306 }
307
308 return buf.String(), nil
309}
310
311// StoreData will store arbitrary data and return the corresponding hash
312func (repo *GoGitRepo) StoreData(data []byte) (Hash, error) {
313 obj := repo.r.Storer.NewEncodedObject()
314 obj.SetType(plumbing.BlobObject)
315
316 w, err := obj.Writer()
317 if err != nil {
318 return "", err
319 }
320
321 _, err = w.Write(data)
322 if err != nil {
323 return "", err
324 }
325
326 h, err := repo.r.Storer.SetEncodedObject(obj)
327 if err != nil {
328 return "", err
329 }
330
331 return Hash(h.String()), nil
332}
333
334// ReadData will attempt to read arbitrary data from the given hash
335func (repo *GoGitRepo) ReadData(hash Hash) ([]byte, error) {
336 obj, err := repo.r.BlobObject(plumbing.NewHash(hash.String()))
337 if err != nil {
338 return nil, err
339 }
340
341 r, err := obj.Reader()
342 if err != nil {
343 return nil, err
344 }
345
346 return ioutil.ReadAll(r)
347}
348
349// StoreTree will store a mapping key-->Hash as a Git tree
350func (repo *GoGitRepo) StoreTree(mapping []TreeEntry) (Hash, error) {
351 var tree object.Tree
352
353 // TODO: can be removed once https://github.com/go-git/go-git/issues/193 is resolved
354 sorted := make([]TreeEntry, len(mapping))
355 copy(sorted, mapping)
356 sort.Slice(sorted, func(i, j int) bool {
357 nameI := sorted[i].Name
358 if sorted[i].ObjectType == Tree {
359 nameI += "/"
360 }
361 nameJ := sorted[j].Name
362 if sorted[j].ObjectType == Tree {
363 nameJ += "/"
364 }
365 return nameI < nameJ
366 })
367
368 for _, entry := range sorted {
369 mode := filemode.Regular
370 if entry.ObjectType == Tree {
371 mode = filemode.Dir
372 }
373
374 tree.Entries = append(tree.Entries, object.TreeEntry{
375 Name: entry.Name,
376 Mode: mode,
377 Hash: plumbing.NewHash(entry.Hash.String()),
378 })
379 }
380
381 obj := repo.r.Storer.NewEncodedObject()
382 obj.SetType(plumbing.TreeObject)
383 err := tree.Encode(obj)
384 if err != nil {
385 return "", err
386 }
387
388 hash, err := repo.r.Storer.SetEncodedObject(obj)
389 if err != nil {
390 return "", err
391 }
392
393 return Hash(hash.String()), nil
394}
395
396// ReadTree will return the list of entries in a Git tree
397func (repo *GoGitRepo) ReadTree(hash Hash) ([]TreeEntry, error) {
398 h := plumbing.NewHash(hash.String())
399
400 // the given hash could be a tree or a commit
401 obj, err := repo.r.Storer.EncodedObject(plumbing.AnyObject, h)
402 if err != nil {
403 return nil, err
404 }
405
406 var tree *object.Tree
407 switch obj.Type() {
408 case plumbing.TreeObject:
409 tree, err = object.DecodeTree(repo.r.Storer, obj)
410 case plumbing.CommitObject:
411 var commit *object.Commit
412 commit, err = object.DecodeCommit(repo.r.Storer, obj)
413 if err != nil {
414 return nil, err
415 }
416 tree, err = commit.Tree()
417 default:
418 return nil, fmt.Errorf("given hash is not a tree")
419 }
420 if err != nil {
421 return nil, err
422 }
423
424 treeEntries := make([]TreeEntry, len(tree.Entries))
425 for i, entry := range tree.Entries {
426 objType := Blob
427 if entry.Mode == filemode.Dir {
428 objType = Tree
429 }
430
431 treeEntries[i] = TreeEntry{
432 ObjectType: objType,
433 Hash: Hash(entry.Hash.String()),
434 Name: entry.Name,
435 }
436 }
437
438 return treeEntries, nil
439}
440
441// StoreCommit will store a Git commit with the given Git tree
442func (repo *GoGitRepo) StoreCommit(treeHash Hash) (Hash, error) {
443 return repo.StoreCommitWithParent(treeHash, "")
444}
445
446// StoreCommit will store a Git commit with the given Git tree
447func (repo *GoGitRepo) StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, error) {
448 cfg, err := repo.r.Config()
449 if err != nil {
450 return "", err
451 }
452
453 commit := object.Commit{
454 Author: object.Signature{
455 Name: cfg.Author.Name,
456 Email: cfg.Author.Email,
457 When: time.Now(),
458 },
459 Committer: object.Signature{
460 Name: cfg.Committer.Name,
461 Email: cfg.Committer.Email,
462 When: time.Now(),
463 },
464 Message: "",
465 TreeHash: plumbing.NewHash(treeHash.String()),
466 }
467
468 if parent != "" {
469 commit.ParentHashes = []plumbing.Hash{plumbing.NewHash(parent.String())}
470 }
471
472 obj := repo.r.Storer.NewEncodedObject()
473 obj.SetType(plumbing.CommitObject)
474 err = commit.Encode(obj)
475 if err != nil {
476 return "", err
477 }
478
479 hash, err := repo.r.Storer.SetEncodedObject(obj)
480 if err != nil {
481 return "", err
482 }
483
484 return Hash(hash.String()), nil
485}
486
487// GetTreeHash return the git tree hash referenced in a commit
488func (repo *GoGitRepo) GetTreeHash(commit Hash) (Hash, error) {
489 obj, err := repo.r.CommitObject(plumbing.NewHash(commit.String()))
490 if err != nil {
491 return "", err
492 }
493
494 return Hash(obj.TreeHash.String()), nil
495}
496
497// FindCommonAncestor will return the last common ancestor of two chain of commit
498func (repo *GoGitRepo) FindCommonAncestor(commit1 Hash, commit2 Hash) (Hash, error) {
499 obj1, err := repo.r.CommitObject(plumbing.NewHash(commit1.String()))
500 if err != nil {
501 return "", err
502 }
503 obj2, err := repo.r.CommitObject(plumbing.NewHash(commit2.String()))
504 if err != nil {
505 return "", err
506 }
507
508 commits, err := obj1.MergeBase(obj2)
509 if err != nil {
510 return "", err
511 }
512
513 return Hash(commits[0].Hash.String()), nil
514}
515
516// UpdateRef will create or update a Git reference
517func (repo *GoGitRepo) UpdateRef(ref string, hash Hash) error {
518 return repo.r.Storer.SetReference(plumbing.NewHashReference(plumbing.ReferenceName(ref), plumbing.NewHash(hash.String())))
519}
520
521// RemoveRef will remove a Git reference
522func (repo *GoGitRepo) RemoveRef(ref string) error {
523 return repo.r.Storer.RemoveReference(plumbing.ReferenceName(ref))
524}
525
526// ListRefs will return a list of Git ref matching the given refspec
527func (repo *GoGitRepo) ListRefs(refPrefix string) ([]string, error) {
528 refIter, err := repo.r.References()
529 if err != nil {
530 return nil, err
531 }
532
533 refs := make([]string, 0)
534
535 err = refIter.ForEach(func(ref *plumbing.Reference) error {
536 if strings.HasPrefix(ref.Name().String(), refPrefix) {
537 refs = append(refs, ref.Name().String())
538 }
539 return nil
540 })
541 if err != nil {
542 return nil, err
543 }
544
545 return refs, nil
546}
547
548// RefExist will check if a reference exist in Git
549func (repo *GoGitRepo) RefExist(ref string) (bool, error) {
550 _, err := repo.r.Reference(plumbing.ReferenceName(ref), false)
551 if err == nil {
552 return true, nil
553 } else if err == plumbing.ErrReferenceNotFound {
554 return false, nil
555 }
556 return false, err
557}
558
559// CopyRef will create a new reference with the same value as another one
560func (repo *GoGitRepo) CopyRef(source string, dest string) error {
561 r, err := repo.r.Reference(plumbing.ReferenceName(source), false)
562 if err != nil {
563 return err
564 }
565 return repo.r.Storer.SetReference(plumbing.NewHashReference(plumbing.ReferenceName(dest), r.Hash()))
566}
567
568// ListCommits will return the list of tree hashes of a ref, in chronological order
569func (repo *GoGitRepo) ListCommits(ref string) ([]Hash, error) {
570 r, err := repo.r.Reference(plumbing.ReferenceName(ref), false)
571 if err != nil {
572 return nil, err
573 }
574
575 commit, err := repo.r.CommitObject(r.Hash())
576 if err != nil {
577 return nil, err
578 }
579 hashes := []Hash{Hash(commit.Hash.String())}
580
581 for {
582 commit, err = commit.Parent(0)
583 if err == object.ErrParentNotFound {
584 break
585 }
586 if err != nil {
587 return nil, err
588 }
589
590 if commit.NumParents() > 1 {
591 return nil, fmt.Errorf("multiple parents")
592 }
593
594 hashes = append([]Hash{Hash(commit.Hash.String())}, hashes...)
595 }
596
597 return hashes, nil
598}
599
600// GetOrCreateClock return a Lamport clock stored in the Repo.
601// If the clock doesn't exist, it's created.
602func (repo *GoGitRepo) GetOrCreateClock(name string) (lamport.Clock, error) {
603 c, err := repo.getClock(name)
604 if err == nil {
605 return c, nil
606 }
607 if err != ErrClockNotExist {
608 return nil, err
609 }
610
611 repo.clocksMutex.Lock()
612 defer repo.clocksMutex.Unlock()
613
614 p := stdpath.Join(repo.path, clockPath, name+"-clock")
615
616 c, err = lamport.NewPersistedClock(p)
617 if err != nil {
618 return nil, err
619 }
620
621 repo.clocks[name] = c
622 return c, nil
623}
624
625func (repo *GoGitRepo) getClock(name string) (lamport.Clock, error) {
626 repo.clocksMutex.Lock()
627 defer repo.clocksMutex.Unlock()
628
629 if c, ok := repo.clocks[name]; ok {
630 return c, nil
631 }
632
633 p := stdpath.Join(repo.path, clockPath, name+"-clock")
634
635 c, err := lamport.LoadPersistedClock(p)
636 if err == nil {
637 repo.clocks[name] = c
638 return c, nil
639 }
640 if err == lamport.ErrClockNotExist {
641 return nil, ErrClockNotExist
642 }
643 return nil, err
644}
645
646// AddRemote add a new remote to the repository
647// Not in the interface because it's only used for testing
648func (repo *GoGitRepo) AddRemote(name string, url string) error {
649 _, err := repo.r.CreateRemote(&config.RemoteConfig{
650 Name: name,
651 URLs: []string{url},
652 })
653
654 return err
655}