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