gh-stack 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -93
- package/package.json +4 -4
- package/src/commands/bottom.ts +54 -0
- package/src/commands/{switch.ts → checkout.ts} +11 -8
- package/src/commands/create.ts +83 -0
- package/src/commands/{remove.ts → delete.ts} +11 -10
- package/src/commands/down.ts +67 -0
- package/src/commands/init.ts +142 -93
- package/src/commands/list.ts +1 -1
- package/src/commands/{show.ts → log.ts} +3 -3
- package/src/commands/merge.ts +2 -1
- package/src/commands/restack.ts +14 -8
- package/src/commands/submit.ts +310 -0
- package/src/commands/sync.ts +3 -2
- package/src/commands/top.ts +79 -0
- package/src/commands/undo.ts +13 -1
- package/src/commands/up.ts +82 -0
- package/src/index.ts +59 -25
- package/src/lib/cli.ts +32 -0
- package/src/lib/git.ts +36 -3
- package/src/lib/safety.ts +110 -2
- package/src/commands/add.ts +0 -139
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Stacked PR manager for squash-merge workflows.
|
|
4
4
|
|
|
5
|
-
Manages stacked pull requests with metadata stored in `.git/gh-stack-metadata.json`. Designed for repositories that use squash-merge (where tools like Graphite break down).
|
|
5
|
+
Manages stacked pull requests with metadata stored in `.git/gh-stack-metadata.json`. Designed for repositories that use squash-merge (where tools like Graphite break down). Inspired by [Graphite](https://graphite.dev/) (`gt`) but works directly against GitHub — no backend required.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -22,68 +22,98 @@ bun install -g gh-stack
|
|
|
22
22
|
- [GitHub CLI](https://cli.github.com/) (`gh`) — authenticated
|
|
23
23
|
- Git 2.30+
|
|
24
24
|
|
|
25
|
+
## Terms
|
|
26
|
+
|
|
27
|
+
| Term | Definition |
|
|
28
|
+
|------|-----------|
|
|
29
|
+
| **trunk** | Base branch of the repository (usually `main`) |
|
|
30
|
+
| **stack** | A chain of dependent branches |
|
|
31
|
+
| **upstack** | Branches that depend on the current branch (children) |
|
|
32
|
+
| **downstack** | Branches the current branch depends on (ancestors) |
|
|
33
|
+
|
|
25
34
|
## Quick Start
|
|
26
35
|
|
|
27
36
|
```bash
|
|
28
|
-
#
|
|
29
|
-
|
|
37
|
+
# You have branches: main → feat-1 → feat-2
|
|
38
|
+
# Go to the top of the chain and init:
|
|
39
|
+
git checkout feat-2
|
|
40
|
+
gh-stack init # Auto-detects the chain!
|
|
30
41
|
|
|
31
|
-
#
|
|
32
|
-
|
|
42
|
+
# Or start fresh:
|
|
43
|
+
git checkout kiliman/first-pr
|
|
44
|
+
gh-stack init # Creates stack with this branch
|
|
45
|
+
gh-stack create kiliman/second-pr # Branch off current, add to stack
|
|
46
|
+
|
|
47
|
+
# Push everything to GitHub and create PRs
|
|
48
|
+
gh-stack submit
|
|
33
49
|
|
|
34
50
|
# View the stack
|
|
35
|
-
gh-stack
|
|
36
|
-
gh-stack
|
|
51
|
+
gh-stack log # tree view (default)
|
|
52
|
+
gh-stack ls # numbered list
|
|
53
|
+
|
|
54
|
+
# Navigate
|
|
55
|
+
gh-stack up # move to child branch
|
|
56
|
+
gh-stack down # move to parent branch
|
|
57
|
+
gh-stack top # jump to tip of stack
|
|
58
|
+
gh-stack bottom # jump to base of stack
|
|
37
59
|
|
|
38
|
-
# Sync with main and restack
|
|
60
|
+
# Sync with main and restack
|
|
39
61
|
gh-stack sync
|
|
40
62
|
|
|
41
|
-
# Check PR status
|
|
63
|
+
# Check PR status
|
|
42
64
|
gh-stack status
|
|
43
65
|
```
|
|
44
66
|
|
|
45
67
|
## Commands
|
|
46
68
|
|
|
47
|
-
###
|
|
69
|
+
### Core Workflow
|
|
48
70
|
|
|
49
71
|
```
|
|
50
72
|
init [--name <name>] [--description <desc>] [--parent <branch>]
|
|
51
|
-
Create a new stack
|
|
73
|
+
Create a new stack from the current branch. Auto-detects branch
|
|
74
|
+
chains — if you're at the top of main → feat-1 → feat-2, all
|
|
75
|
+
branches are added to the stack automatically.
|
|
52
76
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
77
|
+
create <branch-name> [--description <desc>]
|
|
78
|
+
Create a new git branch off the current branch and add it to
|
|
79
|
+
the stack. The current branch must already be tracked.
|
|
56
80
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
81
|
+
submit [-d|--draft] [-n|--no-edit] [--dry-run]
|
|
82
|
+
Push all downstack branches to GitHub, create PRs for branches
|
|
83
|
+
that don't have them, and update all PR descriptions with stack
|
|
84
|
+
visualization. Idempotent — safe to run repeatedly.
|
|
85
|
+
|
|
86
|
+
In --yes mode, auto-generates PR titles from branch names.
|
|
60
87
|
```
|
|
61
88
|
|
|
62
|
-
### Navigation
|
|
89
|
+
### Stack Navigation
|
|
63
90
|
|
|
64
91
|
```
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
92
|
+
checkout [<branch>]
|
|
93
|
+
Switch to a branch by name, or interactive picker.
|
|
94
|
+
--stack Switch between stacks instead of branches
|
|
95
|
+
(alias: co)
|
|
68
96
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
scripting and agents. (alias: ls)
|
|
97
|
+
up [steps]
|
|
98
|
+
Move to child branch (upstack). Prompts if multiple children.
|
|
72
99
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
--stack Switch between stacks instead of branches
|
|
100
|
+
down [steps]
|
|
101
|
+
Move to parent branch (downstack).
|
|
76
102
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
103
|
+
top
|
|
104
|
+
Jump to the tip (leaf) of the current stack.
|
|
105
|
+
|
|
106
|
+
bottom
|
|
107
|
+
Jump to the base (first branch above trunk).
|
|
108
|
+
|
|
109
|
+
ls
|
|
110
|
+
List branches with position numbers.
|
|
81
111
|
```
|
|
82
112
|
|
|
83
|
-
###
|
|
113
|
+
### Stack Management
|
|
84
114
|
|
|
85
115
|
```
|
|
86
|
-
restack [--resume] [--dry-run] [--verbose]
|
|
116
|
+
restack [--resume] [--dry-run] [--verbose]
|
|
87
117
|
Rebase the current branch and all descendants onto their parents.
|
|
88
118
|
Uses tag-based references for stable rebasing across the chain.
|
|
89
119
|
|
|
@@ -93,51 +123,37 @@ restack [--resume] [--dry-run] [--verbose] [--yes]
|
|
|
93
123
|
|
|
94
124
|
(alias: rebase)
|
|
95
125
|
|
|
96
|
-
sync [--dry-run]
|
|
126
|
+
sync [--dry-run]
|
|
97
127
|
Fetch main, rebase the base branch onto main, then restack all
|
|
98
128
|
children. Creates tags for ALL branches before any rebasing starts.
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
### Merge & Ship
|
|
102
129
|
|
|
103
|
-
```
|
|
104
130
|
merge [--dry-run]
|
|
105
|
-
Squash-merge the stack top-down locally (PR3
|
|
106
|
-
then optionally rebase onto main.
|
|
107
|
-
|
|
108
|
-
Can also close intermediate PRs and archives the stack on completion.
|
|
109
|
-
|
|
110
|
-
update-prs
|
|
111
|
-
Update all PR descriptions with a stack visualization:
|
|
131
|
+
Squash-merge the stack top-down locally (PR3 → PR2 → PR1),
|
|
132
|
+
then optionally rebase onto main. Closes intermediate PRs and
|
|
133
|
+
archives the stack on completion.
|
|
112
134
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
┣━ ✅ #123 Backend models 👈
|
|
117
|
-
┃
|
|
118
|
-
┗━ ⏳ #124 Frontend UI
|
|
135
|
+
delete [<branch>]
|
|
136
|
+
Remove a branch from the stack and re-parent its children.
|
|
137
|
+
Interactive selector if no branch specified.
|
|
119
138
|
```
|
|
120
139
|
|
|
121
|
-
### Maintenance
|
|
140
|
+
### Info & Maintenance
|
|
122
141
|
|
|
123
142
|
```
|
|
124
|
-
|
|
125
|
-
|
|
143
|
+
log
|
|
144
|
+
Display the current stack as a tree with branch numbers,
|
|
145
|
+
PR info, and descriptions. This is the default command.
|
|
146
|
+
|
|
147
|
+
status [--current] [--json]
|
|
148
|
+
PR dashboard showing review state, CI status, and merge readiness.
|
|
149
|
+
--current Show only the current stack or standalone PR
|
|
150
|
+
--json Structured JSON output (progress goes to stderr)
|
|
126
151
|
|
|
127
152
|
undo
|
|
128
153
|
Restore the last snapshot taken before a destructive operation.
|
|
129
|
-
```
|
|
130
154
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
These are still intended, but not shipped in `v0.1.1`:
|
|
134
|
-
|
|
135
|
-
```text
|
|
136
|
-
split
|
|
137
|
-
Guided helper for splitting a branch/PR into two and updating stack metadata.
|
|
138
|
-
|
|
139
|
-
insert
|
|
140
|
-
Insert a branch into the middle of an existing stack and re-parent the chain.
|
|
155
|
+
archive [--restore <name>]
|
|
156
|
+
List archived stacks by default, or restore one by name.
|
|
141
157
|
```
|
|
142
158
|
|
|
143
159
|
## Global Options
|
|
@@ -157,10 +173,27 @@ GH_STACK_NO_COLOR=1 Disable colored output
|
|
|
157
173
|
|
|
158
174
|
## How It Works
|
|
159
175
|
|
|
176
|
+
### Smart Init
|
|
177
|
+
|
|
178
|
+
`gh-stack init` auto-detects branch chains by walking git ancestry. From the top branch, it finds all local branches whose tips are strict ancestors (but not already merged into trunk) and reconstructs the chain with correct parent relationships.
|
|
179
|
+
|
|
160
180
|
### Tag-Based Rebasing
|
|
161
181
|
|
|
162
182
|
The critical insight: after rebasing a parent branch, `git merge-base` returns wrong results for its children. gh-stack solves this by creating temporary `stack-sync-*` tags marking each branch's divergence point **before** any rebasing starts, then using those stable references for `git rebase --onto`.
|
|
163
183
|
|
|
184
|
+
### Stack Visualization
|
|
185
|
+
|
|
186
|
+
`submit` automatically adds a stack section to all PR descriptions:
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
### 📚 Stacked on
|
|
190
|
+
⚫ main
|
|
191
|
+
┃
|
|
192
|
+
┣━ ✅ #123 Backend models
|
|
193
|
+
┃
|
|
194
|
+
┗━ ⏳ #124 Frontend UI 👈
|
|
195
|
+
```
|
|
196
|
+
|
|
164
197
|
### Metadata
|
|
165
198
|
|
|
166
199
|
Stack metadata lives at `.git/gh-stack-metadata.json` (never committed):
|
|
@@ -168,21 +201,19 @@ Stack metadata lives at `.git/gh-stack-metadata.json` (never committed):
|
|
|
168
201
|
```json
|
|
169
202
|
{
|
|
170
203
|
"version": 2,
|
|
171
|
-
"current_stack": "
|
|
204
|
+
"current_stack": "kiliman/feature-2",
|
|
172
205
|
"stacks": {
|
|
173
|
-
"
|
|
174
|
-
"description": "
|
|
175
|
-
"last_branch": "kiliman/
|
|
206
|
+
"kiliman/feature-2": {
|
|
207
|
+
"description": "",
|
|
208
|
+
"last_branch": "kiliman/feature-2",
|
|
176
209
|
"branches": {
|
|
177
|
-
"kiliman/
|
|
210
|
+
"kiliman/feature-1": {
|
|
178
211
|
"parent": "main",
|
|
179
|
-
"pr":
|
|
180
|
-
"description": "Backend models"
|
|
212
|
+
"pr": 21729
|
|
181
213
|
},
|
|
182
|
-
"kiliman/
|
|
183
|
-
"parent": "kiliman/
|
|
184
|
-
"pr":
|
|
185
|
-
"description": "Frontend UI"
|
|
214
|
+
"kiliman/feature-2": {
|
|
215
|
+
"parent": "kiliman/feature-1",
|
|
216
|
+
"pr": 21730
|
|
186
217
|
}
|
|
187
218
|
}
|
|
188
219
|
}
|
|
@@ -192,28 +223,32 @@ Stack metadata lives at `.git/gh-stack-metadata.json` (never committed):
|
|
|
192
223
|
|
|
193
224
|
### Snapshots
|
|
194
225
|
|
|
195
|
-
Before any destructive operation (restack, sync, merge,
|
|
226
|
+
Before any destructive operation (restack, sync, merge, delete), gh-stack saves a snapshot of all branch HEADs. Run `gh-stack undo` to restore.
|
|
196
227
|
|
|
197
228
|
## Example Workflow
|
|
198
229
|
|
|
199
230
|
```bash
|
|
200
|
-
# Start a
|
|
201
|
-
git checkout
|
|
202
|
-
|
|
203
|
-
gh-stack init --name api-feature
|
|
231
|
+
# Start from an existing branch with a PR
|
|
232
|
+
git checkout kiliman/api-layer-WEB-1234
|
|
233
|
+
gh-stack init
|
|
204
234
|
|
|
205
|
-
#
|
|
206
|
-
gh-stack
|
|
207
|
-
# ... code, commit
|
|
235
|
+
# Create second PR on top
|
|
236
|
+
gh-stack create kiliman/frontend-WEB-1234
|
|
237
|
+
# ... code, commit ...
|
|
208
238
|
|
|
209
|
-
#
|
|
210
|
-
gh-stack
|
|
239
|
+
# Push everything and create PRs
|
|
240
|
+
gh-stack submit
|
|
211
241
|
|
|
212
242
|
# Later: sync everything with main
|
|
213
|
-
gh-stack sync
|
|
243
|
+
gh-stack sync
|
|
244
|
+
|
|
245
|
+
# Navigate the stack
|
|
246
|
+
gh-stack up # go to child
|
|
247
|
+
gh-stack down # go to parent
|
|
248
|
+
gh-stack top # jump to tip
|
|
214
249
|
|
|
215
250
|
# Check status
|
|
216
|
-
gh-stack status
|
|
251
|
+
gh-stack status
|
|
217
252
|
|
|
218
253
|
# When PRs are approved, merge the stack
|
|
219
254
|
gh-stack merge
|
|
@@ -224,18 +259,18 @@ gh-stack merge
|
|
|
224
259
|
gh-stack is designed to be used by AI agents and CI pipelines:
|
|
225
260
|
|
|
226
261
|
```bash
|
|
227
|
-
# Non-interactive mode
|
|
262
|
+
# Non-interactive mode — all prompts auto-resolved
|
|
228
263
|
export GH_STACK_YES=1
|
|
264
|
+
|
|
265
|
+
gh-stack init # No confirmations
|
|
266
|
+
gh-stack submit # Auto-generates PR titles
|
|
229
267
|
gh-stack sync
|
|
230
268
|
gh-stack restack
|
|
231
269
|
|
|
232
270
|
# Structured output
|
|
233
271
|
gh-stack status --json
|
|
234
272
|
gh-stack status --current --json
|
|
235
|
-
gh-stack
|
|
236
|
-
|
|
237
|
-
# Quick branch switching
|
|
238
|
-
gh-stack switch 2
|
|
273
|
+
gh-stack ls
|
|
239
274
|
```
|
|
240
275
|
|
|
241
276
|
## License
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gh-stack",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Stacked PR manager for squash-merge workflows",
|
|
5
5
|
"author": "Kiliman <kiliman@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/kiliman/gh-stack"
|
|
9
|
+
"url": "git+https://github.com/kiliman/gh-stack.git"
|
|
10
10
|
},
|
|
11
11
|
"keywords": [
|
|
12
12
|
"git",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"format:check": "oxfmt --check src/",
|
|
41
41
|
"typecheck": "tsc --noEmit",
|
|
42
42
|
"check": "bun run lint && bun run typecheck && bun test",
|
|
43
|
-
"prepublishOnly": "
|
|
43
|
+
"prepublishOnly": "echo 'Checks already ran on push'",
|
|
44
44
|
"prepare": "husky"
|
|
45
45
|
},
|
|
46
46
|
"lint-staged": {
|
|
@@ -63,4 +63,4 @@
|
|
|
63
63
|
"@clack/prompts": "^1.1.0",
|
|
64
64
|
"picocolors": "^1.1.1"
|
|
65
65
|
}
|
|
66
|
-
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// gh-stack bottom — Jump to the base of the stack
|
|
2
|
+
import * as p from "@clack/prompts";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
import * as git from "../lib/git.ts";
|
|
5
|
+
import { findStackForBranch, getOrderedBranches, writeMetadata } from "../lib/metadata.ts";
|
|
6
|
+
import { ensureMetadata } from "../lib/safety.ts";
|
|
7
|
+
|
|
8
|
+
export default async function bottom(args: string[]): Promise<void> {
|
|
9
|
+
if (args.includes("--help")) {
|
|
10
|
+
console.log(`
|
|
11
|
+
gh-stack bottom — Jump to the base of the stack
|
|
12
|
+
|
|
13
|
+
USAGE
|
|
14
|
+
gh-stack bottom
|
|
15
|
+
|
|
16
|
+
Switches to the bottom-most branch of the current stack
|
|
17
|
+
(the first branch above trunk).
|
|
18
|
+
`);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const meta = await ensureMetadata();
|
|
23
|
+
const currentBranch = await git.currentBranch();
|
|
24
|
+
const stackName = findStackForBranch(meta, currentBranch);
|
|
25
|
+
|
|
26
|
+
if (!stackName) {
|
|
27
|
+
p.cancel(`Branch ${pc.yellow(currentBranch)} is not in any stack`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const stack = meta.stacks[stackName]!;
|
|
32
|
+
const ordered = getOrderedBranches(stack);
|
|
33
|
+
|
|
34
|
+
if (ordered.length === 0) {
|
|
35
|
+
p.log.warn("No branches in stack");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const target = ordered[0]!;
|
|
40
|
+
|
|
41
|
+
if (target === currentBranch) {
|
|
42
|
+
p.log.info(`Already at the bottom: ${pc.yellow(currentBranch)}`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
await git.checkout(target);
|
|
47
|
+
|
|
48
|
+
// Update tracking
|
|
49
|
+
stack.last_branch = target;
|
|
50
|
+
meta.current_stack = stackName;
|
|
51
|
+
await writeMetadata(meta);
|
|
52
|
+
|
|
53
|
+
p.log.success(`On ${pc.yellow(target)}`);
|
|
54
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// gh-stack
|
|
1
|
+
// gh-stack checkout — Switch between branches or stacks
|
|
2
2
|
import * as p from "@clack/prompts";
|
|
3
3
|
import pc from "picocolors";
|
|
4
4
|
import * as git from "../lib/git.ts";
|
|
@@ -6,22 +6,25 @@ import { writeMetadata, getOrderedBranches, findStackForBranch } from "../lib/me
|
|
|
6
6
|
import { ensureMetadata } from "../lib/safety.ts";
|
|
7
7
|
import { selectBranch, selectStack } from "../lib/ui.ts";
|
|
8
8
|
|
|
9
|
-
export default async function
|
|
9
|
+
export default async function checkout(args: string[]): Promise<void> {
|
|
10
10
|
const isStackMode = args.includes("--stack");
|
|
11
11
|
const numberArg = args.find((a) => /^\d+$/.test(a));
|
|
12
12
|
|
|
13
13
|
if (args.includes("--help")) {
|
|
14
14
|
console.log(`
|
|
15
|
-
gh-stack
|
|
15
|
+
gh-stack checkout — Switch branch or stack
|
|
16
16
|
|
|
17
17
|
USAGE
|
|
18
|
-
gh-stack
|
|
19
|
-
gh-stack
|
|
20
|
-
gh-stack
|
|
18
|
+
gh-stack checkout <number> Switch to branch by position (non-interactive)
|
|
19
|
+
gh-stack checkout Interactive branch picker (arrow keys)
|
|
20
|
+
gh-stack checkout --stack Switch between stacks (interactive)
|
|
21
|
+
|
|
22
|
+
ALIASES
|
|
23
|
+
gh-stack co
|
|
21
24
|
|
|
22
25
|
EXAMPLES
|
|
23
|
-
gh-stack
|
|
24
|
-
gh-stack
|
|
26
|
+
gh-stack checkout 1 Jump to first branch in stack (base)
|
|
27
|
+
gh-stack checkout 2 Jump to second branch in stack
|
|
25
28
|
|
|
26
29
|
TIP
|
|
27
30
|
Use 'gh-stack list' to see branch numbers.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// gh-stack create — Create a new branch and add to the stack
|
|
2
|
+
import * as p from "@clack/prompts";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
import * as git from "../lib/git.ts";
|
|
5
|
+
import { findStackForBranch, addBranchToStack } from "../lib/metadata.ts";
|
|
6
|
+
import { ensureMetadata } from "../lib/safety.ts";
|
|
7
|
+
import type { Branch } from "../types.ts";
|
|
8
|
+
|
|
9
|
+
export default async function create(args: string[]): Promise<void> {
|
|
10
|
+
// Parse flags
|
|
11
|
+
let descFlag: string | undefined;
|
|
12
|
+
let branchArg: string | undefined;
|
|
13
|
+
|
|
14
|
+
for (let i = 0; i < args.length; i++) {
|
|
15
|
+
switch (args[i]) {
|
|
16
|
+
case "--description":
|
|
17
|
+
descFlag = args[++i];
|
|
18
|
+
break;
|
|
19
|
+
case "--help":
|
|
20
|
+
console.log(`
|
|
21
|
+
gh-stack create — Create a new branch and add to the stack
|
|
22
|
+
|
|
23
|
+
USAGE
|
|
24
|
+
gh-stack create <branch-name> [options]
|
|
25
|
+
|
|
26
|
+
Creates a new git branch off the current branch and adds it to the
|
|
27
|
+
current stack. The current branch must already be tracked in a stack.
|
|
28
|
+
|
|
29
|
+
OPTIONS
|
|
30
|
+
--description <desc> Description for the branch
|
|
31
|
+
|
|
32
|
+
EXAMPLES
|
|
33
|
+
gh-stack create kiliman/feature-2 # Branch off current, add to stack
|
|
34
|
+
gh-stack create kiliman/api --description "API layer"
|
|
35
|
+
`);
|
|
36
|
+
return;
|
|
37
|
+
default:
|
|
38
|
+
if (!args[i]!.startsWith("--")) {
|
|
39
|
+
branchArg = args[i];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Branch name is required
|
|
45
|
+
if (!branchArg) {
|
|
46
|
+
p.cancel("Branch name is required.\n\n Usage: gh-stack create <branch-name>");
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const meta = await ensureMetadata();
|
|
51
|
+
const currentBranch = await git.currentBranch();
|
|
52
|
+
|
|
53
|
+
// Current branch must be in a stack
|
|
54
|
+
const stackName = findStackForBranch(meta, currentBranch);
|
|
55
|
+
if (!stackName) {
|
|
56
|
+
p.cancel(
|
|
57
|
+
`Current branch ${pc.yellow(currentBranch)} is not in any stack.\n\n Add it to a stack first with:\n ${pc.green("gh-stack init")}`,
|
|
58
|
+
);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// New branch must not already exist
|
|
63
|
+
if (await git.localBranchExists(branchArg)) {
|
|
64
|
+
p.cancel(`Branch ${pc.yellow(branchArg)} already exists.`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
p.intro(pc.cyan(`Create branch in ${pc.yellow(stackName)}`));
|
|
69
|
+
|
|
70
|
+
// Create the new branch off current HEAD
|
|
71
|
+
p.log.info(`Creating ${pc.yellow(branchArg)} off ${pc.yellow(currentBranch)}`);
|
|
72
|
+
await git.createBranch(branchArg);
|
|
73
|
+
|
|
74
|
+
// Add to the stack with parent = current branch
|
|
75
|
+
const branchData: Branch = {
|
|
76
|
+
parent: currentBranch,
|
|
77
|
+
...(descFlag && { description: descFlag }),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
await addBranchToStack(meta, stackName, branchArg, branchData);
|
|
81
|
+
|
|
82
|
+
p.outro(pc.green(`Created ${pc.yellow(branchArg)} → parent ${pc.yellow(currentBranch)}`));
|
|
83
|
+
}
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
// gh-stack
|
|
1
|
+
// gh-stack delete — Delete a branch from the stack (re-link parent chain)
|
|
2
2
|
import * as p from "@clack/prompts";
|
|
3
3
|
import pc from "picocolors";
|
|
4
4
|
import { removeBranchFromStack, getChildren } from "../lib/metadata.ts";
|
|
5
|
-
import { ensureMetadata, ensureCurrentStack } from "../lib/safety.ts";
|
|
5
|
+
import { ensureMetadata, ensureCurrentStack, ensureValidStack } from "../lib/safety.ts";
|
|
6
6
|
import { takeSnapshot } from "../lib/snapshot.ts";
|
|
7
7
|
import { selectBranch } from "../lib/ui.ts";
|
|
8
8
|
import * as git from "../lib/git.ts";
|
|
9
9
|
|
|
10
|
-
export default async function
|
|
10
|
+
export default async function deleteCmd(args: string[]): Promise<void> {
|
|
11
11
|
if (args.includes("--help")) {
|
|
12
12
|
console.log(`
|
|
13
|
-
gh-stack
|
|
13
|
+
gh-stack delete — Delete a branch from the stack
|
|
14
14
|
|
|
15
15
|
USAGE
|
|
16
|
-
gh-stack
|
|
16
|
+
gh-stack delete [<branch>]
|
|
17
17
|
|
|
18
18
|
Removes the branch and re-parents its children to its parent.
|
|
19
19
|
If no branch is specified, shows interactive selector.
|
|
@@ -23,15 +23,16 @@ If no branch is specified, shows interactive selector.
|
|
|
23
23
|
|
|
24
24
|
const meta = await ensureMetadata();
|
|
25
25
|
const stackName = ensureCurrentStack(meta);
|
|
26
|
+
await ensureValidStack(meta, stackName);
|
|
26
27
|
const stack = meta.stacks[stackName]!;
|
|
27
28
|
const currentBranch = await git.currentBranch();
|
|
28
29
|
|
|
29
|
-
p.intro(pc.cyan("
|
|
30
|
+
p.intro(pc.cyan("Delete Branch from Stack"));
|
|
30
31
|
|
|
31
|
-
// Get branch to
|
|
32
|
+
// Get branch to delete
|
|
32
33
|
let branchName = args[0];
|
|
33
34
|
if (!branchName) {
|
|
34
|
-
branchName = (await selectBranch(stack, "Select branch to
|
|
35
|
+
branchName = (await selectBranch(stack, "Select branch to delete", currentBranch)) ?? undefined;
|
|
35
36
|
if (!branchName) {
|
|
36
37
|
p.cancel("Cancelled");
|
|
37
38
|
process.exit(0);
|
|
@@ -61,7 +62,7 @@ If no branch is specified, shows interactive selector.
|
|
|
61
62
|
|
|
62
63
|
// Confirm
|
|
63
64
|
const confirmed = await p.confirm({
|
|
64
|
-
message: `
|
|
65
|
+
message: `Delete ${pc.yellow(branchName)} from stack?`,
|
|
65
66
|
});
|
|
66
67
|
if (p.isCancel(confirmed) || !confirmed) {
|
|
67
68
|
p.cancel("Cancelled");
|
|
@@ -69,7 +70,7 @@ If no branch is specified, shows interactive selector.
|
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
// Take snapshot before destructive operation
|
|
72
|
-
await takeSnapshot(meta, stackName, "
|
|
73
|
+
await takeSnapshot(meta, stackName, "delete");
|
|
73
74
|
|
|
74
75
|
// Remove branch
|
|
75
76
|
await removeBranchFromStack(meta, stackName, branchName);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// gh-stack down — Move to parent branch (downstack)
|
|
2
|
+
import * as p from "@clack/prompts";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
import * as git from "../lib/git.ts";
|
|
5
|
+
import { findStackForBranch, writeMetadata } from "../lib/metadata.ts";
|
|
6
|
+
import { ensureMetadata } from "../lib/safety.ts";
|
|
7
|
+
|
|
8
|
+
export default async function down(args: string[]): Promise<void> {
|
|
9
|
+
if (args.includes("--help")) {
|
|
10
|
+
console.log(`
|
|
11
|
+
gh-stack down — Move to parent branch (downstack)
|
|
12
|
+
|
|
13
|
+
USAGE
|
|
14
|
+
gh-stack down [steps]
|
|
15
|
+
|
|
16
|
+
OPTIONS
|
|
17
|
+
[steps] Number of levels to move down (default: 1)
|
|
18
|
+
`);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const steps = parseInt(args[0] || "1", 10);
|
|
23
|
+
if (isNaN(steps) || steps < 1) {
|
|
24
|
+
p.cancel("Invalid step count");
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const meta = await ensureMetadata();
|
|
29
|
+
let currentBranch = await git.currentBranch();
|
|
30
|
+
const stackName = findStackForBranch(meta, currentBranch);
|
|
31
|
+
|
|
32
|
+
if (!stackName) {
|
|
33
|
+
p.cancel(`Branch ${pc.yellow(currentBranch)} is not in any stack`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const stack = meta.stacks[stackName]!;
|
|
38
|
+
|
|
39
|
+
for (let i = 0; i < steps; i++) {
|
|
40
|
+
const branchMeta = stack.branches[currentBranch];
|
|
41
|
+
if (!branchMeta) {
|
|
42
|
+
p.log.warn(`${pc.yellow(currentBranch)} is not tracked in the stack`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const parent = branchMeta.parent;
|
|
47
|
+
if (parent === "main" || parent === "master") {
|
|
48
|
+
if (i === 0) {
|
|
49
|
+
p.log.warn(`${pc.yellow(currentBranch)} is already at the bottom of the stack`);
|
|
50
|
+
} else {
|
|
51
|
+
p.log.info(`Reached bottom of stack at ${pc.yellow(currentBranch)} after ${i} step(s)`);
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
currentBranch = parent;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
await git.checkout(currentBranch);
|
|
60
|
+
|
|
61
|
+
// Update tracking
|
|
62
|
+
stack.last_branch = currentBranch;
|
|
63
|
+
meta.current_stack = stackName;
|
|
64
|
+
await writeMetadata(meta);
|
|
65
|
+
|
|
66
|
+
p.log.success(`On ${pc.yellow(currentBranch)}`);
|
|
67
|
+
}
|