sourcegraph/internal/execute/git.go
Bolaji Olajide 067115910c
fix(ci): check command out for error when git fails (#63993)
Closes [#1110](https://github.com/sourcegraph/devx-support/issues/1110)
Closes DINF-96

We don't print the stdErr when a command fails … in particular when git
fails. Therefore we see very little in the panic of what went wrong.

Explanation:
> There's a weird behavior that occurs where an error isn't accessible
in the err variable
// from a *Cmd executing a git command after calling CombinedOutput().
// This occurs due to how Git handles errors and how the exec package in
Go interprets the command's output.
// Git often writes error messages to stderr, but it might still exit
with a status code of 0 (indicating success).
// In this case, CombinedOutput() won't return an error, but the error
message will be in the out variable.

## Test plan

Manual testing

```go
func main() {
	ctx := context.Background()
	cmd := exec.CommandContext(ctx, "git", "rev-parse", "--is-inside-work-tree")
	out, err := handleGitCommandExec(cmd)
	if err != nil {
		// er := errors.Wrap(err, fmt.Sprintf("idsdsd: %s", string(out)))
		panic(err)
	}
	fmt.Println("hello", string(out))
}
```

## Changelog

<!-- OPTIONAL; info at
https://www.notion.so/sourcegraph/Writing-a-changelog-entry-dd997f411d524caabf0d8d38a24a878c
-->
2024-07-23 09:56:33 -05:00

84 lines
2.0 KiB
Go

package execute
import (
"bytes"
"context"
"os"
"os/exec"
"strings"
"github.com/sourcegraph/sourcegraph/lib/errors"
)
type cmdErr struct {
err error
exitCode int
}
func (g *cmdErr) Error() string {
return g.err.Error()
}
func (g *cmdErr) ExitCode() int {
return g.exitCode
}
func (g *cmdErr) Unwrap() error {
return g.err
}
// HandleGitCommandExec There's a weird behavior that occurs where an error isn't accessible in the err variable
// from a *Cmd executing a git command after calling CombinedOutput().
// This occurs due to how Git handles errors and how the exec package in Go interprets the command's output.
// Git often writes error messages to stderr, but it might still exit with a status code of 0 (indicating success).
// In this case, CombinedOutput() won't return an error, but the error message will be in the out variable.
func handleGitCommandExec(cmd *exec.Cmd) ([]byte, error) {
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
maybeErrMessage := strings.Trim(stderr.String(), "\n")
if strings.HasPrefix(maybeErrMessage, "fatal:") || strings.HasPrefix(maybeErrMessage, "error:") {
exitCode := 1
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
exitCode = exitErr.ExitCode()
}
return nil, &cmdErr{
err: errors.New(maybeErrMessage),
exitCode: exitCode,
}
}
return nil, err
}
return stdout.Bytes(), nil
}
func Git(ctx context.Context, args ...string) ([]byte, error) {
return handleGitCommandExec(GitCmd(ctx, args...))
}
func GitCmd(ctx context.Context, args ...string) *exec.Cmd {
return exec.CommandContext(ctx, "git", args...)
}
func GHCmd(ctx context.Context, args ...string) *exec.Cmd {
return exec.CommandContext(ctx, "gh", args...)
}
func GH(ctx context.Context, args ...string) ([]byte, error) {
cmd := GHCmd(ctx, args...)
var stdout bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
if err := cmd.Run(); err != nil {
return nil, err
}
return stdout.Bytes(), nil
}