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