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
161func (repo *GoGitRepo) LocalConfig() Config {
162 return newGoGitConfig(repo.r)
163}
164
165func (repo *GoGitRepo) GlobalConfig() Config {
166 panic("go-git doesn't support writing global config")
167}
168
169func (repo *GoGitRepo) Keyring() Keyring {
170 return repo.keyring
171}
172
173// GetPath returns the path to the repo.
174func (repo *GoGitRepo) GetPath() string {
175 return repo.path
176}
177
178// GetUserName returns the name the the user has used to configure git
179func (repo *GoGitRepo) GetUserName() (string, error) {
180 cfg, err := repo.r.Config()
181 if err != nil {
182 return "", err
183 }
184
185 return cfg.User.Name, nil
186}
187
188// GetUserEmail returns the email address that the user has used to configure git.
189func (repo *GoGitRepo) GetUserEmail() (string, error) {
190 cfg, err := repo.r.Config()
191 if err != nil {
192 return "", err
193 }
194
195 return cfg.User.Email, nil
196}
197
198// GetCoreEditor returns the name of the editor that the user has used to configure git.
199func (repo *GoGitRepo) GetCoreEditor() (string, error) {
200
201 panic("implement me")
202}
203
204// GetRemotes returns the configured remotes repositories.
205func (repo *GoGitRepo) GetRemotes() (map[string]string, error) {
206 cfg, err := repo.r.Config()
207 if err != nil {
208 return nil, err
209 }
210
211 result := make(map[string]string, len(cfg.Remotes))
212 for name, remote := range cfg.Remotes {
213 if len(remote.URLs) > 0 {
214 result[name] = remote.URLs[0]
215 }
216 }
217
218 return result, nil
219}
220
221// FetchRefs fetch git refs from a remote
222func (repo *GoGitRepo) FetchRefs(remote string, refSpec string) (string, error) {
223 buf := bytes.NewBuffer(nil)
224
225 err := repo.r.Fetch(&gogit.FetchOptions{
226 RemoteName: remote,
227 RefSpecs: []config.RefSpec{config.RefSpec(refSpec)},
228 Progress: buf,
229 })
230 if err != nil {
231 return "", err
232 }
233
234 return buf.String(), nil
235}
236
237// PushRefs push git refs to a remote
238func (repo *GoGitRepo) PushRefs(remote string, refSpec string) (string, error) {
239 buf := bytes.NewBuffer(nil)
240
241 err := repo.r.Push(&gogit.PushOptions{
242 RemoteName: remote,
243 RefSpecs: []config.RefSpec{config.RefSpec(refSpec)},
244 Progress: buf,
245 })
246 if err != nil {
247 return "", err
248 }
249
250 return buf.String(), nil
251}
252
253// StoreData will store arbitrary data and return the corresponding hash
254func (repo *GoGitRepo) StoreData(data []byte) (Hash, error) {
255 obj := repo.r.Storer.NewEncodedObject()
256 obj.SetType(plumbing.BlobObject)
257
258 w, err := obj.Writer()
259 if err != nil {
260 return "", err
261 }
262
263 _, err = w.Write(data)
264 if err != nil {
265 return "", err
266 }
267
268 h, err := repo.r.Storer.SetEncodedObject(obj)
269 if err != nil {
270 return "", err
271 }
272
273 return Hash(h.String()), nil
274}
275
276// ReadData will attempt to read arbitrary data from the given hash
277func (repo *GoGitRepo) ReadData(hash Hash) ([]byte, error) {
278 obj, err := repo.r.BlobObject(plumbing.NewHash(hash.String()))
279 if err != nil {
280 return nil, err
281 }
282
283 r, err := obj.Reader()
284 if err != nil {
285 return nil, err
286 }
287
288 return ioutil.ReadAll(r)
289}
290
291func (repo *GoGitRepo) StoreTree(mapping []TreeEntry) (Hash, error) {
292 var tree object.Tree
293
294 for _, entry := range mapping {
295 mode := filemode.Regular
296 if entry.ObjectType == Tree {
297 mode = filemode.Dir
298 }
299
300 tree.Entries = append(tree.Entries, object.TreeEntry{
301 Name: entry.Name,
302 Mode: mode,
303 Hash: plumbing.NewHash(entry.Hash.String()),
304 })
305 }
306
307 obj := repo.r.Storer.NewEncodedObject()
308 obj.SetType(plumbing.TreeObject)
309 err := tree.Encode(obj)
310 if err != nil {
311 return "", err
312 }
313
314 hash, err := repo.r.Storer.SetEncodedObject(obj)
315 if err != nil {
316 return "", err
317 }
318
319 return Hash(hash.String()), nil
320}
321
322func (repo *GoGitRepo) ReadTree(hash Hash) ([]TreeEntry, error) {
323 obj, err := repo.r.TreeObject(plumbing.NewHash(hash.String()))
324 if err != nil {
325 return nil, err
326 }
327
328 treeEntries := make([]TreeEntry, len(obj.Entries))
329 for i, entry := range obj.Entries {
330 objType := Blob
331 if entry.Mode == filemode.Dir {
332 objType = Tree
333 }
334
335 treeEntries[i] = TreeEntry{
336 ObjectType: objType,
337 Hash: Hash(entry.Hash.String()),
338 Name: entry.Name,
339 }
340 }
341
342 return treeEntries, nil
343}
344
345func (repo *GoGitRepo) StoreCommit(treeHash Hash) (Hash, error) {
346 return repo.StoreCommitWithParent(treeHash, "")
347}
348
349func (repo *GoGitRepo) StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, error) {
350 cfg, err := repo.r.Config()
351 if err != nil {
352 return "", err
353 }
354
355 commit := object.Commit{
356 Author: object.Signature{
357 cfg.Author.Name,
358 cfg.Author.Email,
359 time.Now(),
360 },
361 Committer: object.Signature{
362 cfg.Committer.Name,
363 cfg.Committer.Email,
364 time.Now(),
365 },
366 Message: "",
367 TreeHash: plumbing.NewHash(treeHash.String()),
368 }
369
370 if parent != "" {
371 commit.ParentHashes = []plumbing.Hash{plumbing.NewHash(parent.String())}
372 }
373
374 obj := repo.r.Storer.NewEncodedObject()
375 obj.SetType(plumbing.CommitObject)
376 err = commit.Encode(obj)
377 if err != nil {
378 return "", err
379 }
380
381 hash, err := repo.r.Storer.SetEncodedObject(obj)
382 if err != nil {
383 return "", err
384 }
385
386 return Hash(hash.String()), nil
387}
388
389func (repo *GoGitRepo) GetTreeHash(commit Hash) (Hash, error) {
390 obj, err := repo.r.CommitObject(plumbing.NewHash(commit.String()))
391 if err != nil {
392 return "", err
393 }
394
395 return Hash(obj.TreeHash.String()), nil
396}
397
398func (repo *GoGitRepo) FindCommonAncestor(commit1 Hash, commit2 Hash) (Hash, error) {
399 obj1, err := repo.r.CommitObject(plumbing.NewHash(commit1.String()))
400 if err != nil {
401 return "", err
402 }
403 obj2, err := repo.r.CommitObject(plumbing.NewHash(commit2.String()))
404 if err != nil {
405 return "", err
406 }
407
408 commits, err := obj1.MergeBase(obj2)
409 if err != nil {
410 return "", err
411 }
412
413 return Hash(commits[0].Hash.String()), nil
414}
415
416func (repo *GoGitRepo) UpdateRef(ref string, hash Hash) error {
417 return repo.r.Storer.SetReference(plumbing.NewHashReference(plumbing.ReferenceName(ref), plumbing.NewHash(hash.String())))
418}
419
420func (repo *GoGitRepo) RemoveRef(ref string) error {
421 return repo.r.Storer.RemoveReference(plumbing.ReferenceName(ref))
422}
423
424func (repo *GoGitRepo) ListRefs(refPrefix string) ([]string, error) {
425 refIter, err := repo.r.References()
426 if err != nil {
427 return nil, err
428 }
429
430 refs := make([]string, 0)
431
432 err = refIter.ForEach(func(ref *plumbing.Reference) error {
433 if strings.HasPrefix(ref.Name().String(), refPrefix) {
434 refs = append(refs, ref.Name().String())
435 }
436 return nil
437 })
438 if err != nil {
439 return nil, err
440 }
441
442 return refs, nil
443}
444
445func (repo *GoGitRepo) RefExist(ref string) (bool, error) {
446 _, err := repo.r.Reference(plumbing.ReferenceName(ref), false)
447 if err == nil {
448 return true, nil
449 } else if err == plumbing.ErrReferenceNotFound {
450 return false, nil
451 }
452 return false, err
453}
454
455func (repo *GoGitRepo) CopyRef(source string, dest string) error {
456 r, err := repo.r.Reference(plumbing.ReferenceName(source), false)
457 if err != nil {
458 return err
459 }
460 return repo.r.Storer.SetReference(plumbing.NewHashReference(plumbing.ReferenceName(dest), r.Hash()))
461}
462
463func (repo *GoGitRepo) ListCommits(ref string) ([]Hash, error) {
464 r, err := repo.r.Reference(plumbing.ReferenceName(ref), false)
465 if err != nil {
466 return nil, err
467 }
468
469 commit, err := repo.r.CommitObject(r.Hash())
470 if err != nil {
471 return nil, err
472 }
473 commits := []Hash{Hash(commit.Hash.String())}
474
475 for {
476 commit, err = commit.Parent(0)
477
478 if err != nil {
479 if err == object.ErrParentNotFound {
480 break
481 }
482
483 return nil, err
484 }
485
486 if commit.NumParents() > 1 {
487 return nil, fmt.Errorf("multiple parents")
488 }
489
490 commits = append(commits, Hash(commit.Hash.String()))
491 }
492
493 return commits, nil
494}
495
496// GetOrCreateClock return a Lamport clock stored in the Repo.
497// If the clock doesn't exist, it's created.
498func (repo *GoGitRepo) GetOrCreateClock(name string) (lamport.Clock, error) {
499 c, err := repo.getClock(name)
500 if err == nil {
501 return c, nil
502 }
503 if err != ErrClockNotExist {
504 return nil, err
505 }
506
507 repo.clocksMutex.Lock()
508 defer repo.clocksMutex.Unlock()
509
510 p := stdpath.Join(repo.path, clockPath, name+"-clock")
511
512 c, err = lamport.NewPersistedClock(p)
513 if err != nil {
514 return nil, err
515 }
516
517 repo.clocks[name] = c
518 return c, nil
519}
520
521func (repo *GoGitRepo) getClock(name string) (lamport.Clock, error) {
522 repo.clocksMutex.Lock()
523 defer repo.clocksMutex.Unlock()
524
525 if c, ok := repo.clocks[name]; ok {
526 return c, nil
527 }
528
529 p := stdpath.Join(repo.path, clockPath, name+"-clock")
530
531 c, err := lamport.LoadPersistedClock(p)
532 if err == nil {
533 repo.clocks[name] = c
534 return c, nil
535 }
536 if err == lamport.ErrClockNotExist {
537 return nil, ErrClockNotExist
538 }
539 return nil, err
540}
541
542// AddRemote add a new remote to the repository
543// Not in the interface because it's only used for testing
544func (repo *GoGitRepo) AddRemote(name string, url string) error {
545 _, err := repo.r.CreateRemote(&config.RemoteConfig{
546 Name: name,
547 URLs: []string{url},
548 })
549
550 return err
551}