Skip to content

Commit c1aa1f2

Browse files
committed
abbreviated workflow to quickly add to stacks
1 parent 07ea259 commit c1aa1f2

6 files changed

Lines changed: 437 additions & 15 deletions

File tree

README.md

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,15 @@ Initialize a new stack in the current repository.
5959
gh stack init [branches...] [flags]
6060
```
6161

62-
Creates an entry in `.git/gh-stack` to track stack state. In interactive mode (no arguments), prompts you to name branches and offers to use the current branch as the first layer. When explicit branch names are given, creates any that don't already exist (branching from the trunk). The trunk defaults to the repository's default branch unless overridden with `--base`.
62+
Creates an entry in `.git/gh-stack` to track stack state. In interactive mode (no arguments), prompts you to name branches and offers to use the current branch as the first layer. In interactive mode, you'll also be prompted to set an optional branch prefix for auto-naming (unless adopting existing branches). When explicit branch names are given, creates any that don't already exist (branching from the trunk). The trunk defaults to the repository's default branch unless overridden with `--base`.
6363

6464
Enables `git rerere` automatically so that conflict resolutions are remembered across rebases.
6565

6666
| Flag | Description |
6767
|------|-------------|
6868
| `-b, --base <branch>` | Trunk branch for the stack (defaults to the repository's default branch) |
6969
| `-a, --adopt` | Adopt existing branches into a stack instead of creating new ones |
70+
| `-p, --prefix <string>` | Set a branch name prefix for the stack |
7071

7172
**Examples:**
7273

@@ -82,23 +83,57 @@ gh stack init --base develop feature-auth
8283

8384
# Adopt existing branches into a stack
8485
gh stack init --adopt feature-auth feature-api
86+
87+
# Set a prefix for auto-naming branches
88+
gh stack init -p feat
8589
```
8690

8791
### `gh stack add`
8892

8993
Add a new branch on top of the current stack.
9094

9195
```
92-
gh stack add [branch]
96+
gh stack add [branch] [flags]
9397
```
9498

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

101+
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. Auto-generated names use either numbered format (`prefix/01`, `prefix/02`) or date+slug format depending on prefix configuration and existing branch naming patterns.
102+
103+
| Flag | Description |
104+
|------|-------------|
105+
| `-a, --all` | Stage all changes (including untracked files); requires `-m` |
106+
| `-u, --update` | Stage changes to tracked files only; requires `-m` |
107+
| `-m, --message <string>` | Create a commit with this message before creating the branch |
108+
109+
> **Note:** `-a` and `-u` are mutually exclusive.
110+
97111
**Examples:**
98112

99113
```sh
114+
# Create a branch by name
100115
gh stack add api-routes
101-
gh stack add # prompts for name
116+
117+
# Prompt for a branch name interactively
118+
gh stack add
119+
120+
# Stage all changes, commit, and auto-generate the branch name
121+
gh stack add -am "Add login endpoint"
122+
123+
# Stage only tracked files, commit, and auto-generate the branch name
124+
gh stack add -um "Fix auth bug"
125+
126+
# Commit already-staged changes and auto-generate the branch name
127+
gh stack add -m "Add user model"
128+
129+
# Stage all changes, commit, and use an explicit branch name
130+
gh stack add -am "Add tests" test-layer
131+
132+
# Stage only tracked files, commit, and use an explicit branch name
133+
gh stack add -um "Update docs" docs-layer
134+
135+
# Commit already-staged changes and use an explicit branch name
136+
gh stack add -m "Refactor utils" cleanup-layer
102137
```
103138

