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