dubstack 0.3.0 → 0.5.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.
- package/README.md +67 -0
- package/dist/index.js +1753 -289
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
10
11
|
|
|
11
12
|
// src/lib/errors.ts
|
|
12
|
-
var DubError
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
var DubError;
|
|
14
|
+
var init_errors = __esm({
|
|
15
|
+
"src/lib/errors.ts"() {
|
|
16
|
+
"use strict";
|
|
17
|
+
DubError = class extends Error {
|
|
18
|
+
constructor(message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "DubError";
|
|
21
|
+
}
|
|
22
|
+
};
|
|
16
23
|
}
|
|
17
|
-
};
|
|
24
|
+
});
|
|
18
25
|
|
|
19
26
|
// src/lib/git.ts
|
|
20
27
|
import { execa } from "execa";
|
|
@@ -204,6 +211,69 @@ async function commitStaged(message, cwd) {
|
|
|
204
211
|
);
|
|
205
212
|
}
|
|
206
213
|
}
|
|
214
|
+
async function commit(cwd, options) {
|
|
215
|
+
const args = ["commit"];
|
|
216
|
+
if (options?.message) {
|
|
217
|
+
args.push("-m", options.message);
|
|
218
|
+
}
|
|
219
|
+
if (options?.noEdit) {
|
|
220
|
+
args.push("--no-edit");
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
await execa("git", args, { cwd, stdio: "inherit" });
|
|
224
|
+
} catch {
|
|
225
|
+
throw new DubError(
|
|
226
|
+
"Commit failed. Ensure there are staged changes and git hooks pass."
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
async function amendCommit(cwd, options) {
|
|
231
|
+
const args = ["commit", "--amend"];
|
|
232
|
+
if (options?.message) {
|
|
233
|
+
args.push("-m", options.message);
|
|
234
|
+
}
|
|
235
|
+
if (options?.noEdit) {
|
|
236
|
+
args.push("--no-edit");
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
await execa("git", args, { cwd, stdio: "inherit" });
|
|
240
|
+
} catch (e) {
|
|
241
|
+
throw new DubError(
|
|
242
|
+
`Amend failed: ${e instanceof Error ? e.message : String(e)}`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
async function interactiveRebase(base, cwd) {
|
|
247
|
+
try {
|
|
248
|
+
await execa("git", ["rebase", "-i", base], { cwd, stdio: "inherit" });
|
|
249
|
+
} catch {
|
|
250
|
+
throw new DubError("Interactive rebase failed or was cancelled.");
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async function interactiveStage(cwd) {
|
|
254
|
+
try {
|
|
255
|
+
await execa("git", ["add", "-p"], { cwd, stdio: "inherit" });
|
|
256
|
+
} catch {
|
|
257
|
+
throw new DubError("Interactive staging failed.");
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async function stageUpdate(cwd) {
|
|
261
|
+
try {
|
|
262
|
+
await execa("git", ["add", "-u"], { cwd });
|
|
263
|
+
} catch {
|
|
264
|
+
throw new DubError("Failed to stage updates.");
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
async function getDiff(cwd, staged) {
|
|
268
|
+
try {
|
|
269
|
+
const args = ["diff"];
|
|
270
|
+
if (staged) args.push("--cached");
|
|
271
|
+
const { stdout } = await execa("git", args, { cwd });
|
|
272
|
+
return stdout;
|
|
273
|
+
} catch {
|
|
274
|
+
return "";
|
|
275
|
+
}
|
|
276
|
+
}
|
|
207
277
|
async function listBranches(cwd) {
|
|
208
278
|
try {
|
|
209
279
|
const { stdout } = await execa(
|
|
@@ -216,6 +286,111 @@ async function listBranches(cwd) {
|
|
|
216
286
|
throw new DubError("Failed to list branches.");
|
|
217
287
|
}
|
|
218
288
|
}
|
|
289
|
+
async function fetchBranches(branches, cwd, remote = "origin") {
|
|
290
|
+
if (branches.length === 0) return;
|
|
291
|
+
for (const branch of branches) {
|
|
292
|
+
try {
|
|
293
|
+
await execa("git", ["fetch", remote, branch], { cwd });
|
|
294
|
+
} catch (error) {
|
|
295
|
+
const stderr = typeof error?.stderr === "string" ? error.stderr : "";
|
|
296
|
+
const stdout = typeof error?.stdout === "string" ? error.stdout : "";
|
|
297
|
+
const output2 = `${stderr}
|
|
298
|
+
${stdout}`;
|
|
299
|
+
if (output2.includes("couldn't find remote ref")) {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
throw new DubError(`Failed to fetch branches from '${remote}'.`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
async function remoteBranchExists(branch, cwd, remote = "origin") {
|
|
307
|
+
try {
|
|
308
|
+
await execa("git", ["rev-parse", "--verify", `${remote}/${branch}`], {
|
|
309
|
+
cwd
|
|
310
|
+
});
|
|
311
|
+
return true;
|
|
312
|
+
} catch {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
async function getRefSha(ref, cwd) {
|
|
317
|
+
try {
|
|
318
|
+
const { stdout } = await execa("git", ["rev-parse", ref], { cwd });
|
|
319
|
+
return stdout.trim();
|
|
320
|
+
} catch {
|
|
321
|
+
throw new DubError(`Failed to read ref '${ref}'.`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async function isAncestor(ancestor, descendant, cwd) {
|
|
325
|
+
try {
|
|
326
|
+
await execa("git", ["merge-base", "--is-ancestor", ancestor, descendant], {
|
|
327
|
+
cwd
|
|
328
|
+
});
|
|
329
|
+
return true;
|
|
330
|
+
} catch (error) {
|
|
331
|
+
const exitCode = error.exitCode;
|
|
332
|
+
if (exitCode === 1) return false;
|
|
333
|
+
throw new DubError(
|
|
334
|
+
`Failed to compare ancestry between '${ancestor}' and '${descendant}'.`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
async function checkoutRemoteBranch(branch, cwd, remote = "origin") {
|
|
339
|
+
try {
|
|
340
|
+
await execa("git", ["checkout", "-B", branch, `${remote}/${branch}`], {
|
|
341
|
+
cwd
|
|
342
|
+
});
|
|
343
|
+
} catch {
|
|
344
|
+
throw new DubError(
|
|
345
|
+
`Failed to create local branch '${branch}' from '${remote}/${branch}'.`
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
async function hardResetBranchToRef(branch, ref, cwd) {
|
|
350
|
+
try {
|
|
351
|
+
const current = await getCurrentBranch(cwd).catch(() => null);
|
|
352
|
+
if (current !== branch) {
|
|
353
|
+
await checkoutBranch(branch, cwd);
|
|
354
|
+
}
|
|
355
|
+
await execa("git", ["reset", "--hard", ref], { cwd });
|
|
356
|
+
} catch {
|
|
357
|
+
throw new DubError(`Failed to hard reset '${branch}' to '${ref}'.`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
async function fastForwardBranchToRef(branch, ref, cwd) {
|
|
361
|
+
try {
|
|
362
|
+
const current = await getCurrentBranch(cwd).catch(() => null);
|
|
363
|
+
if (current !== branch) {
|
|
364
|
+
await checkoutBranch(branch, cwd);
|
|
365
|
+
}
|
|
366
|
+
await execa("git", ["merge", "--ff-only", ref], { cwd });
|
|
367
|
+
return true;
|
|
368
|
+
} catch {
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
async function rebaseBranchOntoRef(branch, ref, cwd) {
|
|
373
|
+
try {
|
|
374
|
+
const current = await getCurrentBranch(cwd).catch(() => null);
|
|
375
|
+
if (current !== branch) {
|
|
376
|
+
await checkoutBranch(branch, cwd);
|
|
377
|
+
}
|
|
378
|
+
await execa("git", ["rebase", ref], { cwd });
|
|
379
|
+
return true;
|
|
380
|
+
} catch {
|
|
381
|
+
try {
|
|
382
|
+
await execa("git", ["rebase", "--abort"], { cwd });
|
|
383
|
+
} catch {
|
|
384
|
+
}
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
var init_git = __esm({
|
|
389
|
+
"src/lib/git.ts"() {
|
|
390
|
+
"use strict";
|
|
391
|
+
init_errors();
|
|
392
|
+
}
|
|
393
|
+
});
|
|
219
394
|
|
|
220
395
|
// src/lib/state.ts
|
|
221
396
|
import * as crypto from "crypto";
|
|
@@ -236,7 +411,7 @@ async function readState(cwd) {
|
|
|
236
411
|
}
|
|
237
412
|
try {
|
|
238
413
|
const raw = fs.readFileSync(statePath, "utf-8");
|
|
239
|
-
return JSON.parse(raw);
|
|
414
|
+
return normalizeState(JSON.parse(raw));
|
|
240
415
|
} catch {
|
|
241
416
|
throw new DubError(
|
|
242
417
|
"State file is corrupted. Delete .git/dubstack and run 'dub init' to re-initialize."
|
|
@@ -280,6 +455,12 @@ function findStackForBranch(state, name) {
|
|
|
280
455
|
(stack) => stack.branches.some((b) => b.name === name)
|
|
281
456
|
);
|
|
282
457
|
}
|
|
458
|
+
function getParent(state, branchName) {
|
|
459
|
+
const stack = findStackForBranch(state, branchName);
|
|
460
|
+
if (!stack) return void 0;
|
|
461
|
+
const branch = stack.branches.find((b) => b.name === branchName);
|
|
462
|
+
return branch?.parent ?? void 0;
|
|
463
|
+
}
|
|
283
464
|
function addBranchToStack(state, child, parent) {
|
|
284
465
|
if (findStackForBranch(state, child)) {
|
|
285
466
|
throw new DubError(`Branch '${child}' is already tracked in a stack.`);
|
|
@@ -288,7 +469,10 @@ function addBranchToStack(state, child, parent) {
|
|
|
288
469
|
name: child,
|
|
289
470
|
parent,
|
|
290
471
|
pr_number: null,
|
|
291
|
-
pr_link: null
|
|
472
|
+
pr_link: null,
|
|
473
|
+
last_submitted_version: null,
|
|
474
|
+
last_synced_at: null,
|
|
475
|
+
sync_source: null
|
|
292
476
|
};
|
|
293
477
|
const existingStack = findStackForBranch(state, parent);
|
|
294
478
|
if (existingStack) {
|
|
@@ -299,7 +483,10 @@ function addBranchToStack(state, child, parent) {
|
|
|
299
483
|
type: "root",
|
|
300
484
|
parent: null,
|
|
301
485
|
pr_number: null,
|
|
302
|
-
pr_link: null
|
|
486
|
+
pr_link: null,
|
|
487
|
+
last_submitted_version: null,
|
|
488
|
+
last_synced_at: null,
|
|
489
|
+
sync_source: null
|
|
303
490
|
};
|
|
304
491
|
state.stacks.push({
|
|
305
492
|
id: crypto.randomUUID(),
|
|
@@ -307,6 +494,22 @@ function addBranchToStack(state, child, parent) {
|
|
|
307
494
|
});
|
|
308
495
|
}
|
|
309
496
|
}
|
|
497
|
+
function normalizeState(state) {
|
|
498
|
+
return {
|
|
499
|
+
stacks: state.stacks.map((stack) => ({
|
|
500
|
+
...stack,
|
|
501
|
+
branches: stack.branches.map((branch) => normalizeBranch(branch))
|
|
502
|
+
}))
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
function normalizeBranch(branch) {
|
|
506
|
+
return {
|
|
507
|
+
...branch,
|
|
508
|
+
last_submitted_version: branch.last_submitted_version ?? null,
|
|
509
|
+
last_synced_at: branch.last_synced_at ?? null,
|
|
510
|
+
sync_source: branch.sync_source ?? null
|
|
511
|
+
};
|
|
512
|
+
}
|
|
310
513
|
function topologicalOrder(stack) {
|
|
311
514
|
const result = [];
|
|
312
515
|
const root = stack.branches.find((b) => b.type === "root");
|
|
@@ -329,76 +532,13 @@ function topologicalOrder(stack) {
|
|
|
329
532
|
}
|
|
330
533
|
return result;
|
|
331
534
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
for (const branch of stack.branches) {
|
|
338
|
-
names.add(branch.name);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
return [...names].sort();
|
|
342
|
-
}
|
|
343
|
-
function getValidBranches(tracked, local) {
|
|
344
|
-
const localSet = new Set(local);
|
|
345
|
-
return tracked.filter((b) => localSet.has(b));
|
|
346
|
-
}
|
|
347
|
-
async function checkout(name, cwd) {
|
|
348
|
-
await checkoutBranch(name, cwd);
|
|
349
|
-
return { branch: name };
|
|
350
|
-
}
|
|
351
|
-
async function interactiveCheckout(cwd) {
|
|
352
|
-
const state = await readState(cwd);
|
|
353
|
-
const trackedBranches = getTrackedBranches(state);
|
|
354
|
-
const localBranches = await listBranches(cwd);
|
|
355
|
-
const validBranches = getValidBranches(trackedBranches, localBranches);
|
|
356
|
-
if (validBranches.length === 0) {
|
|
357
|
-
throw new DubError(
|
|
358
|
-
"No valid tracked branches found. Run 'dub create' first."
|
|
359
|
-
);
|
|
360
|
-
}
|
|
361
|
-
let currentBranch = null;
|
|
362
|
-
try {
|
|
363
|
-
currentBranch = await getCurrentBranch(cwd);
|
|
364
|
-
} catch {
|
|
365
|
-
}
|
|
366
|
-
const controller = new AbortController();
|
|
367
|
-
const onKeypress = (_str, key) => {
|
|
368
|
-
if (key && key.name === "escape") {
|
|
369
|
-
controller.abort();
|
|
370
|
-
}
|
|
371
|
-
};
|
|
372
|
-
process.stdin.on("keypress", onKeypress);
|
|
373
|
-
try {
|
|
374
|
-
const selected = await search(
|
|
375
|
-
{
|
|
376
|
-
message: "Checkout a branch (autocomplete or arrow keys)",
|
|
377
|
-
source(term) {
|
|
378
|
-
const filtered = term ? validBranches.filter(
|
|
379
|
-
(b) => b.toLowerCase().includes(term.toLowerCase())
|
|
380
|
-
) : validBranches;
|
|
381
|
-
return filtered.map((name) => ({
|
|
382
|
-
name,
|
|
383
|
-
value: name,
|
|
384
|
-
disabled: name === currentBranch ? "(current)" : false
|
|
385
|
-
}));
|
|
386
|
-
}
|
|
387
|
-
},
|
|
388
|
-
{ signal: controller.signal }
|
|
389
|
-
);
|
|
390
|
-
return checkout(selected, cwd);
|
|
391
|
-
} catch (error) {
|
|
392
|
-
if (error instanceof Error) {
|
|
393
|
-
if (error.name === "ExitPromptError" || error.name === "AbortError" || error.name === "AbortPromptError") {
|
|
394
|
-
return null;
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
throw error;
|
|
398
|
-
} finally {
|
|
399
|
-
process.stdin.off("keypress", onKeypress);
|
|
535
|
+
var init_state = __esm({
|
|
536
|
+
"src/lib/state.ts"() {
|
|
537
|
+
"use strict";
|
|
538
|
+
init_errors();
|
|
539
|
+
init_git();
|
|
400
540
|
}
|
|
401
|
-
}
|
|
541
|
+
});
|
|
402
542
|
|
|
403
543
|
// src/lib/undo-log.ts
|
|
404
544
|
import * as fs2 from "fs";
|
|
@@ -426,55 +566,546 @@ async function clearUndoEntry(cwd) {
|
|
|
426
566
|
fs2.unlinkSync(undoPath);
|
|
427
567
|
}
|
|
428
568
|
}
|
|
569
|
+
var init_undo_log = __esm({
|
|
570
|
+
"src/lib/undo-log.ts"() {
|
|
571
|
+
"use strict";
|
|
572
|
+
init_errors();
|
|
573
|
+
init_state();
|
|
574
|
+
}
|
|
575
|
+
});
|
|
429
576
|
|
|
430
|
-
// src/commands/
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
577
|
+
// src/commands/restack.ts
|
|
578
|
+
import * as fs4 from "fs";
|
|
579
|
+
import * as path4 from "path";
|
|
580
|
+
async function restack(cwd) {
|
|
581
|
+
const state = await readState(cwd);
|
|
582
|
+
if (!await isWorkingTreeClean(cwd)) {
|
|
583
|
+
throw new DubError(
|
|
584
|
+
"Working tree has uncommitted changes. Commit or stash them before restacking."
|
|
585
|
+
);
|
|
434
586
|
}
|
|
435
|
-
const
|
|
436
|
-
const
|
|
437
|
-
if (
|
|
438
|
-
throw new DubError(
|
|
587
|
+
const originalBranch = await getCurrentBranch(cwd);
|
|
588
|
+
const targetStacks = getTargetStacks(state.stacks, originalBranch);
|
|
589
|
+
if (targetStacks.length === 0) {
|
|
590
|
+
throw new DubError(
|
|
591
|
+
`Branch '${originalBranch}' is not part of any stack. Run 'dub create' first.`
|
|
592
|
+
);
|
|
439
593
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
594
|
+
const allBranches = targetStacks.flatMap((s) => s.branches);
|
|
595
|
+
for (const branch of allBranches) {
|
|
596
|
+
if (!await branchExists(branch.name, cwd)) {
|
|
597
|
+
throw new DubError(
|
|
598
|
+
`Branch '${branch.name}' is tracked in state but no longer exists in git.
|
|
599
|
+
Remove it from the stack or recreate it before restacking.`
|
|
600
|
+
);
|
|
447
601
|
}
|
|
448
602
|
}
|
|
603
|
+
const branchTips = {};
|
|
604
|
+
for (const branch of allBranches) {
|
|
605
|
+
branchTips[branch.name] = await getBranchTip(branch.name, cwd);
|
|
606
|
+
}
|
|
607
|
+
const steps = await buildRestackSteps(targetStacks, cwd);
|
|
608
|
+
if (steps.length === 0) {
|
|
609
|
+
return { status: "up-to-date", rebased: [] };
|
|
610
|
+
}
|
|
449
611
|
await saveUndoEntry(
|
|
450
612
|
{
|
|
451
|
-
operation: "
|
|
613
|
+
operation: "restack",
|
|
452
614
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
453
|
-
previousBranch:
|
|
615
|
+
previousBranch: originalBranch,
|
|
454
616
|
previousState: structuredClone(state),
|
|
455
|
-
branchTips
|
|
456
|
-
createdBranches: [
|
|
617
|
+
branchTips,
|
|
618
|
+
createdBranches: []
|
|
457
619
|
},
|
|
458
620
|
cwd
|
|
459
621
|
);
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
}
|
|
474
|
-
return
|
|
622
|
+
const progress = { originalBranch, steps };
|
|
623
|
+
await writeProgress(progress, cwd);
|
|
624
|
+
return executeRestackSteps(progress, cwd);
|
|
625
|
+
}
|
|
626
|
+
async function restackContinue(cwd) {
|
|
627
|
+
const progress = await readProgress(cwd);
|
|
628
|
+
if (!progress) {
|
|
629
|
+
throw new DubError("No restack in progress. Run 'dub restack' to start.");
|
|
630
|
+
}
|
|
631
|
+
await rebaseContinue(cwd);
|
|
632
|
+
const conflictedStep = progress.steps.find((s) => s.status === "conflicted");
|
|
633
|
+
if (conflictedStep) {
|
|
634
|
+
conflictedStep.status = "done";
|
|
635
|
+
}
|
|
636
|
+
return executeRestackSteps(progress, cwd);
|
|
637
|
+
}
|
|
638
|
+
async function executeRestackSteps(progress, cwd) {
|
|
639
|
+
const rebased = [];
|
|
640
|
+
for (const step of progress.steps) {
|
|
641
|
+
if (step.status !== "pending") {
|
|
642
|
+
if (step.status === "done") rebased.push(step.branch);
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
const parentNewTip = await getBranchTip(step.parent, cwd);
|
|
646
|
+
if (parentNewTip === step.parentOldTip) {
|
|
647
|
+
step.status = "skipped";
|
|
648
|
+
await writeProgress(progress, cwd);
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
try {
|
|
652
|
+
await rebaseOnto(parentNewTip, step.parentOldTip, step.branch, cwd);
|
|
653
|
+
step.status = "done";
|
|
654
|
+
rebased.push(step.branch);
|
|
655
|
+
await writeProgress(progress, cwd);
|
|
656
|
+
} catch (error) {
|
|
657
|
+
if (error instanceof DubError && error.message.includes("Conflict")) {
|
|
658
|
+
step.status = "conflicted";
|
|
659
|
+
await writeProgress(progress, cwd);
|
|
660
|
+
return { status: "conflict", rebased, conflictBranch: step.branch };
|
|
661
|
+
}
|
|
662
|
+
throw error;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
await clearProgress(cwd);
|
|
666
|
+
await checkoutBranch(progress.originalBranch, cwd);
|
|
667
|
+
const allSkipped = progress.steps.every(
|
|
668
|
+
(s) => s.status === "skipped" || s.status === "done"
|
|
669
|
+
);
|
|
670
|
+
return {
|
|
671
|
+
status: rebased.length === 0 && allSkipped ? "up-to-date" : "success",
|
|
672
|
+
rebased
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
function getTargetStacks(stacks, currentBranch) {
|
|
676
|
+
const rootStacks = stacks.filter(
|
|
677
|
+
(s) => s.branches.some((b) => b.name === currentBranch && b.type === "root")
|
|
678
|
+
);
|
|
679
|
+
if (rootStacks.length > 0) return rootStacks;
|
|
680
|
+
const stack = stacks.find(
|
|
681
|
+
(s) => s.branches.some((b) => b.name === currentBranch)
|
|
682
|
+
);
|
|
683
|
+
return stack ? [stack] : [];
|
|
684
|
+
}
|
|
685
|
+
async function buildRestackSteps(stacks, cwd) {
|
|
686
|
+
const steps = [];
|
|
687
|
+
for (const stack of stacks) {
|
|
688
|
+
const ordered = topologicalOrder(stack);
|
|
689
|
+
for (const branch of ordered) {
|
|
690
|
+
if (branch.type === "root" || !branch.parent) continue;
|
|
691
|
+
const mergeBase = await getMergeBase(branch.parent, branch.name, cwd);
|
|
692
|
+
steps.push({
|
|
693
|
+
branch: branch.name,
|
|
694
|
+
parent: branch.parent,
|
|
695
|
+
parentOldTip: mergeBase,
|
|
696
|
+
status: "pending"
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return steps;
|
|
701
|
+
}
|
|
702
|
+
async function getProgressPath(cwd) {
|
|
703
|
+
const dubDir = await getDubDir(cwd);
|
|
704
|
+
return path4.join(dubDir, "restack-progress.json");
|
|
705
|
+
}
|
|
706
|
+
async function writeProgress(progress, cwd) {
|
|
707
|
+
const progressPath = await getProgressPath(cwd);
|
|
708
|
+
fs4.writeFileSync(progressPath, `${JSON.stringify(progress, null, 2)}
|
|
709
|
+
`);
|
|
710
|
+
}
|
|
711
|
+
async function readProgress(cwd) {
|
|
712
|
+
const progressPath = await getProgressPath(cwd);
|
|
713
|
+
if (!fs4.existsSync(progressPath)) return null;
|
|
714
|
+
const raw = fs4.readFileSync(progressPath, "utf-8");
|
|
715
|
+
return JSON.parse(raw);
|
|
716
|
+
}
|
|
717
|
+
async function clearProgress(cwd) {
|
|
718
|
+
const progressPath = await getProgressPath(cwd);
|
|
719
|
+
if (fs4.existsSync(progressPath)) {
|
|
720
|
+
fs4.unlinkSync(progressPath);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
var init_restack = __esm({
|
|
724
|
+
"src/commands/restack.ts"() {
|
|
725
|
+
"use strict";
|
|
726
|
+
init_errors();
|
|
727
|
+
init_git();
|
|
728
|
+
init_state();
|
|
729
|
+
init_undo_log();
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
// src/lib/skills.ts
|
|
734
|
+
function getSkillRemote(name) {
|
|
735
|
+
return AVAILABLE_SKILLS[name];
|
|
736
|
+
}
|
|
737
|
+
var AVAILABLE_SKILLS;
|
|
738
|
+
var init_skills = __esm({
|
|
739
|
+
"src/lib/skills.ts"() {
|
|
740
|
+
"use strict";
|
|
741
|
+
AVAILABLE_SKILLS = {
|
|
742
|
+
dubstack: "wiseiodev/dubstack/skills/dubstack",
|
|
743
|
+
"dub-flow": "wiseiodev/dubstack/skills/dub-flow"
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
// src/commands/skills.ts
|
|
749
|
+
var skills_exports = {};
|
|
750
|
+
__export(skills_exports, {
|
|
751
|
+
addSkills: () => addSkills,
|
|
752
|
+
removeSkills: () => removeSkills
|
|
753
|
+
});
|
|
754
|
+
import chalk from "chalk";
|
|
755
|
+
import { execa as execa3 } from "execa";
|
|
756
|
+
function validateSkills(skills) {
|
|
757
|
+
const invalidSkills = skills.filter((s) => !(s in AVAILABLE_SKILLS));
|
|
758
|
+
if (invalidSkills.length > 0) {
|
|
759
|
+
throw new DubError(
|
|
760
|
+
`Unknown skill(s): ${invalidSkills.join(", ")}. Available skills: ${Object.keys(AVAILABLE_SKILLS).join(", ")}`
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
return skills;
|
|
764
|
+
}
|
|
765
|
+
async function addSkills(skills, options = {}) {
|
|
766
|
+
const targets = skills.length > 0 ? validateSkills(skills) : Object.keys(AVAILABLE_SKILLS);
|
|
767
|
+
console.log(chalk.blue(`Adding ${targets.length} skill(s)...`));
|
|
768
|
+
for (const skill of targets) {
|
|
769
|
+
const remote = getSkillRemote(skill);
|
|
770
|
+
const args = ["skills", "add", remote];
|
|
771
|
+
if (options.global) args.push("--global");
|
|
772
|
+
const command = `npx ${args.join(" ")}`;
|
|
773
|
+
console.log(chalk.dim(`Running: ${command}`));
|
|
774
|
+
if (!options.dryRun) {
|
|
775
|
+
try {
|
|
776
|
+
await execa3("npx", args, { stdio: "inherit" });
|
|
777
|
+
console.log(chalk.green(`\u2714 Added skill: ${skill}`));
|
|
778
|
+
} catch (error) {
|
|
779
|
+
console.error(chalk.red(`\u2716 Failed to add skill: ${skill}`));
|
|
780
|
+
throw error;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
async function removeSkills(skills, options = {}) {
|
|
786
|
+
const targets = skills.length > 0 ? validateSkills(skills) : Object.keys(AVAILABLE_SKILLS);
|
|
787
|
+
console.log(chalk.blue(`Removing ${targets.length} skill(s)...`));
|
|
788
|
+
for (const skill of targets) {
|
|
789
|
+
const args = ["skills", "remove", skill];
|
|
790
|
+
if (options.global) args.push("--global");
|
|
791
|
+
const command = `npx ${args.join(" ")}`;
|
|
792
|
+
console.log(chalk.dim(`Running: ${command}`));
|
|
793
|
+
if (!options.dryRun) {
|
|
794
|
+
try {
|
|
795
|
+
await execa3("npx", args, { stdio: "inherit" });
|
|
796
|
+
console.log(chalk.green(`\u2714 Removed skill: ${skill}`));
|
|
797
|
+
} catch (error) {
|
|
798
|
+
console.error(chalk.red(`\u2716 Failed to remove skill: ${skill}`));
|
|
799
|
+
throw error;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
var init_skills2 = __esm({
|
|
805
|
+
"src/commands/skills.ts"() {
|
|
806
|
+
"use strict";
|
|
807
|
+
init_errors();
|
|
808
|
+
init_skills();
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
// src/commands/modify.ts
|
|
813
|
+
var modify_exports = {};
|
|
814
|
+
__export(modify_exports, {
|
|
815
|
+
modify: () => modify
|
|
816
|
+
});
|
|
817
|
+
async function modify(cwd, options) {
|
|
818
|
+
const currentBranch = await getCurrentBranch(cwd);
|
|
819
|
+
const state = await readState(cwd);
|
|
820
|
+
if (options.interactiveRebase) {
|
|
821
|
+
const parent = getParent(state, currentBranch);
|
|
822
|
+
if (!parent) {
|
|
823
|
+
throw new DubError(
|
|
824
|
+
`Could not determine parent branch for '${currentBranch}'. Cannot start interactive rebase.`
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
const parentTip = await getBranchTip(parent, cwd);
|
|
828
|
+
console.log(`Starting interactive rebase on top of '${parent}'...`);
|
|
829
|
+
await interactiveRebase(parentTip, cwd);
|
|
830
|
+
await restackChildren(cwd);
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
if (options.patch) {
|
|
834
|
+
await interactiveStage(cwd);
|
|
835
|
+
} else if (options.all) {
|
|
836
|
+
await stageAll(cwd);
|
|
837
|
+
} else if (options.update) {
|
|
838
|
+
await stageUpdate(cwd);
|
|
839
|
+
}
|
|
840
|
+
await printVerboseDiff(cwd, options.verbose ?? 0);
|
|
841
|
+
const hasStaged = await hasStagedChanges(cwd);
|
|
842
|
+
const shouldCreateNew = options.commit;
|
|
843
|
+
const message = normalizeMessage(options.message);
|
|
844
|
+
const noEdit = !options.edit && !!message;
|
|
845
|
+
if (shouldCreateNew) {
|
|
846
|
+
if (!hasStaged) {
|
|
847
|
+
throw new DubError("No staged changes to commit.");
|
|
848
|
+
}
|
|
849
|
+
await commit(cwd, { message, noEdit: !options.edit });
|
|
850
|
+
} else {
|
|
851
|
+
await amendCommit(cwd, { message, noEdit });
|
|
852
|
+
}
|
|
853
|
+
await restackChildren(cwd);
|
|
854
|
+
}
|
|
855
|
+
function normalizeMessage(message) {
|
|
856
|
+
if (Array.isArray(message)) {
|
|
857
|
+
const chunks = message.map((part) => part.trim()).filter(Boolean);
|
|
858
|
+
return chunks.length > 0 ? chunks.join("\n\n") : void 0;
|
|
859
|
+
}
|
|
860
|
+
return message;
|
|
861
|
+
}
|
|
862
|
+
async function printVerboseDiff(cwd, level) {
|
|
863
|
+
if (level < 1) return;
|
|
864
|
+
const staged = await getDiff(cwd, true);
|
|
865
|
+
console.log(staged || "(no staged diff)");
|
|
866
|
+
if (level > 1) {
|
|
867
|
+
const unstaged = await getDiff(cwd, false);
|
|
868
|
+
console.log(unstaged || "(no unstaged diff)");
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
async function restackChildren(cwd) {
|
|
872
|
+
try {
|
|
873
|
+
await restack(cwd);
|
|
874
|
+
} catch (e) {
|
|
875
|
+
if (e instanceof DubError && e.message.includes("Conflict")) {
|
|
876
|
+
console.log(
|
|
877
|
+
"\u26A0 Modify successful, but auto-restacking encountered conflicts."
|
|
878
|
+
);
|
|
879
|
+
console.log(" Run 'dub restack --continue' to resolve.");
|
|
880
|
+
} else {
|
|
881
|
+
console.log("\u26A0 Modify successful, but auto-restacking failed.");
|
|
882
|
+
console.log(` ${e instanceof Error ? e.message : String(e)}`);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
var init_modify = __esm({
|
|
887
|
+
"src/commands/modify.ts"() {
|
|
888
|
+
"use strict";
|
|
889
|
+
init_errors();
|
|
890
|
+
init_git();
|
|
891
|
+
init_state();
|
|
892
|
+
init_restack();
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
// src/index.ts
|
|
897
|
+
import { createRequire } from "module";
|
|
898
|
+
import chalk2 from "chalk";
|
|
899
|
+
import { Command } from "commander";
|
|
900
|
+
|
|
901
|
+
// src/commands/branch.ts
|
|
902
|
+
init_git();
|
|
903
|
+
init_state();
|
|
904
|
+
function findRootName(stack) {
|
|
905
|
+
return stack.branches.find((branch) => branch.type === "root")?.name ?? null;
|
|
906
|
+
}
|
|
907
|
+
function getChildren(stack, branchName) {
|
|
908
|
+
return stack.branches.filter((branch) => branch.parent === branchName).map((branch) => branch.name).sort();
|
|
909
|
+
}
|
|
910
|
+
async function branchInfo(cwd, branchName) {
|
|
911
|
+
const state = await readState(cwd);
|
|
912
|
+
const resolvedBranch = branchName ?? await getCurrentBranch(cwd);
|
|
913
|
+
const stack = findStackForBranch(state, resolvedBranch);
|
|
914
|
+
if (!stack) {
|
|
915
|
+
return {
|
|
916
|
+
currentBranch: resolvedBranch,
|
|
917
|
+
tracked: false,
|
|
918
|
+
stackId: null,
|
|
919
|
+
root: null,
|
|
920
|
+
parent: null,
|
|
921
|
+
children: []
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
const current = stack.branches.find(
|
|
925
|
+
(branch) => branch.name === resolvedBranch
|
|
926
|
+
);
|
|
927
|
+
return {
|
|
928
|
+
currentBranch: resolvedBranch,
|
|
929
|
+
tracked: true,
|
|
930
|
+
stackId: stack.id,
|
|
931
|
+
root: findRootName(stack),
|
|
932
|
+
parent: current?.parent ?? null,
|
|
933
|
+
children: getChildren(stack, resolvedBranch)
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
function formatBranchInfo(info) {
|
|
937
|
+
if (!info.tracked) {
|
|
938
|
+
return [
|
|
939
|
+
`Branch: ${info.currentBranch}`,
|
|
940
|
+
"Tracked: no",
|
|
941
|
+
"Status: not tracked by DubStack"
|
|
942
|
+
].join("\n");
|
|
943
|
+
}
|
|
944
|
+
const childrenLabel = info.children.length > 0 ? info.children.join(", ") : "(none)";
|
|
945
|
+
return [
|
|
946
|
+
`Branch: ${info.currentBranch}`,
|
|
947
|
+
"Tracked: yes",
|
|
948
|
+
`Stack ID: ${info.stackId ?? "(unknown)"}`,
|
|
949
|
+
`Root: ${info.root ?? "(unknown)"}`,
|
|
950
|
+
`Parent: ${info.parent ?? "(root)"}`,
|
|
951
|
+
`Children: ${childrenLabel}`
|
|
952
|
+
].join("\n");
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// src/commands/checkout.ts
|
|
956
|
+
init_errors();
|
|
957
|
+
init_git();
|
|
958
|
+
init_state();
|
|
959
|
+
import search from "@inquirer/search";
|
|
960
|
+
function getTrackedBranches(state) {
|
|
961
|
+
const names = /* @__PURE__ */ new Set();
|
|
962
|
+
for (const stack of state.stacks) {
|
|
963
|
+
for (const branch of stack.branches) {
|
|
964
|
+
names.add(branch.name);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
return [...names].sort();
|
|
968
|
+
}
|
|
969
|
+
function getValidBranches(tracked, local) {
|
|
970
|
+
const localSet = new Set(local);
|
|
971
|
+
return tracked.filter((b) => localSet.has(b));
|
|
972
|
+
}
|
|
973
|
+
function getStackRelativeBranches(state, branchName) {
|
|
974
|
+
const stack = findStackForBranch(state, branchName);
|
|
975
|
+
if (!stack) return [];
|
|
976
|
+
return [...new Set(stack.branches.map((branch) => branch.name))].sort();
|
|
977
|
+
}
|
|
978
|
+
async function resolveCheckoutTrunk(cwd) {
|
|
979
|
+
const state = await readState(cwd);
|
|
980
|
+
const currentBranch = await getCurrentBranch(cwd);
|
|
981
|
+
const stack = findStackForBranch(state, currentBranch);
|
|
982
|
+
const trackedRoot = stack?.branches.find((branch) => branch.type === "root")?.name ?? null;
|
|
983
|
+
if (trackedRoot) return trackedRoot;
|
|
984
|
+
if (await branchExists("main", cwd)) return "main";
|
|
985
|
+
if (await branchExists("master", cwd)) return "master";
|
|
986
|
+
throw new DubError(
|
|
987
|
+
`Could not determine trunk branch for '${currentBranch}'.`
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
async function checkout(name, cwd) {
|
|
991
|
+
await checkoutBranch(name, cwd);
|
|
992
|
+
return { branch: name };
|
|
993
|
+
}
|
|
994
|
+
async function interactiveCheckout(cwd, options = {}) {
|
|
995
|
+
const state = await readState(cwd);
|
|
996
|
+
const localBranches = await listBranches(cwd);
|
|
997
|
+
const currentBranch = await getCurrentBranch(cwd).catch(() => null);
|
|
998
|
+
const trackedBranches = getTrackedBranches(state);
|
|
999
|
+
const stackBranches = currentBranch ? getStackRelativeBranches(state, currentBranch) : [];
|
|
1000
|
+
let branchCandidates = options.showUntracked ? [...new Set(localBranches)].sort() : getValidBranches(trackedBranches, localBranches);
|
|
1001
|
+
if (options.stack && stackBranches.length > 0) {
|
|
1002
|
+
const stackSet = new Set(stackBranches);
|
|
1003
|
+
branchCandidates = branchCandidates.filter((name) => stackSet.has(name));
|
|
1004
|
+
}
|
|
1005
|
+
const validBranches = branchCandidates;
|
|
1006
|
+
if (validBranches.length === 0) {
|
|
1007
|
+
throw new DubError(
|
|
1008
|
+
"No valid tracked branches found. Run 'dub create' first."
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
const controller = new AbortController();
|
|
1012
|
+
const onKeypress = (_str, key) => {
|
|
1013
|
+
if (key && key.name === "escape") {
|
|
1014
|
+
controller.abort();
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
process.stdin.on("keypress", onKeypress);
|
|
1018
|
+
try {
|
|
1019
|
+
const selected = await search(
|
|
1020
|
+
{
|
|
1021
|
+
message: "Checkout a branch (autocomplete or arrow keys)",
|
|
1022
|
+
source(term) {
|
|
1023
|
+
const filtered = term ? validBranches.filter(
|
|
1024
|
+
(b) => b.toLowerCase().includes(term.toLowerCase())
|
|
1025
|
+
) : validBranches;
|
|
1026
|
+
return filtered.map((name) => ({
|
|
1027
|
+
name,
|
|
1028
|
+
value: name,
|
|
1029
|
+
disabled: name === currentBranch ? "(current)" : false
|
|
1030
|
+
}));
|
|
1031
|
+
}
|
|
1032
|
+
},
|
|
1033
|
+
{ signal: controller.signal }
|
|
1034
|
+
);
|
|
1035
|
+
return checkout(selected, cwd);
|
|
1036
|
+
} catch (error) {
|
|
1037
|
+
if (error instanceof Error) {
|
|
1038
|
+
if (error.name === "ExitPromptError" || error.name === "AbortError" || error.name === "AbortPromptError") {
|
|
1039
|
+
return null;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
throw error;
|
|
1043
|
+
} finally {
|
|
1044
|
+
process.stdin.off("keypress", onKeypress);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// src/commands/create.ts
|
|
1049
|
+
init_errors();
|
|
1050
|
+
init_git();
|
|
1051
|
+
init_state();
|
|
1052
|
+
init_undo_log();
|
|
1053
|
+
async function create(name, cwd, options) {
|
|
1054
|
+
if ((options?.all || options?.update || options?.patch) && !options.message) {
|
|
1055
|
+
throw new DubError(
|
|
1056
|
+
"'--all', '--update', and '--patch' require '-m'. Pass a commit message."
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
const state = await ensureState(cwd);
|
|
1060
|
+
const parent = await getCurrentBranch(cwd);
|
|
1061
|
+
if (await branchExists(name, cwd)) {
|
|
1062
|
+
throw new DubError(`Branch '${name}' already exists.`);
|
|
1063
|
+
}
|
|
1064
|
+
if (options?.message) {
|
|
1065
|
+
if (options.patch) {
|
|
1066
|
+
await interactiveStage(cwd);
|
|
1067
|
+
} else if (options.all) {
|
|
1068
|
+
await stageAll(cwd);
|
|
1069
|
+
} else if (options.update) {
|
|
1070
|
+
await stageUpdate(cwd);
|
|
1071
|
+
}
|
|
1072
|
+
if (!await hasStagedChanges(cwd)) {
|
|
1073
|
+
const hint = options.all ? "No changes to commit." : "No staged changes. Stage files with 'git add' or use '-a' to stage all.";
|
|
1074
|
+
throw new DubError(hint);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
await saveUndoEntry(
|
|
1078
|
+
{
|
|
1079
|
+
operation: "create",
|
|
1080
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1081
|
+
previousBranch: parent,
|
|
1082
|
+
previousState: structuredClone(state),
|
|
1083
|
+
branchTips: {},
|
|
1084
|
+
createdBranches: [name]
|
|
1085
|
+
},
|
|
1086
|
+
cwd
|
|
1087
|
+
);
|
|
1088
|
+
await createBranch(name, cwd);
|
|
1089
|
+
addBranchToStack(state, name, parent);
|
|
1090
|
+
await writeState(state, cwd);
|
|
1091
|
+
if (options?.message) {
|
|
1092
|
+
try {
|
|
1093
|
+
await commitStaged(options.message, cwd);
|
|
1094
|
+
} catch (error) {
|
|
1095
|
+
const reason = error instanceof DubError ? error.message : String(error);
|
|
1096
|
+
throw new DubError(
|
|
1097
|
+
`Branch '${name}' was created but commit failed: ${reason}. Run 'dub undo' to clean up.`
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
return { branch: name, parent, committed: options.message };
|
|
1101
|
+
}
|
|
1102
|
+
return { branch: name, parent };
|
|
475
1103
|
}
|
|
476
1104
|
|
|
477
1105
|
// src/commands/init.ts
|
|
1106
|
+
init_errors();
|
|
1107
|
+
init_git();
|
|
1108
|
+
init_state();
|
|
478
1109
|
import * as fs3 from "fs";
|
|
479
1110
|
import * as path3 from "path";
|
|
480
1111
|
async function init(cwd) {
|
|
@@ -506,6 +1137,8 @@ async function init(cwd) {
|
|
|
506
1137
|
}
|
|
507
1138
|
|
|
508
1139
|
// src/commands/log.ts
|
|
1140
|
+
init_git();
|
|
1141
|
+
init_state();
|
|
509
1142
|
async function log(cwd) {
|
|
510
1143
|
const state = await readState(cwd);
|
|
511
1144
|
if (state.stacks.length === 0) {
|
|
@@ -573,159 +1206,150 @@ async function renderNode(branch, currentBranch, childMap, prefix, isRoot, isLas
|
|
|
573
1206
|
}
|
|
574
1207
|
}
|
|
575
1208
|
|
|
576
|
-
// src/commands/
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
1209
|
+
// src/commands/navigate.ts
|
|
1210
|
+
init_errors();
|
|
1211
|
+
init_git();
|
|
1212
|
+
init_state();
|
|
1213
|
+
function getBranchByName(stack, name) {
|
|
1214
|
+
return stack.branches.find((branch) => branch.name === name);
|
|
1215
|
+
}
|
|
1216
|
+
function getChildren2(stack, parent) {
|
|
1217
|
+
return stack.branches.filter((branch) => branch.parent === parent).map((branch) => branch.name);
|
|
1218
|
+
}
|
|
1219
|
+
function getTrackedStackOrThrow(stateBranch, stack) {
|
|
1220
|
+
if (!stack) {
|
|
582
1221
|
throw new DubError(
|
|
583
|
-
|
|
1222
|
+
`Current branch '${stateBranch}' is not tracked by DubStack.`
|
|
584
1223
|
);
|
|
585
1224
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
);
|
|
1225
|
+
return stack;
|
|
1226
|
+
}
|
|
1227
|
+
async function upBySteps(cwd, steps) {
|
|
1228
|
+
if (!Number.isInteger(steps) || steps < 1) {
|
|
1229
|
+
throw new DubError("'steps' must be a positive integer.");
|
|
592
1230
|
}
|
|
593
|
-
const
|
|
594
|
-
|
|
595
|
-
|
|
1231
|
+
const state = await readState(cwd);
|
|
1232
|
+
const current = await getCurrentBranch(cwd);
|
|
1233
|
+
const stack = getTrackedStackOrThrow(
|
|
1234
|
+
current,
|
|
1235
|
+
findStackForBranch(state, current)
|
|
1236
|
+
);
|
|
1237
|
+
let target = current;
|
|
1238
|
+
for (let i = 0; i < steps; i++) {
|
|
1239
|
+
const children = getChildren2(stack, target);
|
|
1240
|
+
if (children.length === 0) {
|
|
1241
|
+
throw new DubError(`No branch above '${target}' in the current stack.`);
|
|
1242
|
+
}
|
|
1243
|
+
if (children.length > 1) {
|
|
596
1244
|
throw new DubError(
|
|
597
|
-
`Branch '${
|
|
598
|
-
Remove it from the stack or recreate it before restacking.`
|
|
1245
|
+
`Branch '${target}' has multiple children; 'dub up' requires a linear stack path.`
|
|
599
1246
|
);
|
|
600
1247
|
}
|
|
1248
|
+
target = children[0];
|
|
601
1249
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
branchTips[branch.name] = await getBranchTip(branch.name, cwd);
|
|
605
|
-
}
|
|
606
|
-
const steps = await buildRestackSteps(targetStacks, cwd);
|
|
607
|
-
if (steps.length === 0) {
|
|
608
|
-
return { status: "up-to-date", rebased: [] };
|
|
609
|
-
}
|
|
610
|
-
await saveUndoEntry(
|
|
611
|
-
{
|
|
612
|
-
operation: "restack",
|
|
613
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
614
|
-
previousBranch: originalBranch,
|
|
615
|
-
previousState: structuredClone(state),
|
|
616
|
-
branchTips,
|
|
617
|
-
createdBranches: []
|
|
618
|
-
},
|
|
619
|
-
cwd
|
|
620
|
-
);
|
|
621
|
-
const progress = { originalBranch, steps };
|
|
622
|
-
await writeProgress(progress, cwd);
|
|
623
|
-
return executeRestackSteps(progress, cwd);
|
|
1250
|
+
await checkoutBranch(target, cwd);
|
|
1251
|
+
return { branch: target, changed: target !== current };
|
|
624
1252
|
}
|
|
625
|
-
async function
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
throw new DubError("No restack in progress. Run 'dub restack' to start.");
|
|
629
|
-
}
|
|
630
|
-
await rebaseContinue(cwd);
|
|
631
|
-
const conflictedStep = progress.steps.find((s) => s.status === "conflicted");
|
|
632
|
-
if (conflictedStep) {
|
|
633
|
-
conflictedStep.status = "done";
|
|
1253
|
+
async function downBySteps(cwd, steps) {
|
|
1254
|
+
if (!Number.isInteger(steps) || steps < 1) {
|
|
1255
|
+
throw new DubError("'steps' must be a positive integer.");
|
|
634
1256
|
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
continue;
|
|
1257
|
+
const state = await readState(cwd);
|
|
1258
|
+
const current = await getCurrentBranch(cwd);
|
|
1259
|
+
const stack = getTrackedStackOrThrow(
|
|
1260
|
+
current,
|
|
1261
|
+
findStackForBranch(state, current)
|
|
1262
|
+
);
|
|
1263
|
+
let target = current;
|
|
1264
|
+
for (let i = 0; i < steps; i++) {
|
|
1265
|
+
const branch = getBranchByName(stack, target);
|
|
1266
|
+
if (!branch) {
|
|
1267
|
+
throw new DubError(
|
|
1268
|
+
`Current branch '${target}' is not tracked by DubStack.`
|
|
1269
|
+
);
|
|
649
1270
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
await writeProgress(progress, cwd);
|
|
655
|
-
} catch (error) {
|
|
656
|
-
if (error instanceof DubError && error.message.includes("Conflict")) {
|
|
657
|
-
step.status = "conflicted";
|
|
658
|
-
await writeProgress(progress, cwd);
|
|
659
|
-
return { status: "conflict", rebased, conflictBranch: step.branch };
|
|
660
|
-
}
|
|
661
|
-
throw error;
|
|
1271
|
+
if (!branch.parent) {
|
|
1272
|
+
throw new DubError(
|
|
1273
|
+
`Already at the bottom of the stack (root branch '${target}').`
|
|
1274
|
+
);
|
|
662
1275
|
}
|
|
1276
|
+
target = branch.parent;
|
|
663
1277
|
}
|
|
664
|
-
await
|
|
665
|
-
|
|
666
|
-
const allSkipped = progress.steps.every(
|
|
667
|
-
(s) => s.status === "skipped" || s.status === "done"
|
|
668
|
-
);
|
|
669
|
-
return {
|
|
670
|
-
status: rebased.length === 0 && allSkipped ? "up-to-date" : "success",
|
|
671
|
-
rebased
|
|
672
|
-
};
|
|
1278
|
+
await checkoutBranch(target, cwd);
|
|
1279
|
+
return { branch: target, changed: target !== current };
|
|
673
1280
|
}
|
|
674
|
-
function
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
(s) => s.branches.some((b) => b.name === currentBranch)
|
|
1281
|
+
async function top(cwd) {
|
|
1282
|
+
const state = await readState(cwd);
|
|
1283
|
+
const current = await getCurrentBranch(cwd);
|
|
1284
|
+
const stack = getTrackedStackOrThrow(
|
|
1285
|
+
current,
|
|
1286
|
+
findStackForBranch(state, current)
|
|
681
1287
|
);
|
|
682
|
-
|
|
1288
|
+
let target = current;
|
|
1289
|
+
while (true) {
|
|
1290
|
+
const children = getChildren2(stack, target);
|
|
1291
|
+
if (children.length === 0) break;
|
|
1292
|
+
if (children.length > 1) {
|
|
1293
|
+
throw new DubError(
|
|
1294
|
+
`Branch '${target}' has multiple children; 'dub top' requires a linear stack path.`
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
1297
|
+
target = children[0];
|
|
1298
|
+
}
|
|
1299
|
+
if (target !== current) {
|
|
1300
|
+
await checkoutBranch(target, cwd);
|
|
1301
|
+
}
|
|
1302
|
+
return { branch: target, changed: target !== current };
|
|
683
1303
|
}
|
|
684
|
-
async function
|
|
685
|
-
const
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
1304
|
+
async function bottom(cwd) {
|
|
1305
|
+
const state = await readState(cwd);
|
|
1306
|
+
const current = await getCurrentBranch(cwd);
|
|
1307
|
+
const stack = getTrackedStackOrThrow(
|
|
1308
|
+
current,
|
|
1309
|
+
findStackForBranch(state, current)
|
|
1310
|
+
);
|
|
1311
|
+
const branch = getBranchByName(stack, current);
|
|
1312
|
+
if (!branch) {
|
|
1313
|
+
throw new DubError(
|
|
1314
|
+
`Current branch '${current}' is not tracked by DubStack.`
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
let target = current;
|
|
1318
|
+
if (!branch.parent) {
|
|
1319
|
+
const children = getChildren2(stack, current);
|
|
1320
|
+
if (children.length === 0) {
|
|
1321
|
+
throw new DubError(
|
|
1322
|
+
`No branch above root '${current}' in the current stack.`
|
|
1323
|
+
);
|
|
1324
|
+
}
|
|
1325
|
+
if (children.length > 1) {
|
|
1326
|
+
throw new DubError(
|
|
1327
|
+
`Root branch '${current}' has multiple children; 'dub bottom' requires a linear stack path.`
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1330
|
+
target = children[0];
|
|
1331
|
+
} else {
|
|
1332
|
+
let node = branch;
|
|
1333
|
+
while (node.parent) {
|
|
1334
|
+
const parent = getBranchByName(stack, node.parent);
|
|
1335
|
+
if (!parent) {
|
|
1336
|
+
break;
|
|
1337
|
+
}
|
|
1338
|
+
if (parent.parent === null) {
|
|
1339
|
+
target = node.name;
|
|
1340
|
+
break;
|
|
1341
|
+
}
|
|
1342
|
+
node = parent;
|
|
697
1343
|
}
|
|
698
1344
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
async function getProgressPath(cwd) {
|
|
702
|
-
const dubDir = await getDubDir(cwd);
|
|
703
|
-
return path4.join(dubDir, "restack-progress.json");
|
|
704
|
-
}
|
|
705
|
-
async function writeProgress(progress, cwd) {
|
|
706
|
-
const progressPath = await getProgressPath(cwd);
|
|
707
|
-
fs4.writeFileSync(progressPath, `${JSON.stringify(progress, null, 2)}
|
|
708
|
-
`);
|
|
709
|
-
}
|
|
710
|
-
async function readProgress(cwd) {
|
|
711
|
-
const progressPath = await getProgressPath(cwd);
|
|
712
|
-
if (!fs4.existsSync(progressPath)) return null;
|
|
713
|
-
const raw = fs4.readFileSync(progressPath, "utf-8");
|
|
714
|
-
return JSON.parse(raw);
|
|
715
|
-
}
|
|
716
|
-
async function clearProgress(cwd) {
|
|
717
|
-
const progressPath = await getProgressPath(cwd);
|
|
718
|
-
if (fs4.existsSync(progressPath)) {
|
|
719
|
-
fs4.unlinkSync(progressPath);
|
|
1345
|
+
if (target !== current) {
|
|
1346
|
+
await checkoutBranch(target, cwd);
|
|
720
1347
|
}
|
|
1348
|
+
return { branch: target, changed: target !== current };
|
|
721
1349
|
}
|
|
722
1350
|
|
|
723
|
-
// src/commands/submit.ts
|
|
724
|
-
import * as fs5 from "fs";
|
|
725
|
-
import * as os from "os";
|
|
726
|
-
import * as path5 from "path";
|
|
727
|
-
|
|
728
1351
|
// src/lib/github.ts
|
|
1352
|
+
init_errors();
|
|
729
1353
|
import { execa as execa2 } from "execa";
|
|
730
1354
|
async function ensureGhInstalled() {
|
|
731
1355
|
try {
|
|
@@ -766,6 +1390,58 @@ async function getPr(branch, cwd) {
|
|
|
766
1390
|
throw new DubError(`Failed to parse PR info for branch '${branch}'.`);
|
|
767
1391
|
}
|
|
768
1392
|
}
|
|
1393
|
+
async function getBranchPrLifecycleState(branch, cwd) {
|
|
1394
|
+
const info = await getBranchPrSyncInfo(branch, cwd);
|
|
1395
|
+
return info.state;
|
|
1396
|
+
}
|
|
1397
|
+
async function getBranchPrSyncInfo(branch, cwd) {
|
|
1398
|
+
const { stdout } = await execa2(
|
|
1399
|
+
"gh",
|
|
1400
|
+
[
|
|
1401
|
+
"pr",
|
|
1402
|
+
"list",
|
|
1403
|
+
"--head",
|
|
1404
|
+
branch,
|
|
1405
|
+
"--state",
|
|
1406
|
+
"all",
|
|
1407
|
+
"--json",
|
|
1408
|
+
"state,mergedAt,baseRefName",
|
|
1409
|
+
"--jq",
|
|
1410
|
+
".[0]"
|
|
1411
|
+
],
|
|
1412
|
+
{ cwd }
|
|
1413
|
+
);
|
|
1414
|
+
const trimmed = stdout.trim();
|
|
1415
|
+
if (!trimmed || trimmed === "null") {
|
|
1416
|
+
return { state: "NONE", baseRefName: null };
|
|
1417
|
+
}
|
|
1418
|
+
try {
|
|
1419
|
+
const parsed = JSON.parse(trimmed);
|
|
1420
|
+
if (parsed.mergedAt) {
|
|
1421
|
+
return {
|
|
1422
|
+
state: "MERGED",
|
|
1423
|
+
baseRefName: parsed.baseRefName ?? null
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
if (parsed.state === "CLOSED") {
|
|
1427
|
+
return {
|
|
1428
|
+
state: "CLOSED",
|
|
1429
|
+
baseRefName: parsed.baseRefName ?? null
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
if (parsed.state === "OPEN") {
|
|
1433
|
+
return {
|
|
1434
|
+
state: "OPEN",
|
|
1435
|
+
baseRefName: parsed.baseRefName ?? null
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
return { state: "NONE", baseRefName: parsed.baseRefName ?? null };
|
|
1439
|
+
} catch {
|
|
1440
|
+
throw new DubError(
|
|
1441
|
+
`Failed to parse PR lifecycle state for branch '${branch}'.`
|
|
1442
|
+
);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
769
1445
|
async function createPr(branch, base, title, bodyFile, cwd) {
|
|
770
1446
|
let stdout;
|
|
771
1447
|
try {
|
|
@@ -824,6 +1500,39 @@ async function updatePrBody(prNumber, bodyFile, cwd) {
|
|
|
824
1500
|
throw new DubError(`Failed to update PR #${prNumber}: ${message}`);
|
|
825
1501
|
}
|
|
826
1502
|
}
|
|
1503
|
+
async function openPrInBrowser(cwd, target) {
|
|
1504
|
+
const args = target ? ["pr", "view", target, "--web"] : ["pr", "view", "--web"];
|
|
1505
|
+
try {
|
|
1506
|
+
await execa2("gh", args, { cwd, stdio: "inherit" });
|
|
1507
|
+
} catch (error) {
|
|
1508
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1509
|
+
if (message.toLowerCase().includes("no pull requests")) {
|
|
1510
|
+
throw new DubError(
|
|
1511
|
+
target ? `No PR found for '${target}'.` : "No PR found for the current branch."
|
|
1512
|
+
);
|
|
1513
|
+
}
|
|
1514
|
+
throw new DubError(
|
|
1515
|
+
target ? `Failed to open PR for '${target}': ${message}` : `Failed to open PR: ${message}`
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
// src/commands/pr.ts
|
|
1521
|
+
async function pr(cwd, branch) {
|
|
1522
|
+
await ensureGhInstalled();
|
|
1523
|
+
await checkGhAuth();
|
|
1524
|
+
await openPrInBrowser(cwd, branch);
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// src/index.ts
|
|
1528
|
+
init_restack();
|
|
1529
|
+
|
|
1530
|
+
// src/commands/submit.ts
|
|
1531
|
+
init_errors();
|
|
1532
|
+
init_git();
|
|
1533
|
+
import * as fs5 from "fs";
|
|
1534
|
+
import * as os from "os";
|
|
1535
|
+
import * as path5 from "path";
|
|
827
1536
|
|
|
828
1537
|
// src/lib/pr-body.ts
|
|
829
1538
|
var DUBSTACK_START = "<!-- dubstack:start -->";
|
|
@@ -883,6 +1592,7 @@ function composePrBody(existingBody, stackTable, metadataBlock) {
|
|
|
883
1592
|
}
|
|
884
1593
|
|
|
885
1594
|
// src/commands/submit.ts
|
|
1595
|
+
init_state();
|
|
886
1596
|
async function submit(cwd, dryRun) {
|
|
887
1597
|
await ensureGhInstalled();
|
|
888
1598
|
await checkGhAuth();
|
|
@@ -938,12 +1648,23 @@ async function submit(cwd, dryRun) {
|
|
|
938
1648
|
if (!dryRun) {
|
|
939
1649
|
await updateAllPrBodies(nonRootBranches, prMap, stack.id, cwd);
|
|
940
1650
|
for (const branch of nonRootBranches) {
|
|
941
|
-
const
|
|
942
|
-
if (
|
|
1651
|
+
const pr2 = prMap.get(branch.name);
|
|
1652
|
+
if (pr2) {
|
|
943
1653
|
const stateBranch = stack.branches.find((b) => b.name === branch.name);
|
|
944
1654
|
if (stateBranch) {
|
|
945
|
-
stateBranch.pr_number =
|
|
946
|
-
stateBranch.pr_link =
|
|
1655
|
+
stateBranch.pr_number = pr2.number;
|
|
1656
|
+
stateBranch.pr_link = pr2.url;
|
|
1657
|
+
const headSha = await getBranchTip(branch.name, cwd);
|
|
1658
|
+
const baseSha = await getBranchTip(branch.parent, cwd);
|
|
1659
|
+
stateBranch.last_submitted_version = {
|
|
1660
|
+
head_sha: headSha,
|
|
1661
|
+
base_sha: baseSha,
|
|
1662
|
+
base_branch: branch.parent,
|
|
1663
|
+
version_number: null,
|
|
1664
|
+
source: "submit"
|
|
1665
|
+
};
|
|
1666
|
+
stateBranch.last_synced_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
1667
|
+
stateBranch.sync_source = "submit";
|
|
947
1668
|
}
|
|
948
1669
|
}
|
|
949
1670
|
}
|
|
@@ -969,30 +1690,30 @@ function validateLinearStack(ordered) {
|
|
|
969
1690
|
async function updateAllPrBodies(branches, prMap, stackId, cwd) {
|
|
970
1691
|
const tableEntries = /* @__PURE__ */ new Map();
|
|
971
1692
|
for (const branch of branches) {
|
|
972
|
-
const
|
|
973
|
-
if (
|
|
974
|
-
tableEntries.set(branch.name, { number:
|
|
1693
|
+
const pr2 = prMap.get(branch.name);
|
|
1694
|
+
if (pr2) {
|
|
1695
|
+
tableEntries.set(branch.name, { number: pr2.number, title: pr2.title });
|
|
975
1696
|
}
|
|
976
1697
|
}
|
|
977
1698
|
for (let i = 0; i < branches.length; i++) {
|
|
978
1699
|
const branch = branches[i];
|
|
979
|
-
const
|
|
980
|
-
if (!
|
|
1700
|
+
const pr2 = prMap.get(branch.name);
|
|
1701
|
+
if (!pr2) continue;
|
|
981
1702
|
const prevPr = i > 0 ? prMap.get(branches[i - 1].name)?.number ?? null : null;
|
|
982
1703
|
const nextPr = i < branches.length - 1 ? prMap.get(branches[i + 1].name)?.number ?? null : null;
|
|
983
1704
|
const stackTable = buildStackTable(branches, tableEntries, branch.name);
|
|
984
1705
|
const metadataBlock = buildMetadataBlock(
|
|
985
1706
|
stackId,
|
|
986
|
-
|
|
1707
|
+
pr2.number,
|
|
987
1708
|
prevPr,
|
|
988
1709
|
nextPr,
|
|
989
1710
|
branch.name
|
|
990
1711
|
);
|
|
991
|
-
const existingBody =
|
|
1712
|
+
const existingBody = pr2.body;
|
|
992
1713
|
const finalBody = composePrBody(existingBody, stackTable, metadataBlock);
|
|
993
1714
|
const tmpFile = writeTempBody(finalBody);
|
|
994
1715
|
try {
|
|
995
|
-
await updatePrBody(
|
|
1716
|
+
await updatePrBody(pr2.number, tmpFile, cwd);
|
|
996
1717
|
} finally {
|
|
997
1718
|
cleanupTempFile(tmpFile);
|
|
998
1719
|
}
|
|
@@ -1011,7 +1732,612 @@ function cleanupTempFile(filePath) {
|
|
|
1011
1732
|
}
|
|
1012
1733
|
}
|
|
1013
1734
|
|
|
1735
|
+
// src/commands/sync.ts
|
|
1736
|
+
init_errors();
|
|
1737
|
+
init_git();
|
|
1738
|
+
import { stdin as input, stdout as output } from "process";
|
|
1739
|
+
import * as readline from "readline/promises";
|
|
1740
|
+
init_state();
|
|
1741
|
+
|
|
1742
|
+
// src/lib/sync/branch-status.ts
|
|
1743
|
+
function classifyBranchSyncStatus(input2) {
|
|
1744
|
+
if (!input2.hasRemote) return "missing-remote";
|
|
1745
|
+
if (!input2.hasLocal) return "missing-local";
|
|
1746
|
+
if (input2.localSha && input2.remoteSha && input2.localSha === input2.remoteSha) {
|
|
1747
|
+
if (!input2.hasSubmittedBaseline) {
|
|
1748
|
+
return "updated-outside-dubstack-but-up-to-date";
|
|
1749
|
+
}
|
|
1750
|
+
return "up-to-date";
|
|
1751
|
+
}
|
|
1752
|
+
if (!input2.hasSubmittedBaseline) return "unsubmitted";
|
|
1753
|
+
if (input2.localBehind) return "needs-remote-sync-safe";
|
|
1754
|
+
if (input2.remoteBehind) return "local-ahead";
|
|
1755
|
+
return "reconcile-needed";
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
// src/lib/sync/cleanup.ts
|
|
1759
|
+
async function buildCleanupPlan(input2) {
|
|
1760
|
+
const toDelete = [];
|
|
1761
|
+
const skipped = [];
|
|
1762
|
+
for (const branch of input2.branches) {
|
|
1763
|
+
const prState = await input2.getPrStatus(branch);
|
|
1764
|
+
if (prState !== "MERGED" && prState !== "CLOSED") {
|
|
1765
|
+
continue;
|
|
1766
|
+
}
|
|
1767
|
+
const mergedIntoRoot = await input2.isMergedIntoAnyRoot(branch);
|
|
1768
|
+
if (!mergedIntoRoot) {
|
|
1769
|
+
skipped.push({ branch, reason: "commits-not-in-trunk" });
|
|
1770
|
+
continue;
|
|
1771
|
+
}
|
|
1772
|
+
toDelete.push(branch);
|
|
1773
|
+
}
|
|
1774
|
+
return { toDelete, skipped };
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
// src/lib/sync/reconcile.ts
|
|
1778
|
+
async function resolveReconcileDecision(input2) {
|
|
1779
|
+
if (input2.force) return "take-remote";
|
|
1780
|
+
if (!input2.interactive) return "skip";
|
|
1781
|
+
const raw = await input2.promptChoice();
|
|
1782
|
+
if (raw === "take-remote" || raw === "keep-local" || raw === "reconcile" || raw === "skip") {
|
|
1783
|
+
return raw;
|
|
1784
|
+
}
|
|
1785
|
+
return "skip";
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
// src/lib/sync/report.ts
|
|
1789
|
+
function printBranchOutcome(outcome) {
|
|
1790
|
+
console.log(outcome.message);
|
|
1791
|
+
}
|
|
1792
|
+
function printSyncSummary(result) {
|
|
1793
|
+
const synced = result.branches.filter((b) => b.action === "synced").length;
|
|
1794
|
+
const skipped = result.branches.filter((b) => b.action === "skipped").length;
|
|
1795
|
+
const keptLocal = result.branches.filter(
|
|
1796
|
+
(b) => b.action === "kept-local"
|
|
1797
|
+
).length;
|
|
1798
|
+
console.log(
|
|
1799
|
+
`\u2714 Sync complete: ${synced} synced, ${keptLocal} kept-local, ${skipped} skipped, ${result.cleaned.length} cleaned`
|
|
1800
|
+
);
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
// src/commands/sync.ts
|
|
1804
|
+
init_restack();
|
|
1805
|
+
function isInteractiveShell() {
|
|
1806
|
+
return Boolean(process.stdout.isTTY && process.stdin.isTTY);
|
|
1807
|
+
}
|
|
1808
|
+
async function confirm(question) {
|
|
1809
|
+
const rl = readline.createInterface({ input, output });
|
|
1810
|
+
try {
|
|
1811
|
+
const answer = await rl.question(`${question} [Y/n] `);
|
|
1812
|
+
const normalized = answer.trim().toLowerCase();
|
|
1813
|
+
return normalized === "" || normalized === "y" || normalized === "yes";
|
|
1814
|
+
} finally {
|
|
1815
|
+
rl.close();
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
async function choose(question, choices) {
|
|
1819
|
+
const rl = readline.createInterface({ input, output });
|
|
1820
|
+
try {
|
|
1821
|
+
console.log(question);
|
|
1822
|
+
for (let i = 0; i < choices.length; i++) {
|
|
1823
|
+
console.log(` ${i + 1}. ${choices[i].label}`);
|
|
1824
|
+
}
|
|
1825
|
+
const answer = await rl.question("Select option: ");
|
|
1826
|
+
const idx = Number.parseInt(answer.trim(), 10) - 1;
|
|
1827
|
+
if (Number.isNaN(idx) || idx < 0 || idx >= choices.length) {
|
|
1828
|
+
return choices[choices.length - 1].value;
|
|
1829
|
+
}
|
|
1830
|
+
return choices[idx].value;
|
|
1831
|
+
} finally {
|
|
1832
|
+
rl.close();
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
async function sync(cwd, rawOptions = {}) {
|
|
1836
|
+
await ensureGhInstalled();
|
|
1837
|
+
await checkGhAuth();
|
|
1838
|
+
const options = {
|
|
1839
|
+
restack: rawOptions.restack ?? true,
|
|
1840
|
+
force: rawOptions.force ?? false,
|
|
1841
|
+
all: rawOptions.all ?? false,
|
|
1842
|
+
interactive: rawOptions.interactive ?? isInteractiveShell()
|
|
1843
|
+
};
|
|
1844
|
+
const state = await readState(cwd);
|
|
1845
|
+
const originalBranch = await getCurrentBranch(cwd);
|
|
1846
|
+
const scopeStacks = options.all ? state.stacks : (() => {
|
|
1847
|
+
const stack = findStackForBranch(state, originalBranch);
|
|
1848
|
+
if (!stack) {
|
|
1849
|
+
throw new DubError(
|
|
1850
|
+
`Branch '${originalBranch}' is not part of any stack. Run 'dub create' first.`
|
|
1851
|
+
);
|
|
1852
|
+
}
|
|
1853
|
+
return [stack];
|
|
1854
|
+
})();
|
|
1855
|
+
const stateBranchMap = new Map(
|
|
1856
|
+
scopeStacks.flatMap((stack) => stack.branches.map((b) => [b.name, b]))
|
|
1857
|
+
);
|
|
1858
|
+
const roots = Array.from(
|
|
1859
|
+
new Set(
|
|
1860
|
+
scopeStacks.flatMap((s) => s.branches).filter((b) => b.type === "root").map((b) => b.name)
|
|
1861
|
+
)
|
|
1862
|
+
);
|
|
1863
|
+
const stackBranches = Array.from(
|
|
1864
|
+
new Set(
|
|
1865
|
+
scopeStacks.flatMap((s) => s.branches).filter((b) => b.type !== "root").map((b) => b.name)
|
|
1866
|
+
)
|
|
1867
|
+
);
|
|
1868
|
+
const result = {
|
|
1869
|
+
fetched: [],
|
|
1870
|
+
trunksSynced: [],
|
|
1871
|
+
cleaned: [],
|
|
1872
|
+
branches: [],
|
|
1873
|
+
restacked: false
|
|
1874
|
+
};
|
|
1875
|
+
const rootHasRemote = /* @__PURE__ */ new Map();
|
|
1876
|
+
console.log("\u{1F332} Fetching branches from remote...");
|
|
1877
|
+
const toFetch = [.../* @__PURE__ */ new Set([...roots, ...stackBranches])];
|
|
1878
|
+
if (toFetch.length > 0) {
|
|
1879
|
+
await fetchBranches(toFetch, cwd);
|
|
1880
|
+
result.fetched = toFetch;
|
|
1881
|
+
}
|
|
1882
|
+
for (const root of roots) {
|
|
1883
|
+
const remoteRef = `origin/${root}`;
|
|
1884
|
+
const hasRemoteRoot = await remoteBranchExists(root, cwd);
|
|
1885
|
+
rootHasRemote.set(root, hasRemoteRoot);
|
|
1886
|
+
if (!hasRemoteRoot) continue;
|
|
1887
|
+
const ff = await fastForwardBranchToRef(root, remoteRef, cwd);
|
|
1888
|
+
if (ff) {
|
|
1889
|
+
result.trunksSynced.push(root);
|
|
1890
|
+
continue;
|
|
1891
|
+
}
|
|
1892
|
+
if (options.force) {
|
|
1893
|
+
await hardResetBranchToRef(root, remoteRef, cwd);
|
|
1894
|
+
result.trunksSynced.push(root);
|
|
1895
|
+
continue;
|
|
1896
|
+
}
|
|
1897
|
+
if (options.interactive) {
|
|
1898
|
+
const takeRemote = await confirm(
|
|
1899
|
+
`Trunk '${root}' cannot be fast-forwarded. Overwrite local trunk with '${remoteRef}'?`
|
|
1900
|
+
);
|
|
1901
|
+
if (takeRemote) {
|
|
1902
|
+
await hardResetBranchToRef(root, remoteRef, cwd);
|
|
1903
|
+
result.trunksSynced.push(root);
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
console.log("\u{1F9F9} Cleaning up branches with merged/closed PRs...");
|
|
1908
|
+
const localTrackedBranches = [];
|
|
1909
|
+
for (const branch of stackBranches) {
|
|
1910
|
+
const hasLocal = await branchExists(branch, cwd);
|
|
1911
|
+
if (hasLocal) localTrackedBranches.push(branch);
|
|
1912
|
+
}
|
|
1913
|
+
const cleanupPlan = await buildCleanupPlan({
|
|
1914
|
+
branches: localTrackedBranches,
|
|
1915
|
+
getPrStatus: (branch) => getBranchPrLifecycleState(branch, cwd),
|
|
1916
|
+
isMergedIntoAnyRoot: async (branch) => {
|
|
1917
|
+
for (const root of roots) {
|
|
1918
|
+
const compareRef = rootHasRemote.get(root) ? `origin/${root}` : root;
|
|
1919
|
+
if (await isAncestor(branch, compareRef, cwd)) return true;
|
|
1920
|
+
}
|
|
1921
|
+
return false;
|
|
1922
|
+
}
|
|
1923
|
+
});
|
|
1924
|
+
const excludedFromSync = /* @__PURE__ */ new Set();
|
|
1925
|
+
for (const skipped of cleanupPlan.skipped) {
|
|
1926
|
+
if (skipped.reason === "commits-not-in-trunk") {
|
|
1927
|
+
excludedFromSync.add(skipped.branch);
|
|
1928
|
+
for (const child of getDescendants(scopeStacks, skipped.branch)) {
|
|
1929
|
+
excludedFromSync.add(child);
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
for (const branch of cleanupPlan.toDelete) {
|
|
1934
|
+
if (excludedFromSync.has(branch)) continue;
|
|
1935
|
+
let shouldDelete = options.force;
|
|
1936
|
+
if (!shouldDelete && options.interactive) {
|
|
1937
|
+
shouldDelete = await confirm(
|
|
1938
|
+
`Branch '${branch}' has merged/closed PR and is in trunk. Delete local branch?`
|
|
1939
|
+
);
|
|
1940
|
+
}
|
|
1941
|
+
if (shouldDelete) {
|
|
1942
|
+
await checkoutBranch(roots[0] ?? originalBranch, cwd);
|
|
1943
|
+
await deleteBranch(branch, cwd);
|
|
1944
|
+
removeBranchFromState(scopeStacks, branch);
|
|
1945
|
+
result.cleaned.push(branch);
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
for (const skipped of cleanupPlan.skipped) {
|
|
1949
|
+
console.log(
|
|
1950
|
+
`\u2022 Skipped cleanup for '${skipped.branch}' (${skipped.reason}).`
|
|
1951
|
+
);
|
|
1952
|
+
}
|
|
1953
|
+
for (const excluded of excludedFromSync) {
|
|
1954
|
+
console.log(
|
|
1955
|
+
`\u2022 Excluding '${excluded}' from sync because its stack is not cleanable yet.`
|
|
1956
|
+
);
|
|
1957
|
+
}
|
|
1958
|
+
console.log("\u{1F504} Syncing branches...");
|
|
1959
|
+
for (const branch of stackBranches) {
|
|
1960
|
+
if (result.cleaned.includes(branch) || excludedFromSync.has(branch))
|
|
1961
|
+
continue;
|
|
1962
|
+
const hasRemote = await remoteBranchExists(branch, cwd);
|
|
1963
|
+
const hasLocal = await branchExists(branch, cwd);
|
|
1964
|
+
let outcome;
|
|
1965
|
+
const remoteRef = `origin/${branch}`;
|
|
1966
|
+
const localSha = hasLocal ? await getRefSha(branch, cwd) : null;
|
|
1967
|
+
const remoteSha = hasRemote ? await getRefSha(remoteRef, cwd) : null;
|
|
1968
|
+
const localBehind = hasLocal && hasRemote ? await isAncestor(branch, remoteRef, cwd) : false;
|
|
1969
|
+
const remoteBehind = hasLocal && hasRemote ? await isAncestor(remoteRef, branch, cwd) : false;
|
|
1970
|
+
let status = classifyBranchSyncStatus({
|
|
1971
|
+
hasRemote,
|
|
1972
|
+
hasLocal,
|
|
1973
|
+
localSha,
|
|
1974
|
+
remoteSha,
|
|
1975
|
+
localBehind,
|
|
1976
|
+
remoteBehind,
|
|
1977
|
+
hasSubmittedBaseline: stateBranchMap.get(branch)?.last_submitted_version != null
|
|
1978
|
+
});
|
|
1979
|
+
const prSyncInfo = hasRemote ? await getBranchPrSyncInfo(branch, cwd) : { state: "NONE", baseRefName: null };
|
|
1980
|
+
const localParent = stateBranchMap.get(branch)?.parent ?? null;
|
|
1981
|
+
if (hasRemote && hasLocal && localSha !== remoteSha && prSyncInfo.baseRefName && localParent && prSyncInfo.baseRefName !== localParent) {
|
|
1982
|
+
status = "needs-remote-sync";
|
|
1983
|
+
}
|
|
1984
|
+
if (status === "missing-remote") {
|
|
1985
|
+
outcome = {
|
|
1986
|
+
branch,
|
|
1987
|
+
status,
|
|
1988
|
+
action: "skipped",
|
|
1989
|
+
message: `\u26A0 Skipped '${branch}' (missing on remote).`
|
|
1990
|
+
};
|
|
1991
|
+
result.branches.push(outcome);
|
|
1992
|
+
printBranchOutcome(outcome);
|
|
1993
|
+
continue;
|
|
1994
|
+
}
|
|
1995
|
+
if (status === "missing-local") {
|
|
1996
|
+
await checkoutRemoteBranch(branch, cwd);
|
|
1997
|
+
outcome = {
|
|
1998
|
+
branch,
|
|
1999
|
+
status,
|
|
2000
|
+
action: "synced",
|
|
2001
|
+
message: `\u2714 Restored '${branch}' from remote.`
|
|
2002
|
+
};
|
|
2003
|
+
result.branches.push(outcome);
|
|
2004
|
+
printBranchOutcome(outcome);
|
|
2005
|
+
const restoredSha = await getRefSha(branch, cwd);
|
|
2006
|
+
await markBranchSynced(stateBranchMap, branch, restoredSha, cwd, {
|
|
2007
|
+
source: "sync",
|
|
2008
|
+
baseBranch: stateBranchMap.get(branch)?.parent ?? null
|
|
2009
|
+
});
|
|
2010
|
+
continue;
|
|
2011
|
+
}
|
|
2012
|
+
if (status === "up-to-date") {
|
|
2013
|
+
outcome = {
|
|
2014
|
+
branch,
|
|
2015
|
+
status,
|
|
2016
|
+
action: "none",
|
|
2017
|
+
message: `\u2022 '${branch}' is up to date.`
|
|
2018
|
+
};
|
|
2019
|
+
result.branches.push(outcome);
|
|
2020
|
+
printBranchOutcome(outcome);
|
|
2021
|
+
await markBranchSynced(
|
|
2022
|
+
stateBranchMap,
|
|
2023
|
+
branch,
|
|
2024
|
+
localSha ?? remoteSha ?? null,
|
|
2025
|
+
cwd,
|
|
2026
|
+
{
|
|
2027
|
+
source: "sync",
|
|
2028
|
+
baseBranch: stateBranchMap.get(branch)?.parent ?? null
|
|
2029
|
+
}
|
|
2030
|
+
);
|
|
2031
|
+
continue;
|
|
2032
|
+
}
|
|
2033
|
+
if (status === "updated-outside-dubstack-but-up-to-date") {
|
|
2034
|
+
outcome = {
|
|
2035
|
+
branch,
|
|
2036
|
+
status,
|
|
2037
|
+
action: "none",
|
|
2038
|
+
message: `\u2022 '${branch}' is up to date but was previously unmanaged by DubStack sync metadata.`
|
|
2039
|
+
};
|
|
2040
|
+
result.branches.push(outcome);
|
|
2041
|
+
printBranchOutcome(outcome);
|
|
2042
|
+
await markBranchSynced(
|
|
2043
|
+
stateBranchMap,
|
|
2044
|
+
branch,
|
|
2045
|
+
localSha ?? remoteSha ?? null,
|
|
2046
|
+
cwd,
|
|
2047
|
+
{
|
|
2048
|
+
source: "imported",
|
|
2049
|
+
baseBranch: stateBranchMap.get(branch)?.parent ?? null
|
|
2050
|
+
}
|
|
2051
|
+
);
|
|
2052
|
+
continue;
|
|
2053
|
+
}
|
|
2054
|
+
if (status === "needs-remote-sync-safe") {
|
|
2055
|
+
await hardResetBranchToRef(branch, remoteRef, cwd);
|
|
2056
|
+
outcome = {
|
|
2057
|
+
branch,
|
|
2058
|
+
status,
|
|
2059
|
+
action: "synced",
|
|
2060
|
+
message: `\u2714 Synced '${branch}' to remote head.`
|
|
2061
|
+
};
|
|
2062
|
+
result.branches.push(outcome);
|
|
2063
|
+
printBranchOutcome(outcome);
|
|
2064
|
+
await markBranchSynced(stateBranchMap, branch, remoteSha, cwd, {
|
|
2065
|
+
source: "sync",
|
|
2066
|
+
baseBranch: stateBranchMap.get(branch)?.parent ?? null
|
|
2067
|
+
});
|
|
2068
|
+
continue;
|
|
2069
|
+
}
|
|
2070
|
+
if (status === "local-ahead") {
|
|
2071
|
+
outcome = {
|
|
2072
|
+
branch,
|
|
2073
|
+
status,
|
|
2074
|
+
action: "kept-local",
|
|
2075
|
+
message: `\u2022 Kept local '${branch}' (local commits ahead of remote).`
|
|
2076
|
+
};
|
|
2077
|
+
result.branches.push(outcome);
|
|
2078
|
+
printBranchOutcome(outcome);
|
|
2079
|
+
continue;
|
|
2080
|
+
}
|
|
2081
|
+
if (status === "unsubmitted") {
|
|
2082
|
+
if (options.force) {
|
|
2083
|
+
await hardResetBranchToRef(branch, remoteRef, cwd);
|
|
2084
|
+
outcome = {
|
|
2085
|
+
branch,
|
|
2086
|
+
status,
|
|
2087
|
+
action: "synced",
|
|
2088
|
+
message: `\u2714 Synced unsubmitted branch '${branch}' to remote with --force.`
|
|
2089
|
+
};
|
|
2090
|
+
await markBranchSynced(stateBranchMap, branch, remoteSha, cwd, {
|
|
2091
|
+
source: "sync",
|
|
2092
|
+
baseBranch: localParent
|
|
2093
|
+
});
|
|
2094
|
+
} else if (!options.interactive) {
|
|
2095
|
+
outcome = {
|
|
2096
|
+
branch,
|
|
2097
|
+
status,
|
|
2098
|
+
action: "skipped",
|
|
2099
|
+
message: `\u26A0 Skipped unsubmitted branch '${branch}' (use --force or interactive mode).`
|
|
2100
|
+
};
|
|
2101
|
+
} else {
|
|
2102
|
+
const takeRemote = await confirm(
|
|
2103
|
+
`Branch '${branch}' has no DubStack submit baseline. Overwrite local with remote version?`
|
|
2104
|
+
);
|
|
2105
|
+
if (takeRemote) {
|
|
2106
|
+
await hardResetBranchToRef(branch, remoteRef, cwd);
|
|
2107
|
+
outcome = {
|
|
2108
|
+
branch,
|
|
2109
|
+
status,
|
|
2110
|
+
action: "synced",
|
|
2111
|
+
message: `\u2714 Synced unsubmitted branch '${branch}' to remote.`
|
|
2112
|
+
};
|
|
2113
|
+
await markBranchSynced(stateBranchMap, branch, remoteSha, cwd, {
|
|
2114
|
+
source: "sync",
|
|
2115
|
+
baseBranch: localParent
|
|
2116
|
+
});
|
|
2117
|
+
} else {
|
|
2118
|
+
outcome = {
|
|
2119
|
+
branch,
|
|
2120
|
+
status,
|
|
2121
|
+
action: "kept-local",
|
|
2122
|
+
message: `\u2022 Kept local unsubmitted branch '${branch}'.`
|
|
2123
|
+
};
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
result.branches.push(outcome);
|
|
2127
|
+
printBranchOutcome(outcome);
|
|
2128
|
+
continue;
|
|
2129
|
+
}
|
|
2130
|
+
if (status === "needs-remote-sync") {
|
|
2131
|
+
if (options.force) {
|
|
2132
|
+
await hardResetBranchToRef(branch, remoteRef, cwd);
|
|
2133
|
+
if (prSyncInfo.baseRefName && localParent !== prSyncInfo.baseRefName) {
|
|
2134
|
+
const stateBranch = stateBranchMap.get(branch);
|
|
2135
|
+
if (stateBranch) stateBranch.parent = prSyncInfo.baseRefName;
|
|
2136
|
+
}
|
|
2137
|
+
outcome = {
|
|
2138
|
+
branch,
|
|
2139
|
+
status,
|
|
2140
|
+
action: "synced",
|
|
2141
|
+
message: `\u2714 Synced '${branch}' to remote and adopted remote parent '${prSyncInfo.baseRefName ?? "unknown"}'.`
|
|
2142
|
+
};
|
|
2143
|
+
await markBranchSynced(stateBranchMap, branch, remoteSha, cwd, {
|
|
2144
|
+
source: "sync",
|
|
2145
|
+
baseBranch: prSyncInfo.baseRefName ?? localParent
|
|
2146
|
+
});
|
|
2147
|
+
} else if (!options.interactive) {
|
|
2148
|
+
outcome = {
|
|
2149
|
+
branch,
|
|
2150
|
+
status,
|
|
2151
|
+
action: "skipped",
|
|
2152
|
+
message: `\u26A0 Skipped '${branch}' parent-mismatch sync (run interactively or with --force).`
|
|
2153
|
+
};
|
|
2154
|
+
} else {
|
|
2155
|
+
const parentDecision = await choose(
|
|
2156
|
+
`Branch '${branch}' parent differs locally ('${localParent}') vs remote ('${prSyncInfo.baseRefName}').`,
|
|
2157
|
+
[
|
|
2158
|
+
{ label: "Take remote version and remote parent", value: "remote" },
|
|
2159
|
+
{ label: "Keep local branch and parent", value: "local" },
|
|
2160
|
+
{ label: "Skip for now", value: "skip" }
|
|
2161
|
+
]
|
|
2162
|
+
);
|
|
2163
|
+
if (parentDecision === "remote") {
|
|
2164
|
+
await hardResetBranchToRef(branch, remoteRef, cwd);
|
|
2165
|
+
const stateBranch = stateBranchMap.get(branch);
|
|
2166
|
+
if (stateBranch && prSyncInfo.baseRefName) {
|
|
2167
|
+
stateBranch.parent = prSyncInfo.baseRefName;
|
|
2168
|
+
}
|
|
2169
|
+
outcome = {
|
|
2170
|
+
branch,
|
|
2171
|
+
status,
|
|
2172
|
+
action: "synced",
|
|
2173
|
+
message: `\u2714 Synced '${branch}' to remote and adopted remote parent.`
|
|
2174
|
+
};
|
|
2175
|
+
await markBranchSynced(stateBranchMap, branch, remoteSha, cwd, {
|
|
2176
|
+
source: "sync",
|
|
2177
|
+
baseBranch: prSyncInfo.baseRefName ?? localParent
|
|
2178
|
+
});
|
|
2179
|
+
} else if (parentDecision === "local") {
|
|
2180
|
+
outcome = {
|
|
2181
|
+
branch,
|
|
2182
|
+
status,
|
|
2183
|
+
action: "kept-local",
|
|
2184
|
+
message: `\u2022 Kept local parent and local state for '${branch}'.`
|
|
2185
|
+
};
|
|
2186
|
+
} else {
|
|
2187
|
+
outcome = {
|
|
2188
|
+
branch,
|
|
2189
|
+
status,
|
|
2190
|
+
action: "skipped",
|
|
2191
|
+
message: `\u26A0 Skipped '${branch}' parent-mismatch sync by user choice.`
|
|
2192
|
+
};
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
result.branches.push(outcome);
|
|
2196
|
+
printBranchOutcome(outcome);
|
|
2197
|
+
continue;
|
|
2198
|
+
}
|
|
2199
|
+
const decision = await resolveReconcileDecision({
|
|
2200
|
+
branch,
|
|
2201
|
+
force: options.force,
|
|
2202
|
+
interactive: options.interactive,
|
|
2203
|
+
promptChoice: () => choose(
|
|
2204
|
+
`Branch '${branch}' diverged from remote. How should sync proceed?`,
|
|
2205
|
+
[
|
|
2206
|
+
{
|
|
2207
|
+
label: "Take remote version (discard local divergence)",
|
|
2208
|
+
value: "take-remote"
|
|
2209
|
+
},
|
|
2210
|
+
{ label: "Keep local version", value: "keep-local" },
|
|
2211
|
+
{
|
|
2212
|
+
label: "Attempt reconciliation and keep local commits",
|
|
2213
|
+
value: "reconcile"
|
|
2214
|
+
},
|
|
2215
|
+
{ label: "Skip this branch", value: "skip" }
|
|
2216
|
+
]
|
|
2217
|
+
)
|
|
2218
|
+
});
|
|
2219
|
+
if (decision === "take-remote") {
|
|
2220
|
+
await hardResetBranchToRef(branch, remoteRef, cwd);
|
|
2221
|
+
outcome = {
|
|
2222
|
+
branch,
|
|
2223
|
+
status: "reconcile-needed",
|
|
2224
|
+
action: "synced",
|
|
2225
|
+
message: `\u2714 Synced '${branch}' to remote version.`
|
|
2226
|
+
};
|
|
2227
|
+
await markBranchSynced(stateBranchMap, branch, remoteSha, cwd, {
|
|
2228
|
+
source: "sync",
|
|
2229
|
+
baseBranch: stateBranchMap.get(branch)?.parent ?? null
|
|
2230
|
+
});
|
|
2231
|
+
} else if (decision === "keep-local") {
|
|
2232
|
+
outcome = {
|
|
2233
|
+
branch,
|
|
2234
|
+
status: "reconcile-needed",
|
|
2235
|
+
action: "kept-local",
|
|
2236
|
+
message: `\u2022 Kept local '${branch}' (remote divergence ignored).`
|
|
2237
|
+
};
|
|
2238
|
+
} else if (decision === "reconcile") {
|
|
2239
|
+
const reconciled = await rebaseBranchOntoRef(branch, remoteRef, cwd);
|
|
2240
|
+
outcome = {
|
|
2241
|
+
branch,
|
|
2242
|
+
status: "reconcile-needed",
|
|
2243
|
+
action: reconciled ? "synced" : "kept-local",
|
|
2244
|
+
message: reconciled ? `\u2714 Reconciled '${branch}' by rebasing local commits onto remote.` : `\u26A0 Could not auto-reconcile '${branch}'. Kept local state; reconcile manually.`
|
|
2245
|
+
};
|
|
2246
|
+
if (reconciled) {
|
|
2247
|
+
const newSha = await getRefSha(branch, cwd);
|
|
2248
|
+
await markBranchSynced(stateBranchMap, branch, newSha, cwd, {
|
|
2249
|
+
source: "sync",
|
|
2250
|
+
baseBranch: stateBranchMap.get(branch)?.parent ?? null
|
|
2251
|
+
});
|
|
2252
|
+
}
|
|
2253
|
+
} else {
|
|
2254
|
+
outcome = {
|
|
2255
|
+
branch,
|
|
2256
|
+
status: "reconcile-needed",
|
|
2257
|
+
action: "skipped",
|
|
2258
|
+
message: options.interactive ? `\u26A0 Skipped '${branch}' by user choice.` : `\u26A0 Skipped '${branch}' (diverged from remote; rerun with --force or interactive).`
|
|
2259
|
+
};
|
|
2260
|
+
}
|
|
2261
|
+
result.branches.push(outcome);
|
|
2262
|
+
printBranchOutcome(outcome);
|
|
2263
|
+
}
|
|
2264
|
+
if (options.restack) {
|
|
2265
|
+
console.log("\u{1F95E} Restacking branches...");
|
|
2266
|
+
const rootsToRestack = options.all ? roots : [roots[0]].filter(Boolean);
|
|
2267
|
+
for (const root of rootsToRestack) {
|
|
2268
|
+
await checkoutBranch(root, cwd);
|
|
2269
|
+
await restack(cwd);
|
|
2270
|
+
}
|
|
2271
|
+
result.restacked = true;
|
|
2272
|
+
}
|
|
2273
|
+
await writeState(state, cwd);
|
|
2274
|
+
await checkoutBranch(originalBranch, cwd);
|
|
2275
|
+
printSyncSummary(result);
|
|
2276
|
+
return result;
|
|
2277
|
+
}
|
|
2278
|
+
async function markBranchSynced(branchMap, branchName, headSha, cwd, options) {
|
|
2279
|
+
if (!headSha) return;
|
|
2280
|
+
const entry = branchMap.get(branchName);
|
|
2281
|
+
if (!entry) return;
|
|
2282
|
+
const priorBaseline = entry.last_submitted_version;
|
|
2283
|
+
const resolvedBaseBranch = options.baseBranch ?? priorBaseline?.base_branch ?? null;
|
|
2284
|
+
let resolvedBaseSha = priorBaseline?.base_sha ?? null;
|
|
2285
|
+
if (resolvedBaseBranch) {
|
|
2286
|
+
try {
|
|
2287
|
+
resolvedBaseSha = await getRefSha(resolvedBaseBranch, cwd);
|
|
2288
|
+
} catch {
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
if (!resolvedBaseBranch || !resolvedBaseSha) return;
|
|
2292
|
+
entry.last_submitted_version = {
|
|
2293
|
+
head_sha: headSha,
|
|
2294
|
+
base_sha: resolvedBaseSha,
|
|
2295
|
+
base_branch: resolvedBaseBranch,
|
|
2296
|
+
version_number: priorBaseline?.version_number ?? null,
|
|
2297
|
+
source: options.source
|
|
2298
|
+
};
|
|
2299
|
+
entry.last_synced_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
2300
|
+
entry.sync_source = options.source;
|
|
2301
|
+
}
|
|
2302
|
+
function getDescendants(stacks, branch) {
|
|
2303
|
+
const descendants = [];
|
|
2304
|
+
const childMap = /* @__PURE__ */ new Map();
|
|
2305
|
+
for (const stack of stacks) {
|
|
2306
|
+
for (const node of stack.branches) {
|
|
2307
|
+
if (!node.parent) continue;
|
|
2308
|
+
const children = childMap.get(node.parent) ?? [];
|
|
2309
|
+
children.push(node.name);
|
|
2310
|
+
childMap.set(node.parent, children);
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
const queue = [...childMap.get(branch) ?? []];
|
|
2314
|
+
while (queue.length > 0) {
|
|
2315
|
+
const next = queue.shift();
|
|
2316
|
+
if (!next) break;
|
|
2317
|
+
descendants.push(next);
|
|
2318
|
+
queue.push(...childMap.get(next) ?? []);
|
|
2319
|
+
}
|
|
2320
|
+
return descendants;
|
|
2321
|
+
}
|
|
2322
|
+
function removeBranchFromState(stacks, branch) {
|
|
2323
|
+
for (const stack of stacks) {
|
|
2324
|
+
const deleted = stack.branches.find((b) => b.name === branch);
|
|
2325
|
+
if (!deleted) continue;
|
|
2326
|
+
const newParent = deleted.parent;
|
|
2327
|
+
for (const child of stack.branches) {
|
|
2328
|
+
if (child.parent === branch) {
|
|
2329
|
+
child.parent = newParent;
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
stack.branches = stack.branches.filter((b) => b.name !== branch);
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
|
|
1014
2336
|
// src/commands/undo.ts
|
|
2337
|
+
init_errors();
|
|
2338
|
+
init_git();
|
|
2339
|
+
init_state();
|
|
2340
|
+
init_undo_log();
|
|
1015
2341
|
async function undo(cwd) {
|
|
1016
2342
|
const entry = await readUndoEntry(cwd);
|
|
1017
2343
|
if (!await isWorkingTreeClean(cwd)) {
|
|
@@ -1059,6 +2385,7 @@ async function undo(cwd) {
|
|
|
1059
2385
|
}
|
|
1060
2386
|
|
|
1061
2387
|
// src/index.ts
|
|
2388
|
+
init_errors();
|
|
1062
2389
|
var require2 = createRequire(import.meta.url);
|
|
1063
2390
|
var { version } = require2("../package.json");
|
|
1064
2391
|
var program = new Command();
|
|
@@ -1071,12 +2398,15 @@ Examples:
|
|
|
1071
2398
|
).action(async () => {
|
|
1072
2399
|
const result = await init(process.cwd());
|
|
1073
2400
|
if (result.status === "created") {
|
|
1074
|
-
console.log(
|
|
2401
|
+
console.log(chalk2.green("\u2714 DubStack initialized"));
|
|
1075
2402
|
} else {
|
|
1076
|
-
console.log(
|
|
2403
|
+
console.log(chalk2.yellow("\u26A0 DubStack already initialized"));
|
|
1077
2404
|
}
|
|
1078
2405
|
});
|
|
1079
|
-
program.command("create").argument("<branch-name>", "Name of the new branch to create").description("Create a new branch stacked on top of the current branch").option("-m, --message <message>", "Commit staged changes with this message").option("-a, --all", "Stage all changes before committing (requires -m)").
|
|
2406
|
+
program.command("create").argument("<branch-name>", "Name of the new branch to create").description("Create a new branch stacked on top of the current branch").option("-m, --message <message>", "Commit staged changes with this message").option("-a, --all", "Stage all changes before committing (requires -m)").option(
|
|
2407
|
+
"-u, --update",
|
|
2408
|
+
"Stage tracked file updates before committing (requires -m)"
|
|
2409
|
+
).option("-p, --patch", "Pick hunks to stage before committing (requires -m)").addHelpText(
|
|
1080
2410
|
"after",
|
|
1081
2411
|
`
|
|
1082
2412
|
Examples:
|
|
@@ -1087,33 +2417,97 @@ Examples:
|
|
|
1087
2417
|
async (branchName, options) => {
|
|
1088
2418
|
const result = await create(branchName, process.cwd(), {
|
|
1089
2419
|
message: options.message,
|
|
1090
|
-
all: options.all
|
|
2420
|
+
all: options.all,
|
|
2421
|
+
update: options.update,
|
|
2422
|
+
patch: options.patch
|
|
1091
2423
|
});
|
|
1092
2424
|
if (result.committed) {
|
|
1093
2425
|
console.log(
|
|
1094
|
-
|
|
2426
|
+
chalk2.green(
|
|
1095
2427
|
`\u2714 Created '${result.branch}' on '${result.parent}' \u2022 ${result.committed}`
|
|
1096
2428
|
)
|
|
1097
2429
|
);
|
|
1098
2430
|
} else {
|
|
1099
2431
|
console.log(
|
|
1100
|
-
|
|
2432
|
+
chalk2.green(
|
|
1101
2433
|
`\u2714 Created branch '${result.branch}' on top of '${result.parent}'`
|
|
1102
2434
|
)
|
|
1103
2435
|
);
|
|
1104
2436
|
}
|
|
1105
2437
|
}
|
|
1106
2438
|
);
|
|
1107
|
-
program.command("log").description("Display an ASCII tree of the current stack").addHelpText(
|
|
2439
|
+
program.command("log").alias("l").description("Display an ASCII tree of the current stack").addHelpText(
|
|
1108
2440
|
"after",
|
|
1109
2441
|
`
|
|
1110
2442
|
Examples:
|
|
1111
2443
|
$ dub log Show the branch tree with current branch highlighted`
|
|
1112
2444
|
).action(async () => {
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
2445
|
+
await printLog(process.cwd());
|
|
2446
|
+
});
|
|
2447
|
+
program.command("ls").description("Display an ASCII tree of the current stack").action(async () => {
|
|
2448
|
+
await printLog(process.cwd());
|
|
2449
|
+
});
|
|
2450
|
+
program.command("up").argument("[steps]", "Number of levels to traverse upstack").option("-n, --steps <count>", "Number of levels to traverse upstack").description("Checkout the child branch directly above the current branch").action(async (stepsArg, options) => {
|
|
2451
|
+
const steps = parseSteps(stepsArg, options.steps);
|
|
2452
|
+
const result = await upBySteps(process.cwd(), steps);
|
|
2453
|
+
if (result.changed) {
|
|
2454
|
+
console.log(chalk2.green(`\u2714 Switched up to '${result.branch}'`));
|
|
2455
|
+
} else {
|
|
2456
|
+
console.log(chalk2.yellow(`\u26A0 Already at top branch '${result.branch}'`));
|
|
2457
|
+
}
|
|
2458
|
+
});
|
|
2459
|
+
program.command("down").argument("[steps]", "Number of levels to traverse downstack").option("-n, --steps <count>", "Number of levels to traverse downstack").description("Checkout the parent branch directly below the current branch").action(async (stepsArg, options) => {
|
|
2460
|
+
const steps = parseSteps(stepsArg, options.steps);
|
|
2461
|
+
const result = await downBySteps(process.cwd(), steps);
|
|
2462
|
+
if (result.changed) {
|
|
2463
|
+
console.log(chalk2.green(`\u2714 Switched down to '${result.branch}'`));
|
|
2464
|
+
} else {
|
|
2465
|
+
console.log(
|
|
2466
|
+
chalk2.yellow(`\u26A0 Already at bottom branch '${result.branch}'`)
|
|
2467
|
+
);
|
|
2468
|
+
}
|
|
2469
|
+
});
|
|
2470
|
+
program.command("top").description("Checkout the topmost branch in the current stack path").action(async () => {
|
|
2471
|
+
const result = await top(process.cwd());
|
|
2472
|
+
if (result.changed) {
|
|
2473
|
+
console.log(chalk2.green(`\u2714 Switched to top branch '${result.branch}'`));
|
|
2474
|
+
} else {
|
|
2475
|
+
console.log(chalk2.yellow(`\u26A0 Already at top branch '${result.branch}'`));
|
|
2476
|
+
}
|
|
2477
|
+
});
|
|
2478
|
+
program.command("bottom").description(
|
|
2479
|
+
"Checkout the first branch above the root in the current stack path"
|
|
2480
|
+
).action(async () => {
|
|
2481
|
+
const result = await bottom(process.cwd());
|
|
2482
|
+
if (result.changed) {
|
|
2483
|
+
console.log(
|
|
2484
|
+
chalk2.green(`\u2714 Switched to bottom stack branch '${result.branch}'`)
|
|
2485
|
+
);
|
|
2486
|
+
} else {
|
|
2487
|
+
console.log(
|
|
2488
|
+
chalk2.yellow(`\u26A0 Already at bottom stack branch '${result.branch}'`)
|
|
2489
|
+
);
|
|
2490
|
+
}
|
|
2491
|
+
});
|
|
2492
|
+
program.command("branch").description("Show DubStack branch metadata").addCommand(
|
|
2493
|
+
new Command("info").description("Show tracked stack info for the current branch").argument("[branch]", "Branch to inspect (defaults to current branch)").action(async (branch) => {
|
|
2494
|
+
const info = await branchInfo(process.cwd(), branch);
|
|
2495
|
+
console.log(formatBranchInfo(info));
|
|
2496
|
+
})
|
|
2497
|
+
);
|
|
2498
|
+
program.command("info").argument("[branch]", "Branch to inspect (defaults to current branch)").description("Show tracked stack info for a branch").action(async (branch) => {
|
|
2499
|
+
const info = await branchInfo(process.cwd(), branch);
|
|
2500
|
+
console.log(formatBranchInfo(info));
|
|
1116
2501
|
});
|
|
2502
|
+
program.command("sync").description("Sync tracked branches with remote and reconcile divergence").option(
|
|
2503
|
+
"--restack",
|
|
2504
|
+
"Restack branches after sync (disable with --no-restack)",
|
|
2505
|
+
true
|
|
2506
|
+
).option("-f, --force", "Skip prompts for destructive sync decisions").option("-a, --all", "Sync all tracked stacks across trunks").option("--no-interactive", "Disable prompts and use deterministic behavior").action(
|
|
2507
|
+
async (options) => {
|
|
2508
|
+
await sync(process.cwd(), options);
|
|
2509
|
+
}
|
|
2510
|
+
);
|
|
1117
2511
|
program.command("restack").description("Rebase all branches in the stack onto their updated parents").option("--continue", "Continue restacking after resolving conflicts").addHelpText(
|
|
1118
2512
|
"after",
|
|
1119
2513
|
`
|
|
@@ -1123,22 +2517,22 @@ Examples:
|
|
|
1123
2517
|
).action(async (options) => {
|
|
1124
2518
|
const result = options.continue ? await restackContinue(process.cwd()) : await restack(process.cwd());
|
|
1125
2519
|
if (result.status === "up-to-date") {
|
|
1126
|
-
console.log(
|
|
2520
|
+
console.log(chalk2.green("\u2714 Stack is already up to date"));
|
|
1127
2521
|
} else if (result.status === "conflict") {
|
|
1128
2522
|
console.log(
|
|
1129
|
-
|
|
2523
|
+
chalk2.yellow(`\u26A0 Conflict while restacking '${result.conflictBranch}'`)
|
|
1130
2524
|
);
|
|
1131
2525
|
console.log(
|
|
1132
|
-
|
|
2526
|
+
chalk2.dim(
|
|
1133
2527
|
" Resolve conflicts, stage changes, then run: dub restack --continue"
|
|
1134
2528
|
)
|
|
1135
2529
|
);
|
|
1136
2530
|
} else {
|
|
1137
2531
|
console.log(
|
|
1138
|
-
|
|
2532
|
+
chalk2.green(`\u2714 Restacked ${result.rebased.length} branch(es)`)
|
|
1139
2533
|
);
|
|
1140
2534
|
for (const branch of result.rebased) {
|
|
1141
|
-
console.log(
|
|
2535
|
+
console.log(chalk2.dim(` \u21B3 ${branch}`));
|
|
1142
2536
|
}
|
|
1143
2537
|
}
|
|
1144
2538
|
});
|
|
@@ -1149,7 +2543,7 @@ Examples:
|
|
|
1149
2543
|
$ dub undo Roll back the last dub operation`
|
|
1150
2544
|
).action(async () => {
|
|
1151
2545
|
const result = await undo(process.cwd());
|
|
1152
|
-
console.log(
|
|
2546
|
+
console.log(chalk2.green(`\u2714 Undid '${result.undone}': ${result.details}`));
|
|
1153
2547
|
});
|
|
1154
2548
|
program.command("submit").description(
|
|
1155
2549
|
"Push branches and create/update GitHub PRs for the current stack"
|
|
@@ -1161,36 +2555,106 @@ Examples:
|
|
|
1161
2555
|
$ dub submit --dry-run Preview what would happen`
|
|
1162
2556
|
).action(runSubmit);
|
|
1163
2557
|
program.command("ss").description("Submit the current stack (alias for submit)").option("--dry-run", "Print what would happen without executing").action(runSubmit);
|
|
1164
|
-
program.command("co").argument("[branch]", "Branch to checkout (interactive if omitted)").
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
2558
|
+
program.command("checkout").alias("co").argument("[branch]", "Branch to checkout (interactive if omitted)").option("-t, --trunk", "Checkout the current trunk").option(
|
|
2559
|
+
"-u, --show-untracked",
|
|
2560
|
+
"Include untracked branches in interactive selection"
|
|
2561
|
+
).option(
|
|
2562
|
+
"-s, --stack",
|
|
2563
|
+
"Only show ancestors and descendants of current branch in interactive selection"
|
|
2564
|
+
).option(
|
|
2565
|
+
"-a, --all",
|
|
2566
|
+
"Show branches across all tracked stacks in interactive selection"
|
|
2567
|
+
).description("Checkout a branch (interactive picker if no name given)").action(
|
|
2568
|
+
async (branch, options) => {
|
|
2569
|
+
if (branch) {
|
|
2570
|
+
const result = await checkout(branch, process.cwd());
|
|
2571
|
+
console.log(chalk2.green(`\u2714 Switched to '${result.branch}'`));
|
|
2572
|
+
} else if (options.trunk) {
|
|
2573
|
+
const trunk = await resolveCheckoutTrunk(process.cwd());
|
|
2574
|
+
const result = await checkout(trunk, process.cwd());
|
|
2575
|
+
console.log(chalk2.green(`\u2714 Switched to '${result.branch}'`));
|
|
2576
|
+
} else {
|
|
2577
|
+
const result = await interactiveCheckout(process.cwd(), {
|
|
2578
|
+
showUntracked: options.showUntracked,
|
|
2579
|
+
stack: options.stack,
|
|
2580
|
+
all: options.all
|
|
2581
|
+
});
|
|
2582
|
+
if (result) {
|
|
2583
|
+
console.log(chalk2.green(`\u2714 Switched to '${result.branch}'`));
|
|
2584
|
+
}
|
|
1172
2585
|
}
|
|
1173
2586
|
}
|
|
2587
|
+
);
|
|
2588
|
+
program.command("skills").description("Manage DubStack agent skills").addCommand(
|
|
2589
|
+
new Command("add").description("Install agent skills (e.g. dubstack, dub-flow)").argument("[skills...]", "Names of skills to install (default: all)").option("-g, --global", "Install skills globally").option("--dry-run", "Preview actions without installing").action(async (skills, options) => {
|
|
2590
|
+
const { addSkills: addSkills2 } = await Promise.resolve().then(() => (init_skills2(), skills_exports));
|
|
2591
|
+
await addSkills2(skills, options);
|
|
2592
|
+
})
|
|
2593
|
+
).addCommand(
|
|
2594
|
+
new Command("remove").description("Remove agent skills").argument("[skills...]", "Names of skills to remove (default: all)").option("-g, --global", "Remove skills globally").option("--dry-run", "Preview actions without removing").action(async (skills, options) => {
|
|
2595
|
+
const { removeSkills: removeSkills2 } = await Promise.resolve().then(() => (init_skills2(), skills_exports));
|
|
2596
|
+
await removeSkills2(skills, options);
|
|
2597
|
+
})
|
|
2598
|
+
);
|
|
2599
|
+
program.command("modify").alias("m").description(
|
|
2600
|
+
"Modify the current branch by amending commits or creating new ones"
|
|
2601
|
+
).option("-a, --all", "Stage all changes before committing").option("-c, --commit", "Create a new commit instead of amending").option("-e, --edit", "Open editor to edit the commit message").option(
|
|
2602
|
+
"-m, --message <message>",
|
|
2603
|
+
"Message for the new or amended commit",
|
|
2604
|
+
(value, previous = []) => [...previous, value],
|
|
2605
|
+
[]
|
|
2606
|
+
).option("-p, --patch", "Pick hunks to stage before committing").option("-u, --update", "Stage all updates to tracked files").option(
|
|
2607
|
+
"-v, --verbose",
|
|
2608
|
+
"Show staged diff before modify (repeat for unstaged diff too)",
|
|
2609
|
+
(_value, previous = 0) => previous + 1,
|
|
2610
|
+
0
|
|
2611
|
+
).option(
|
|
2612
|
+
"--interactive-rebase",
|
|
2613
|
+
"Start an interactive rebase on the branch commits"
|
|
2614
|
+
).action(async (options) => {
|
|
2615
|
+
const { modify: modify2 } = await Promise.resolve().then(() => (init_modify(), modify_exports));
|
|
2616
|
+
const normalizedOptions = {
|
|
2617
|
+
...options,
|
|
2618
|
+
message: Array.isArray(options.message) && options.message.length === 1 ? options.message[0] : options.message
|
|
2619
|
+
};
|
|
2620
|
+
await modify2(process.cwd(), normalizedOptions);
|
|
2621
|
+
});
|
|
2622
|
+
program.command("pr").argument("[branch]", "Branch name or PR number to open").description("Open a branch PR in your browser").action(async (branch) => {
|
|
2623
|
+
await pr(process.cwd(), branch);
|
|
1174
2624
|
});
|
|
1175
2625
|
async function runSubmit(options) {
|
|
1176
2626
|
const result = await submit(process.cwd(), options.dryRun ?? false);
|
|
1177
2627
|
if (result.pushed.length > 0) {
|
|
1178
2628
|
console.log(
|
|
1179
|
-
|
|
2629
|
+
chalk2.green(
|
|
1180
2630
|
`\u2714 Pushed ${result.pushed.length} branch(es), created ${result.created.length} PR(s), updated ${result.updated.length} PR(s)`
|
|
1181
2631
|
)
|
|
1182
2632
|
);
|
|
1183
2633
|
for (const branch of [...result.created, ...result.updated]) {
|
|
1184
|
-
console.log(
|
|
2634
|
+
console.log(chalk2.dim(` \u21B3 ${branch}`));
|
|
1185
2635
|
}
|
|
1186
2636
|
}
|
|
1187
2637
|
}
|
|
2638
|
+
async function printLog(cwd) {
|
|
2639
|
+
const output2 = await log(cwd);
|
|
2640
|
+
const styled = output2.replace(/\*(.+?) \(Current\)\*/g, chalk2.bold.cyan("$1 (Current)")).replace(/⚠ \(missing\)/g, chalk2.yellow("\u26A0 (missing)"));
|
|
2641
|
+
console.log(styled);
|
|
2642
|
+
}
|
|
2643
|
+
function parseSteps(positional, option) {
|
|
2644
|
+
const raw = option ?? positional;
|
|
2645
|
+
if (!raw) return 1;
|
|
2646
|
+
const parsed = Number.parseInt(raw, 10);
|
|
2647
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
2648
|
+
throw new DubError("Steps must be a positive integer.");
|
|
2649
|
+
}
|
|
2650
|
+
return parsed;
|
|
2651
|
+
}
|
|
1188
2652
|
async function main() {
|
|
1189
2653
|
try {
|
|
1190
2654
|
await program.parseAsync(process.argv);
|
|
1191
2655
|
} catch (error) {
|
|
1192
2656
|
if (error instanceof DubError) {
|
|
1193
|
-
console.error(
|
|
2657
|
+
console.error(chalk2.red(`\u2716 ${error.message}`));
|
|
1194
2658
|
process.exit(1);
|
|
1195
2659
|
}
|
|
1196
2660
|
throw error;
|