gh-stack 0.1.0

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.
@@ -0,0 +1,372 @@
1
+ // gh-stack status — PR dashboard (CI, reviews)
2
+ // Port of reference/check-my-prs-fast.sh
3
+ import pc from "picocolors";
4
+ import * as git from "../lib/git.ts";
5
+ import {
6
+ readMetadata,
7
+ metadataExists,
8
+ getOrderedBranches,
9
+ findStackForBranch,
10
+ } from "../lib/metadata.ts";
11
+ import { getMyOpenPrs, reviewEmoji, ciEmoji } from "../lib/github.ts";
12
+ import type { PrStatus } from "../types.ts";
13
+
14
+ export default async function status(args: string[]): Promise<void> {
15
+ if (args.includes("--help")) {
16
+ console.log(`
17
+ gh-stack status — PR dashboard
18
+
19
+ USAGE
20
+ gh-stack status [options]
21
+
22
+ OPTIONS
23
+ --current Show only the current stack (or current PR if standalone)
24
+ --json Output as JSON (progress goes to stderr)
25
+
26
+ Shows open PRs across stacks with review state, CI status, and merge readiness.
27
+ Progress/spinner output goes to stderr so stdout stays clean for piping.
28
+ `);
29
+ return;
30
+ }
31
+
32
+ const jsonMode = args.includes("--json");
33
+ const currentOnly = args.includes("--current");
34
+
35
+ // Progress goes to stderr so stdout is clean for agents
36
+ process.stderr.write("Fetching open PRs...\n");
37
+
38
+ const prs = await getMyOpenPrs();
39
+
40
+ process.stderr.write(`Found ${prs.length} open PR(s)\n`);
41
+
42
+ if (prs.length === 0) {
43
+ if (jsonMode) {
44
+ console.log(JSON.stringify({ stacks: [], standalone: [] }, null, 2));
45
+ } else {
46
+ console.log("No open PRs found. 🎉");
47
+ }
48
+ return;
49
+ }
50
+
51
+ // Build lookup by PR number
52
+ const prMap = new Map<number, PrStatus>();
53
+ for (const pr of prs) {
54
+ prMap.set(pr.number, pr);
55
+ }
56
+
57
+ // Determine current branch and stack for --current filtering.
58
+ // --current means: "show me the status of whatever branch I'm ON right now."
59
+ // If that branch is in a stack → show that stack's PRs.
60
+ // If standalone → show just that branch's PR.
61
+ // Do NOT fall back to metadata's current_stack — that's stale state.
62
+ let currentStackName: string | null = null;
63
+ let currentBranch: string | null = null;
64
+ let currentPrNumber: number | null = null;
65
+
66
+ if (currentOnly) {
67
+ try {
68
+ currentBranch = await git.currentBranch();
69
+ } catch {
70
+ // Not on a branch — show nothing for --current
71
+ }
72
+
73
+ if (currentBranch && (await metadataExists())) {
74
+ const meta = await readMetadata();
75
+ if (meta) {
76
+ // Only use findStackForBranch — no fallback to current_stack
77
+ currentStackName = findStackForBranch(meta, currentBranch);
78
+
79
+ // If not in any stack, find the PR for this branch directly
80
+ if (!currentStackName) {
81
+ for (const pr of prs) {
82
+ if (pr.headRefName === currentBranch) {
83
+ currentPrNumber = pr.number;
84
+ break;
85
+ }
86
+ }
87
+ // Also check metadata for the PR number
88
+ if (!currentPrNumber && meta) {
89
+ for (const stack of Object.values(meta.stacks)) {
90
+ const branch = stack.branches[currentBranch];
91
+ if (branch?.pr) {
92
+ currentPrNumber = branch.pr;
93
+ break;
94
+ }
95
+ }
96
+ }
97
+ }
98
+ }
99
+ }
100
+
101
+ // If not in metadata at all, try to match by branch name from open PRs
102
+ if (!currentStackName && !currentPrNumber && currentBranch) {
103
+ for (const pr of prs) {
104
+ if (pr.headRefName === currentBranch) {
105
+ currentPrNumber = pr.number;
106
+ break;
107
+ }
108
+ }
109
+ }
110
+ }
111
+
112
+ // ── JSON mode ──
113
+ if (jsonMode) {
114
+ outputJson(prs, prMap, currentOnly, currentStackName, currentPrNumber);
115
+ return;
116
+ }
117
+
118
+ // ── Human mode ──
119
+ outputHuman(prs, prMap, currentOnly, currentStackName, currentBranch, currentPrNumber);
120
+ }
121
+
122
+ // ────────────────────────────────────────────────────────
123
+ // JSON output
124
+ // ────────────────────────────────────────────────────────
125
+
126
+ async function outputJson(
127
+ prs: PrStatus[],
128
+ prMap: Map<number, PrStatus>,
129
+ currentOnly: boolean,
130
+ currentStackName: string | null,
131
+ currentPrNumber: number | null,
132
+ ): Promise<void> {
133
+ const stackedPrNumbers = new Set<number>();
134
+ const stacks: any[] = [];
135
+
136
+ if (await metadataExists()) {
137
+ const meta = await readMetadata();
138
+ if (meta) {
139
+ for (const [stackName, stack] of Object.entries(meta.stacks)) {
140
+ if (currentOnly && stackName !== currentStackName) continue;
141
+
142
+ const ordered = getOrderedBranches(stack);
143
+ const branches: any[] = [];
144
+
145
+ for (const branchName of ordered) {
146
+ const branch = stack.branches[branchName]!;
147
+ const prNum = branch.pr;
148
+ const pr = prNum ? prMap.get(prNum) : undefined;
149
+
150
+ if (prNum && pr) {
151
+ stackedPrNumbers.add(prNum);
152
+ branches.push({
153
+ branch: branchName,
154
+ pr: prNum,
155
+ title: pr.title,
156
+ state: pr.state,
157
+ isDraft: pr.isDraft,
158
+ url: pr.url,
159
+ review: pr.reviewDecision || "PENDING",
160
+ ci: {
161
+ total: pr.totalChecks,
162
+ passed: pr.passedChecks,
163
+ failed: pr.failedChecks,
164
+ pending: pr.pendingChecks,
165
+ failedNames: pr.failedNames,
166
+ },
167
+ });
168
+ } else if (prNum) {
169
+ // PR exists in metadata but not in open PRs (merged/closed)
170
+ stackedPrNumbers.add(prNum);
171
+ branches.push({
172
+ branch: branchName,
173
+ pr: prNum,
174
+ title: branch.description || branchName,
175
+ state: "CLOSED",
176
+ isDraft: false,
177
+ url: null,
178
+ review: null,
179
+ ci: null,
180
+ });
181
+ }
182
+ }
183
+
184
+ if (branches.length > 0 || !currentOnly) {
185
+ stacks.push({
186
+ name: stackName,
187
+ description: stack.description,
188
+ branches,
189
+ });
190
+ }
191
+ }
192
+ }
193
+ }
194
+
195
+ // Standalone PRs
196
+ // In --current mode: only show the specific standalone PR for this branch
197
+ // (and only if the branch isn't already in a stack)
198
+ // Otherwise: show all standalone PRs
199
+ const filteredStandalone = prs
200
+ .filter((pr) => {
201
+ if (stackedPrNumbers.has(pr.number)) return false;
202
+ if (currentOnly) return pr.number === currentPrNumber;
203
+ return true;
204
+ })
205
+ .toSorted(
206
+ (a, b) => new Date(b.updatedAt || "").getTime() - new Date(a.updatedAt || "").getTime(),
207
+ );
208
+
209
+ const standalone = filteredStandalone.map((pr) => ({
210
+ pr: pr.number,
211
+ title: pr.title,
212
+ state: pr.state,
213
+ isDraft: pr.isDraft,
214
+ url: pr.url,
215
+ review: pr.reviewDecision || "PENDING",
216
+ ci: {
217
+ total: pr.totalChecks,
218
+ passed: pr.passedChecks,
219
+ failed: pr.failedChecks,
220
+ pending: pr.pendingChecks,
221
+ failedNames: pr.failedNames,
222
+ },
223
+ }));
224
+
225
+ // --current mode: return a single unwrapped object (stack or standalone PR)
226
+ if (currentOnly) {
227
+ if (stacks.length > 0) {
228
+ console.log(JSON.stringify(stacks[0], null, 2));
229
+ } else if (standalone.length > 0) {
230
+ console.log(JSON.stringify(standalone[0], null, 2));
231
+ } else {
232
+ console.log(JSON.stringify(null));
233
+ }
234
+ return;
235
+ }
236
+
237
+ console.log(JSON.stringify({ stacks, standalone }, null, 2));
238
+ }
239
+
240
+ // ────────────────────────────────────────────────────────
241
+ // Human-readable output
242
+ // ────────────────────────────────────────────────────────
243
+
244
+ async function outputHuman(
245
+ prs: PrStatus[],
246
+ prMap: Map<number, PrStatus>,
247
+ currentOnly: boolean,
248
+ currentStackName: string | null,
249
+ currentBranch: string | null,
250
+ currentPrNumber: number | null,
251
+ ): Promise<void> {
252
+ const stackedPrNumbers = new Set<number>();
253
+
254
+ console.log();
255
+ console.log(`${pc.bold("📊 My Open PRs")} ${pc.dim(`(${prs.length} open)`)}`);
256
+ console.log();
257
+
258
+ // ── Stacked PRs ──
259
+ if (await metadataExists()) {
260
+ const meta = await readMetadata();
261
+ if (meta) {
262
+ for (const [stackName, stack] of Object.entries(meta.stacks)) {
263
+ if (currentOnly && stackName !== currentStackName) continue;
264
+
265
+ const ordered = getOrderedBranches(stack);
266
+
267
+ // Find which PRs in this stack are still open
268
+ const openPrsInStack: number[] = [];
269
+ for (const branch of ordered) {
270
+ const prNum = stack.branches[branch]?.pr;
271
+ if (prNum && prMap.has(prNum)) {
272
+ openPrsInStack.push(prNum);
273
+ stackedPrNumbers.add(prNum);
274
+ }
275
+ }
276
+
277
+ if (openPrsInStack.length === 0) continue;
278
+
279
+ console.log(`${pc.cyan("📚 Stack:")} ${pc.yellow(stackName)}`);
280
+ if (stack.description) {
281
+ console.log(` ${pc.dim(stack.description)}`);
282
+ }
283
+ console.log();
284
+
285
+ for (let i = 0; i < openPrsInStack.length; i++) {
286
+ const prNum = openPrsInStack[i]!;
287
+ const pr = prMap.get(prNum)!;
288
+ const isLast = i === openPrsInStack.length - 1;
289
+ const tree = isLast ? "┗━" : "┣━";
290
+
291
+ if (i > 0) {
292
+ console.log(" ┃");
293
+ }
294
+
295
+ const formatted = formatPr(pr);
296
+ console.log(` ${tree} ${formatted.line1}`);
297
+ console.log(` ${formatted.line2}`);
298
+ console.log(` ${pc.dim(pr.url)}`);
299
+
300
+ for (const name of pr.failedNames) {
301
+ console.log(` ${pc.red(`↳ ${name}`)}`);
302
+ }
303
+ }
304
+
305
+ console.log();
306
+ }
307
+ }
308
+ }
309
+
310
+ // ── Standalone PRs ──
311
+ // In --current mode: only show if current branch is a standalone PR
312
+ // Otherwise: show all standalone PRs
313
+ {
314
+ const standalonePrs = prs
315
+ .filter((pr) => {
316
+ if (stackedPrNumbers.has(pr.number)) return false;
317
+ if (currentOnly) return pr.number === currentPrNumber;
318
+ return true;
319
+ })
320
+ .toSorted(
321
+ (a, b) => new Date(b.updatedAt || "").getTime() - new Date(a.updatedAt || "").getTime(),
322
+ );
323
+
324
+ if (standalonePrs.length > 0) {
325
+ if (!currentOnly) {
326
+ console.log(`${pc.cyan("📋 Standalone PRs")}`);
327
+ console.log();
328
+ }
329
+
330
+ for (const pr of standalonePrs) {
331
+ const formatted = formatPr(pr);
332
+ console.log(formatted.line1);
333
+ console.log(` ${formatted.line2}`);
334
+ console.log(` ${pc.dim(pr.url)}`);
335
+
336
+ for (const name of pr.failedNames) {
337
+ console.log(` ${pc.red(`↳ ${name}`)}`);
338
+ }
339
+ console.log();
340
+ }
341
+ }
342
+ }
343
+
344
+ // Footer
345
+ console.log(pc.dim("─".repeat(40)));
346
+ }
347
+
348
+ function formatPr(pr: PrStatus): { line1: string; line2: string } {
349
+ const stateIcon = pr.isDraft ? "📝" : "🔵";
350
+ const reviewIcon = reviewEmoji(pr.reviewDecision);
351
+ const reviewStr = reviewText(pr.reviewDecision);
352
+ const ci = ciEmoji(pr);
353
+ const title = pr.title.length > 70 ? pr.title.slice(0, 67) + "..." : pr.title;
354
+
355
+ return {
356
+ line1: `${stateIcon} #${pr.number}: ${title}`,
357
+ line2: `${reviewIcon} ${reviewStr} │ ${ci.icon} ${ci.text}`,
358
+ };
359
+ }
360
+
361
+ function reviewText(decision: string | null | undefined): string {
362
+ switch (decision) {
363
+ case "APPROVED":
364
+ return "Approved";
365
+ case "CHANGES_REQUESTED":
366
+ return "Changes requested";
367
+ case "REVIEW_REQUIRED":
368
+ return "Needs review";
369
+ default:
370
+ return "Pending";
371
+ }
372
+ }
@@ -0,0 +1,114 @@
1
+ // gh-stack switch — Switch between branches or stacks
2
+ import * as p from "@clack/prompts";
3
+ import pc from "picocolors";
4
+ import * as git from "../lib/git.ts";
5
+ import { writeMetadata, getOrderedBranches, findStackForBranch } from "../lib/metadata.ts";
6
+ import { ensureMetadata } from "../lib/safety.ts";
7
+ import { selectBranch, selectStack } from "../lib/ui.ts";
8
+
9
+ export default async function switchCmd(args: string[]): Promise<void> {
10
+ const isStackMode = args.includes("--stack");
11
+ const numberArg = args.find((a) => /^\d+$/.test(a));
12
+
13
+ if (args.includes("--help")) {
14
+ console.log(`
15
+ gh-stack switch — Switch branch or stack
16
+
17
+ USAGE
18
+ gh-stack switch <number> Switch to branch by position (non-interactive)
19
+ gh-stack switch Interactive branch picker (arrow keys)
20
+ gh-stack switch --stack Switch between stacks (interactive)
21
+
22
+ EXAMPLES
23
+ gh-stack switch 1 Jump to first branch in stack (base)
24
+ gh-stack switch 2 Jump to second branch in stack
25
+
26
+ TIP
27
+ Use 'gh-stack list' to see branch numbers.
28
+ `);
29
+ return;
30
+ }
31
+
32
+ const meta = await ensureMetadata();
33
+ const currentBranch = await git.currentBranch();
34
+
35
+ if (isStackMode) {
36
+ // ── Stack switching ──
37
+ p.intro(pc.cyan("Switch Stack"));
38
+
39
+ const stackName = await selectStack(meta, "Switch to stack");
40
+ if (!stackName) {
41
+ p.cancel("Cancelled");
42
+ process.exit(0);
43
+ }
44
+
45
+ // Update current_stack
46
+ meta.current_stack = stackName;
47
+ await writeMetadata(meta);
48
+
49
+ // Checkout last branch in stack
50
+ const stack = meta.stacks[stackName]!;
51
+ const lastBranch = stack.last_branch;
52
+
53
+ if (lastBranch && lastBranch !== currentBranch) {
54
+ p.log.info(`Switching to stack ${pc.yellow(stackName)}`);
55
+ p.log.info(`Checking out ${pc.yellow(lastBranch)}`);
56
+ await git.checkout(lastBranch);
57
+ }
58
+
59
+ p.outro(pc.green(`Switched to stack ${pc.yellow(stackName)}`));
60
+ } else {
61
+ // ── Branch switching ──
62
+ // Find current stack
63
+ let stackName = findStackForBranch(meta, currentBranch);
64
+ if (!stackName) stackName = meta.current_stack;
65
+
66
+ if (!stackName || !meta.stacks[stackName]) {
67
+ p.cancel(`No active stack found.\n\n Create one with:\n ${pc.green("gh-stack init")}`);
68
+ process.exit(1);
69
+ }
70
+
71
+ const stack = meta.stacks[stackName]!;
72
+ const ordered = getOrderedBranches(stack);
73
+
74
+ if (ordered.length === 0) {
75
+ p.cancel("No branches in this stack");
76
+ process.exit(1);
77
+ }
78
+
79
+ let targetBranch: string | null = null;
80
+
81
+ if (numberArg) {
82
+ // Switch by number
83
+ const index = parseInt(numberArg, 10) - 1;
84
+ if (index < 0 || index >= ordered.length) {
85
+ p.cancel(`Invalid branch number. Valid range: 1-${ordered.length}`);
86
+ process.exit(1);
87
+ }
88
+ targetBranch = ordered[index]!;
89
+ } else {
90
+ // Interactive selector
91
+ targetBranch = await selectBranch(stack, "Switch to branch", currentBranch);
92
+ }
93
+
94
+ if (!targetBranch) {
95
+ p.cancel("Cancelled");
96
+ process.exit(0);
97
+ }
98
+
99
+ if (targetBranch === currentBranch) {
100
+ p.log.info(`Already on ${pc.yellow(targetBranch)}`);
101
+ return;
102
+ }
103
+
104
+ p.log.info(`Switching to ${pc.yellow(targetBranch)}`);
105
+ await git.checkout(targetBranch);
106
+
107
+ // Update last_branch
108
+ stack.last_branch = targetBranch;
109
+ meta.current_stack = stackName;
110
+ await writeMetadata(meta);
111
+
112
+ p.log.success(`On branch ${pc.yellow(targetBranch)}`);
113
+ }
114
+ }
@@ -0,0 +1,189 @@
1
+ // gh-stack sync — Fetch main, rebase base branch onto main, then restack all
2
+ import * as p from "@clack/prompts";
3
+ import pc from "picocolors";
4
+ import * as git from "../lib/git.ts";
5
+ import { findStackForBranch, getOrderedBranches, saveRestackState } from "../lib/metadata.ts";
6
+ import { ensureMetadata, ensureCleanWorkingTree } from "../lib/safety.ts";
7
+ import { takeSnapshot } from "../lib/snapshot.ts";
8
+ import { confirmAction } from "../lib/ui.ts";
9
+
10
+ export default async function sync(args: string[]): Promise<void> {
11
+ const dryRun = args.includes("--dry-run");
12
+
13
+ if (args.includes("--help")) {
14
+ console.log(`
15
+ gh-stack sync — Sync base with main + restack all
16
+
17
+ USAGE
18
+ gh-stack sync [--dry-run]
19
+
20
+ Fetches latest main, rebases the base branch onto main,
21
+ then restacks all children. Equivalent to running restack
22
+ with the base branch included.
23
+ `);
24
+ return;
25
+ }
26
+
27
+ await ensureCleanWorkingTree();
28
+
29
+ const meta = await ensureMetadata();
30
+ const currentBranch = await git.currentBranch();
31
+ const stackName = findStackForBranch(meta, currentBranch);
32
+
33
+ if (!stackName) {
34
+ p.cancel(`Branch ${pc.blue(currentBranch)} not found in any stack`);
35
+ process.exit(1);
36
+ }
37
+
38
+ const stack = meta.stacks[stackName]!;
39
+ const ordered = getOrderedBranches(stack);
40
+
41
+ if (ordered.length === 0) {
42
+ p.cancel("No branches in this stack");
43
+ process.exit(1);
44
+ }
45
+
46
+ p.intro(pc.cyan("Git Stack Sync"));
47
+
48
+ // Find the base branch (first branch, parent = main)
49
+ const baseBranch = ordered[0]!;
50
+ const baseParent = stack.branches[baseBranch]?.parent;
51
+
52
+ if (baseParent !== "main") {
53
+ p.cancel(`Base branch ${pc.yellow(baseBranch)} doesn't have main as parent`);
54
+ process.exit(1);
55
+ }
56
+
57
+ // Fetch main
58
+ const fetchSpinner = p.spinner();
59
+ fetchSpinner.start("Fetching latest main...");
60
+ await git.fetchMain();
61
+ fetchSpinner.stop("Fetched latest main");
62
+
63
+ // Take snapshot
64
+ await takeSnapshot(meta, stackName, "sync");
65
+
66
+ // Create temporary tags for ALL branches BEFORE any rebasing starts.
67
+ // This is critical: after rebasing PR1, `git merge-base PR2 PR1` returns
68
+ // wrong results. Tags capture the true divergence points while history
69
+ // is still intact.
70
+ p.log.info("Creating temporary base tags for all branches...");
71
+ for (const branch of ordered) {
72
+ const parent = stack.branches[branch]?.parent;
73
+ if (!parent) continue;
74
+
75
+ // For the base branch, the "parent" is main — tag its merge-base with main
76
+ const parentRef = parent === "main" ? "origin/main" : parent;
77
+ const mb = await git.mergeBase(branch, parentRef);
78
+ if (mb) {
79
+ const tagName = `stack-sync-base-${git.sanitizeBranchForTag(branch)}`;
80
+ await git.createTag(tagName, mb);
81
+ console.log(
82
+ ` ${pc.green("✓")} Tagged base for ${branch}: ${pc.cyan(tagName)} (${mb.slice(0, 8)})`,
83
+ );
84
+ }
85
+ }
86
+ console.log();
87
+
88
+ // Step 1: Rebase base branch onto main
89
+ console.log();
90
+ console.log(pc.cyan("━".repeat(40)));
91
+ console.log(`${pc.blue("Step 1:")} Rebase ${pc.yellow(baseBranch)} onto main`);
92
+ console.log(pc.cyan("━".repeat(40)));
93
+ console.log();
94
+
95
+ // Check if already up to date
96
+ if (await git.isAncestor("origin/main", baseBranch)) {
97
+ p.log.success(`${baseBranch} is already up to date with main`);
98
+ } else if (dryRun) {
99
+ p.log.warn(`[DRY RUN] Would rebase ${baseBranch} onto origin/main`);
100
+ } else {
101
+ const confirmed = await confirmAction(`Rebase ${pc.yellow(baseBranch)} onto origin/main?`);
102
+ if (!confirmed) {
103
+ p.cancel("Cancelled");
104
+ process.exit(0);
105
+ }
106
+
107
+ p.log.info(`Checking out ${pc.yellow(baseBranch)}...`);
108
+ await git.checkout(baseBranch);
109
+
110
+ // Save restack state BEFORE rebasing so --resume can find it.
111
+ // The chain is the full ordered list — base branch at index 0,
112
+ // so resume knows where we were and can continue with children.
113
+ await saveRestackState({
114
+ current_index: 0,
115
+ stack_name: stackName,
116
+ chain: ordered,
117
+ });
118
+
119
+ p.log.info("Rebasing onto origin/main...");
120
+ const success = await git.rebase("origin/main");
121
+
122
+ if (!success) {
123
+ console.log();
124
+ p.log.error("Rebase conflict — resolve and run:");
125
+ console.log(` ${pc.green("git rebase --continue")}`);
126
+ console.log(` ${pc.green("gh-stack restack --resume")}`);
127
+ process.exit(2);
128
+ }
129
+
130
+ p.log.success("Base branch rebased onto main");
131
+
132
+ // Prompt to push base
133
+ await promptForcePush(baseBranch);
134
+ }
135
+
136
+ // Step 2: Restack children
137
+ const children = ordered.slice(1);
138
+
139
+ if (children.length === 0) {
140
+ p.outro(pc.green("Sync complete! (no children to restack)"));
141
+ return;
142
+ }
143
+
144
+ console.log();
145
+ console.log(pc.cyan("━".repeat(40)));
146
+ console.log(`${pc.blue("Step 2:")} Restack ${children.length} child branch(es)`);
147
+ console.log(pc.cyan("━".repeat(40)));
148
+ console.log();
149
+
150
+ // Delegate to restack for the remaining branches
151
+ // We import and run restack in --rebase mode effectively
152
+ const { default: restack } = await import("./restack.ts");
153
+
154
+ // Switch to first child and run restack
155
+ if (children.length > 0) {
156
+ await git.checkout(children[0]!);
157
+ await restack(dryRun ? ["--dry-run"] : []);
158
+ }
159
+ }
160
+
161
+ async function promptForcePush(branch: string): Promise<void> {
162
+ if (branch === "main" || branch === "master") return;
163
+ if (!(await git.remoteBranchExists(branch))) return;
164
+
165
+ const localSha = await git.revParse(branch);
166
+ let remoteSha: string;
167
+ try {
168
+ remoteSha = await git.revParse(`origin/${branch}`);
169
+ } catch {
170
+ return;
171
+ }
172
+
173
+ if (localSha === remoteSha) return;
174
+
175
+ console.log();
176
+ p.log.info(`${pc.yellow("⚠")} ${branch} is out of sync with remote`);
177
+
178
+ const confirmed = await confirmAction("Push with --force-with-lease?");
179
+ if (confirmed) {
180
+ const ok = await git.forcePushWithLease(branch);
181
+ if (ok) {
182
+ p.log.success("Pushed successfully");
183
+ } else {
184
+ p.log.error("Push failed");
185
+ }
186
+ } else {
187
+ p.log.warn("Skipping push");
188
+ }
189
+ }