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