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