gh-stack 0.7.0 → 0.7.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 CHANGED
@@ -149,9 +149,12 @@ merge [--dry-run] [-d|--delete-branch] [--collapse]
149
149
  review the cumulative diff on GitHub. Re-run
150
150
  `gh-stack merge` (without --collapse) to finish.
151
151
 
152
- delete [<branch>]
153
- Remove a branch from the stack and re-parent its children.
154
- Interactive selector if no branch specified.
152
+ delete [<branch>] [-k|--keep-branch] [--no-remote]
153
+ Remove a branch from the stack and re-parent its children, then
154
+ delete the underlying local git branch (and the remote branch if
155
+ it was pushed). Interactive selector if no branch specified.
156
+ --keep-branch leaves the git branches untouched (metadata only);
157
+ --no-remote deletes the local branch but keeps the remote.
155
158
  ```
156
159
 
157
160
  ### Info & Maintenance
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gh-stack",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Stacked PR manager for squash-merge workflows",
5
5
  "author": "Kiliman <kiliman@gmail.com>",
6
6
  "license": "MIT",
@@ -4,7 +4,7 @@ import pc from "picocolors";
4
4
  import { removeBranchFromStack, getChildren } from "../lib/metadata.ts";
5
5
  import { ensureMetadata, ensureCurrentStack, ensureValidStack } from "../lib/safety.ts";
6
6
  import { takeSnapshot } from "../lib/snapshot.ts";
7
- import { selectBranch } from "../lib/ui.ts";
7
+ import { selectBranch, confirmAction } from "../lib/ui.ts";
8
8
  import * as git from "../lib/git.ts";
9
9
 
10
10
  export default async function deleteCmd(args: string[]): Promise<void> {
@@ -13,14 +13,26 @@ export default async function deleteCmd(args: string[]): Promise<void> {
13
13
  gh-stack delete — Delete a branch from the stack
14
14
 
15
15
  USAGE
16
- gh-stack delete [<branch>]
16
+ gh-stack delete [<branch>] [options]
17
17
 
18
- Removes the branch and re-parents its children to its parent.
19
- If no branch is specified, shows interactive selector.
18
+ Removes the branch from the stack and deletes the underlying git branch,
19
+ re-parenting any children to the removed branch's parent. If the deleted
20
+ branch was pushed, its remote counterpart is deleted too.
21
+
22
+ If no branch is specified, shows an interactive selector.
23
+
24
+ OPTIONS
25
+ -k, --keep-branch Only remove from the stack metadata; leave the
26
+ local and remote git branches untouched
27
+ --no-remote Delete the local branch but keep the remote branch
20
28
  `);
21
29
  return;
22
30
  }
23
31
 
32
+ const keepBranch = args.includes("--keep-branch") || args.includes("-k");
33
+ const noRemote = args.includes("--no-remote");
34
+ const positional = args.filter((a) => !a.startsWith("-"));
35
+
24
36
  const meta = await ensureMetadata();
25
37
  const stackName = ensureCurrentStack(meta);
26
38
  await ensureValidStack(meta, stackName);
@@ -30,7 +42,7 @@ If no branch is specified, shows interactive selector.
30
42
  p.intro(pc.cyan("Delete Branch from Stack"));
31
43
 
32
44
  // Get branch to delete
33
- let branchName = args[0];
45
+ let branchName = positional[0];
34
46
  if (!branchName) {
35
47
  branchName = (await selectBranch(stack, "Select branch to delete", currentBranch)) ?? undefined;
36
48
  if (!branchName) {
@@ -46,9 +58,13 @@ If no branch is specified, shows interactive selector.
46
58
  process.exit(1);
47
59
  }
48
60
 
61
+ // Discover what actually exists in git so we report (and delete) accurately
62
+ const hasLocal = !keepBranch && (await git.localBranchExists(branchName));
63
+ const hasRemote = !keepBranch && !noRemote && (await git.remoteBranchExists(branchName));
64
+
49
65
  // Show what will happen
50
66
  const children = getChildren(stack, branchName);
51
- p.log.info(`Removing: ${pc.yellow(branchName)}`);
67
+ p.log.info(`Removing: ${pc.yellow(branchName)} from stack ${pc.blue(stackName)}`);
52
68
  p.log.info(`Parent was: ${pc.dim(branch.parent)}`);
53
69
 
54
70
  if (children.length > 0) {
@@ -60,11 +76,19 @@ If no branch is specified, shows interactive selector.
60
76
  }
61
77
  }
