1// Package repository contains helper methods for working with the Git repo.
2package repository
3
4import (
5 "bytes"
6 "fmt"
7 "io"
8 "os"
9 "os/exec"
10 "strings"
11
12 "github.com/MichaelMure/git-bug/util"
13)
14
15// GitRepo represents an instance of a (local) git repository.
16type GitRepo struct {
17 Path string
18}
19
20// Run the given git command with the given I/O reader/writers, returning an error if it fails.
21func (repo *GitRepo) runGitCommandWithIO(stdin io.Reader, stdout, stderr io.Writer, args ...string) error {
22 //fmt.Println("Running git", strings.Join(args, " "))
23
24 cmd := exec.Command("git", args...)
25 cmd.Dir = repo.Path
26 cmd.Stdin = stdin
27 cmd.Stdout = stdout
28 cmd.Stderr = stderr
29
30 return cmd.Run()
31}
32
33// Run the given git command and return its stdout, or an error if the command fails.
34func (repo *GitRepo) runGitCommandRaw(stdin io.Reader, args ...string) (string, string, error) {
35 var stdout bytes.Buffer
36 var stderr bytes.Buffer
37 err := repo.runGitCommandWithIO(stdin, &stdout, &stderr, args...)
38 return strings.TrimSpace(stdout.String()), strings.TrimSpace(stderr.String()), err
39}
40
41// Run the given git command and return its stdout, or an error if the command fails.
42func (repo *GitRepo) runGitCommandWithStdin(stdin io.Reader, args ...string) (string, error) {
43 stdout, stderr, err := repo.runGitCommandRaw(stdin, args...)
44 if err != nil {
45 if stderr == "" {
46 stderr = "Error running git command: " + strings.Join(args, " ")
47 }
48 err = fmt.Errorf(stderr)
49 }
50 return stdout, err
51}
52
53// Run the given git command and return its stdout, or an error if the command fails.
54func (repo *GitRepo) runGitCommand(args ...string) (string, error) {
55 return repo.runGitCommandWithStdin(nil, args...)
56}
57
58// Run the given git command using the same stdin, stdout, and stderr as the review tool.
59func (repo *GitRepo) runGitCommandInline(args ...string) error {
60 return repo.runGitCommandWithIO(os.Stdin, os.Stdout, os.Stderr, args...)
61}
62
63// NewGitRepo determines if the given working directory is inside of a git repository,
64// and returns the corresponding GitRepo instance if it is.
65func NewGitRepo(path string) (*GitRepo, error) {
66 repo := &GitRepo{Path: path}
67 stdout, err := repo.runGitCommand("rev-parse", "--show-toplevel")
68
69 if err != nil {
70 return nil, err
71 }
72
73 // Fix the path to be sure we are at the root
74 repo.Path = stdout
75
76 return repo, nil
77}
78
79func InitGitRepo(path string) (*GitRepo, error) {
80 repo := &GitRepo{Path: path}
81 _, err := repo.runGitCommand("init", path)
82 if err != nil {
83 return nil, err
84 }
85 return repo, nil
86}
87
88func InitBareGitRepo(path string) (*GitRepo, error) {
89 repo := &GitRepo{Path: path}
90 _, err := repo.runGitCommand("init", "--bare", path)
91 if err != nil {
92 return nil, err
93 }
94 return repo, nil
95}
96
97// GetPath returns the path to the repo.
98func (repo *GitRepo) GetPath() string {
99 return repo.Path
100}
101
102// GetUserName returns the name the the user has used to configure git
103func (repo *GitRepo) GetUserName() (string, error) {
104 return repo.runGitCommand("config", "user.name")
105}
106
107// GetUserEmail returns the email address that the user has used to configure git.
108func (repo *GitRepo) GetUserEmail() (string, error) {
109 return repo.runGitCommand("config", "user.email")
110}
111
112// GetCoreEditor returns the name of the editor that the user has used to configure git.
113func (repo *GitRepo) GetCoreEditor() (string, error) {
114 return repo.runGitCommand("var", "GIT_EDITOR")
115}
116
117// FetchRefs fetch git refs from a remote
118func (repo *GitRepo) FetchRefs(remote, refSpec string) error {
119 err := repo.runGitCommandInline("fetch", remote, refSpec)
120
121 if err != nil {
122 return fmt.Errorf("failed to fetch from the remote '%s': %v", remote, err)
123 }
124
125 return err
126}
127
128// PushRefs push git refs to a remote
129func (repo *GitRepo) PushRefs(remote string, refSpec string) error {
130 err := repo.runGitCommandInline("push", remote, refSpec)
131
132 if err != nil {
133 return fmt.Errorf("failed to push to the remote '%s': %v", remote, err)
134 }
135 return nil
136}
137
138// StoreData will store arbitrary data and return the corresponding hash
139func (repo *GitRepo) StoreData(data []byte) (util.Hash, error) {
140 var stdin = bytes.NewReader(data)
141
142 stdout, err := repo.runGitCommandWithStdin(stdin, "hash-object", "--stdin", "-w")
143
144 return util.Hash(stdout), err
145}
146
147// ReadData will attempt to read arbitrary data from the given hash
148func (repo *GitRepo) ReadData(hash util.Hash) ([]byte, error) {
149 var stdout bytes.Buffer
150 var stderr bytes.Buffer
151
152 err := repo.runGitCommandWithIO(nil, &stdout, &stderr, "cat-file", "-p", string(hash))
153
154 if err != nil {
155 return []byte{}, err
156 }
157
158 return stdout.Bytes(), nil
159}
160
161// StoreTree will store a mapping key-->Hash as a Git tree
162func (repo *GitRepo) StoreTree(entries []TreeEntry) (util.Hash, error) {
163 buffer := prepareTreeEntries(entries)
164
165 stdout, err := repo.runGitCommandWithStdin(&buffer, "mktree")
166
167 if err != nil {
168 return "", err
169 }
170
171 return util.Hash(stdout), nil
172}
173
174// StoreCommit will store a Git commit with the given Git tree
175func (repo *GitRepo) StoreCommit(treeHash util.Hash) (util.Hash, error) {
176 stdout, err := repo.runGitCommand("commit-tree", string(treeHash))
177
178 if err != nil {
179 return "", err
180 }
181
182 return util.Hash(stdout), nil
183}
184
185// StoreCommitWithParent will store a Git commit with the given Git tree
186func (repo *GitRepo) StoreCommitWithParent(treeHash util.Hash, parent util.Hash) (util.Hash, error) {
187 stdout, err := repo.runGitCommand("commit-tree", string(treeHash),
188 "-p", string(parent))
189
190 if err != nil {
191 return "", err
192 }
193
194 return util.Hash(stdout), nil
195}
196
197// UpdateRef will create or update a Git reference
198func (repo *GitRepo) UpdateRef(ref string, hash util.Hash) error {
199 _, err := repo.runGitCommand("update-ref", ref, string(hash))
200
201 return err
202}
203
204// ListRefs will return a list of Git ref matching the given refspec
205func (repo *GitRepo) ListRefs(refspec string) ([]string, error) {
206 stdout, err := repo.runGitCommand("for-each-ref", "--format=%(refname)", refspec)
207
208 if err != nil {
209 return nil, err
210 }
211
212 splitted := strings.Split(stdout, "\n")
213
214 if len(splitted) == 1 && splitted[0] == "" {
215 return []string{}, nil
216 }
217
218 return splitted, nil
219}
220
221// ListIds will return a list of Git ref matching the given refspec,
222// stripped to only the last part of the ref
223func (repo *GitRepo) ListIds(refspec string) ([]string, error) {
224 // the format option will strip the ref name to keep only the last part (ie, the bug id)
225 stdout, err := repo.runGitCommand("for-each-ref", "--format=%(refname:lstrip=-1)", refspec)
226
227 if err != nil {
228 return nil, err
229 }
230
231 splitted := strings.Split(stdout, "\n")
232
233 if len(splitted) == 1 && splitted[0] == "" {
234 return []string{}, nil
235 }
236
237 return splitted, nil
238}
239
240// RefExist will check if a reference exist in Git
241func (repo *GitRepo) RefExist(ref string) (bool, error) {
242 stdout, err := repo.runGitCommand("for-each-ref", ref)
243
244 if err != nil {
245 return false, err
246 }
247
248 return stdout != "", nil
249}
250
251// CopyRef will create a new reference with the same value as another one
252func (repo *GitRepo) CopyRef(source string, dest string) error {
253 _, err := repo.runGitCommand("update-ref", dest, source)
254
255 return err
256}
257
258// ListCommits will return the list of commit hashes of a ref, in chronological order
259func (repo *GitRepo) ListCommits(ref string) ([]util.Hash, error) {
260 stdout, err := repo.runGitCommand("rev-list", "--first-parent", "--reverse", ref)
261
262 if err != nil {
263 return nil, err
264 }
265
266 splitted := strings.Split(stdout, "\n")
267
268 casted := make([]util.Hash, len(splitted))
269 for i, line := range splitted {
270 casted[i] = util.Hash(line)
271 }
272
273 return casted, nil
274
275}
276
277// ListEntries will return the list of entries in a Git tree
278func (repo *GitRepo) ListEntries(hash util.Hash) ([]TreeEntry, error) {
279 stdout, err := repo.runGitCommand("ls-tree", string(hash))
280
281 if err != nil {
282 return nil, err
283 }
284
285 return readTreeEntries(stdout)
286}
287
288// FindCommonAncestor will return the last common ancestor of two chain of commit
289func (repo *GitRepo) FindCommonAncestor(hash1 util.Hash, hash2 util.Hash) (util.Hash, error) {
290 stdout, err := repo.runGitCommand("merge-base", string(hash1), string(hash2))
291
292 if err != nil {
293 return "", nil
294 }
295
296 return util.Hash(stdout), nil
297}
298
299// Return the git tree hash referenced in a commit
300func (repo *GitRepo) GetTreeHash(commit util.Hash) (util.Hash, error) {
301 stdout, err := repo.runGitCommand("rev-parse", string(commit)+"^{tree}")
302
303 if err != nil {
304 return "", nil
305 }
306
307 return util.Hash(stdout), nil
308}
309
310// Add a new remote to the repository
311func (repo *GitRepo) AddRemote(name string, url string) error {
312 _, err := repo.runGitCommand("remote", "add", name, url)
313
314 return err
315}