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 cmd := exec.Command("git", args...)
23 cmd.Dir = repo.Path
24 cmd.Stdin = stdin
25 cmd.Stdout = stdout
26 cmd.Stderr = stderr
27
28 return cmd.Run()
29}
30
31// Run the given git command and return its stdout, or an error if the command fails.
32func (repo *GitRepo) runGitCommandRaw(stdin io.Reader, args ...string) (string, string, error) {
33 var stdout bytes.Buffer
34 var stderr bytes.Buffer
35 err := repo.runGitCommandWithIO(stdin, &stdout, &stderr, args...)
36 return strings.TrimSpace(stdout.String()), strings.TrimSpace(stderr.String()), err
37}
38
39// Run the given git command and return its stdout, or an error if the command fails.
40func (repo *GitRepo) runGitCommandWithStdin(stdin io.Reader, args ...string) (string, error) {
41 stdout, stderr, err := repo.runGitCommandRaw(stdin, args...)
42 if err != nil {
43 if stderr == "" {
44 stderr = "Error running git command: " + strings.Join(args, " ")
45 }
46 err = fmt.Errorf(stderr)
47 }
48 return stdout, err
49}
50
51// Run the given git command and return its stdout, or an error if the command fails.
52func (repo *GitRepo) runGitCommand(args ...string) (string, error) {
53 return repo.runGitCommandWithStdin(nil, args...)
54}
55
56// Run the given git command using the same stdin, stdout, and stderr as the review tool.
57func (repo *GitRepo) runGitCommandInline(args ...string) error {
58 return repo.runGitCommandWithIO(os.Stdin, os.Stdout, os.Stderr, args...)
59}
60
61// NewGitRepo determines if the given working directory is inside of a git repository,
62// and returns the corresponding GitRepo instance if it is.
63func NewGitRepo(path string) (*GitRepo, error) {
64 repo := &GitRepo{Path: path}
65 _, err := repo.runGitCommand("rev-parse")
66 if err == nil {
67 return repo, nil
68 }
69 if _, ok := err.(*exec.ExitError); ok {
70 return nil, err
71 }
72 return nil, err
73}
74
75// GetPath returns the path to the repo.
76func (repo *GitRepo) GetPath() string {
77 return repo.Path
78}
79
80// GetRepoStateHash returns a hash which embodies the entire current state of a repository.
81func (repo *GitRepo) GetRepoStateHash() (string, error) {
82 stateSummary, err := repo.runGitCommand("show-ref")
83 return fmt.Sprintf("%x", sha1.Sum([]byte(stateSummary))), err
84}
85
86// GetUserName returns the name the the user has used to configure git
87func (repo *GitRepo) GetUserName() (string, error) {
88 return repo.runGitCommand("config", "user.name")
89}
90
91// GetUserEmail returns the email address that the user has used to configure git.
92func (repo *GitRepo) GetUserEmail() (string, error) {
93 return repo.runGitCommand("config", "user.email")
94}
95
96// GetCoreEditor returns the name of the editor that the user has used to configure git.
97func (repo *GitRepo) GetCoreEditor() (string, error) {
98 return repo.runGitCommand("var", "GIT_EDITOR")
99}
100
101// FetchRefs fetch git refs from a remote
102func (repo *GitRepo) FetchRefs(remote, refPattern, remoteRefPattern string) error {
103 remoteRefSpec := fmt.Sprintf(remoteRefPattern, remote)
104 fetchRefSpec := fmt.Sprintf("%s:%s", refPattern, remoteRefSpec)
105 err := repo.runGitCommandInline("fetch", remote, fetchRefSpec)
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, refPattern string) error {
116 err := repo.runGitCommandInline("push", remote, refPattern)
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}