gh-stack 0.1.0 → 0.1.1
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 +22 -8
- package/package.json +1 -1
- package/src/commands/add.ts +3 -1
- package/src/commands/list.ts +5 -2
- package/src/commands/restack.ts +17 -15
- package/src/commands/show.ts +5 -2
- package/src/commands/sync.ts +22 -0
- package/src/commands/update-prs.ts +7 -2
package/README.md
CHANGED
|
@@ -47,23 +47,24 @@ gh-stack status
|
|
|
47
47
|
### Stack Management
|
|
48
48
|
|
|
49
49
|
```
|
|
50
|
-
init [--name <name>] [--description <desc>]
|
|
50
|
+
init [--name <name>] [--description <desc>] [--parent <branch>]
|
|
51
51
|
Create a new stack and add the current branch as the first entry.
|
|
52
52
|
|
|
53
|
-
add [<branch>] [--parent <branch>] [--create] [--description <desc>]
|
|
53
|
+
add [<branch>] [--parent <branch>] [--create <branch>] [--description <desc>]
|
|
54
54
|
Add a branch to the current stack. Defaults to the current branch.
|
|
55
|
-
--create makes a new branch off the top of stack first.
|
|
55
|
+
--create <branch> makes a new branch off the top of stack first.
|
|
56
56
|
|
|
57
|
-
remove <branch>
|
|
57
|
+
remove [<branch>]
|
|
58
58
|
Remove a branch from the stack and re-parent its children.
|
|
59
|
+
If omitted, an interactive selector is shown.
|
|
59
60
|
```
|
|
60
61
|
|
|
61
62
|
### Navigation
|
|
62
63
|
|
|
63
64
|
```
|
|
64
65
|
show
|
|
65
|
-
Display the current stack as a tree with
|
|
66
|
-
|
|
66
|
+
Display the current stack as a tree with branch numbers,
|
|
67
|
+
PR numbers, and descriptions. This is the default command.
|
|
67
68
|
|
|
68
69
|
list
|
|
69
70
|
List branches with position numbers. Lightweight output for
|
|
@@ -104,6 +105,7 @@ merge [--dry-run]
|
|
|
104
105
|
Squash-merge the stack top-down locally (PR3 -> PR2 -> PR1),
|
|
105
106
|
then optionally rebase onto main. Keeps all commits local to
|
|
106
107
|
avoid orphaned squash commits from GitHub-only merges.
|
|
108
|
+
Can also close intermediate PRs and archives the stack on completion.
|
|
107
109
|
|
|
108
110
|
update-prs
|
|
109
111
|
Update all PR descriptions with a stack visualization:
|
|
@@ -119,13 +121,25 @@ update-prs
|
|
|
119
121
|
### Maintenance
|
|
120
122
|
|
|
121
123
|
```
|
|
122
|
-
archive [--
|
|
123
|
-
|
|
124
|
+
archive [--restore <name>]
|
|
125
|
+
List archived stacks by default, or restore one by name.
|
|
124
126
|
|
|
125
127
|
undo
|
|
126
128
|
Restore the last snapshot taken before a destructive operation.
|
|
127
129
|
```
|
|
128
130
|
|
|
131
|
+
## Not Implemented Yet
|
|
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.
|
|
141
|
+
```
|
|
142
|
+
|
|
129
143
|
## Global Options
|
|
130
144
|
|
|
131
145
|
```
|
package/package.json
CHANGED
package/src/commands/add.ts
CHANGED
|
@@ -5,7 +5,7 @@ import * as git from "../lib/git.ts";
|
|
|
5
5
|
import { addBranchToStack, getTopOfStack, getOrderedBranches } from "../lib/metadata.ts";
|
|
6
6
|
import { ensureMetadata, ensureCurrentStack } from "../lib/safety.ts";
|
|
7
7
|
import { getPrNumber } from "../lib/github.ts";
|
|
8
|
-
import { selectParent } from "../lib/ui.ts";
|
|
8
|
+
import { isAutoYes, selectParent } from "../lib/ui.ts";
|
|
9
9
|
import type { Branch } from "../types.ts";
|
|
10
10
|
|
|
11
11
|
export default async function add(args: string[]): Promise<void> {
|
|
@@ -95,6 +95,8 @@ OPTIONS
|
|
|
95
95
|
|
|
96
96
|
if (ordered.length === 0) {
|
|
97
97
|
parent = "main";
|
|
98
|
+
} else if (isAutoYes()) {
|
|
99
|
+
parent = getTopOfStack(stack) ?? "main";
|
|
98
100
|
} else {
|
|
99
101
|
parent = (await selectParent(stack, branchToAdd)) ?? undefined;
|
|
100
102
|
if (!parent) {
|
package/src/commands/list.ts
CHANGED
|
@@ -26,7 +26,8 @@ ALIASES
|
|
|
26
26
|
const currentBranch = await git.currentBranch();
|
|
27
27
|
|
|
28
28
|
// Find which stack contains the current branch
|
|
29
|
-
|
|
29
|
+
const branchStackName = findStackForBranch(meta, currentBranch);
|
|
30
|
+
let stackName = branchStackName;
|
|
30
31
|
if (!stackName) stackName = meta.current_stack;
|
|
31
32
|
|
|
32
33
|
if (!stackName || !meta.stacks[stackName]) {
|
|
@@ -39,7 +40,9 @@ ALIASES
|
|
|
39
40
|
|
|
40
41
|
// Update tracking
|
|
41
42
|
meta.current_stack = stackName;
|
|
42
|
-
|
|
43
|
+
if (branchStackName === stackName) {
|
|
44
|
+
stack.last_branch = currentBranch;
|
|
45
|
+
}
|
|
43
46
|
await writeMetadata(meta);
|
|
44
47
|
|
|
45
48
|
// Header
|
package/src/commands/restack.ts
CHANGED
|
@@ -235,25 +235,27 @@ async function handleFreshRestack(
|
|
|
235
235
|
console.log();
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
-
|
|
239
|
-
|
|
238
|
+
if (!dryRun) {
|
|
239
|
+
// Take snapshot before destructive operation
|
|
240
|
+
await takeSnapshot(meta, stackName, "restack");
|
|
240
241
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
242
|
+
// Create temporary tags for stable base references
|
|
243
|
+
p.log.info("Creating temporary base tags...");
|
|
244
|
+
for (const branch of chain) {
|
|
245
|
+
const parent = stack.branches[branch]?.parent;
|
|
246
|
+
if (!parent) continue;
|
|
246
247
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
248
|
+
const mb = await git.mergeBase(branch, parent);
|
|
249
|
+
if (mb) {
|
|
250
|
+
const tagName = `stack-sync-base-${git.sanitizeBranchForTag(branch)}`;
|
|
251
|
+
await git.createTag(tagName, mb);
|
|
252
|
+
console.log(
|
|
253
|
+
` ${pc.green("✓")} Tagged base for ${branch}: ${pc.cyan(tagName)} (${mb.slice(0, 8)})`,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
254
256
|
}
|
|
257
|
+
console.log();
|
|
255
258
|
}
|
|
256
|
-
console.log();
|
|
257
259
|
|
|
258
260
|
// Process the chain
|
|
259
261
|
await processChain(meta, stackName, chain, 0, rebasedBranches, dryRun, verbose);
|
package/src/commands/show.ts
CHANGED
|
@@ -29,7 +29,8 @@ export default async function show(_args: string[]): Promise<void> {
|
|
|
29
29
|
const branch = await git.currentBranch();
|
|
30
30
|
|
|
31
31
|
// Find which stack contains the current branch
|
|
32
|
-
|
|
32
|
+
const branchStackName = findStackForBranch(meta, branch);
|
|
33
|
+
let stackName = branchStackName;
|
|
33
34
|
|
|
34
35
|
if (!stackName) {
|
|
35
36
|
// Fall back to current_stack
|
|
@@ -51,7 +52,9 @@ export default async function show(_args: string[]): Promise<void> {
|
|
|
51
52
|
|
|
52
53
|
// Update current_stack and last_branch
|
|
53
54
|
meta.current_stack = stackName;
|
|
54
|
-
|
|
55
|
+
if (branchStackName === stackName) {
|
|
56
|
+
stack.last_branch = branch;
|
|
57
|
+
}
|
|
55
58
|
await writeMetadata(meta);
|
|
56
59
|
|
|
57
60
|
// Header
|
package/src/commands/sync.ts
CHANGED
|
@@ -54,6 +54,28 @@ with the base branch included.
|
|
|
54
54
|
process.exit(1);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
if (dryRun) {
|
|
58
|
+
p.log.warn("[DRY RUN] No changes will be made");
|
|
59
|
+
p.log.info("Planned operations:");
|
|
60
|
+
console.log(` ${pc.yellow("1.")} Fetch latest ${pc.cyan("origin/main")}`);
|
|
61
|
+
console.log(
|
|
62
|
+
` ${pc.yellow("2.")} Rebase ${pc.yellow(baseBranch)} onto ${pc.cyan("origin/main")}`,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const children = ordered.slice(1);
|
|
66
|
+
if (children.length === 0) {
|
|
67
|
+
console.log(` ${pc.yellow("3.")} No child branches to restack`);
|
|
68
|
+
} else {
|
|
69
|
+
console.log(` ${pc.yellow("3.")} Restack child branches in order:`);
|
|
70
|
+
for (const branch of children) {
|
|
71
|
+
const parent = stack.branches[branch]?.parent || "(unknown)";
|
|
72
|
+
console.log(` ${pc.yellow("→")} ${branch} ${pc.blue(`(parent: ${parent})`)}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
p.outro(pc.green("Sync dry run complete"));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
57
79
|
// Fetch main
|
|
58
80
|
const fetchSpinner = p.spinner();
|
|
59
81
|
fetchSpinner.start("Fetching latest main...");
|
|
@@ -46,6 +46,7 @@ showing the tree structure, PR links, and review status.
|
|
|
46
46
|
branch: string;
|
|
47
47
|
prNumber: number | null;
|
|
48
48
|
prTitle: string;
|
|
49
|
+
prUrl: string | null;
|
|
49
50
|
reviewEmojiStr: string;
|
|
50
51
|
}
|
|
51
52
|
|
|
@@ -55,12 +56,14 @@ showing the tree structure, PR links, and review status.
|
|
|
55
56
|
const branchMeta = stack.branches[branchName]!;
|
|
56
57
|
const prNumber = branchMeta.pr ?? null;
|
|
57
58
|
let prTitle = branchMeta.description || branchName;
|
|
59
|
+
let prUrl: string | null = null;
|
|
58
60
|
let review = "PENDING";
|
|
59
61
|
|
|
60
62
|
if (prNumber) {
|
|
61
63
|
const info = await getPrInfo(prNumber);
|
|
62
64
|
if (info) {
|
|
63
65
|
prTitle = info.title || prTitle;
|
|
66
|
+
prUrl = info.url;
|
|
64
67
|
review = info.reviewDecision || "PENDING";
|
|
65
68
|
}
|
|
66
69
|
}
|
|
@@ -69,6 +72,7 @@ showing the tree structure, PR links, and review status.
|
|
|
69
72
|
branch: branchName,
|
|
70
73
|
prNumber,
|
|
71
74
|
prTitle,
|
|
75
|
+
prUrl,
|
|
72
76
|
reviewEmojiStr: reviewEmoji(review),
|
|
73
77
|
});
|
|
74
78
|
}
|
|
@@ -131,10 +135,11 @@ interface BranchPrInfo {
|
|
|
131
135
|
branch: string;
|
|
132
136
|
prNumber: number | null;
|
|
133
137
|
prTitle: string;
|
|
138
|
+
prUrl: string | null;
|
|
134
139
|
reviewEmojiStr: string;
|
|
135
140
|
}
|
|
136
141
|
|
|
137
|
-
function buildStackViz(branches: BranchPrInfo[], targetIndex: number): string {
|
|
142
|
+
export function buildStackViz(branches: BranchPrInfo[], targetIndex: number): string {
|
|
138
143
|
const lines: string[] = ["### 📚 Stacked on", ""];
|
|
139
144
|
|
|
140
145
|
if (branches.length === 1) {
|
|
@@ -154,7 +159,7 @@ function buildStackViz(branches: BranchPrInfo[], targetIndex: number): string {
|
|
|
154
159
|
// PR link
|
|
155
160
|
let prLink: string;
|
|
156
161
|
if (info.prNumber) {
|
|
157
|
-
prLink = `<a href="
|
|
162
|
+
prLink = info.prUrl ? `<a href="${info.prUrl}">#${info.prNumber}</a>` : `#${info.prNumber}`;
|
|
158
163
|
} else {
|
|
159
164
|
prLink = "(no PR yet)";
|
|
160
165
|
}
|