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