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