104139
### `gh stack checkout`
@@ -355,3 +390,42 @@ gh stack push
355390
# 8. When the first PR is merged, sync the stack
356391
gh stack sync
357392
```
393+
394+
## Abbreviated workflow
395+
396+
If you want to minimize keystrokes, use a branch prefix and the `-am` flags to fold staging, committing, and branch creation into a single command. Branch names are auto-generated from your commit messages.
397+
398+
When a branch has no commits yet (e.g., right after `init`), `add -am` stages and commits directly on that branch instead of creating a new one. Once a branch has commits, `add -am` creates a new branch, checks it out, and commits there.
399+
400+
```sh
401+
# 1. Start a stack with a prefix
402+
gh stack init -p feat
403+
# → creates feat/01 and checks it out
404+
405+
# 2. Write code for the first layer
406+
# ... write code ...
407+
408+
# 3. Stage and commit on the current branch
409+
gh stack add -am "Auth middleware"
410+
# → feat/01 has no commits yet, so the commit lands here
411+
# (no new branch is created)
412+
413+
# 4. Write code for the next layer
414+
# ... write code ...
415+
416+
# 5. Create the next branch and commit
417+
gh stack add -am "API routes"
418+
# → feat/01 already has commits, so a new branch feat/02 is
419+
# created, checked out, and the commit lands there
420+
421+
# 6. Keep going
422+
# ... write code ...
423+
424+
gh stack add -am "Frontend components"
425+
# → feat/02 already has commits, creates feat/03 and commits there
426+
427+
# 7. Push everything and create PRs
428+
gh stack push
429+
```
430+
431+
Compared to the typical workflow, there's no need to name branches, run `git add`, or run `git commit` separately. Each `gh stack add -am "..."` does it all.

cmd/add.go

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

6+
"github.com/github/gh-stack/internal/branch"
67
"github.com/github/gh-stack/internal/config"
78
"github.com/github/gh-stack/internal/git"
89
"github.com/github/gh-stack/internal/stack"
910
"github.com/spf13/cobra"
1011
)
1112

13+
type addOptions struct {
14+
stageAll bool
15+
stageTracked bool
16+
message string
17+
}
18+
1219
func AddCmd(cfg *config.Config) *cobra.Command {
20+
opts := &addOptions{}
21+
1322
cmd := &cobra.Command{
1423
Use: "add [branch]",
1524
Short: "Add a new branch on top of the current stack",
16-
Args: cobra.MaximumNArgs(1),
25+
Long: `Add a new branch on top of the current stack.
26+
27+
Optionally stage changes and create a commit before creating the branch:
28+
-a Stage all changes (including untracked) before committing
29+
-u Stage tracked file changes before committing
30+
-m Create a commit with the given message
31+
32+
When -m is provided without an explicit branch name, the branch name
33+
is auto-generated based on the commit message and stack prefix.`,
34+
Args: cobra.MaximumNArgs(1),
1735
RunE: func(cmd *cobra.Command, args []string) error {
18-
return runAdd(cfg, args)
36+
return runAdd(cfg, opts, args)
1937
},
2038
}
39+
40+
cmd.Flags().BoolVarP(&opts.stageAll, "all", "a", false, "Stage all changes including untracked files")
41+
cmd.Flags().BoolVarP(&opts.stageTracked, "update", "u", false, "Stage changes to tracked files only")
42+
cmd.Flags().StringVarP(&opts.message, "message", "m", "", "Create a commit with this message")
43+
2144
return cmd
2245
}
2346

24-
func runAdd(cfg *config.Config, args []string) error {
47+
func runAdd(cfg *config.Config, opts *addOptions, args []string) error {
48+
// Validate flag combinations
49+
if opts.stageAll && opts.stageTracked {
50+
cfg.Errorf("flags -a and -u are mutually exclusive")
51+
return nil
52+
}
53+
if (opts.stageAll || opts.stageTracked) && opts.message == "" {
54+
cfg.Errorf("staging flags (-a, -u) require -m to create a commit")
55+
return nil
56+
}
57+
2558
gitDir, err := git.GitDir()
2659
if err != nil {
2760
cfg.Errorf("not a git repository")
@@ -69,14 +102,82 @@ func runAdd(cfg *config.Config, args []string) error {
69102
return nil
70103
}
71104

105+
// When -m is provided, check if the current branch is a stack branch with
106+
// no unique commits relative to its parent. If so, the commit should land
107+
// on this branch without creating a new one (e.g., right after init).
108+
var branchIsEmpty bool
109+
if opts.message != "" && idx >= 0 {
110+
parentBranch := s.ActiveBaseBranch(currentBranch)
111+
commits, _ := git.LogRange(parentBranch, currentBranch)
112+
branchIsEmpty = len(commits) == 0
113+
}
114+
115+
// Empty branch path: stage and commit here, don't create a new branch.
116+
if branchIsEmpty && opts.message != "" {
117+
if opts.stageAll {
118+
if err := git.StageAll(); err != nil {
119+
cfg.Errorf("failed to stage changes: %s", err)
120+
return nil
121+
}
122+
} else if opts.stageTracked {
123+
if err := git.StageTracked(); err != nil {
124+
cfg.Errorf("failed to stage changes: %s", err)
125+
return nil
126+
}
127+
}
128+
if !git.HasStagedChanges() {
129+
cfg.Errorf("nothing to commit; stage changes first or use -a/-u")
130+
return nil
131+
}
132+
sha, err := git.Commit(opts.message)
133+
if err != nil {
134+
cfg.Errorf("failed to commit: %s", err)
135+
return nil
136+
}
137+
cfg.Successf("Created commit %s on %s", cfg.ColorBold(sha[:7]), currentBranch)
138+
cfg.Warningf("Branch %s has no prior commits — adding your commit here instead of creating a new branch", currentBranch)
139+
cfg.Printf("When you're ready for the next layer, run %s again", cfg.ColorCyan("gh stack add"))
140+
return nil
141+
}
142+
143+
// Resolve branch name
72144
var branchName string
145+
var explicitName string
73146
if len(args) > 0 {
74-
branchName = args[0]
147+
explicitName = args[0]
148+
}
149+
150+
if opts.message != "" {
151+
// Auto-naming mode
152+
existingBranches := s.BranchNames()
153+
isFirstBranch := len(existingBranches) == 0
154+
name, info := branch.ResolveBranchName(s.Prefix, opts.message, explicitName, existingBranches, isFirstBranch)
155+
if name == "" {
156+
cfg.Errorf("could not generate branch name")
157+
return nil
158+
}
159+
branchName = name
160+
if info != "" {
161+
cfg.Infof("%s", info)
162+
}
163+
} else if explicitName != "" {
164+
// No -m, but explicit name given
165+
if s.Prefix != "" {
166+
branchName = s.Prefix + "/" + explicitName
167+
cfg.Infof("Branch name prefixed: %s", branchName)
168+
} else {
169+
branchName = explicitName
170+
}
75171
} else {
172+
// No -m, no explicit name — prompt
76173
fmt.Fprintf(cfg.Err, "Enter a name for the new branch: ")
77174
if _, err := fmt.Fscan(cfg.In, &branchName); err != nil {
78175
return fmt.Errorf("could not read branch name: %w", err)
79176
}
177+
if s.Prefix != "" && branchName != "" {
178+
branchName = s.Prefix + "/" + branchName
179+
cfg.Infof("Branch name prefixed: %s", branchName)
180+
}
80181
}
81182

82183
if branchName == "" {
@@ -90,10 +191,11 @@ func runAdd(cfg *config.Config, args []string) error {
90191
}
91192

92193
if git.BranchExists(branchName) {
93-
cfg.Errorf("branch %q already exists", branchName)
194+
cfg.Errorf("branch %q already exists; provide an explicit name", branchName)
94195
return nil
95196
}
96197

198+
// Create the new branch from the current HEAD and check it out
97199
if err := git.CreateBranch(branchName, currentBranch); err != nil {
98200
cfg.Errorf("failed to create branch: %s", err)
99201
return nil
@@ -107,11 +209,44 @@ func runAdd(cfg *config.Config, args []string) error {
107209
base, _ := git.HeadSHA(currentBranch)
108210
s.Branches = append(s.Branches, stack.BranchRef{Branch: branchName, Base: base})
109211

212+
// Stage and commit on the NEW branch if -m is provided
213+
var commitSHA string
214+
if opts.message != "" {
215+
if opts.stageAll {
216+
if err := git.StageAll(); err != nil {
217+
cfg.Errorf("failed to stage changes: %s", err)
218+
return nil
219+
}
220+
} else if opts.stageTracked {
221+
if err := git.StageTracked(); err != nil {
222+
cfg.Errorf("failed to stage changes: %s", err)
223+
return nil
224+
}
225+
}
226+
if !git.HasStagedChanges() {
227+
cfg.Errorf("nothing to commit; stage changes first or use -a/-u")
228+
return nil
229+
}
230+
sha, err := git.Commit(opts.message)
231+
if err != nil {
232+
cfg.Errorf("failed to commit: %s", err)
233+
return nil
234+
}
235+
commitSHA = sha[:7]
236+
}
237+
110238
if err := stack.Save(gitDir, sf); err != nil {
111239
cfg.Errorf("failed to save stack state: %s", err)
112240
return nil
113241
}
114242

115-
cfg.Successf("Created and checked out branch %q", branchName)
243+
// Print summary
244+
position := len(s.Branches)
245+
if commitSHA != "" {
246+
cfg.Successf("Created branch %s (layer %d) with commit %s", cfg.ColorBold(branchName), position, commitSHA)
247+
} else {
248+
cfg.Successf("Created and checked out branch %q", branchName)
249+
}
250+
116251
return nil
117252
}

0 commit comments

Comments
 (0)