1// Package repository contains helper methods for working with the Git repo.
2package repository
3
4import (
5 "bytes"
6 "fmt"
7 "os"
8 "path"
9 "path/filepath"
10 "strings"
11 "sync"
12
13 "github.com/go-git/go-billy/v5"
14 "github.com/go-git/go-billy/v5/osfs"
15
16 "github.com/MichaelMure/git-bug/util/lamport"
17)
18
19const (
20 clockPath = "git-bug"
21)
22
23var _ ClockedRepo = &GitRepo{}
24var _ TestedRepo = &GitRepo{}
25
26// GitRepo represents an instance of a (local) git repository.
27type GitRepo struct {
28 gitCli
29 path string
30
31 clocksMutex sync.Mutex
32 clocks map[string]lamport.Clock
33
34 keyring Keyring
35}
36
37// NewGitRepo determines if the given working directory is inside of a git repository,
38// and returns the corresponding GitRepo instance if it is.
39func NewGitRepo(path string, clockLoaders []ClockLoader) (*GitRepo, error) {
40 k, err := defaultKeyring()
41 if err != nil {
42 return nil, err
43 }
44
45 repo := &GitRepo{
46 gitCli: gitCli{path: path},
47 path: path,
48 clocks: make(map[string]lamport.Clock),
49 keyring: k,
50 }
51
52 // Check the repo and retrieve the root path
53 stdout, err := repo.runGitCommand("rev-parse", "--absolute-git-dir")
54
55 // Now dir is fetched with "git rev-parse --git-dir". May be it can
56 // still return nothing in some cases. Then empty stdout check is
57 // kept.
58 if err != nil || stdout == "" {
59 return nil, ErrNotARepo
60 }
61
62 // Fix the path to be sure we are at the root
63 repo.path = stdout
64 repo.gitCli.path = stdout
65
66 for _, loader := range clockLoaders {
67 allExist := true
68 for _, name := range loader.Clocks {
69 if _, err := repo.getClock(name); err != nil {
70 allExist = false
71 }
72 }
73
74 if !allExist {
75 err = loader.Witnesser(repo)
76 if err != nil {
77 return nil, err
78 }
79 }
80 }
81
82 return repo, nil
83}
84
85// InitGitRepo create a new empty git repo at the given path
86func InitGitRepo(path string) (*GitRepo, error) {
87 repo := &GitRepo{
88 gitCli: gitCli{path: path},
89 path: path + "/.git",
90 clocks: make(map[string]lamport.Clock),
91 }
92
93 _, err := repo.runGitCommand("init", path)
94 if err != nil {
95 return nil, err
96 }
97
98 return repo, nil
99}
100
101// InitBareGitRepo create a new --bare empty git repo at the given path
102func InitBareGitRepo(path string) (*GitRepo, error) {
103 repo := &GitRepo{
104 gitCli: gitCli{path: path},
105 path: path,
106 clocks: make(map[string]lamport.Clock),
107 }
108
109 _, err := repo.runGitCommand("init", "--bare", path)
110 if err != nil {
111 return nil, err
112 }
113
114 return repo, nil
115}
116
117// LocalConfig give access to the repository scoped configuration
118func (repo *GitRepo) LocalConfig() Config {
119 return newGitConfig(repo.gitCli, false)
120}
121
122// GlobalConfig give access to the global scoped configuration
123func (repo *GitRepo) GlobalConfig() Config {
124 return newGitConfig(repo.gitCli, true)
125}
126
127// AnyConfig give access to a merged local/global configuration
128func (repo *GitRepo) AnyConfig() ConfigRead {
129 return mergeConfig(repo.LocalConfig(), repo.GlobalConfig())
130}
131
132// Keyring give access to a user-wide storage for secrets
133func (repo *GitRepo) Keyring() Keyring {
134 return repo.keyring
135}
136
137// GetPath returns the path to the repo.
138func (repo *GitRepo) GetPath() string {
139 return repo.path
140}
141
142// GetUserName returns the name the the user has used to configure git
143func (repo *GitRepo) GetUserName() (string, error) {
144 return repo.runGitCommand("config", "user.name")
145}
146
147// GetUserEmail returns the email address that the user has used to configure git.
148func (repo *GitRepo) GetUserEmail() (string, error) {
149 return repo.runGitCommand("config", "user.email")
150}
151
152// GetCoreEditor returns the name of the editor that the user has used to configure git.
153func (repo *GitRepo) GetCoreEditor() (string, error) {
154 return repo.runGitCommand("var", "GIT_EDITOR")
155}
156
157// GetRemotes returns the configured remotes repositories.
158func (repo *GitRepo) GetRemotes() (map[string]string, error) {
159 stdout, err := repo.runGitCommand("remote", "--verbose")
160 if err != nil {
161 return nil, err
162 }
163
164 lines := strings.Split(stdout, "\n")
165 remotes := make(map[string]string, len(lines))
166
167 for _, line := range lines {
168 if strings.TrimSpace(line) == "" {
169 continue
170 }
171 elements := strings.Fields(line)
172 if len(elements) != 3 {
173 return nil, fmt.Errorf("git remote: unexpected output format: %s", line)
174 }
175
176 remotes[elements[0]] = elements[1]
177 }
178
179 return remotes, nil
180}
181
182// LocalStorage return a billy.Filesystem giving access to $RepoPath/.git/git-bug
183func (repo *GitRepo) LocalStorage() billy.Filesystem {
184 return osfs.New(repo.path)
185}
186
187// FetchRefs fetch git refs from a remote
188func (repo *GitRepo) FetchRefs(remote, refSpec string) (string, error) {
189 stdout, err := repo.runGitCommand("fetch", remote, refSpec)
190
191 if err != nil {
192 return stdout, fmt.Errorf("failed to fetch from the remote '%s': %v", remote, err)
193 }
194
195 return stdout, err
196}
197
198// PushRefs push git refs to a remote
199func (repo *GitRepo) PushRefs(remote string, refSpec string) (string, error) {
200 stdout, stderr, err := repo.runGitCommandRaw(nil, "push", remote, refSpec)
201
202 if err != nil {
203 return stdout + stderr, fmt.Errorf("failed to push to the remote '%s': %v", remote, stderr)
204 }
205 return stdout + stderr, nil
206}
207
208// StoreData will store arbitrary data and return the corresponding hash
209func (repo *GitRepo) StoreData(data []byte) (Hash, error) {
210 var stdin = bytes.NewReader(data)
211
212 stdout, err := repo.runGitCommandWithStdin(stdin, "hash-object", "--stdin", "-w")
213
214 return Hash(stdout), err
215}
216
217// ReadData will attempt to read arbitrary data from the given hash
218func (repo *GitRepo) ReadData(hash Hash) ([]byte, error) {
219 var stdout bytes.Buffer
220 var stderr bytes.Buffer
221
222 err := repo.runGitCommandWithIO(nil, &stdout, &stderr, "cat-file", "-p", string(hash))
223
224 if err != nil {
225 return []byte{}, err
226 }
227
228 return stdout.Bytes(), nil
229}
230
231// StoreTree will store a mapping key-->Hash as a Git tree
232func (repo *GitRepo) StoreTree(entries []TreeEntry) (Hash, error) {
233 buffer := prepareTreeEntries(entries)
234
235 stdout, err := repo.runGitCommandWithStdin(&buffer, "mktree")
236
237 if err != nil {
238 return "", err
239 }
240
241 return Hash(stdout), nil
242}
243
244// StoreCommit will store a Git commit with the given Git tree
245func (repo *GitRepo) StoreCommit(treeHash Hash) (Hash, error) {
246 stdout, err := repo.runGitCommand("commit-tree", string(treeHash))
247
248 if err != nil {
249 return "", err
250 }
251
252 return Hash(stdout), nil
253}
254
255// StoreCommitWithParent will store a Git commit with the given Git tree
256func (repo *GitRepo) StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, error) {
257 stdout, err := repo.runGitCommand("commit-tree", string(treeHash),
258 "-p", string(parent))
259
260 if err != nil {
261 return "", err
262 }
263
264 return Hash(stdout), nil
265}
266
267// UpdateRef will create or update a Git reference
268func (repo *GitRepo) UpdateRef(ref string, hash Hash) error {
269 _, err := repo.runGitCommand("update-ref", ref, string(hash))
270
271 return err
272}
273
274// RemoveRef will remove a Git reference
275func (repo *GitRepo) RemoveRef(ref string) error {
276 _, err := repo.runGitCommand("update-ref", "-d", ref)
277
278 return err
279}
280
281// ListRefs will return a list of Git ref matching the given refspec
282func (repo *GitRepo) ListRefs(refPrefix string) ([]string, error) {
283 stdout, err := repo.runGitCommand("for-each-ref", "--format=%(refname)", refPrefix)
284
285 if err != nil {
286 return nil, err
287 }
288
289 split := strings.Split(stdout, "\n")
290
291 if len(split) == 1 && split[0] == "" {
292 return []string{}, nil
293 }
294
295 return split, nil
296}
297
298// RefExist will check if a reference exist in Git
299func (repo *GitRepo) RefExist(ref string) (bool, error) {
300 stdout, err := repo.runGitCommand("for-each-ref", ref)
301
302 if err != nil {
303 return false, err
304 }
305
306 return stdout != "", nil
307}
308
309// CopyRef will create a new reference with the same value as another one
310func (repo *GitRepo) CopyRef(source string, dest string) error {
311 _, err := repo.runGitCommand("update-ref", dest, source)
312
313 return err
314}
315
316// ListCommits will return the list of commit hashes of a ref, in chronological order
317func (repo *GitRepo) ListCommits(ref string) ([]Hash, error) {
318 stdout, err := repo.runGitCommand("rev-list", "--first-parent", "--reverse", ref)
319
320 if err != nil {
321 return nil, err
322 }
323
324 split := strings.Split(stdout, "\n")
325
326 casted := make([]Hash, len(split))
327 for i, line := range split {
328 casted[i] = Hash(line)
329 }
330
331 return casted, nil
332
333}
334
335// ReadTree will return the list of entries in a Git tree
336func (repo *GitRepo) ReadTree(hash Hash) ([]TreeEntry, error) {
337 stdout, err := repo.runGitCommand("ls-tree", string(hash))
338
339 if err != nil {
340 return nil, err
341 }
342
343 return readTreeEntries(stdout)
344}
345
346// FindCommonAncestor will return the last common ancestor of two chain of commit
347func (repo *GitRepo) FindCommonAncestor(hash1 Hash, hash2 Hash) (Hash, error) {
348 stdout, err := repo.runGitCommand("merge-base", string(hash1), string(hash2))
349
350 if err != nil {
351 return "", err
352 }
353
354 return Hash(stdout), nil
355}
356
357// GetTreeHash return the git tree hash referenced in a commit
358func (repo *GitRepo) GetTreeHash(commit Hash) (Hash, error) {
359 stdout, err := repo.runGitCommand("rev-parse", string(commit)+"^{tree}")
360
361 if err != nil {
362 return "", err
363 }
364
365 return Hash(stdout), nil
366}
367
368// GetOrCreateClock return a Lamport clock stored in the Repo.
369// If the clock doesn't exist, it's created.
370func (repo *GitRepo) GetOrCreateClock(name string) (lamport.Clock, error) {
371 c, err := repo.getClock(name)
372 if err == nil {
373 return c, nil
374 }
375 if err != ErrClockNotExist {
376 return nil, err
377 }
378
379 repo.clocksMutex.Lock()
380 defer repo.clocksMutex.Unlock()
381
382 p := path.Join(repo.path, clockPath, name+"-clock")
383
384 c, err = lamport.NewPersistedClock(p)
385 if err != nil {
386 return nil, err
387 }
388
389 repo.clocks[name] = c
390 return c, nil
391}
392
393func (repo *GitRepo) getClock(name string) (lamport.Clock, error) {
394 repo.clocksMutex.Lock()
395 defer repo.clocksMutex.Unlock()
396
397 if c, ok := repo.clocks[name]; ok {
398 return c, nil
399 }
400
401 p := path.Join(repo.path, clockPath, name+"-clock")
402
403 c, err := lamport.LoadPersistedClock(p)
404 if err == nil {
405 repo.clocks[name] = c
406 return c, nil
407 }
408 if err == lamport.ErrClockNotExist {
409 return nil, ErrClockNotExist
410 }
411 return nil, err
412}
413
414// AddRemote add a new remote to the repository
415// Not in the interface because it's only used for testing
416func (repo *GitRepo) AddRemote(name string, url string) error {
417 _, err := repo.runGitCommand("remote", "add", name, url)
418
419 return err
420}
421
422// GetLocalRemote return the URL to use to add this repo as a local remote
423func (repo *GitRepo) GetLocalRemote() string {
424 return repo.path
425}
426
427// EraseFromDisk delete this repository entirely from the disk
428func (repo *GitRepo) EraseFromDisk() error {
429 path := filepath.Clean(strings.TrimSuffix(repo.path, string(filepath.Separator)+".git"))
430
431 // fmt.Println("Cleaning repo:", path)
432 return os.RemoveAll(path)
433}