-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathexec.go
More file actions
162 lines (148 loc) · 4.79 KB
/
Copy pathexec.go
File metadata and controls
162 lines (148 loc) · 4.79 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
package git
import (
"bytes"
"context"
"fmt"
"os"
"os/exec"
"strings"
"time"
"github.com/b404dev/gitm8/internal/logging"
)
// run executes git and ignores normal output, but still returns useful errors.
func (r Runner) run(ctx context.Context, args ...string) error {
_, err := r.output(ctx, args...)
return err
}
// output executes git and returns normal output for the UI to show.
func (r Runner) output(ctx context.Context, args ...string) (string, error) {
return r.commandOutput(ctx, "git", args...)
}
// commandOutput starts a git, gh, or helper process, captures output, and turns
// failures into readable errors.
func (r Runner) commandOutput(ctx context.Context, name string, args ...string) (string, error) {
return r.commandOutputEnv(ctx, nil, name, args...)
}
// commandOutputEnv is commandOutput with extra environment variables appended.
// The squash workflow uses it to point git's sequence and commit editors at
// non-interactive helpers so rebasing never opens an external editor.
func (r Runner) commandOutputEnv(ctx context.Context, extraEnv []string, name string, args ...string) (string, error) {
cmd := exec.CommandContext(ctx, name, args...)
if r.Dir != "" {
cmd.Dir = r.Dir
}
env := os.Environ()
if len(extraEnv) > 0 {
env = append(env, extraEnv...)
}
start := time.Now()
logging.Debug("git", "commandOutput", "command_start",
logging.F("name", name),
logging.F("args", safeCommandArgs(args)),
logging.F("dir", r.Dir),
)
// gh commands are used from inside the TUI, where interactive prompts
// would look broken. Fail fast instead and show the user the CLI message.
if name == "gh" {
env = append(env, "GH_PROMPT_DISABLED=1")
}
if len(env) > 0 {
cmd.Env = env
}
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
detail := strings.TrimSpace(combinedOutput(stdout.String(), stderr.String()))
if detail == "" {
detail = err.Error()
}
logging.Error("git", "commandOutput", "command_failed",
logging.F("name", name),
logging.F("args", safeCommandArgs(args)),
logging.F("duration_ms", time.Since(start).Milliseconds()),
logging.F("stdout_bytes", stdout.Len()),
logging.F("stderr_bytes", stderr.Len()),
logging.F("error", detail),
)
return "", fmt.Errorf("%s %s: %s", name, strings.Join(args, " "), detail)
}
logging.Debug("git", "commandOutput", "command_complete",
logging.F("name", name),
logging.F("args", safeCommandArgs(args)),
logging.F("duration_ms", time.Since(start).Milliseconds()),
logging.F("stdout_bytes", stdout.Len()),
logging.F("stderr_bytes", stderr.Len()),
)
return stdout.String(), nil
}
// commandInputOutput is commandOutput with stdin attached.
func (r Runner) commandInputOutput(ctx context.Context, input string, name string, args ...string) (string, error) {
cmd := exec.CommandContext(ctx, name, args...)
if r.Dir != "" {
cmd.Dir = r.Dir
}
cmd.Stdin = strings.NewReader(input)
start := time.Now()
logging.Info("git", "commandInputOutput", "command_start",
logging.F("name", name),
logging.F("args", safeCommandArgs(args)),
logging.F("dir", r.Dir),
logging.F("stdin_bytes", len(input)),
)
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
detail := strings.TrimSpace(combinedOutput(stdout.String(), stderr.String()))
if detail == "" {
detail = err.Error()
}
logging.Error("git", "commandInputOutput", "command_failed",
logging.F("name", name),
logging.F("args", safeCommandArgs(args)),
logging.F("duration_ms", time.Since(start).Milliseconds()),
logging.F("stdin_bytes", len(input)),
logging.F("stdout_bytes", stdout.Len()),
logging.F("stderr_bytes", stderr.Len()),
logging.F("error", detail),
)
return "", fmt.Errorf("%s %s: %s", name, strings.Join(args, " "), detail)
}
logging.Info("git", "commandInputOutput", "command_complete",
logging.F("name", name),
logging.F("args", safeCommandArgs(args)),
logging.F("duration_ms", time.Since(start).Milliseconds()),
logging.F("stdin_bytes", len(input)),
logging.F("stdout_bytes", stdout.Len()),
logging.F("stderr_bytes", stderr.Len()),
)
return stdout.String(), nil
}
// bestEffort reads optional Git details. If Git fails, it returns an empty string.
func (r Runner) bestEffort(ctx context.Context, args ...string) string {
out, err := r.output(ctx, args...)
if err != nil {
return ""
}
return out
}
func safeCommandArgs(args []string) string {
safe := make([]string, 0, len(args))
redactNext := false
for _, arg := range args {
if redactNext {
safe = append(safe, "[text]")
redactNext = false
continue
}
safe = append(safe, arg)
switch arg {
case "-m", "--message", "--title", "--body":
redactNext = true
}
}
return strings.Join(safe, " ")
}