|
1 | 1 | package cmd |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "errors" |
4 | 5 | "fmt" |
5 | 6 |
|
| 7 | + "github.com/AlecAivazis/survey/v2/terminal" |
6 | 8 | "github.com/cli/go-gh/v2/pkg/prompter" |
7 | 9 | "github.com/github/gh-stack/internal/config" |
8 | 10 | "github.com/github/gh-stack/internal/git" |
9 | 11 | "github.com/github/gh-stack/internal/stack" |
10 | 12 | ) |
11 | 13 |
|
| 14 | +// errInterrupt is a sentinel returned when a prompt is cancelled via Ctrl+C. |
| 15 | +// Callers should exit silently (the friendly message is already printed). |
| 16 | +var errInterrupt = errors.New("interrupt") |
| 17 | + |
| 18 | +// isInterruptError reports whether err is (or wraps) the survey interrupt, |
| 19 | +// which is raised when the user presses Ctrl+C during a prompt. |
| 20 | +func isInterruptError(err error) bool { |
| 21 | + return errors.Is(err, terminal.InterruptErr) |
| 22 | +} |
| 23 | + |
| 24 | +// printInterrupt prints a friendly message and should be called exactly once |
| 25 | +// per interrupted operation. The leading newline ensures the message starts |
| 26 | +// on its own line even if the cursor was mid-prompt. |
| 27 | +func printInterrupt(cfg *config.Config) { |
| 28 | + fmt.Fprintln(cfg.Err) |
| 29 | + cfg.Infof("Received interrupt, aborting operation") |
| 30 | +} |
| 31 | + |
12 | 32 | // loadStackResult holds everything returned by loadStack. |
13 | 33 | type loadStackResult struct { |
14 | 34 | GitDir string |
@@ -46,6 +66,9 @@ func loadStack(cfg *config.Config, branch string) (*loadStackResult, error) { |
46 | 66 |
|
47 | 67 | s, err := resolveStack(sf, branch, cfg) |
48 | 68 | if err != nil { |
| 69 | + if errors.Is(err, errInterrupt) { |
| 70 | + return nil, errInterrupt |
| 71 | + } |
49 | 72 | cfg.Errorf("%s", err) |
50 | 73 | return nil, err |
51 | 74 | } |
@@ -105,6 +128,10 @@ func resolveStack(sf *stack.StackFile, branch string, cfg *config.Config) (*stac |
105 | 128 | p := prompter.New(cfg.In, cfg.Out, cfg.Err) |
106 | 129 | selected, err := p.Select("Which stack would you like to use?", "", options) |
107 | 130 | if err != nil { |
| 131 | + if isInterruptError(err) { |
| 132 | + printInterrupt(cfg) |
| 133 | + return nil, errInterrupt |
| 134 | + } |
108 | 135 | return nil, fmt.Errorf("stack selection: %w", err) |
109 | 136 | } |
110 | 137 |
|
@@ -217,30 +244,37 @@ func activeBranchNames(s *stack.Stack) []string { |
217 | 244 | // user for permission before enabling it. If the user previously declined, |
218 | 245 | // the prompt is suppressed. In non-interactive sessions the function is a |
219 | 246 | // no-op so commands can still run in CI/scripting. |
220 | | -func ensureRerere(cfg *config.Config) { |
| 247 | +// |
| 248 | +// Returns errInterrupt if the user pressed Ctrl+C during the prompt. |
| 249 | +func ensureRerere(cfg *config.Config) error { |
221 | 250 | enabled, err := git.IsRerereEnabled() |
222 | 251 | if err != nil || enabled { |
223 | | - return |
| 252 | + return nil |
224 | 253 | } |
225 | 254 |
|
226 | 255 | declined, _ := git.IsRerereDeclined() |
227 | 256 | if declined { |
228 | | - return |
| 257 | + return nil |
229 | 258 | } |
230 | 259 |
|
231 | 260 | if !cfg.IsInteractive() { |
232 | | - return |
| 261 | + return nil |
233 | 262 | } |
234 | 263 |
|
235 | 264 | p := prompter.New(cfg.In, cfg.Out, cfg.Err) |
236 | 265 | ok, err := p.Confirm("Enable git rerere to remember conflict resolutions?", true) |
237 | 266 | if err != nil { |
238 | | - return |
| 267 | + if isInterruptError(err) { |
| 268 | + printInterrupt(cfg) |
| 269 | + return errInterrupt |
| 270 | + } |
| 271 | + return nil |
239 | 272 | } |
240 | 273 |
|
241 | 274 | if ok { |
242 | 275 | _ = git.EnableRerere() |
243 | 276 | } else { |
244 | 277 | _ = git.SaveRerereDeclined() |
245 | 278 | } |
| 279 | + return nil |
246 | 280 | } |
0 commit comments