X Tutup
Skip to content

Commit d881a2e

Browse files
committed
Ensure git operations preserve their stderr in error output
This also provides a SetPrepareCmd hook for tests to be able to define stubs for commands that are supposed to be run
1 parent a66aaaf commit d881a2e

File tree

2 files changed

+72
-26
lines changed

2 files changed

+72
-26
lines changed

git/git.go

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@ import (
88
"os/exec"
99
"path/filepath"
1010
"strings"
11+
12+
"github.com/github/gh-cli/utils"
1113
)
1214

1315
func Dir() (string, error) {
1416
dirCmd := exec.Command("git", "rev-parse", "-q", "--git-dir")
15-
dirCmd.Stderr = nil
16-
output, err := dirCmd.Output()
17+
output, err := utils.PrepareCmd(dirCmd).Output()
1718
if err != nil {
1819
return "", fmt.Errorf("Not a git repository (or any of the parent directories): .git")
1920
}
@@ -33,7 +34,7 @@ func Dir() (string, error) {
3334
func WorkdirName() (string, error) {
3435
toplevelCmd := exec.Command("git", "rev-parse", "--show-toplevel")
3536
toplevelCmd.Stderr = nil
36-
output, err := toplevelCmd.Output()
37+
output, err := utils.PrepareCmd(toplevelCmd).Output()
3738
dir := firstLine(output)
3839
if dir == "" {
3940
return "", fmt.Errorf("unable to determine git working directory")
@@ -44,8 +45,7 @@ func WorkdirName() (string, error) {
4445
func HasFile(segments ...string) bool {
4546
// The blessed way to resolve paths within git dir since Git 2.5.0
4647
pathCmd := exec.Command("git", "rev-parse", "-q", "--git-path", filepath.Join(segments...))
47-
pathCmd.Stderr = nil
48-
if output, err := pathCmd.Output(); err == nil {
48+
if output, err := utils.PrepareCmd(pathCmd).Output(); err == nil {
4949
if lines := outputLines(output); len(lines) == 1 {
5050
if _, err := os.Stat(lines[0]); err == nil {
5151
return true
@@ -97,8 +97,7 @@ func BranchAtRef(paths ...string) (name string, err error) {
9797

9898
func Editor() (string, error) {
9999
varCmd := exec.Command("git", "var", "GIT_EDITOR")
100-
varCmd.Stderr = nil
101-
output, err := varCmd.Output()
100+
output, err := utils.PrepareCmd(varCmd).Output()
102101
if err != nil {
103102
return "", fmt.Errorf("Can't load git var: GIT_EDITOR")
104103
}
@@ -112,8 +111,7 @@ func Head() (string, error) {
112111

113112
func SymbolicFullName(name string) (string, error) {
114113
parseCmd := exec.Command("git", "rev-parse", "--symbolic-full-name", name)
115-
parseCmd.Stderr = nil
116-
output, err := parseCmd.Output()
114+
output, err := utils.PrepareCmd(parseCmd).Output()
117115
if err != nil {
118116
return "", fmt.Errorf("Unknown revision or path not in the working tree: %s", name)
119117
}
@@ -145,9 +143,7 @@ func CommentChar(text string) (string, error) {
145143

146144
func Show(sha string) (string, error) {
147145
cmd := exec.Command("git", "-c", "log.showSignature=false", "show", "-s", "--format=%s%n%+b", sha)
148-
cmd.Stderr = nil
149-
150-
output, err := cmd.Output()
146+
output, err := utils.PrepareCmd(cmd).Output()
151147
return strings.TrimSpace(string(output)), err
152148
}
153149

@@ -157,7 +153,7 @@ func Log(sha1, sha2 string) (string, error) {
157153
"-c", "log.showSignature=false", "log", "--no-color",
158154
"--format=%h (%aN, %ar)%n%w(78,3,3)%s%n%+b",
159155
"--cherry", shaRange)
160-
outputs, err := cmd.Output()
156+
outputs, err := utils.PrepareCmd(cmd).Output()
161157
if err != nil {
162158
return "", fmt.Errorf("Can't load git log %s..%s", sha1, sha2)
163159
}
@@ -167,14 +163,13 @@ func Log(sha1, sha2 string) (string, error) {
167163

168164
func listRemotes() ([]string, error) {
169165
remoteCmd := exec.Command("git", "remote", "-v")
170-
remoteCmd.Stderr = nil
171-
output, err := remoteCmd.Output()
166+
output, err := utils.PrepareCmd(remoteCmd).Output()
172167
return outputLines(output), err
173168
}
174169

175170
func Config(name string) (string, error) {
176171
configCmd := exec.Command("git", "config", name)
177-
output, err := configCmd.Output()
172+
output, err := utils.PrepareCmd(configCmd).Output()
178173
if err != nil {
179174
return "", fmt.Errorf("unknown config key: %s", name)
180175
}
@@ -190,21 +185,16 @@ func ConfigAll(name string) ([]string, error) {
190185
}
191186

192187
configCmd := exec.Command("git", "config", mode, name)
193-
output, err := configCmd.Output()
188+
output, err := utils.PrepareCmd(configCmd).Output()
194189
if err != nil {
195190
return nil, fmt.Errorf("Unknown config %s", name)
196191
}
197192
return outputLines(output), nil
198193
}
199194

200-
func Run(args ...string) error {
201-
cmd := exec.Command("git", args...)
202-
return cmd.Run()
203-
}
204-
205195
func LocalBranches() ([]string, error) {
206196
branchesCmd := exec.Command("git", "branch", "--list")
207-
output, err := branchesCmd.Output()
197+
output, err := utils.PrepareCmd(branchesCmd).Output()
208198
if err != nil {
209199
return nil, err
210200
}
@@ -218,9 +208,6 @@ func LocalBranches() ([]string, error) {
218208

219209
func outputLines(output []byte) []string {
220210
lines := strings.TrimSuffix(string(output), "\n")
221-
if lines == "" {
222-
return []string{}
223-
}
224211
return strings.Split(lines, "\n")
225212

226213
}

utils/prepare_cmd.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package utils
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"os/exec"
7+
"strings"
8+
)
9+
10+
// Runnable is typically an exec.Cmd or its stub in tests
11+
type Runnable interface {
12+
Output() ([]byte, error)
13+
Run() error
14+
}
15+
16+
// PrepareCmd extends exec.Cmd with extra error reporting features and provides a
17+
// hook to stub command execution in tests
18+
var PrepareCmd = func(cmd *exec.Cmd) Runnable {
19+
return &cmdWithStderr{cmd}
20+
}
21+
22+
// SetPrepareCmd overrides PrepareCmd and returns a func to revert it back
23+
func SetPrepareCmd(fn func(*exec.Cmd) Runnable) func() {
24+
origPrepare := PrepareCmd
25+
PrepareCmd = fn
26+
return func() {
27+
PrepareCmd = origPrepare
28+
}
29+
}
30+
31+
// cmdWithStderr augments Output() by adding stderr to the error message
32+
type cmdWithStderr struct {
33+
*exec.Cmd
34+
}
35+
36+
func (c cmdWithStderr) Output() ([]byte, error) {
37+
errStream := &bytes.Buffer{}
38+
c.Cmd.Stderr = errStream
39+
out, err := c.Cmd.Output()
40+
if err != nil {
41+
err = &CmdError{errStream, c.Cmd.Args, err}
42+
}
43+
return out, err
44+
}
45+
46+
// CmdError provides more visibility into why an exec.Cmd had failed
47+
type CmdError struct {
48+
Stderr *bytes.Buffer
49+
Args []string
50+
Err error
51+
}
52+
53+
func (e CmdError) Error() string {
54+
msg := e.Stderr.String()
55+
if msg != "" && !strings.HasSuffix(msg, "\n") {
56+
msg += "\n"
57+
}
58+
return fmt.Sprintf("%s%s: %s", msg, e.Args[0], e.Err)
59+
}

0 commit comments

Comments
 (0)
X Tutup