gh-stack 0.2.0 → 0.2.2
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/package.json +1 -1
- package/src/commands/submit.ts +58 -11
package/package.json
CHANGED
package/src/commands/submit.ts
CHANGED
|
@@ -20,15 +20,19 @@ for branches that don't have them and updating stack visualization
|
|
|
20
20
|
in all PR descriptions. Idempotent — safe to run repeatedly.
|
|
21
21
|
|
|
22
22
|
OPTIONS
|
|
23
|
-
-d, --draft
|
|
24
|
-
-n, --no-edit
|
|
25
|
-
|
|
23
|
+
-d, --draft Create new PRs as drafts
|
|
24
|
+
-n, --no-edit Don't prompt for PR titles (auto-generate from branch name)
|
|
25
|
+
-t, --title <title> PR title for new PRs (skips prompt)
|
|
26
|
+
-b, --body <body> PR body/description for new PRs
|
|
27
|
+
--body-file <f> Read PR body from a file
|
|
28
|
+
--dry-run Show what would happen without doing anything
|
|
26
29
|
|
|
27
30
|
EXAMPLES
|
|
28
|
-
gh-stack submit
|
|
29
|
-
gh-stack submit -n
|
|
30
|
-
gh-stack submit
|
|
31
|
-
gh-stack submit --
|
|
31
|
+
gh-stack submit # Push + create PRs interactively
|
|
32
|
+
gh-stack submit -n # Push + create PRs with auto-titles
|
|
33
|
+
gh-stack submit -t "My PR" -b "Description" # Explicit title and body
|
|
34
|
+
gh-stack submit --draft # Create new PRs as drafts
|
|
35
|
+
gh-stack submit --dry-run # Preview what would happen
|
|
32
36
|
`;
|
|
33
37
|
|
|
34
38
|
/**
|
|
@@ -45,6 +49,20 @@ function branchToTitle(branch: string): string {
|
|
|
45
49
|
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
46
50
|
}
|
|
47
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Check if a local branch is up-to-date with its remote counterpart.
|
|
54
|
+
*/
|
|
55
|
+
async function isBranchUpToDate(branch: string): Promise<boolean> {
|
|
56
|
+
try {
|
|
57
|
+
const localSha = (await $`git rev-parse ${branch}`.text()).trim();
|
|
58
|
+
const remoteSha = (await $`git rev-parse origin/${branch}`.text()).trim();
|
|
59
|
+
return localSha === remoteSha;
|
|
60
|
+
} catch {
|
|
61
|
+
// Remote branch doesn't exist yet
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
48
66
|
/**
|
|
49
67
|
* Push a branch to origin with upstream tracking and force-with-lease.
|
|
50
68
|
*/
|
|
@@ -65,7 +83,32 @@ export default async function submit(args: string[]): Promise<void> {
|
|
|
65
83
|
|
|
66
84
|
const draftFlag = args.includes("--draft") || args.includes("-d");
|
|
67
85
|
const dryRun = args.includes("--dry-run");
|
|
68
|
-
|
|
86
|
+
|
|
87
|
+
// Parse value flags
|
|
88
|
+
let titleFlag: string | undefined;
|
|
89
|
+
let bodyFlag: string | undefined;
|
|
90
|
+
let bodyFileFlag: string | undefined;
|
|
91
|
+
for (let i = 0; i < args.length; i++) {
|
|
92
|
+
if ((args[i] === "--title" || args[i] === "-t") && args[i + 1]) {
|
|
93
|
+
titleFlag = args[++i];
|
|
94
|
+
} else if ((args[i] === "--body" || args[i] === "-b") && args[i + 1]) {
|
|
95
|
+
bodyFlag = args[++i];
|
|
96
|
+
} else if (args[i] === "--body-file" && args[i + 1]) {
|
|
97
|
+
bodyFileFlag = args[++i];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Resolve body from file if provided
|
|
102
|
+
if (bodyFileFlag && !bodyFlag) {
|
|
103
|
+
try {
|
|
104
|
+
bodyFlag = await Bun.file(bodyFileFlag).text();
|
|
105
|
+
} catch {
|
|
106
|
+
p.cancel(`Could not read body file: ${bodyFileFlag}`);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const noEdit = args.includes("--no-edit") || args.includes("-n") || isAutoYes() || !!titleFlag;
|
|
69
112
|
|
|
70
113
|
const meta = await ensureMetadata();
|
|
71
114
|
const branch = await git.currentBranch();
|
|
@@ -105,9 +148,11 @@ export default async function submit(args: string[]): Promise<void> {
|
|
|
105
148
|
const branchMeta = stack.branches[branchName]!;
|
|
106
149
|
const parentBranch = branchMeta.parent;
|
|
107
150
|
|
|
108
|
-
// ── Push ──
|
|
151
|
+
// ── Push (skip if already up-to-date) ──
|
|
109
152
|
if (dryRun) {
|
|
110
153
|
p.log.step(`${pc.dim("push")} ${branchName} → origin/${branchName}`);
|
|
154
|
+
} else if (await isBranchUpToDate(branchName)) {
|
|
155
|
+
p.log.info(`${pc.dim("✓")} ${pc.blue(branchName)} already up to date`);
|
|
111
156
|
} else {
|
|
112
157
|
const s = p.spinner();
|
|
113
158
|
s.start(`Pushing ${pc.blue(branchName)}...`);
|
|
@@ -150,7 +195,7 @@ export default async function submit(args: string[]): Promise<void> {
|
|
|
150
195
|
}
|
|
151
196
|
} else {
|
|
152
197
|
// ── Create new PR ──
|
|
153
|
-
const defaultTitle = branchToTitle(branchName);
|
|
198
|
+
const defaultTitle = titleFlag || branchToTitle(branchName);
|
|
154
199
|
let title = defaultTitle;
|
|
155
200
|
|
|
156
201
|
if (!noEdit && !dryRun) {
|
|
@@ -166,6 +211,8 @@ export default async function submit(args: string[]): Promise<void> {
|
|
|
166
211
|
title = (input as string).trim() || defaultTitle;
|
|
167
212
|
}
|
|
168
213
|
|
|
214
|
+
const body = bodyFlag ?? "";
|
|
215
|
+
|
|
169
216
|
if (dryRun) {
|
|
170
217
|
p.log.step(
|
|
171
218
|
`${pc.dim("create")} PR for ${branchName} → ${parentBranch} "${title}"${draftFlag ? " (draft)" : ""}`,
|
|
@@ -185,7 +232,7 @@ export default async function submit(args: string[]): Promise<void> {
|
|
|
185
232
|
"--title",
|
|
186
233
|
title,
|
|
187
234
|
"--body",
|
|
188
|
-
|
|
235
|
+
body,
|
|
189
236
|
];
|
|
190
237
|
if (draftFlag) ghArgs.push("--draft");
|
|
191
238
|
|