Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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