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