62
78
 
63
- // Confirm
64
- const confirmed = await p.confirm({
65
- message: `Delete ${pc.yellow(branchName)} from stack?`,
66
- });
67
- if (p.isCancel(confirmed) || !confirmed) {
79
+ if (keepBranch) {
80
+ p.log.info(pc.dim("Keeping git branch (--keep-branch) — stack metadata only"));
81
+ } else {
82
+ if (hasLocal) p.log.info(`Deleting local branch ${pc.yellow(branchName)}`);
83
+ if (hasRemote) p.log.info(`Deleting remote branch ${pc.yellow(`origin/${branchName}`)}`);
84
+ if (!hasLocal && !hasRemote) {
85
+ p.log.info(pc.dim("No git branch found to delete — stack metadata only"));
86
+ }
87
+ }
88
+
89
+ // Confirm (respects --yes / GH_STACK_YES — no interactive hang for agents)
90
+ const confirmed = await confirmAction(`Delete ${pc.yellow(branchName)}?`);
91
+ if (!confirmed) {
68
92
  p.cancel("Cancelled");
69
93
  process.exit(0);
70
94
  }
@@ -72,8 +96,38 @@ If no branch is specified, shows interactive selector.
72
96
  // Take snapshot before destructive operation
73
97
  await takeSnapshot(meta, stackName, "delete");
74
98
 
75
- // Remove branch
99
+ // If we're sitting on the branch we're about to delete, git won't let us
100
+ // remove it — move to the parent (or trunk) first.
101
+ if (hasLocal && branchName === currentBranch) {
102
+ const fallback = (await git.localBranchExists(branch.parent))
103
+ ? branch.parent
104
+ : await git.trunkBranch();
105
+ await git.checkout(fallback);
106
+ p.log.info(`Switched to ${pc.blue(fallback)} (was on the deleted branch)`);
107
+ }
108
+
109
+ // Remove from metadata (re-parents children, fixes last_branch)
76
110
  await removeBranchFromStack(meta, stackName, branchName);
77
111
 
112
+ // Delete the actual git branch(es)
113
+ if (hasRemote) {
114
+ const ok = await git.deleteRemoteBranch(branchName);
115
+ if (ok) {
116
+ p.log.success(`Deleted remote branch ${pc.dim(`origin/${branchName}`)}`);
117
+ } else {
118
+ p.log.warn(
119
+ `Could not delete remote branch ${pc.dim(`origin/${branchName}`)} (delete it manually)`,
120
+ );
121
+ }
122
+ }
123
+ if (hasLocal) {
124
+ const ok = await git.deleteLocalBranch(branchName);
125
+ if (ok) {
126
+ p.log.success(`Deleted local branch ${pc.dim(branchName)}`);
127
+ } else {
128
+ p.log.warn(`Could not delete local branch ${pc.dim(branchName)} (delete it manually)`);
129
+ }
130
+ }
131
+
78
132
  p.outro(pc.green(`Removed ${pc.yellow(branchName)} from stack ${pc.blue(stackName)}`));
79
133
  }
package/src/lib/git.ts CHANGED
@@ -158,6 +158,24 @@ export async function localBranchExists(branch: string): Promise<boolean> {
158
158
  return exitCode === 0;
159
159
  }
160
160
 
161
+ /**
162
+ * Delete a local branch. Uses -D (force) since stacked branches are
163
+ * routinely rebased and won't appear "merged" to git. Returns false if
164
+ * the branch didn't exist or the delete failed.
165
+ */
166
+ export async function deleteLocalBranch(branch: string): Promise<boolean> {
167
+ const { exitCode } = await $`git branch -D ${branch}`.nothrow().quiet();
168
+ return exitCode === 0;
169
+ }
170
+
171
+ /**
172
+ * Delete a branch on the remote (origin). Returns false on failure.
173
+ */
174
+ export async function deleteRemoteBranch(branch: string): Promise<boolean> {
175
+ const { exitCode } = await $`git push origin --delete ${branch}`.nothrow().quiet();
176
+ return exitCode === 0;
177
+ }
178
+
161
179
  /**
162
180
  * Create a temporary tag at a specific commit.
163
181
  */