Skip to content

Commit 2604976

Browse files
committed
exit codes by error type
1 parent 6e9927d commit 2604976

15 files changed

Lines changed: 94 additions & 51 deletions

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,3 +427,16 @@ gh stack push
427427
```
428428

429429
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.
430+
431+
## Exit codes
432+
433+
| Code | Meaning |
434+
|------|---------|
435+
| 0 | Success |
436+
| 1 | Generic error |
437+
| 2 | Not in a stack / stack not found |
438+
| 3 | Rebase conflict |
439+
| 4 | GitHub API failure |
440+
| 5 | Invalid arguments or flags |
441+
| 6 | Disambiguation required (branch belongs to multiple stacks) |
442+
| 7 | Rebase already in progress |

cmd/add.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,12 @@ func runAdd(cfg *config.Config, opts *addOptions, args []string) error {
4646
// Validate flag combinations
4747
if opts.stageAll && opts.stageTracked {
4848
cfg.Errorf("flags -A and -u are mutually exclusive")
49-
return ErrSilent
49+
return ErrInvalidArgs
5050
}
5151

5252
result, err := loadStack(cfg, "")
5353
if err != nil {
54-
return ErrSilent
54+
return ErrNotInStack
5555
}
5656
gitDir := result.GitDir
5757
sf := result.StackFile
@@ -69,7 +69,7 @@ func runAdd(cfg *config.Config, opts *addOptions, args []string) error {
6969
// a new branch from it). Only block if we're in the middle of the stack.
7070
if idx >= 0 && idx < len(s.Branches)-1 {
7171
cfg.Errorf("can only add branches on top of the stack; run `%s` to switch to %q", cfg.ColorCyan("gh stack top"), s.Branches[len(s.Branches)-1].Branch)
72-
return ErrSilent
72+
return ErrInvalidArgs
7373
}
7474

7575
// Check if the current branch is a stack branch with no unique commits
@@ -152,17 +152,17 @@ func runAdd(cfg *config.Config, opts *addOptions, args []string) error {
152152

153153
if branchName == "" {
154154
cfg.Errorf("branch name cannot be empty")
155-
return ErrSilent
155+
return ErrInvalidArgs
156156
}
157157

158158
if err := sf.ValidateNoDuplicateBranch(branchName); err != nil {
159159
cfg.Errorf("branch %q already exists in the stack", branchName)
160-
return ErrSilent
160+
return ErrInvalidArgs
161161
}
162162

163163
if git.BranchExists(branchName) {
164164
cfg.Errorf("branch %q already exists", branchName)
165-
return ErrSilent
165+
return ErrInvalidArgs
166166
}
167167

168168
// Stage changes before creating the branch so we can fail early if

cmd/checkout.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,13 @@ func runCheckout(cfg *config.Config, opts *checkoutOptions) error {
5353
gitDir, err := git.GitDir()
5454
if err != nil {
5555
cfg.Errorf("not a git repository")
56-
return ErrSilent
56+
return ErrNotInStack
5757
}
5858

5959
sf, err := stack.Load(gitDir)
6060
if err != nil {
6161
cfg.Errorf("failed to load stack state: %s", err)
62-
return ErrSilent
62+
return ErrNotInStack
6363
}
6464

6565
var s *stack.Stack
@@ -85,7 +85,7 @@ func runCheckout(cfg *config.Config, opts *checkoutOptions) error {
8585
s, br, err = resolvePR(sf, opts.target)
8686
if err != nil {
8787
cfg.Errorf("%s", err)
88-
return ErrSilent
88+
return ErrNotInStack
8989
}
9090
targetBranch = br.Branch
9191
}

cmd/init.go

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func runInit(cfg *config.Config, opts *initOptions) error {
5252
gitDir, err := git.GitDir()
5353
if err != nil {
5454
cfg.Errorf("not a git repository")
55-
return ErrSilent
55+
return ErrNotInStack
5656
}
5757

5858
// Determine trunk branch
@@ -67,15 +67,15 @@ func runInit(cfg *config.Config, opts *initOptions) error {
6767
trunk, err = git.DefaultBranch()
6868
if err != nil {
6969
cfg.Errorf("unable to determine default branch\nUse -b to specify the trunk branch")
70-
return ErrSilent
70+
return ErrNotInStack
7171
}
7272
}
7373

7474
// Load existing stack file
7575
sf, err := stack.Load(gitDir)
7676
if err != nil {
7777
cfg.Errorf("failed to load stack state: %s", err)
78-
return ErrSilent
78+
return ErrNotInStack
7979
}
8080

8181
// Set repository context
@@ -93,7 +93,7 @@ func runInit(cfg *config.Config, opts *initOptions) error {
9393
for _, s := range sf.FindAllStacksForBranch(currentBranch) {
9494
if s.IndexOf(currentBranch) >= 0 {
9595
cfg.Errorf("current branch %q is already part of a stack", currentBranch)
96-
return ErrSilent
96+
return ErrInvalidArgs
9797
}
9898
}
9999
}
@@ -104,16 +104,16 @@ func runInit(cfg *config.Config, opts *initOptions) error {
104104
// Adopt mode: validate all specified branches exist
105105
if len(opts.branches) == 0 {
106106
cfg.Errorf("--adopt requires at least one branch name")
107-
return ErrSilent
107+
return ErrInvalidArgs
108108
}
109109
for _, b := range opts.branches {
110110
if !git.BranchExists(b) {
111111
cfg.Errorf("branch %q does not exist", b)
112-
return ErrSilent
112+
return ErrInvalidArgs
113113
}
114114
if err := sf.ValidateNoDuplicateBranch(b); err != nil {
115115
cfg.Errorf("branch %q already exists in a stack", b)
116-
return ErrSilent
116+
return ErrInvalidArgs
117117
}
118118
}
119119
branches = opts.branches
@@ -132,7 +132,7 @@ func runInit(cfg *config.Config, opts *initOptions) error {
132132
state = "merged"
133133
}
134134
cfg.Errorf("branch %q already has a %s PR (#%d: %s)", b, state, pr.Number, pr.URL)
135-
return ErrSilent
135+
return ErrInvalidArgs
136136
}
137137
}
138138
}
@@ -141,7 +141,7 @@ func runInit(cfg *config.Config, opts *initOptions) error {
141141
for _, b := range opts.branches {
142142
if err := sf.ValidateNoDuplicateBranch(b); err != nil {
143143
cfg.Errorf("branch %q already exists in a stack", b)
144-
return ErrSilent
144+
return ErrInvalidArgs
145145
}
146146
if !git.BranchExists(b) {
147147
if err := git.CreateBranch(b, trunk); err != nil {
@@ -155,7 +155,7 @@ func runInit(cfg *config.Config, opts *initOptions) error {
155155
// Interactive mode
156156
if !cfg.IsInteractive() {
157157
cfg.Errorf("interactive input required; provide branch names or use --adopt")
158-
return ErrSilent
158+
return ErrInvalidArgs
159159
}
160160
p := prompter.New(cfg.In, cfg.Out, cfg.Err)
161161

@@ -191,7 +191,7 @@ func runInit(cfg *config.Config, opts *initOptions) error {
191191
if useCurrentBranch {
192192
if err := sf.ValidateNoDuplicateBranch(currentBranch); err != nil {
193193
cfg.Errorf("branch %q already exists in the stack", currentBranch)
194-
return ErrSilent
194+
return ErrInvalidArgs
195195
}
196196
branches = []string{currentBranch}
197197
}
@@ -218,15 +218,15 @@ func runInit(cfg *config.Config, opts *initOptions) error {
218218
branchName = branch.NextNumberedName(opts.prefix, nil)
219219
} else if branchName == "" {
220220
cfg.Errorf("branch name cannot be empty")
221-
return ErrSilent
221+
return ErrInvalidArgs
222222
} else if opts.prefix != "" {
223223
// Prepend prefix to the user-provided name
224224
branchName = opts.prefix + "/" + branchName
225225
}
226226

227227
if err := sf.ValidateNoDuplicateBranch(branchName); err != nil {
228228
cfg.Errorf("branch %q already exists in a stack", branchName)
229-
return ErrSilent
229+
return ErrInvalidArgs
230230
}
231231
if !git.BranchExists(branchName) {
232232
if err := git.CreateBranch(branchName, trunk); err != nil {
@@ -242,7 +242,7 @@ func runInit(cfg *config.Config, opts *initOptions) error {
242242
if opts.prefix != "" {
243243
if err := git.ValidateRefName(opts.prefix); err != nil {
244244
cfg.Errorf("invalid prefix %q: must be a valid git ref component", opts.prefix)
245-
return ErrSilent
245+
return ErrInvalidArgs
246246
}
247247
}
248248

cmd/merge.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ func runMerge(cfg *config.Config, target string) error {
3535
// Standard stack loading and validation.
3636
result, err := loadStack(cfg, "")
3737
if err != nil {
38-
return ErrSilent
38+
return ErrNotInStack
3939
}
4040
s := result.Stack
4141
currentBranch := result.CurrentBranch
@@ -52,7 +52,7 @@ func runMerge(cfg *config.Config, target string) error {
5252
_, br, err = resolvePR(result.StackFile, target)
5353
if err != nil {
5454
cfg.Errorf("%s", err)
55-
return ErrSilent
55+
return ErrNotInStack
5656
}
5757
} else {
5858
idx := s.IndexOf(currentBranch)
@@ -62,7 +62,7 @@ func runMerge(cfg *config.Config, target string) error {
6262
return nil
6363
}
6464
cfg.Errorf("current branch %q is not a stack branch (it may be the trunk)", currentBranch)
65-
return ErrSilent
65+
return ErrNotInStack
6666
}
6767
br = &s.Branches[idx]
6868
}

cmd/merge_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ func TestMerge_OnTrunk(t *testing.T) {
145145
errOut, _ := io.ReadAll(errR)
146146
output := string(errOut)
147147

148-
assert.ErrorIs(t, err, ErrSilent)
148+
assert.ErrorIs(t, err, ErrNotInStack)
149149
assert.Contains(t, output, "not a stack branch")
150150
}
151151

cmd/navigate.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ func BottomCmd(cfg *config.Config) *cobra.Command {
6161
func runNavigate(cfg *config.Config, delta int) error {
6262
result, err := loadStack(cfg, "")
6363
if err != nil {
64-
return ErrSilent
64+
return ErrNotInStack
6565
}
6666
s := result.Stack
6767
currentBranch := result.CurrentBranch
@@ -175,14 +175,14 @@ func runNavigate(cfg *config.Config, delta int) error {
175175
func runNavigateToEnd(cfg *config.Config, top bool) error {
176176
result, err := loadStack(cfg, "")
177177
if err != nil {
178-
return ErrSilent
178+
return ErrNotInStack
179179
}
180180
s := result.Stack
181181
currentBranch := result.CurrentBranch
182182

183183
if len(s.Branches) == 0 {
184184
cfg.Errorf("stack has no branches")
185-
return ErrSilent
185+
return ErrNotInStack
186186
}
187187

188188
var targetIdx int

cmd/push.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,38 +40,38 @@ func runPush(cfg *config.Config, opts *pushOptions) error {
4040
gitDir, err := git.GitDir()
4141
if err != nil {
4242
cfg.Errorf("not a git repository")
43-
return ErrSilent
43+
return ErrNotInStack
4444
}
4545

4646
sf, err := stack.Load(gitDir)
4747
if err != nil {
4848
cfg.Errorf("failed to load stack state: %s", err)
49-
return ErrSilent
49+
return ErrNotInStack
5050
}
5151

5252
currentBranch, err := git.CurrentBranch()
5353
if err != nil {
5454
cfg.Errorf("failed to get current branch: %s", err)
55-
return ErrSilent
55+
return ErrNotInStack
5656
}
5757

5858
// Find the stack for the current branch without switching branches.
5959
// Push should never change the user's checked-out branch.
6060
stacks := sf.FindAllStacksForBranch(currentBranch)
6161
if len(stacks) == 0 {
6262
cfg.Errorf("current branch %q is not part of a stack", currentBranch)
63-
return ErrSilent
63+
return ErrNotInStack
6464
}
6565
if len(stacks) > 1 {
6666
cfg.Errorf("branch %q belongs to multiple stacks; checkout a non-trunk branch first", currentBranch)
67-
return ErrSilent
67+
return ErrDisambiguate
6868
}
6969
s := stacks[0]
7070

7171
client, err := cfg.GitHubClient()
7272
if err != nil {
7373
cfg.Errorf("failed to create GitHub client: %s", err)
74-
return ErrSilent
74+
return ErrAPIFailure
7575
}
7676

7777
// Push all active branches atomically

cmd/rebase.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func runRebase(cfg *config.Config, opts *rebaseOptions) error {
6969
gitDir, err := git.GitDir()
7070
if err != nil {
7171
cfg.Errorf("not a git repository")
72-
return ErrSilent
72+
return ErrNotInStack
7373
}
7474

7575
if opts.cont {
@@ -82,7 +82,7 @@ func runRebase(cfg *config.Config, opts *rebaseOptions) error {
8282

8383
result, err := loadStack(cfg, opts.branch)
8484
if err != nil {
85-
return ErrSilent
85+
return ErrNotInStack
8686
}
8787
sf := result.StackFile
8888
s := result.Stack
@@ -242,7 +242,7 @@ func runRebase(cfg *config.Config, opts *rebaseOptions) error {
242242
br.Branch, cfg.ColorCyan("gh stack rebase --continue"))
243243
cfg.Printf("Or abort this operation with `%s`",
244244
cfg.ColorCyan("gh stack rebase --abort"))
245-
return fmt.Errorf("rebase conflict on %s", br.Branch)
245+
return ErrConflict
246246
}
247247

248248
cfg.Successf("Rebased %s onto %s (squash-merge detected)", br.Branch, newBase)
@@ -290,7 +290,7 @@ func runRebase(cfg *config.Config, opts *rebaseOptions) error {
290290
br.Branch, cfg.ColorCyan("gh stack rebase --continue"))
291291
cfg.Printf("Or abort this operation with `%s`",
292292
cfg.ColorCyan("gh stack rebase --abort"))
293-
return fmt.Errorf("rebase conflict on %s", br.Branch)
293+
return ErrConflict
294294
}
295295

296296
cfg.Successf("Rebased %s onto %s", br.Branch, base)
@@ -338,7 +338,7 @@ func continueRebase(cfg *config.Config, gitDir string) error {
338338
sf, err := stack.Load(gitDir)
339339
if err != nil {
340340
cfg.Errorf("failed to load stack state: %s", err)
341-
return ErrSilent
341+
return ErrNotInStack
342342
}
343343

344344
// Use the saved original branch to find the stack, since git may be in
@@ -440,7 +440,7 @@ func continueRebase(cfg *config.Config, gitDir string) error {
440440
branchName, cfg.ColorCyan("gh stack rebase --continue"))
441441
cfg.Printf("Or abort this operation with `%s`",
442442
cfg.ColorCyan("gh stack rebase --abort"))
443-
return fmt.Errorf("rebase conflict on %s", branchName)
443+
return ErrConflict
444444
}
445445

446446
cfg.Successf("Rebased %s onto %s (squash-merge detected)", branchName, newBase)
@@ -480,7 +480,7 @@ func continueRebase(cfg *config.Config, gitDir string) error {
480480
branchName, cfg.ColorCyan("gh stack rebase --continue"))
481481
cfg.Printf("Or abort this operation with `%s`",
482482
cfg.ColorCyan("gh stack rebase --abort"))
483-
return fmt.Errorf("rebase conflict on %s", branchName)
483+
return ErrConflict
484484
}
485485

486486
cfg.Successf("Rebased %s onto %s", branchName, base)

cmd/rebase_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ func TestRebase_ConflictSavesState(t *testing.T) {
250250
output := string(errOut)
251251

252252
assert.Error(t, err)
253-
assert.Contains(t, err.Error(), "rebase conflict on b2")
253+
assert.ErrorIs(t, err, ErrConflict)
254254
assert.Contains(t, output, "--continue")
255255

256256
// Verify state file was saved

0 commit comments

Comments
 (0)