Skip to content

Commit 3765b9b

Browse files
authored
add: offer to initialize a stack automatically (#486)
* offer to initialize stack when running add from outside a stack * fetch trunk if missing locally * disambiguate local trunk branch during init
1 parent cf65746 commit 3765b9b

7 files changed

Lines changed: 476 additions & 44 deletions

File tree

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,9 @@ Add a new branch on top of the current stack.
106106
gh stack add [flags] [branch]
107107
```
108108

109-
Creates a new branch at the current HEAD, adds it to the top of the stack, and checks it out. Must be run while on the topmost branch of a stack. If no branch name is given, prompts for one.
109+
For an existing stack, creates a new branch at the current HEAD, adds it to the top of the stack, and checks it out. Must be run while on the topmost branch of a stack. If no branch name is given, prompts for one.
110+
111+
When run interactively from a branch that is not part of a stack, `add` offers to initialize a new stack instead. The supplied or auto-generated branch name becomes the first layer; without one, the standard `init` prompts are used.
110112

111113
You can optionally stage changes and create a commit as part of the `add` flow. When `-m` is provided without an explicit branch name, the branch name is auto-generated in date+slug format (e.g., `03-24-add_login`).
112114

cmd/add.go

Lines changed: 88 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cmd
33
import (
44
"fmt"
55

6+
"github.com/cli/go-gh/v2/pkg/prompter"
67
"github.com/github/gh-stack/internal/branch"
78
"github.com/github/gh-stack/internal/config"
89
"github.com/github/gh-stack/internal/git"
@@ -59,7 +60,7 @@ func runAdd(cfg *config.Config, opts *addOptions, args []string) error {
5960
return ErrInvalidArgs
6061
}
6162

62-
result, err := loadStack(cfg, "")
63+
result, err := loadStackOptional(cfg, "")
6364
if err != nil {
6465
return ErrNotInStack
6566
}
@@ -70,6 +71,14 @@ func runAdd(cfg *config.Config, opts *addOptions, args []string) error {
7071
return ErrModifyRecovery
7172
}
7273

74+
if result.Stack == nil {
75+
branchName, err := addBranchNameFromArgs(cfg, opts, args)
76+
if err != nil {
77+
return err
78+
}
79+
return initializeStackFromAdd(cfg, opts, branchName, result.CurrentBranch)
80+
}
81+
7382
sf := result.StackFile
7483
s := result.Stack
7584
currentBranch := result.CurrentBranch
@@ -122,21 +131,11 @@ func runAdd(cfg *config.Config, opts *addOptions, args []string) error {
122131
// explicit name -> used verbatim
123132
// -m without a name -> auto-generated from the commit message
124133
// neither -> prompt for a name
125-
var branchName string
126-
var explicitName string
127-
if len(args) > 0 {
128-
explicitName = args[0]
134+
branchName, err := addBranchNameFromArgs(cfg, opts, args)
135+
if err != nil {
136+
return err
129137
}
130-
131-
if explicitName != "" {
132-
branchName = explicitName
133-
} else if opts.message != "" {
134-
branchName = branch.DateSlug(opts.message)
135-
if branchName == "" {
136-
cfg.Errorf("could not generate branch name")
137-
return ErrSilent
138-
}
139-
} else {
138+
if branchName == "" {
140139
// No -m and no explicit name — prompt for one.
141140
for {
142141
input, err := promptInput(cfg, "Enter a name for the new branch:")
@@ -242,6 +241,80 @@ func runAdd(cfg *config.Config, opts *addOptions, args []string) error {
242241
return nil
243242
}
244243

244+
func addBranchNameFromArgs(cfg *config.Config, opts *addOptions, args []string) (string, error) {
245+
if len(args) > 0 && args[0] != "" {
246+
return args[0], nil
247+
}
248+
if opts.message == "" {
249+
return "", nil
250+
}
251+
252+
branchName := branch.DateSlug(opts.message)
253+
if branchName == "" {
254+
cfg.Errorf("could not generate branch name")
255+
return "", ErrSilent
256+
}
257+
return branchName, nil
258+
}
259+
260+
func initializeStackFromAdd(cfg *config.Config, opts *addOptions, branchName, currentBranch string) error {
261+
if !cfg.IsInteractive() {
262+
reportBranchNotInStack(cfg, currentBranch, false)
263+
return ErrNotInStack
264+
}
265+
266+
prompt := "Would you like to initialize a new stack?"
267+
var confirmed bool
268+
var err error
269+
if cfg.ConfirmFn != nil {
270+
confirmed, err = cfg.ConfirmFn(prompt, true)
271+
} else {
272+
p := prompter.New(cfg.In, cfg.Out, cfg.Err)
273+
confirmed, err = p.Confirm(prompt, true)
274+
}
275+
if err != nil {
276+
if isInterruptError(err) {
277+
printInterrupt(cfg)
278+
return ErrSilent
279+
}
280+
cfg.Errorf("failed to read confirmation: %s", err)
281+
return ErrSilent
282+
}
283+
if !confirmed {
284+
reportBranchNotInStack(cfg, currentBranch, false)
285+
return ErrNotInStack
286+
}
287+
288+
wantsCommit := opts.message != "" || opts.stageAll || opts.stageTracked
289+
if wantsCommit {
290+
if err := stageAndValidate(cfg, opts); err != nil {
291+
return ErrSilent
292+
}
293+
}
294+
295+
initOpts := &initOptions{}
296+
if branchName != "" {
297+
initOpts.branches = []string{branchName}
298+
}
299+
if err := runInit(cfg, initOpts); err != nil {
300+
return err
301+
}
302+
303+
if wantsCommit {
304+
sha, err := doCommit(opts.message)
305+
if err != nil {
306+
cfg.Errorf("failed to commit: %s", err)
307+
return ErrSilent
308+
}
309+
if branchName == "" {
310+
branchName, _ = git.CurrentBranch()
311+
}
312+
cfg.Successf("Created commit %s on %s", cfg.ColorBold(sha), branchName)
313+
}
314+
315+
return nil
316+
}
317+
245318
// stageAndValidate stages files (if -A or -u is set) and verifies there are
246319
// staged changes to commit. Prints a user-facing error and returns non-nil
247320
// if staging fails or there is nothing to commit.

0 commit comments

Comments
 (0)