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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gh-stack",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Stacked PR manager for squash-merge workflows",
5
5
  "author": "Kiliman <kiliman@gmail.com>",
6
6
  "license": "MIT",
@@ -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 Create new PRs as drafts
24
- -n, --no-edit Don't prompt for PR titles (auto-generate from branch name)
25
- --dry-run Show what would happen without doing anything
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 # Push + create PRs interactively
29
- gh-stack submit -n # Push + create PRs with auto-titles
30
- gh-stack submit --draft # Create new PRs as drafts
31
- gh-stack submit --dry-run # Preview what would happen
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
- const noEdit = args.includes("--no-edit") || args.includes("-n") || isAutoYes();
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