Skip to content

Commit a813158

Browse files
committed
include prefix in branch name input
1 parent 276321f commit a813158

3 files changed

Lines changed: 156 additions & 4 deletions

File tree

cmd/add.go

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ package cmd
33
import (
44
"fmt"
55

6-
"github.com/cli/go-gh/v2/pkg/prompter"
6+
"github.com/AlecAivazis/survey/v2/terminal"
77
"github.com/github/gh-stack/internal/branch"
88
"github.com/github/gh-stack/internal/config"
99
"github.com/github/gh-stack/internal/git"
1010
"github.com/github/gh-stack/internal/modify"
1111
"github.com/github/gh-stack/internal/stack"
12+
"github.com/mgutz/ansi"
1213
"github.com/spf13/cobra"
1314
)
1415

@@ -146,9 +147,14 @@ func runAdd(cfg *config.Config, opts *addOptions, args []string) error {
146147
if s.Numbered && s.Prefix != "" {
147148
branchName = branch.NextNumberedName(s.Prefix, existingBranches)
148149
} else {
149-
p := prompter.New(cfg.In, cfg.Out, cfg.Err)
150+
// Pre-fill the prompt with the prefix so the user can see
151+
// (and optionally edit) the full branch name.
152+
prefill := ""
153+
if s.Prefix != "" {
154+
prefill = s.Prefix + "/"
155+
}
150156
for {
151-
input, err := p.Input("Enter a name for the new branch", "")
157+
input, err := inputWithPrefill(cfg, "Enter a name for the new branch", prefill)
152158
if err != nil {
153159
if isInterruptError(err) {
154160
printInterrupt(cfg)
@@ -160,7 +166,7 @@ func runAdd(cfg *config.Config, opts *addOptions, args []string) error {
160166
cfg.Warningf("branch name cannot be empty, please try again")
161167
continue
162168
}
163-
branchName = applyPrefix(cfg, s.Prefix, input)
169+
branchName = input
164170
break
165171
}
166172
}
@@ -276,3 +282,34 @@ func applyPrefix(cfg *config.Config, prefix, name string) string {
276282
}
277283
return name
278284
}
285+
286+
// inputWithPrefill prompts the user for text input with the given prefill
287+
// already editable in the input field. Unlike survey.Input's Default (which
288+
// shows in parentheses), this places the prefill text directly in the
289+
// editable line so the user can append to or modify it.
290+
func inputWithPrefill(cfg *config.Config, prompt, prefill string) (string, error) {
291+
if cfg.InputFn != nil {
292+
return cfg.InputFn(prompt, prefill)
293+
}
294+
295+
stdio := terminal.Stdio{In: cfg.In, Out: cfg.Out, Err: cfg.Err}
296+
rr := terminal.NewRuneReader(stdio)
297+
_ = rr.SetTermMode()
298+
defer func() { _ = rr.RestoreTermMode() }()
299+
300+
// Render the prompt in survey style: green bold "?" + bold message
301+
icon := "?"
302+
if cfg.Terminal.IsColorEnabled() {
303+
icon = ansi.Color("?", "green+hb")
304+
}
305+
fmt.Fprintf(cfg.Out, "%s %s ", icon, prompt)
306+
307+
line, err := rr.ReadLineWithDefault(0, []rune(prefill))
308+
// Move to a new line after the input
309+
fmt.Fprintln(cfg.Out)
310+
311+
if err != nil {
312+
return "", err
313+
}
314+
return string(line), nil
315+
}

cmd/add_test.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,117 @@ func TestAdd_NothingToCommit(t *testing.T) {
321321
assert.Contains(t, output, "no changes to commit")
322322
}
323323

324+
func TestAdd_PromptPrefillsPrefix(t *testing.T) {
325+
gitDir := t.TempDir()
326+
saveStack(t, gitDir, stack.Stack{
327+
Prefix: "feat",
328+
Trunk: stack.BranchRef{Branch: "main"},
329+
Branches: []stack.BranchRef{{Branch: "feat/01"}},
330+
})
331+
332+
var createdBranch string
333+
restore := git.SetOps(&git.MockOps{
334+
GitDirFn: func() (string, error) { return gitDir, nil },
335+
CurrentBranchFn: func() (string, error) { return "feat/01", nil },
336+
CreateBranchFn: func(name, base string) error {
337+
createdBranch = name
338+
return nil
339+
},
340+
CheckoutBranchFn: func(name string) error { return nil },
341+
RevParseFn: func(ref string) (string, error) { return "abc", nil },
342+
})
343+
defer restore()
344+
345+
cfg, outR, errR := config.NewTestConfig()
346+
347+
var gotDefault string
348+
cfg.InputFn = func(prompt, defaultValue string) (string, error) {
349+
gotDefault = defaultValue
350+
return "feat/my-branch", nil
351+
}
352+
353+
err := runAdd(cfg, &addOptions{}, nil)
354+
output := collectOutput(cfg, outR, errR)
355+
356+
require.NoError(t, err)
357+
require.NotContains(t, output, "\u2717", "unexpected error")
358+
assert.Equal(t, "feat/", gotDefault, "prompt should pre-fill prefix/")
359+
assert.Equal(t, "feat/my-branch", createdBranch, "full input should be used as branch name")
360+
}
361+
362+
func TestAdd_PromptNoPrefixEmptyDefault(t *testing.T) {
363+
gitDir := t.TempDir()
364+
saveStack(t, gitDir, stack.Stack{
365+
Trunk: stack.BranchRef{Branch: "main"},
366+
Branches: []stack.BranchRef{{Branch: "b1"}},
367+
})
368+
369+
var createdBranch string
370+
restore := git.SetOps(&git.MockOps{
371+
GitDirFn: func() (string, error) { return gitDir, nil },
372+
CurrentBranchFn: func() (string, error) { return "b1", nil },
373+
CreateBranchFn: func(name, base string) error {
374+
createdBranch = name
375+
return nil
376+
},
377+
CheckoutBranchFn: func(name string) error { return nil },
378+
RevParseFn: func(ref string) (string, error) { return "abc", nil },
379+
})
380+
defer restore()
381+
382+
cfg, outR, errR := config.NewTestConfig()
383+
384+
var gotDefault string
385+
cfg.InputFn = func(prompt, defaultValue string) (string, error) {
386+
gotDefault = defaultValue
387+
return "my-branch", nil
388+
}
389+
390+
err := runAdd(cfg, &addOptions{}, nil)
391+
output := collectOutput(cfg, outR, errR)
392+
393+
require.NoError(t, err)
394+
require.NotContains(t, output, "\u2717", "unexpected error")
395+
assert.Equal(t, "", gotDefault, "prompt should have empty default when no prefix")
396+
assert.Equal(t, "my-branch", createdBranch, "input should be used as-is")
397+
}
398+
399+
func TestAdd_PromptUserModifiesPrefix(t *testing.T) {
400+
gitDir := t.TempDir()
401+
saveStack(t, gitDir, stack.Stack{
402+
Prefix: "feat",
403+
Trunk: stack.BranchRef{Branch: "main"},
404+
Branches: []stack.BranchRef{{Branch: "feat/01"}},
405+
})
406+
407+
var createdBranch string
408+
restore := git.SetOps(&git.MockOps{
409+
GitDirFn: func() (string, error) { return gitDir, nil },
410+
CurrentBranchFn: func() (string, error) { return "feat/01", nil },
411+
CreateBranchFn: func(name, base string) error {
412+
createdBranch = name
413+
return nil
414+
},
415+
CheckoutBranchFn: func(name string) error { return nil },
416+
RevParseFn: func(ref string) (string, error) { return "abc", nil },
417+
})
418+
defer restore()
419+
420+
cfg, outR, errR := config.NewTestConfig()
421+
422+
cfg.InputFn = func(prompt, defaultValue string) (string, error) {
423+
// Simulate user changing the prefix entirely
424+
return "custom/other-name", nil
425+
}
426+
427+
err := runAdd(cfg, &addOptions{}, nil)
428+
output := collectOutput(cfg, outR, errR)
429+
430+
require.NoError(t, err)
431+
require.NotContains(t, output, "\u2717", "unexpected error")
432+
assert.Equal(t, "custom/other-name", createdBranch, "user-modified input should be used verbatim")
433+
}
434+
324435
func TestAdd_FromTrunk(t *testing.T) {
325436
gitDir := t.TempDir()
326437
saveStack(t, gitDir, stack.Stack{

internal/config/config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ type Config struct {
4242
// ConfirmFn, when non-nil, is called instead of prompting via the
4343
// terminal. Used in tests to simulate yes/no confirmation prompts.
4444
ConfirmFn func(prompt string, defaultValue bool) (bool, error)
45+
46+
// InputFn, when non-nil, is called instead of prompting via the
47+
// terminal. Used in tests to simulate text input prompts.
48+
InputFn func(prompt, defaultValue string) (string, error)
4549
}
4650

4751
// New creates a new Config with terminal-aware output and color support.

0 commit comments

Comments
 (0)