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