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