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 +6 -3
- package/package.json +1 -1
- package/src/commands/delete.ts +66 -12
- package/src/lib/git.ts +18 -0
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
|
-
|
|
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
package/src/commands/delete.ts
CHANGED
|
@@ -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
|
|
19
|
-
|
|
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 =
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
//
|
|
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
|
*/
|