Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,9 @@ Add a new branch on top of the current stack.
gh stack add [flags] [branch]
```

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.
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.

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.

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`).

Expand Down
103 changes: 88 additions & 15 deletions cmd/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cmd
import (
"fmt"

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

result, err := loadStack(cfg, "")
result, err := loadStackOptional(cfg, "")
if err != nil {
return ErrNotInStack
}
Expand All @@ -70,6 +71,14 @@ func runAdd(cfg *config.Config, opts *addOptions, args []string) error {
return ErrModifyRecovery
}

if result.Stack == nil {
branchName, err := addBranchNameFromArgs(cfg, opts, args)
if err != nil {
return err
}
return initializeStackFromAdd(cfg, opts, branchName, result.CurrentBranch)
}

sf := result.StackFile
s := result.Stack
currentBranch := result.CurrentBranch
Expand Down Expand Up @@ -122,21 +131,11 @@ func runAdd(cfg *config.Config, opts *addOptions, args []string) error {
// explicit name -> used verbatim
// -m without a name -> auto-generated from the commit message
// neither -> prompt for a name
var branchName string
var explicitName string
if len(args) > 0 {
explicitName = args[0]
branchName, err := addBranchNameFromArgs(cfg, opts, args)
if err != nil {
return err
}

if explicitName != "" {
branchName = explicitName
} else if opts.message != "" {
branchName = branch.DateSlug(opts.message)
if branchName == "" {
cfg.Errorf("could not generate branch name")
return ErrSilent
}
} else {
if branchName == "" {
// No -m and no explicit name — prompt for one.
for {
input, err := promptInput(cfg, "Enter a name for the new branch:")
Expand Down Expand Up @@ -242,6 +241,80 @@ func runAdd(cfg *config.Config, opts *addOptions, args []string) error {
return nil
}

func addBranchNameFromArgs(cfg *config.Config, opts *addOptions, args []string) (string, error) {
if len(args) > 0 && args[0] != "" {
return args[0], nil
}
if opts.message == "" {
return "", nil
}

branchName := branch.DateSlug(opts.message)
if branchName == "" {
cfg.Errorf("could not generate branch name")
return "", ErrSilent
}
return branchName, nil
}

func initializeStackFromAdd(cfg *config.Config, opts *addOptions, branchName, currentBranch string) error {
if !cfg.IsInteractive() {
reportBranchNotInStack(cfg, currentBranch, false)
return ErrNotInStack
}

prompt := "Would you like to initialize a new stack?"
var confirmed bool
var err error
if cfg.ConfirmFn != nil {
confirmed, err = cfg.ConfirmFn(prompt, true)
} else {
p := prompter.New(cfg.In, cfg.Out, cfg.Err)
confirmed, err = p.Confirm(prompt, true)
}
if err != nil {
if isInterruptError(err) {
printInterrupt(cfg)
return ErrSilent
}
cfg.Errorf("failed to read confirmation: %s", err)
return ErrSilent
}
if !confirmed {
reportBranchNotInStack(cfg, currentBranch, false)
return ErrNotInStack
}

wantsCommit := opts.message != "" || opts.stageAll || opts.stageTracked
if wantsCommit {
if err := stageAndValidate(cfg, opts); err != nil {
return ErrSilent
}
}

initOpts := &initOptions{}
if branchName != "" {
initOpts.branches = []string{branchName}
}
if err := runInit(cfg, initOpts); err != nil {
return err
}

if wantsCommit {
sha, err := doCommit(opts.message)
if err != nil {
cfg.Errorf("failed to commit: %s", err)
return ErrSilent
}
if branchName == "" {
branchName, _ = git.CurrentBranch()
}
cfg.Successf("Created commit %s on %s", cfg.ColorBold(sha), branchName)
}

return nil
}

// stageAndValidate stages files (if -A or -u is set) and verifies there are
// staged changes to commit. Prints a user-facing error and returns non-nil
// if staging fails or there is nothing to commit.
Expand Down
Loading