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