1// Package repository contains helper methods for working with the Git repo.
2package repository
3
4import (
5 "bytes"
6 "crypto/sha1"
7 "fmt"
8 "github.com/MichaelMure/git-bug/util"
9 "io"
10 "os"
11 "os/exec"
12 "strings"
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 _, err := repo.runGitCommand("rev-parse")
68 if err == nil {
69 return repo, nil
70 }
71 if _, ok := err.(*exec.ExitError); ok {
72 return nil, err
73 }
74 return nil, err
75}
76
77// GetPath returns the path to the repo.
78func (repo *GitRepo) GetPath() string {
79 return repo.Path
80}
81
82// GetRepoStateHash returns a hash which embodies the entire current state of a repository.
83func (repo *GitRepo) GetRepoStateHash() (string, error) {
84 stateSummary, err := repo.runGitCommand("show-ref")
85 return fmt.Sprintf("%x", sha1.Sum([]byte(stateSummary))), err
86}
87
88// GetUserName returns the name the the user has used to configure git
89func (repo *GitRepo) GetUserName() (string, error) {
90 return repo.runGitCommand("config", "user.name")
91}
92
93// GetUserEmail returns the email address that the user has used to configure git.
94func (repo *GitRepo) GetUserEmail() (string, error) {
95 return repo.runGitCommand("config", "user.email")
96}
97
98// GetCoreEditor returns the name of the editor that the user has used to configure git.
99func (repo *GitRepo) GetCoreEditor() (string, error) {
100 return repo.runGitCommand("var", "GIT_EDITOR")
101}
102
103// FetchRefs fetch git refs from a remote
104func (repo *GitRepo) FetchRefs(remote, refSpec string) error {
105 err := repo.runGitCommandInline("fetch", remote, "\""+refSpec+"\"")
106
107 if err != nil {
108 return fmt.Errorf("failed to fetch from the remote '%s': %v", remote, err)
109 }
110
111 return err
112}
113
114// PushRefs push git refs to a remote
115func (repo *GitRepo) PushRefs(remote string, refSpec string) error {
116 err := repo.runGitCommandInline("push", remote, refSpec)
117
118 if err != nil {
119 return fmt.Errorf("failed to push to the remote '%s': %v", remote, err)
120 }
121 return nil
122}
123
124// StoreData will store arbitrary data and return the corresponding hash
125func (repo *GitRepo) StoreData(data []byte) (util.Hash, error) {
126 var stdin = bytes.NewReader(data)
127
128 stdout, err := repo.runGitCommandWithStdin(stdin, "hash-object", "--stdin", "-w")
129
130 return util.Hash(stdout), err
131}
132
133// ReadData will attempt to read arbitrary data from the given hash
134func (repo *GitRepo) ReadData(hash util.Hash) ([]byte, error) {
135 var stdout bytes.Buffer
136 var stderr bytes.Buffer
137
138 err := repo.runGitCommandWithIO(nil, &stdout, &stderr, "cat-file", "-p", string(hash))
139
140 if err != nil {
141 return []byte{}, err
142 }
143
144 return stdout.Bytes(), nil
145}
146
147// StoreTree will store a mapping key-->Hash as a Git tree
148func (repo *GitRepo) StoreTree(entries []TreeEntry) (util.Hash, error) {
149 buffer := prepareTreeEntries(entries)
150
151 stdout, err := repo.runGitCommandWithStdin(&buffer, "mktree")
152
153 if err != nil {
154 return "", err
155 }
156
157 return util.Hash(stdout), nil
158}
159
160// StoreCommit will store a Git commit with the given Git tree
161func (repo *GitRepo) StoreCommit(treeHash util.Hash) (util.Hash, error) {
162 stdout, err := repo.runGitCommand("commit-tree", string(treeHash))
163
164 if err != nil {
165 return "", err
166 }
167
168 return util.Hash(stdout), nil
169}
170
171// StoreCommitWithParent will store a Git commit with the given Git tree
172func (repo *GitRepo) StoreCommitWithParent(treeHash util.Hash, parent util.Hash) (util.Hash, error) {
173 stdout, err := repo.runGitCommand("commit-tree", string(treeHash),
174 "-p", string(parent))
175
176 if err != nil {
177 return "", err
178 }
179
180 return util.Hash(stdout), nil
181}
182
183// UpdateRef will create or update a Git reference
184func (repo *GitRepo) UpdateRef(ref string, hash util.Hash) error {
185 _, err := repo.runGitCommand("update-ref", ref, string(hash))
186
187 return err
188}
189
190// ListRefs will return a list of Git ref matching the given refspec
191func (repo *GitRepo) ListRefs(refspec string) ([]string, error) {
192 // the format option will strip the ref name to keep only the last part (ie, the bug id)
193 stdout, err := repo.runGitCommand("for-each-ref", "--format=%(refname:lstrip=-1)", refspec)
194
195 if err != nil {
196 return nil, err
197 }
198
199 splitted := strings.Split(stdout, "\n")
200
201 if len(splitted) == 1 && splitted[0] == "" {
202 return []string{}, nil
203 }
204
205 return splitted, nil
206}
207
208// RefExist will check if a reference exist in Git
209func (repo *GitRepo) RefExist(ref string) (bool, error) {
210 stdout, err := repo.runGitCommand("for-each-ref", ref)
211
212 if err != nil {
213 return false, err
214 }
215
216 return stdout != "", nil
217}
218
219// CopyRef will create a new reference with the same value as another one
220func (repo *GitRepo) CopyRef(source string, dest string) error {
221 _, err := repo.runGitCommand("update-ref", dest, source)
222
223 return err
224}
225
226// ListCommits will return the list of commit hashes of a ref, in chronological order
227func (repo *GitRepo) ListCommits(ref string) ([]util.Hash, error) {
228 stdout, err := repo.runGitCommand("rev-list", "--first-parent", "--reverse", ref)
229
230 if err != nil {
231 return nil, err
232 }
233
234 splitted := strings.Split(stdout, "\n")
235
236 casted := make([]util.Hash, len(splitted))
237 for i, line := range splitted {
238 casted[i] = util.Hash(line)
239 }
240
241 return casted, nil
242
243}
244
245// ListEntries will return the list of entries in a Git tree
246func (repo *GitRepo) ListEntries(hash util.Hash) ([]TreeEntry, error) {
247 stdout, err := repo.runGitCommand("ls-tree", string(hash))
248
249 if err != nil {
250 return nil, err
251 }
252
253 return readTreeEntries(stdout)
254}
255
256// FindCommonAncestor will return the last common ancestor of two chain of commit
257func (repo *GitRepo) FindCommonAncestor(hash1 util.Hash, hash2 util.Hash) (util.Hash, error) {
258 stdout, err := repo.runGitCommand("merge-base", string(hash1), string(hash2))
259
260 if err != nil {
261 return "", nil
262 }
263
264 return util.Hash(stdout), nil
265}
266
267// Return the git tree hash referenced in a commit
268func (repo *GitRepo) GetTreeHash(commit util.Hash) (util.Hash, error) {
269 stdout, err := repo.runGitCommand("rev-parse", string(commit)+"^{tree}")
270
271 if err != nil {
272 return "", nil
273 }
274
275 return util.Hash(stdout), nil
276}