dubstack 0.5.0 → 0.6.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 +355 -204
- package/dist/index.js +956 -114
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -100,6 +100,20 @@ async function deleteBranch(name, cwd) {
|
|
|
100
100
|
throw new DubError(`Failed to delete branch '${name}'. It may not exist.`);
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
|
+
async function deleteLocalBranch(name, cwd, force = false) {
|
|
104
|
+
try {
|
|
105
|
+
await execa("git", ["branch", force ? "-D" : "-d", name], { cwd });
|
|
106
|
+
} catch {
|
|
107
|
+
if (force) {
|
|
108
|
+
throw new DubError(
|
|
109
|
+
`Failed to delete branch '${name}'. It may not exist or be checked out.`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
throw new DubError(
|
|
113
|
+
`Branch '${name}' is not fully merged. Re-run with --force to delete it.`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
103
117
|
async function forceBranchTo(name, sha, cwd) {
|
|
104
118
|
try {
|
|
105
119
|
const current = await getCurrentBranch(cwd).catch(() => null);
|
|
@@ -139,6 +153,13 @@ async function rebaseContinue(cwd) {
|
|
|
139
153
|
);
|
|
140
154
|
}
|
|
141
155
|
}
|
|
156
|
+
async function rebaseAbort(cwd) {
|
|
157
|
+
try {
|
|
158
|
+
await execa("git", ["rebase", "--abort"], { cwd });
|
|
159
|
+
} catch {
|
|
160
|
+
throw new DubError("Failed to abort rebase.");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
142
163
|
async function getMergeBase(a, b, cwd) {
|
|
143
164
|
try {
|
|
144
165
|
const { stdout } = await execa("git", ["merge-base", a, b], { cwd });
|
|
@@ -294,9 +315,9 @@ async function fetchBranches(branches, cwd, remote = "origin") {
|
|
|
294
315
|
} catch (error) {
|
|
295
316
|
const stderr = typeof error?.stderr === "string" ? error.stderr : "";
|
|
296
317
|
const stdout = typeof error?.stdout === "string" ? error.stdout : "";
|
|
297
|
-
const
|
|
318
|
+
const output5 = `${stderr}
|
|
298
319
|
${stdout}`;
|
|
299
|
-
if (
|
|
320
|
+
if (output5.includes("couldn't find remote ref")) {
|
|
300
321
|
continue;
|
|
301
322
|
}
|
|
302
323
|
throw new DubError(`Failed to fetch branches from '${remote}'.`);
|
|
@@ -461,25 +482,25 @@ function getParent(state, branchName) {
|
|
|
461
482
|
const branch = stack.branches.find((b) => b.name === branchName);
|
|
462
483
|
return branch?.parent ?? void 0;
|
|
463
484
|
}
|
|
464
|
-
function addBranchToStack(state, child,
|
|
485
|
+
function addBranchToStack(state, child, parent2) {
|
|
465
486
|
if (findStackForBranch(state, child)) {
|
|
466
487
|
throw new DubError(`Branch '${child}' is already tracked in a stack.`);
|
|
467
488
|
}
|
|
468
489
|
const childBranch = {
|
|
469
490
|
name: child,
|
|
470
|
-
parent,
|
|
491
|
+
parent: parent2,
|
|
471
492
|
pr_number: null,
|
|
472
493
|
pr_link: null,
|
|
473
494
|
last_submitted_version: null,
|
|
474
495
|
last_synced_at: null,
|
|
475
496
|
sync_source: null
|
|
476
497
|
};
|
|
477
|
-
const existingStack = findStackForBranch(state,
|
|
498
|
+
const existingStack = findStackForBranch(state, parent2);
|
|
478
499
|
if (existingStack) {
|
|
479
500
|
existingStack.branches.push(childBranch);
|
|
480
501
|
} else {
|
|
481
502
|
const rootBranch = {
|
|
482
|
-
name:
|
|
503
|
+
name: parent2,
|
|
483
504
|
type: "root",
|
|
484
505
|
parent: null,
|
|
485
506
|
pr_number: null,
|
|
@@ -517,9 +538,9 @@ function topologicalOrder(stack) {
|
|
|
517
538
|
const childMap = /* @__PURE__ */ new Map();
|
|
518
539
|
for (const branch of stack.branches) {
|
|
519
540
|
if (branch.parent) {
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
childMap.set(branch.parent,
|
|
541
|
+
const children2 = childMap.get(branch.parent) ?? [];
|
|
542
|
+
children2.push(branch);
|
|
543
|
+
childMap.set(branch.parent, children2);
|
|
523
544
|
}
|
|
524
545
|
}
|
|
525
546
|
const queue = [root];
|
|
@@ -527,8 +548,8 @@ function topologicalOrder(stack) {
|
|
|
527
548
|
const current = queue.shift();
|
|
528
549
|
if (!current) break;
|
|
529
550
|
result.push(current);
|
|
530
|
-
const
|
|
531
|
-
queue.push(...
|
|
551
|
+
const children2 = childMap.get(current.name) ?? [];
|
|
552
|
+
queue.push(...children2);
|
|
532
553
|
}
|
|
533
554
|
return result;
|
|
534
555
|
}
|
|
@@ -541,29 +562,29 @@ var init_state = __esm({
|
|
|
541
562
|
});
|
|
542
563
|
|
|
543
564
|
// src/lib/undo-log.ts
|
|
544
|
-
import * as
|
|
545
|
-
import * as
|
|
565
|
+
import * as fs3 from "fs";
|
|
566
|
+
import * as path3 from "path";
|
|
546
567
|
async function getUndoPath(cwd) {
|
|
547
568
|
const dubDir = await getDubDir(cwd);
|
|
548
|
-
return
|
|
569
|
+
return path3.join(dubDir, "undo.json");
|
|
549
570
|
}
|
|
550
571
|
async function saveUndoEntry(entry, cwd) {
|
|
551
572
|
const undoPath = await getUndoPath(cwd);
|
|
552
|
-
|
|
573
|
+
fs3.writeFileSync(undoPath, `${JSON.stringify(entry, null, 2)}
|
|
553
574
|
`);
|
|
554
575
|
}
|
|
555
576
|
async function readUndoEntry(cwd) {
|
|
556
577
|
const undoPath = await getUndoPath(cwd);
|
|
557
|
-
if (!
|
|
578
|
+
if (!fs3.existsSync(undoPath)) {
|
|
558
579
|
throw new DubError("Nothing to undo.");
|
|
559
580
|
}
|
|
560
|
-
const raw =
|
|
581
|
+
const raw = fs3.readFileSync(undoPath, "utf-8");
|
|
561
582
|
return JSON.parse(raw);
|
|
562
583
|
}
|
|
563
584
|
async function clearUndoEntry(cwd) {
|
|
564
585
|
const undoPath = await getUndoPath(cwd);
|
|
565
|
-
if (
|
|
566
|
-
|
|
586
|
+
if (fs3.existsSync(undoPath)) {
|
|
587
|
+
fs3.unlinkSync(undoPath);
|
|
567
588
|
}
|
|
568
589
|
}
|
|
569
590
|
var init_undo_log = __esm({
|
|
@@ -818,14 +839,14 @@ async function modify(cwd, options) {
|
|
|
818
839
|
const currentBranch = await getCurrentBranch(cwd);
|
|
819
840
|
const state = await readState(cwd);
|
|
820
841
|
if (options.interactiveRebase) {
|
|
821
|
-
const
|
|
822
|
-
if (!
|
|
842
|
+
const parent2 = getParent(state, currentBranch);
|
|
843
|
+
if (!parent2) {
|
|
823
844
|
throw new DubError(
|
|
824
845
|
`Could not determine parent branch for '${currentBranch}'. Cannot start interactive rebase.`
|
|
825
846
|
);
|
|
826
847
|
}
|
|
827
|
-
const parentTip = await getBranchTip(
|
|
828
|
-
console.log(`Starting interactive rebase on top of '${
|
|
848
|
+
const parentTip = await getBranchTip(parent2, cwd);
|
|
849
|
+
console.log(`Starting interactive rebase on top of '${parent2}'...`);
|
|
829
850
|
await interactiveRebase(parentTip, cwd);
|
|
830
851
|
await restackChildren(cwd);
|
|
831
852
|
return;
|
|
@@ -898,6 +919,55 @@ import { createRequire } from "module";
|
|
|
898
919
|
import chalk2 from "chalk";
|
|
899
920
|
import { Command } from "commander";
|
|
900
921
|
|
|
922
|
+
// src/commands/abort.ts
|
|
923
|
+
init_errors();
|
|
924
|
+
init_git();
|
|
925
|
+
|
|
926
|
+
// src/lib/operation-state.ts
|
|
927
|
+
init_git();
|
|
928
|
+
init_state();
|
|
929
|
+
import * as fs2 from "fs";
|
|
930
|
+
import * as path2 from "path";
|
|
931
|
+
async function getRestackProgressPath(cwd) {
|
|
932
|
+
const dubDir = await getDubDir(cwd);
|
|
933
|
+
return path2.join(dubDir, "restack-progress.json");
|
|
934
|
+
}
|
|
935
|
+
async function hasRestackProgress(cwd) {
|
|
936
|
+
const progressPath = await getRestackProgressPath(cwd);
|
|
937
|
+
return fs2.existsSync(progressPath);
|
|
938
|
+
}
|
|
939
|
+
async function hasGitRebaseInProgress(cwd) {
|
|
940
|
+
const root = await getRepoRoot(cwd);
|
|
941
|
+
const rebaseMerge = path2.join(root, ".git", "rebase-merge");
|
|
942
|
+
const rebaseApply = path2.join(root, ".git", "rebase-apply");
|
|
943
|
+
return fs2.existsSync(rebaseMerge) || fs2.existsSync(rebaseApply);
|
|
944
|
+
}
|
|
945
|
+
async function detectActiveOperation(cwd) {
|
|
946
|
+
if (await hasRestackProgress(cwd)) return "restack";
|
|
947
|
+
if (await hasGitRebaseInProgress(cwd)) return "rebase";
|
|
948
|
+
return "none";
|
|
949
|
+
}
|
|
950
|
+
async function clearRestackProgress(cwd) {
|
|
951
|
+
const progressPath = await getRestackProgressPath(cwd);
|
|
952
|
+
if (!fs2.existsSync(progressPath)) return;
|
|
953
|
+
fs2.unlinkSync(progressPath);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// src/commands/abort.ts
|
|
957
|
+
async function abortCommand(cwd) {
|
|
958
|
+
const active = await detectActiveOperation(cwd);
|
|
959
|
+
if (active === "none") {
|
|
960
|
+
throw new DubError("No operation in progress. Nothing to abort.");
|
|
961
|
+
}
|
|
962
|
+
if (await hasGitRebaseInProgress(cwd)) {
|
|
963
|
+
await rebaseAbort(cwd);
|
|
964
|
+
}
|
|
965
|
+
if (active === "restack") {
|
|
966
|
+
await clearRestackProgress(cwd);
|
|
967
|
+
}
|
|
968
|
+
return { aborted: active };
|
|
969
|
+
}
|
|
970
|
+
|
|
901
971
|
// src/commands/branch.ts
|
|
902
972
|
init_git();
|
|
903
973
|
init_state();
|
|
@@ -1045,6 +1115,42 @@ async function interactiveCheckout(cwd, options = {}) {
|
|
|
1045
1115
|
}
|
|
1046
1116
|
}
|
|
1047
1117
|
|
|
1118
|
+
// src/commands/children.ts
|
|
1119
|
+
init_errors();
|
|
1120
|
+
init_git();
|
|
1121
|
+
init_state();
|
|
1122
|
+
async function children(cwd, branchArg) {
|
|
1123
|
+
const branch = branchArg ?? await getCurrentBranch(cwd);
|
|
1124
|
+
const state = await readState(cwd);
|
|
1125
|
+
const stack = findStackForBranch(state, branch);
|
|
1126
|
+
if (!stack) {
|
|
1127
|
+
throw new DubError(
|
|
1128
|
+
`Branch '${branch}' is not tracked. Run 'dub track ${branch} --parent <branch>' first.`
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
const childBranches = stack.branches.filter((entry) => entry.parent === branch).map((entry) => entry.name).sort();
|
|
1132
|
+
return { branch, children: childBranches };
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// src/commands/continue.ts
|
|
1136
|
+
init_errors();
|
|
1137
|
+
init_git();
|
|
1138
|
+
init_restack();
|
|
1139
|
+
async function continueCommand(cwd) {
|
|
1140
|
+
const active = await detectActiveOperation(cwd);
|
|
1141
|
+
if (active === "none") {
|
|
1142
|
+
throw new DubError(
|
|
1143
|
+
"No operation in progress. Start a restack or resolve a rebase first."
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
if (active === "restack") {
|
|
1147
|
+
const restackResult = await restackContinue(cwd);
|
|
1148
|
+
return { continued: "restack", restackResult };
|
|
1149
|
+
}
|
|
1150
|
+
await rebaseContinue(cwd);
|
|
1151
|
+
return { continued: "rebase" };
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1048
1154
|
// src/commands/create.ts
|
|
1049
1155
|
init_errors();
|
|
1050
1156
|
init_git();
|
|
@@ -1057,7 +1163,7 @@ async function create(name, cwd, options) {
|
|
|
1057
1163
|
);
|
|
1058
1164
|
}
|
|
1059
1165
|
const state = await ensureState(cwd);
|
|
1060
|
-
const
|
|
1166
|
+
const parent2 = await getCurrentBranch(cwd);
|
|
1061
1167
|
if (await branchExists(name, cwd)) {
|
|
1062
1168
|
throw new DubError(`Branch '${name}' already exists.`);
|
|
1063
1169
|
}
|
|
@@ -1078,7 +1184,7 @@ async function create(name, cwd, options) {
|
|
|
1078
1184
|
{
|
|
1079
1185
|
operation: "create",
|
|
1080
1186
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1081
|
-
previousBranch:
|
|
1187
|
+
previousBranch: parent2,
|
|
1082
1188
|
previousState: structuredClone(state),
|
|
1083
1189
|
branchTips: {},
|
|
1084
1190
|
createdBranches: [name]
|
|
@@ -1086,7 +1192,7 @@ async function create(name, cwd, options) {
|
|
|
1086
1192
|
cwd
|
|
1087
1193
|
);
|
|
1088
1194
|
await createBranch(name, cwd);
|
|
1089
|
-
addBranchToStack(state, name,
|
|
1195
|
+
addBranchToStack(state, name, parent2);
|
|
1090
1196
|
await writeState(state, cwd);
|
|
1091
1197
|
if (options?.message) {
|
|
1092
1198
|
try {
|
|
@@ -1097,17 +1203,267 @@ async function create(name, cwd, options) {
|
|
|
1097
1203
|
`Branch '${name}' was created but commit failed: ${reason}. Run 'dub undo' to clean up.`
|
|
1098
1204
|
);
|
|
1099
1205
|
}
|
|
1100
|
-
return { branch: name, parent, committed: options.message };
|
|
1206
|
+
return { branch: name, parent: parent2, committed: options.message };
|
|
1207
|
+
}
|
|
1208
|
+
return { branch: name, parent: parent2 };
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// src/commands/delete.ts
|
|
1212
|
+
import { stdin as input, stdout as output } from "process";
|
|
1213
|
+
import * as readline from "readline/promises";
|
|
1214
|
+
|
|
1215
|
+
// src/lib/delete.ts
|
|
1216
|
+
init_errors();
|
|
1217
|
+
init_git();
|
|
1218
|
+
|
|
1219
|
+
// src/lib/graph.ts
|
|
1220
|
+
init_errors();
|
|
1221
|
+
function buildChildMap(stack) {
|
|
1222
|
+
const childMap = /* @__PURE__ */ new Map();
|
|
1223
|
+
for (const branch of stack.branches) {
|
|
1224
|
+
if (!branch.parent) continue;
|
|
1225
|
+
const children2 = childMap.get(branch.parent) ?? [];
|
|
1226
|
+
children2.push(branch);
|
|
1227
|
+
childMap.set(branch.parent, children2);
|
|
1228
|
+
}
|
|
1229
|
+
return childMap;
|
|
1230
|
+
}
|
|
1231
|
+
function buildBranchMap(stack) {
|
|
1232
|
+
return new Map(stack.branches.map((branch) => [branch.name, branch]));
|
|
1233
|
+
}
|
|
1234
|
+
function getDescendants(stack, branchName) {
|
|
1235
|
+
const childMap = buildChildMap(stack);
|
|
1236
|
+
const descendants = [];
|
|
1237
|
+
const queue = [...childMap.get(branchName) ?? []];
|
|
1238
|
+
while (queue.length > 0) {
|
|
1239
|
+
const next = queue.shift();
|
|
1240
|
+
if (!next) break;
|
|
1241
|
+
descendants.push(next.name);
|
|
1242
|
+
queue.push(...childMap.get(next.name) ?? []);
|
|
1243
|
+
}
|
|
1244
|
+
return descendants;
|
|
1245
|
+
}
|
|
1246
|
+
function getAncestors(stack, branchName) {
|
|
1247
|
+
const branchMap = buildBranchMap(stack);
|
|
1248
|
+
const ancestors = [];
|
|
1249
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1250
|
+
let current = branchMap.get(branchName);
|
|
1251
|
+
while (current?.parent) {
|
|
1252
|
+
if (seen.has(current.parent)) break;
|
|
1253
|
+
ancestors.push(current.parent);
|
|
1254
|
+
seen.add(current.parent);
|
|
1255
|
+
current = branchMap.get(current.parent);
|
|
1256
|
+
}
|
|
1257
|
+
return ancestors;
|
|
1258
|
+
}
|
|
1259
|
+
function assertAcyclic(stack) {
|
|
1260
|
+
const branchMap = buildBranchMap(stack);
|
|
1261
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
1262
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1263
|
+
function visit(name) {
|
|
1264
|
+
if (visited.has(name)) return;
|
|
1265
|
+
if (visiting.has(name)) {
|
|
1266
|
+
throw new DubError(
|
|
1267
|
+
`Invalid stack '${stack.id}': cycle detected at '${name}'.`
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
visiting.add(name);
|
|
1271
|
+
const branch = branchMap.get(name);
|
|
1272
|
+
if (branch?.parent && branchMap.has(branch.parent)) {
|
|
1273
|
+
visit(branch.parent);
|
|
1274
|
+
}
|
|
1275
|
+
visiting.delete(name);
|
|
1276
|
+
visited.add(name);
|
|
1277
|
+
}
|
|
1278
|
+
for (const branch of stack.branches) {
|
|
1279
|
+
visit(branch.name);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// src/lib/invariants.ts
|
|
1284
|
+
init_errors();
|
|
1285
|
+
function assertStateInvariants(stacks) {
|
|
1286
|
+
for (const stack of stacks) {
|
|
1287
|
+
assertAcyclic(stack);
|
|
1288
|
+
const branchMap = new Map(
|
|
1289
|
+
stack.branches.map((branch) => [branch.name, branch])
|
|
1290
|
+
);
|
|
1291
|
+
for (const branch of stack.branches) {
|
|
1292
|
+
if (branch.type === "root") {
|
|
1293
|
+
if (branch.parent !== null) {
|
|
1294
|
+
throw new DubError(
|
|
1295
|
+
`Invalid stack '${stack.id}': root '${branch.name}' must have no parent.`
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
continue;
|
|
1299
|
+
}
|
|
1300
|
+
if (!branch.parent || !branchMap.has(branch.parent)) {
|
|
1301
|
+
throw new DubError(
|
|
1302
|
+
`Invalid stack '${stack.id}': branch '${branch.name}' has missing parent '${branch.parent ?? "null"}'.`
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1101
1306
|
}
|
|
1102
|
-
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
// src/lib/delete.ts
|
|
1310
|
+
init_state();
|
|
1311
|
+
async function getDeletePreview(cwd, options) {
|
|
1312
|
+
const state = await readState(cwd);
|
|
1313
|
+
const stack = findStackForBranch(state, options.branch);
|
|
1314
|
+
if (!stack) {
|
|
1315
|
+
throw new DubError(
|
|
1316
|
+
`Branch '${options.branch}' is not tracked. Run 'dub track ${options.branch} --parent <branch>' first.`
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
const targets = collectTargets(stack, options);
|
|
1320
|
+
return { branch: options.branch, targets };
|
|
1321
|
+
}
|
|
1322
|
+
async function deleteTrackedBranch(cwd, options) {
|
|
1323
|
+
const state = await readState(cwd);
|
|
1324
|
+
const stack = findStackForBranch(state, options.branch);
|
|
1325
|
+
if (!stack) {
|
|
1326
|
+
throw new DubError(
|
|
1327
|
+
`Branch '${options.branch}' is not tracked by DubStack.`
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1330
|
+
const targets = collectTargets(stack, options);
|
|
1331
|
+
const deleteSet = new Set(targets);
|
|
1332
|
+
const currentBranch = await getCurrentBranch(cwd);
|
|
1333
|
+
if (deleteSet.has(currentBranch)) {
|
|
1334
|
+
const fallback = resolveFallbackBranch(stack, options.branch, deleteSet);
|
|
1335
|
+
await checkoutBranch(fallback, cwd);
|
|
1336
|
+
}
|
|
1337
|
+
for (const branch of targets) {
|
|
1338
|
+
await deleteLocalBranch(branch, cwd, options.force ?? false);
|
|
1339
|
+
}
|
|
1340
|
+
const deletedParent = /* @__PURE__ */ new Map();
|
|
1341
|
+
for (const branch of stack.branches) {
|
|
1342
|
+
if (deleteSet.has(branch.name)) {
|
|
1343
|
+
deletedParent.set(branch.name, branch.parent);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
stack.branches = stack.branches.filter(
|
|
1347
|
+
(branch) => !deleteSet.has(branch.name)
|
|
1348
|
+
);
|
|
1349
|
+
const reparented = [];
|
|
1350
|
+
for (const branch of stack.branches) {
|
|
1351
|
+
let parent2 = branch.parent;
|
|
1352
|
+
while (parent2 && deleteSet.has(parent2)) {
|
|
1353
|
+
parent2 = deletedParent.get(parent2) ?? null;
|
|
1354
|
+
}
|
|
1355
|
+
if (parent2 !== branch.parent) {
|
|
1356
|
+
branch.parent = parent2;
|
|
1357
|
+
reparented.push({ branch: branch.name, parent: branch.parent });
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
state.stacks = state.stacks.filter(
|
|
1361
|
+
(candidate) => candidate.branches.length > 0
|
|
1362
|
+
);
|
|
1363
|
+
assertStateInvariants(state.stacks);
|
|
1364
|
+
await writeState(state, cwd);
|
|
1365
|
+
return {
|
|
1366
|
+
deleted: targets,
|
|
1367
|
+
reparented
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
function collectTargets(stack, options) {
|
|
1371
|
+
const target = stack.branches.find(
|
|
1372
|
+
(branch) => branch.name === options.branch
|
|
1373
|
+
);
|
|
1374
|
+
if (!target) {
|
|
1375
|
+
throw new DubError(
|
|
1376
|
+
`Branch '${options.branch}' is missing from tracked stack.`
|
|
1377
|
+
);
|
|
1378
|
+
}
|
|
1379
|
+
if (target.type === "root") {
|
|
1380
|
+
throw new DubError(
|
|
1381
|
+
`Cannot delete root branch '${options.branch}' via dub delete.`
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
const stackBranchMap = new Map(
|
|
1385
|
+
stack.branches.map((branch) => [branch.name, branch])
|
|
1386
|
+
);
|
|
1387
|
+
const targets = /* @__PURE__ */ new Set([options.branch]);
|
|
1388
|
+
if (options.upstack) {
|
|
1389
|
+
for (const descendant of getDescendants(stack, options.branch)) {
|
|
1390
|
+
targets.add(descendant);
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
if (options.downstack) {
|
|
1394
|
+
for (const ancestor of getAncestors(stack, options.branch)) {
|
|
1395
|
+
if (stackBranchMap.get(ancestor)?.type === "root") continue;
|
|
1396
|
+
targets.add(ancestor);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
return [...targets].sort(
|
|
1400
|
+
(a, b) => getAncestors(stack, b).length - getAncestors(stack, a).length
|
|
1401
|
+
);
|
|
1402
|
+
}
|
|
1403
|
+
function resolveFallbackBranch(stack, targetBranch, deleteSet) {
|
|
1404
|
+
const ancestors = getAncestors(stack, targetBranch);
|
|
1405
|
+
for (const ancestor of ancestors) {
|
|
1406
|
+
if (!deleteSet.has(ancestor)) return ancestor;
|
|
1407
|
+
}
|
|
1408
|
+
const root = stack.branches.find((branch) => branch.type === "root")?.name;
|
|
1409
|
+
if (root && !deleteSet.has(root)) return root;
|
|
1410
|
+
throw new DubError(
|
|
1411
|
+
"Unable to determine a safe checkout target before deleting current branch."
|
|
1412
|
+
);
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
// src/commands/delete.ts
|
|
1416
|
+
init_errors();
|
|
1417
|
+
init_git();
|
|
1418
|
+
function isInteractiveShell() {
|
|
1419
|
+
return Boolean(process.stdout.isTTY && process.stdin.isTTY);
|
|
1420
|
+
}
|
|
1421
|
+
async function confirmDelete(targets) {
|
|
1422
|
+
const rl = readline.createInterface({ input, output });
|
|
1423
|
+
try {
|
|
1424
|
+
const answer = await rl.question(
|
|
1425
|
+
`Delete ${targets.length} branch(es): ${targets.join(", ")}? [y/N] `
|
|
1426
|
+
);
|
|
1427
|
+
const normalized = answer.trim().toLowerCase();
|
|
1428
|
+
return normalized === "y" || normalized === "yes";
|
|
1429
|
+
} finally {
|
|
1430
|
+
rl.close();
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
async function deleteCommand(cwd, branchArg, options = {}) {
|
|
1434
|
+
const branch = branchArg ?? await getCurrentBranch(cwd);
|
|
1435
|
+
const interactive = options.interactive ?? isInteractiveShell();
|
|
1436
|
+
const preview = await getDeletePreview(cwd, {
|
|
1437
|
+
branch,
|
|
1438
|
+
upstack: options.upstack,
|
|
1439
|
+
downstack: options.downstack
|
|
1440
|
+
});
|
|
1441
|
+
if (!options.force && !options.quiet) {
|
|
1442
|
+
if (!interactive) {
|
|
1443
|
+
throw new DubError(
|
|
1444
|
+
"Delete requires confirmation. Re-run with --force or interactively."
|
|
1445
|
+
);
|
|
1446
|
+
}
|
|
1447
|
+
const confirmed = await confirmDelete(preview.targets);
|
|
1448
|
+
if (!confirmed) {
|
|
1449
|
+
return { deleted: [], reparented: [], cancelled: true };
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
const result = await deleteTrackedBranch(cwd, {
|
|
1453
|
+
branch,
|
|
1454
|
+
upstack: options.upstack ?? false,
|
|
1455
|
+
downstack: options.downstack ?? false,
|
|
1456
|
+
force: options.force ?? false
|
|
1457
|
+
});
|
|
1458
|
+
return { ...result, cancelled: false };
|
|
1103
1459
|
}
|
|
1104
1460
|
|
|
1105
1461
|
// src/commands/init.ts
|
|
1106
1462
|
init_errors();
|
|
1107
1463
|
init_git();
|
|
1108
1464
|
init_state();
|
|
1109
|
-
import * as
|
|
1110
|
-
import * as
|
|
1465
|
+
import * as fs5 from "fs";
|
|
1466
|
+
import * as path5 from "path";
|
|
1111
1467
|
async function init(cwd) {
|
|
1112
1468
|
if (!await isGitRepo(cwd)) {
|
|
1113
1469
|
throw new DubError(
|
|
@@ -1116,20 +1472,20 @@ async function init(cwd) {
|
|
|
1116
1472
|
}
|
|
1117
1473
|
const status = await initState(cwd);
|
|
1118
1474
|
const repoRoot = await getRepoRoot(cwd);
|
|
1119
|
-
const gitignorePath =
|
|
1475
|
+
const gitignorePath = path5.join(repoRoot, ".gitignore");
|
|
1120
1476
|
const entry = ".git/dubstack";
|
|
1121
1477
|
let gitignoreUpdated = false;
|
|
1122
|
-
if (
|
|
1123
|
-
const content =
|
|
1478
|
+
if (fs5.existsSync(gitignorePath)) {
|
|
1479
|
+
const content = fs5.readFileSync(gitignorePath, "utf-8");
|
|
1124
1480
|
const lines = content.split("\n");
|
|
1125
1481
|
if (!lines.some((line) => line.trim() === entry)) {
|
|
1126
1482
|
const separator = content.endsWith("\n") ? "" : "\n";
|
|
1127
|
-
|
|
1483
|
+
fs5.writeFileSync(gitignorePath, `${content}${separator}${entry}
|
|
1128
1484
|
`);
|
|
1129
1485
|
gitignoreUpdated = true;
|
|
1130
1486
|
}
|
|
1131
1487
|
} else {
|
|
1132
|
-
|
|
1488
|
+
fs5.writeFileSync(gitignorePath, `${entry}
|
|
1133
1489
|
`);
|
|
1134
1490
|
gitignoreUpdated = true;
|
|
1135
1491
|
}
|
|
@@ -1137,9 +1493,10 @@ async function init(cwd) {
|
|
|
1137
1493
|
}
|
|
1138
1494
|
|
|
1139
1495
|
// src/commands/log.ts
|
|
1496
|
+
init_errors();
|
|
1140
1497
|
init_git();
|
|
1141
1498
|
init_state();
|
|
1142
|
-
async function log(cwd) {
|
|
1499
|
+
async function log(cwd, options = {}) {
|
|
1143
1500
|
const state = await readState(cwd);
|
|
1144
1501
|
if (state.stacks.length === 0) {
|
|
1145
1502
|
return "No stacks. Run 'dub create' to start.";
|
|
@@ -1149,29 +1506,57 @@ async function log(cwd) {
|
|
|
1149
1506
|
currentBranch = await getCurrentBranch(cwd);
|
|
1150
1507
|
} catch {
|
|
1151
1508
|
}
|
|
1509
|
+
let stacksToRender = state.stacks;
|
|
1510
|
+
if (options.stack && !options.all) {
|
|
1511
|
+
if (!currentBranch) {
|
|
1512
|
+
throw new DubError(
|
|
1513
|
+
"Cannot determine current branch for --stack mode. Checkout a branch first."
|
|
1514
|
+
);
|
|
1515
|
+
}
|
|
1516
|
+
const currentStack = findStackForBranch(state, currentBranch);
|
|
1517
|
+
if (!currentStack) {
|
|
1518
|
+
throw new DubError(
|
|
1519
|
+
`Current branch '${currentBranch}' is not tracked. Run 'dub track ${currentBranch} --parent <branch>' first.`
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1522
|
+
stacksToRender = [currentStack];
|
|
1523
|
+
}
|
|
1524
|
+
if (options.reverse) {
|
|
1525
|
+
stacksToRender = [...stacksToRender].reverse();
|
|
1526
|
+
}
|
|
1152
1527
|
const sections = [];
|
|
1153
|
-
for (const stack of
|
|
1154
|
-
const tree = await renderStack(stack, currentBranch, cwd);
|
|
1528
|
+
for (const stack of stacksToRender) {
|
|
1529
|
+
const tree = await renderStack(stack, currentBranch, cwd, options);
|
|
1155
1530
|
sections.push(tree);
|
|
1156
1531
|
}
|
|
1157
1532
|
return sections.join("\n\n");
|
|
1158
1533
|
}
|
|
1159
|
-
async function renderStack(stack, currentBranch, cwd) {
|
|
1534
|
+
async function renderStack(stack, currentBranch, cwd, options) {
|
|
1160
1535
|
const root = stack.branches.find((b) => b.type === "root");
|
|
1161
1536
|
if (!root) return "";
|
|
1162
1537
|
const childMap = /* @__PURE__ */ new Map();
|
|
1163
1538
|
for (const branch of stack.branches) {
|
|
1164
1539
|
if (branch.parent) {
|
|
1165
|
-
const
|
|
1166
|
-
|
|
1167
|
-
childMap.set(branch.parent,
|
|
1540
|
+
const children2 = childMap.get(branch.parent) ?? [];
|
|
1541
|
+
children2.push(branch);
|
|
1542
|
+
childMap.set(branch.parent, children2);
|
|
1168
1543
|
}
|
|
1169
1544
|
}
|
|
1170
1545
|
const lines = [];
|
|
1171
|
-
await renderNode(
|
|
1546
|
+
await renderNode(
|
|
1547
|
+
root,
|
|
1548
|
+
currentBranch,
|
|
1549
|
+
childMap,
|
|
1550
|
+
"",
|
|
1551
|
+
true,
|
|
1552
|
+
true,
|
|
1553
|
+
lines,
|
|
1554
|
+
cwd,
|
|
1555
|
+
options
|
|
1556
|
+
);
|
|
1172
1557
|
return lines.join("\n");
|
|
1173
1558
|
}
|
|
1174
|
-
async function renderNode(branch, currentBranch, childMap, prefix, isRoot, isLast, lines, cwd) {
|
|
1559
|
+
async function renderNode(branch, currentBranch, childMap, prefix, isRoot, isLast, lines, cwd, options) {
|
|
1175
1560
|
let label;
|
|
1176
1561
|
const exists = await branchExists(branch.name, cwd);
|
|
1177
1562
|
if (isRoot) {
|
|
@@ -1189,19 +1574,20 @@ async function renderNode(branch, currentBranch, childMap, prefix, isRoot, isLas
|
|
|
1189
1574
|
const connector = isLast ? "\u2514\u2500 " : "\u251C\u2500 ";
|
|
1190
1575
|
lines.push(`${prefix}${connector}${label}`);
|
|
1191
1576
|
}
|
|
1192
|
-
const
|
|
1577
|
+
const children2 = options.reverse ? [...childMap.get(branch.name) ?? []].reverse() : childMap.get(branch.name) ?? [];
|
|
1193
1578
|
const childPrefix = isRoot ? " " : `${prefix}${isLast ? " " : "\u2502 "}`;
|
|
1194
|
-
for (let i = 0; i <
|
|
1195
|
-
const isChildLast = i ===
|
|
1579
|
+
for (let i = 0; i < children2.length; i++) {
|
|
1580
|
+
const isChildLast = i === children2.length - 1;
|
|
1196
1581
|
await renderNode(
|
|
1197
|
-
|
|
1582
|
+
children2[i],
|
|
1198
1583
|
currentBranch,
|
|
1199
1584
|
childMap,
|
|
1200
1585
|
childPrefix,
|
|
1201
1586
|
false,
|
|
1202
1587
|
isChildLast,
|
|
1203
1588
|
lines,
|
|
1204
|
-
cwd
|
|
1589
|
+
cwd,
|
|
1590
|
+
options
|
|
1205
1591
|
);
|
|
1206
1592
|
}
|
|
1207
1593
|
}
|
|
@@ -1213,8 +1599,8 @@ init_state();
|
|
|
1213
1599
|
function getBranchByName(stack, name) {
|
|
1214
1600
|
return stack.branches.find((branch) => branch.name === name);
|
|
1215
1601
|
}
|
|
1216
|
-
function getChildren2(stack,
|
|
1217
|
-
return stack.branches.filter((branch) => branch.parent ===
|
|
1602
|
+
function getChildren2(stack, parent2) {
|
|
1603
|
+
return stack.branches.filter((branch) => branch.parent === parent2).map((branch) => branch.name);
|
|
1218
1604
|
}
|
|
1219
1605
|
function getTrackedStackOrThrow(stateBranch, stack) {
|
|
1220
1606
|
if (!stack) {
|
|
@@ -1236,16 +1622,16 @@ async function upBySteps(cwd, steps) {
|
|
|
1236
1622
|
);
|
|
1237
1623
|
let target = current;
|
|
1238
1624
|
for (let i = 0; i < steps; i++) {
|
|
1239
|
-
const
|
|
1240
|
-
if (
|
|
1625
|
+
const children2 = getChildren2(stack, target);
|
|
1626
|
+
if (children2.length === 0) {
|
|
1241
1627
|
throw new DubError(`No branch above '${target}' in the current stack.`);
|
|
1242
1628
|
}
|
|
1243
|
-
if (
|
|
1629
|
+
if (children2.length > 1) {
|
|
1244
1630
|
throw new DubError(
|
|
1245
1631
|
`Branch '${target}' has multiple children; 'dub up' requires a linear stack path.`
|
|
1246
1632
|
);
|
|
1247
1633
|
}
|
|
1248
|
-
target =
|
|
1634
|
+
target = children2[0];
|
|
1249
1635
|
}
|
|
1250
1636
|
await checkoutBranch(target, cwd);
|
|
1251
1637
|
return { branch: target, changed: target !== current };
|
|
@@ -1287,14 +1673,14 @@ async function top(cwd) {
|
|
|
1287
1673
|
);
|
|
1288
1674
|
let target = current;
|
|
1289
1675
|
while (true) {
|
|
1290
|
-
const
|
|
1291
|
-
if (
|
|
1292
|
-
if (
|
|
1676
|
+
const children2 = getChildren2(stack, target);
|
|
1677
|
+
if (children2.length === 0) break;
|
|
1678
|
+
if (children2.length > 1) {
|
|
1293
1679
|
throw new DubError(
|
|
1294
1680
|
`Branch '${target}' has multiple children; 'dub top' requires a linear stack path.`
|
|
1295
1681
|
);
|
|
1296
1682
|
}
|
|
1297
|
-
target =
|
|
1683
|
+
target = children2[0];
|
|
1298
1684
|
}
|
|
1299
1685
|
if (target !== current) {
|
|
1300
1686
|
await checkoutBranch(target, cwd);
|
|
@@ -1316,30 +1702,30 @@ async function bottom(cwd) {
|
|
|
1316
1702
|
}
|
|
1317
1703
|
let target = current;
|
|
1318
1704
|
if (!branch.parent) {
|
|
1319
|
-
const
|
|
1320
|
-
if (
|
|
1705
|
+
const children2 = getChildren2(stack, current);
|
|
1706
|
+
if (children2.length === 0) {
|
|
1321
1707
|
throw new DubError(
|
|
1322
1708
|
`No branch above root '${current}' in the current stack.`
|
|
1323
1709
|
);
|
|
1324
1710
|
}
|
|
1325
|
-
if (
|
|
1711
|
+
if (children2.length > 1) {
|
|
1326
1712
|
throw new DubError(
|
|
1327
1713
|
`Root branch '${current}' has multiple children; 'dub bottom' requires a linear stack path.`
|
|
1328
1714
|
);
|
|
1329
1715
|
}
|
|
1330
|
-
target =
|
|
1716
|
+
target = children2[0];
|
|
1331
1717
|
} else {
|
|
1332
1718
|
let node = branch;
|
|
1333
1719
|
while (node.parent) {
|
|
1334
|
-
const
|
|
1335
|
-
if (!
|
|
1720
|
+
const parent2 = getBranchByName(stack, node.parent);
|
|
1721
|
+
if (!parent2) {
|
|
1336
1722
|
break;
|
|
1337
1723
|
}
|
|
1338
|
-
if (
|
|
1724
|
+
if (parent2.parent === null) {
|
|
1339
1725
|
target = node.name;
|
|
1340
1726
|
break;
|
|
1341
1727
|
}
|
|
1342
|
-
node =
|
|
1728
|
+
node = parent2;
|
|
1343
1729
|
}
|
|
1344
1730
|
}
|
|
1345
1731
|
if (target !== current) {
|
|
@@ -1348,6 +1734,26 @@ async function bottom(cwd) {
|
|
|
1348
1734
|
return { branch: target, changed: target !== current };
|
|
1349
1735
|
}
|
|
1350
1736
|
|
|
1737
|
+
// src/commands/parent.ts
|
|
1738
|
+
init_errors();
|
|
1739
|
+
init_git();
|
|
1740
|
+
init_state();
|
|
1741
|
+
async function parent(cwd, branchArg) {
|
|
1742
|
+
const branch = branchArg ?? await getCurrentBranch(cwd);
|
|
1743
|
+
const state = await readState(cwd);
|
|
1744
|
+
const stack = findStackForBranch(state, branch);
|
|
1745
|
+
if (!stack) {
|
|
1746
|
+
throw new DubError(
|
|
1747
|
+
`Branch '${branch}' is not tracked. Run 'dub track ${branch} --parent <branch>' first.`
|
|
1748
|
+
);
|
|
1749
|
+
}
|
|
1750
|
+
const entry = stack.branches.find((candidate) => candidate.name === branch);
|
|
1751
|
+
if (!entry || !entry.parent) {
|
|
1752
|
+
throw new DubError(`Branch '${branch}' is at the root and has no parent.`);
|
|
1753
|
+
}
|
|
1754
|
+
return { branch, parent: entry.parent };
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1351
1757
|
// src/lib/github.ts
|
|
1352
1758
|
init_errors();
|
|
1353
1759
|
import { execa as execa2 } from "execa";
|
|
@@ -1530,9 +1936,9 @@ init_restack();
|
|
|
1530
1936
|
// src/commands/submit.ts
|
|
1531
1937
|
init_errors();
|
|
1532
1938
|
init_git();
|
|
1533
|
-
import * as
|
|
1939
|
+
import * as fs6 from "fs";
|
|
1534
1940
|
import * as os from "os";
|
|
1535
|
-
import * as
|
|
1941
|
+
import * as path6 from "path";
|
|
1536
1942
|
|
|
1537
1943
|
// src/lib/pr-body.ts
|
|
1538
1944
|
var DUBSTACK_START = "<!-- dubstack:start -->";
|
|
@@ -1608,10 +2014,17 @@ async function submit(cwd, dryRun) {
|
|
|
1608
2014
|
const currentEntry = ordered.find((b) => b.name === currentBranch);
|
|
1609
2015
|
if (currentEntry?.type === "root") {
|
|
1610
2016
|
throw new DubError(
|
|
1611
|
-
"Cannot submit from a root branch.
|
|
2017
|
+
"Cannot submit from a root branch. Run 'dub up' or 'dub checkout <branch>' first."
|
|
1612
2018
|
);
|
|
1613
2019
|
}
|
|
1614
2020
|
const nonRootBranches = ordered.filter((b) => b.type !== "root");
|
|
2021
|
+
const rootBranch = ordered.find((branch) => branch.type === "root")?.name ?? "(unknown)";
|
|
2022
|
+
console.log(
|
|
2023
|
+
`Submitting ${nonRootBranches.length} branch(es) from '${currentBranch}' onto trunk '${rootBranch}'.`
|
|
2024
|
+
);
|
|
2025
|
+
if (dryRun) {
|
|
2026
|
+
console.log("[dry-run] no branches will be pushed or mutated.");
|
|
2027
|
+
}
|
|
1615
2028
|
validateLinearStack(ordered);
|
|
1616
2029
|
const result = { pushed: [], created: [], updated: [] };
|
|
1617
2030
|
const prMap = /* @__PURE__ */ new Map();
|
|
@@ -1679,10 +2092,10 @@ function validateLinearStack(ordered) {
|
|
|
1679
2092
|
childCount.set(branch.parent, (childCount.get(branch.parent) ?? 0) + 1);
|
|
1680
2093
|
}
|
|
1681
2094
|
}
|
|
1682
|
-
for (const [
|
|
2095
|
+
for (const [parent2, count] of childCount) {
|
|
1683
2096
|
if (count > 1) {
|
|
1684
2097
|
throw new DubError(
|
|
1685
|
-
`Branch '${
|
|
2098
|
+
`Branch '${parent2}' has ${count} children. Branching stacks are not supported by submit. Ensure each branch has at most one child. Use 'dub track <child> --parent <branch>' to re-parent branches and linearize the stack before submitting.`
|
|
1686
2099
|
);
|
|
1687
2100
|
}
|
|
1688
2101
|
}
|
|
@@ -1721,13 +2134,13 @@ async function updateAllPrBodies(branches, prMap, stackId, cwd) {
|
|
|
1721
2134
|
}
|
|
1722
2135
|
function writeTempBody(content) {
|
|
1723
2136
|
const tmpDir = os.tmpdir();
|
|
1724
|
-
const tmpFile =
|
|
1725
|
-
|
|
2137
|
+
const tmpFile = path6.join(tmpDir, `dubstack-body-${Date.now()}.md`);
|
|
2138
|
+
fs6.writeFileSync(tmpFile, content);
|
|
1726
2139
|
return tmpFile;
|
|
1727
2140
|
}
|
|
1728
2141
|
function cleanupTempFile(filePath) {
|
|
1729
2142
|
try {
|
|
1730
|
-
|
|
2143
|
+
fs6.unlinkSync(filePath);
|
|
1731
2144
|
} catch {
|
|
1732
2145
|
}
|
|
1733
2146
|
}
|
|
@@ -1735,36 +2148,36 @@ function cleanupTempFile(filePath) {
|
|
|
1735
2148
|
// src/commands/sync.ts
|
|
1736
2149
|
init_errors();
|
|
1737
2150
|
init_git();
|
|
1738
|
-
import { stdin as
|
|
1739
|
-
import * as
|
|
2151
|
+
import { stdin as input2, stdout as output2 } from "process";
|
|
2152
|
+
import * as readline2 from "readline/promises";
|
|
1740
2153
|
init_state();
|
|
1741
2154
|
|
|
1742
2155
|
// src/lib/sync/branch-status.ts
|
|
1743
|
-
function classifyBranchSyncStatus(
|
|
1744
|
-
if (!
|
|
1745
|
-
if (!
|
|
1746
|
-
if (
|
|
1747
|
-
if (!
|
|
2156
|
+
function classifyBranchSyncStatus(input5) {
|
|
2157
|
+
if (!input5.hasRemote) return "missing-remote";
|
|
2158
|
+
if (!input5.hasLocal) return "missing-local";
|
|
2159
|
+
if (input5.localSha && input5.remoteSha && input5.localSha === input5.remoteSha) {
|
|
2160
|
+
if (!input5.hasSubmittedBaseline) {
|
|
1748
2161
|
return "updated-outside-dubstack-but-up-to-date";
|
|
1749
2162
|
}
|
|
1750
2163
|
return "up-to-date";
|
|
1751
2164
|
}
|
|
1752
|
-
if (!
|
|
1753
|
-
if (
|
|
1754
|
-
if (
|
|
2165
|
+
if (!input5.hasSubmittedBaseline) return "unsubmitted";
|
|
2166
|
+
if (input5.localBehind) return "needs-remote-sync-safe";
|
|
2167
|
+
if (input5.remoteBehind) return "local-ahead";
|
|
1755
2168
|
return "reconcile-needed";
|
|
1756
2169
|
}
|
|
1757
2170
|
|
|
1758
2171
|
// src/lib/sync/cleanup.ts
|
|
1759
|
-
async function buildCleanupPlan(
|
|
2172
|
+
async function buildCleanupPlan(input5) {
|
|
1760
2173
|
const toDelete = [];
|
|
1761
2174
|
const skipped = [];
|
|
1762
|
-
for (const branch of
|
|
1763
|
-
const prState = await
|
|
2175
|
+
for (const branch of input5.branches) {
|
|
2176
|
+
const prState = await input5.getPrStatus(branch);
|
|
1764
2177
|
if (prState !== "MERGED" && prState !== "CLOSED") {
|
|
1765
2178
|
continue;
|
|
1766
2179
|
}
|
|
1767
|
-
const mergedIntoRoot = await
|
|
2180
|
+
const mergedIntoRoot = await input5.isMergedIntoAnyRoot(branch);
|
|
1768
2181
|
if (!mergedIntoRoot) {
|
|
1769
2182
|
skipped.push({ branch, reason: "commits-not-in-trunk" });
|
|
1770
2183
|
continue;
|
|
@@ -1775,10 +2188,10 @@ async function buildCleanupPlan(input2) {
|
|
|
1775
2188
|
}
|
|
1776
2189
|
|
|
1777
2190
|
// src/lib/sync/reconcile.ts
|
|
1778
|
-
async function resolveReconcileDecision(
|
|
1779
|
-
if (
|
|
1780
|
-
if (!
|
|
1781
|
-
const raw = await
|
|
2191
|
+
async function resolveReconcileDecision(input5) {
|
|
2192
|
+
if (input5.force) return "take-remote";
|
|
2193
|
+
if (!input5.interactive) return "skip";
|
|
2194
|
+
const raw = await input5.promptChoice();
|
|
1782
2195
|
if (raw === "take-remote" || raw === "keep-local" || raw === "reconcile" || raw === "skip") {
|
|
1783
2196
|
return raw;
|
|
1784
2197
|
}
|
|
@@ -1802,11 +2215,11 @@ function printSyncSummary(result) {
|
|
|
1802
2215
|
|
|
1803
2216
|
// src/commands/sync.ts
|
|
1804
2217
|
init_restack();
|
|
1805
|
-
function
|
|
2218
|
+
function isInteractiveShell2() {
|
|
1806
2219
|
return Boolean(process.stdout.isTTY && process.stdin.isTTY);
|
|
1807
2220
|
}
|
|
1808
2221
|
async function confirm(question) {
|
|
1809
|
-
const rl =
|
|
2222
|
+
const rl = readline2.createInterface({ input: input2, output: output2 });
|
|
1810
2223
|
try {
|
|
1811
2224
|
const answer = await rl.question(`${question} [Y/n] `);
|
|
1812
2225
|
const normalized = answer.trim().toLowerCase();
|
|
@@ -1816,7 +2229,7 @@ async function confirm(question) {
|
|
|
1816
2229
|
}
|
|
1817
2230
|
}
|
|
1818
2231
|
async function choose(question, choices) {
|
|
1819
|
-
const rl =
|
|
2232
|
+
const rl = readline2.createInterface({ input: input2, output: output2 });
|
|
1820
2233
|
try {
|
|
1821
2234
|
console.log(question);
|
|
1822
2235
|
for (let i = 0; i < choices.length; i++) {
|
|
@@ -1839,7 +2252,7 @@ async function sync(cwd, rawOptions = {}) {
|
|
|
1839
2252
|
restack: rawOptions.restack ?? true,
|
|
1840
2253
|
force: rawOptions.force ?? false,
|
|
1841
2254
|
all: rawOptions.all ?? false,
|
|
1842
|
-
interactive: rawOptions.interactive ??
|
|
2255
|
+
interactive: rawOptions.interactive ?? isInteractiveShell2()
|
|
1843
2256
|
};
|
|
1844
2257
|
const state = await readState(cwd);
|
|
1845
2258
|
const originalBranch = await getCurrentBranch(cwd);
|
|
@@ -1925,7 +2338,7 @@ async function sync(cwd, rawOptions = {}) {
|
|
|
1925
2338
|
for (const skipped of cleanupPlan.skipped) {
|
|
1926
2339
|
if (skipped.reason === "commits-not-in-trunk") {
|
|
1927
2340
|
excludedFromSync.add(skipped.branch);
|
|
1928
|
-
for (const child of
|
|
2341
|
+
for (const child of getDescendants2(scopeStacks, skipped.branch)) {
|
|
1929
2342
|
excludedFromSync.add(child);
|
|
1930
2343
|
}
|
|
1931
2344
|
}
|
|
@@ -2299,15 +2712,15 @@ async function markBranchSynced(branchMap, branchName, headSha, cwd, options) {
|
|
|
2299
2712
|
entry.last_synced_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
2300
2713
|
entry.sync_source = options.source;
|
|
2301
2714
|
}
|
|
2302
|
-
function
|
|
2715
|
+
function getDescendants2(stacks, branch) {
|
|
2303
2716
|
const descendants = [];
|
|
2304
2717
|
const childMap = /* @__PURE__ */ new Map();
|
|
2305
2718
|
for (const stack of stacks) {
|
|
2306
2719
|
for (const node of stack.branches) {
|
|
2307
2720
|
if (!node.parent) continue;
|
|
2308
|
-
const
|
|
2309
|
-
|
|
2310
|
-
childMap.set(node.parent,
|
|
2721
|
+
const children2 = childMap.get(node.parent) ?? [];
|
|
2722
|
+
children2.push(node.name);
|
|
2723
|
+
childMap.set(node.parent, children2);
|
|
2311
2724
|
}
|
|
2312
2725
|
}
|
|
2313
2726
|
const queue = [...childMap.get(branch) ?? []];
|
|
@@ -2333,6 +2746,180 @@ function removeBranchFromState(stacks, branch) {
|
|
|
2333
2746
|
}
|
|
2334
2747
|
}
|
|
2335
2748
|
|
|
2749
|
+
// src/commands/track.ts
|
|
2750
|
+
init_errors();
|
|
2751
|
+
init_git();
|
|
2752
|
+
import { stdin as input3, stdout as output3 } from "process";
|
|
2753
|
+
import * as readline3 from "readline/promises";
|
|
2754
|
+
|
|
2755
|
+
// src/lib/track.ts
|
|
2756
|
+
init_errors();
|
|
2757
|
+
init_git();
|
|
2758
|
+
import * as crypto2 from "crypto";
|
|
2759
|
+
init_state();
|
|
2760
|
+
async function validateTrackParent(cwd, branch, parent2) {
|
|
2761
|
+
if (branch === parent2) {
|
|
2762
|
+
throw new DubError("Branch cannot be its own parent.");
|
|
2763
|
+
}
|
|
2764
|
+
if (!await branchExists(parent2, cwd)) {
|
|
2765
|
+
throw new DubError(`Parent branch '${parent2}' does not exist locally.`);
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
async function trackBranch(cwd, options) {
|
|
2769
|
+
const { branch, parent: parent2 } = options;
|
|
2770
|
+
if (!await branchExists(branch, cwd)) {
|
|
2771
|
+
throw new DubError(`Branch '${branch}' does not exist locally.`);
|
|
2772
|
+
}
|
|
2773
|
+
await validateTrackParent(cwd, branch, parent2);
|
|
2774
|
+
const state = await ensureState(cwd);
|
|
2775
|
+
const sourceStack = findStackForBranch(state, branch);
|
|
2776
|
+
const destinationStack = findStackForBranch(state, parent2);
|
|
2777
|
+
if (!sourceStack) {
|
|
2778
|
+
addBranchToStack(state, branch, parent2);
|
|
2779
|
+
assertStateInvariants(state.stacks);
|
|
2780
|
+
await writeState(state, cwd);
|
|
2781
|
+
return { branch, parent: parent2, status: "tracked" };
|
|
2782
|
+
}
|
|
2783
|
+
const branchEntry = sourceStack.branches.find(
|
|
2784
|
+
(entry) => entry.name === branch
|
|
2785
|
+
);
|
|
2786
|
+
if (!branchEntry) {
|
|
2787
|
+
throw new DubError(`Branch '${branch}' is missing from tracked state.`);
|
|
2788
|
+
}
|
|
2789
|
+
if (branchEntry.type === "root") {
|
|
2790
|
+
throw new DubError(
|
|
2791
|
+
`Branch '${branch}' is a stack root and cannot be re-parented.`
|
|
2792
|
+
);
|
|
2793
|
+
}
|
|
2794
|
+
if (branchEntry.parent === parent2) {
|
|
2795
|
+
return { branch, parent: parent2, status: "unchanged" };
|
|
2796
|
+
}
|
|
2797
|
+
const descendants = new Set(getDescendants(sourceStack, branch));
|
|
2798
|
+
if (descendants.has(parent2)) {
|
|
2799
|
+
throw new DubError(
|
|
2800
|
+
`Cannot track '${branch}' onto '${parent2}' because it would create a cycle.`
|
|
2801
|
+
);
|
|
2802
|
+
}
|
|
2803
|
+
if (sourceStack.id === destinationStack?.id) {
|
|
2804
|
+
branchEntry.parent = parent2;
|
|
2805
|
+
assertStateInvariants(state.stacks);
|
|
2806
|
+
await writeState(state, cwd);
|
|
2807
|
+
return { branch, parent: parent2, status: "reparented" };
|
|
2808
|
+
}
|
|
2809
|
+
const movingNames = /* @__PURE__ */ new Set([branch, ...descendants]);
|
|
2810
|
+
const movingBranches = sourceStack.branches.filter(
|
|
2811
|
+
(entry) => movingNames.has(entry.name)
|
|
2812
|
+
);
|
|
2813
|
+
sourceStack.branches = sourceStack.branches.filter(
|
|
2814
|
+
(entry) => !movingNames.has(entry.name)
|
|
2815
|
+
);
|
|
2816
|
+
const movingRoot = movingBranches.find((entry) => entry.name === branch);
|
|
2817
|
+
if (!movingRoot) {
|
|
2818
|
+
throw new DubError(`Failed to move subtree for '${branch}'.`);
|
|
2819
|
+
}
|
|
2820
|
+
movingRoot.parent = parent2;
|
|
2821
|
+
movingRoot.type = void 0;
|
|
2822
|
+
if (destinationStack) {
|
|
2823
|
+
destinationStack.branches.push(...movingBranches);
|
|
2824
|
+
} else {
|
|
2825
|
+
state.stacks.push({
|
|
2826
|
+
id: crypto2.randomUUID(),
|
|
2827
|
+
branches: [
|
|
2828
|
+
{
|
|
2829
|
+
name: parent2,
|
|
2830
|
+
type: "root",
|
|
2831
|
+
parent: null,
|
|
2832
|
+
pr_number: null,
|
|
2833
|
+
pr_link: null,
|
|
2834
|
+
last_submitted_version: null,
|
|
2835
|
+
last_synced_at: null,
|
|
2836
|
+
sync_source: null
|
|
2837
|
+
},
|
|
2838
|
+
...movingBranches
|
|
2839
|
+
]
|
|
2840
|
+
});
|
|
2841
|
+
}
|
|
2842
|
+
state.stacks = state.stacks.filter((stack) => stack.branches.length > 0);
|
|
2843
|
+
assertStateInvariants(state.stacks);
|
|
2844
|
+
await writeState(state, cwd);
|
|
2845
|
+
return { branch, parent: parent2, status: "reparented" };
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
// src/commands/track.ts
|
|
2849
|
+
function isInteractiveShell3() {
|
|
2850
|
+
return Boolean(process.stdout.isTTY && process.stdin.isTTY);
|
|
2851
|
+
}
|
|
2852
|
+
async function promptForParent(branch, suggestedParent) {
|
|
2853
|
+
const rl = readline3.createInterface({ input: input3, output: output3 });
|
|
2854
|
+
try {
|
|
2855
|
+
const suffix = suggestedParent ? ` [${suggestedParent}]` : "";
|
|
2856
|
+
const answer = await rl.question(
|
|
2857
|
+
`Parent branch for '${branch}'${suffix}: `
|
|
2858
|
+
);
|
|
2859
|
+
const parent2 = answer.trim() || suggestedParent;
|
|
2860
|
+
if (!parent2) {
|
|
2861
|
+
throw new DubError(
|
|
2862
|
+
`No parent selected for '${branch}'. Re-run with --parent <branch>.`
|
|
2863
|
+
);
|
|
2864
|
+
}
|
|
2865
|
+
return parent2;
|
|
2866
|
+
} finally {
|
|
2867
|
+
rl.close();
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
async function resolveSuggestedParent(cwd, branch, currentBranch) {
|
|
2871
|
+
if (currentBranch !== branch) return currentBranch;
|
|
2872
|
+
if (await branchExists("main", cwd)) return "main";
|
|
2873
|
+
if (await branchExists("master", cwd)) return "master";
|
|
2874
|
+
return null;
|
|
2875
|
+
}
|
|
2876
|
+
async function track(cwd, branchArg, options = {}) {
|
|
2877
|
+
const currentBranch = await getCurrentBranch(cwd);
|
|
2878
|
+
const branch = branchArg ?? currentBranch;
|
|
2879
|
+
const interactive = options.interactive ?? isInteractiveShell3();
|
|
2880
|
+
let parent2 = options.parent;
|
|
2881
|
+
if (!parent2) {
|
|
2882
|
+
const suggestedParent = await resolveSuggestedParent(
|
|
2883
|
+
cwd,
|
|
2884
|
+
branch,
|
|
2885
|
+
currentBranch
|
|
2886
|
+
);
|
|
2887
|
+
if (interactive) {
|
|
2888
|
+
parent2 = await promptForParent(branch, suggestedParent);
|
|
2889
|
+
} else if (suggestedParent) {
|
|
2890
|
+
parent2 = suggestedParent;
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
if (!parent2) {
|
|
2894
|
+
throw new DubError(
|
|
2895
|
+
`Could not infer parent for '${branch}'. Re-run with --parent <branch>.`
|
|
2896
|
+
);
|
|
2897
|
+
}
|
|
2898
|
+
return trackBranch(cwd, { branch, parent: parent2 });
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2901
|
+
// src/commands/trunk.ts
|
|
2902
|
+
init_errors();
|
|
2903
|
+
init_git();
|
|
2904
|
+
init_state();
|
|
2905
|
+
async function trunk(cwd, branchArg) {
|
|
2906
|
+
const branch = branchArg ?? await getCurrentBranch(cwd);
|
|
2907
|
+
const state = await readState(cwd);
|
|
2908
|
+
const stack = findStackForBranch(state, branch);
|
|
2909
|
+
if (!stack) {
|
|
2910
|
+
throw new DubError(
|
|
2911
|
+
`Branch '${branch}' is not tracked. Run 'dub track ${branch} --parent <branch>' first.`
|
|
2912
|
+
);
|
|
2913
|
+
}
|
|
2914
|
+
const root = stack.branches.find((candidate) => candidate.type === "root");
|
|
2915
|
+
if (!root) {
|
|
2916
|
+
throw new DubError(
|
|
2917
|
+
`Stack for '${branch}' is missing a root branch. Re-run 'dub track' to repair metadata.`
|
|
2918
|
+
);
|
|
2919
|
+
}
|
|
2920
|
+
return { branch, trunk: root.name };
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2336
2923
|
// src/commands/undo.ts
|
|
2337
2924
|
init_errors();
|
|
2338
2925
|
init_git();
|
|
@@ -2384,6 +2971,106 @@ async function undo(cwd) {
|
|
|
2384
2971
|
};
|
|
2385
2972
|
}
|
|
2386
2973
|
|
|
2974
|
+
// src/commands/untrack.ts
|
|
2975
|
+
init_errors();
|
|
2976
|
+
init_git();
|
|
2977
|
+
import { stdin as input4, stdout as output4 } from "process";
|
|
2978
|
+
import * as readline4 from "readline/promises";
|
|
2979
|
+
|
|
2980
|
+
// src/lib/untrack.ts
|
|
2981
|
+
init_errors();
|
|
2982
|
+
init_state();
|
|
2983
|
+
async function getUntrackContext(cwd, branch) {
|
|
2984
|
+
const state = await readState(cwd);
|
|
2985
|
+
const stack = findStackForBranch(state, branch);
|
|
2986
|
+
if (!stack) {
|
|
2987
|
+
throw new DubError(
|
|
2988
|
+
`Branch '${branch}' is not tracked. Run 'dub track ${branch} --parent <branch>' first.`
|
|
2989
|
+
);
|
|
2990
|
+
}
|
|
2991
|
+
return {
|
|
2992
|
+
stack,
|
|
2993
|
+
branch,
|
|
2994
|
+
descendants: getDescendants(stack, branch)
|
|
2995
|
+
};
|
|
2996
|
+
}
|
|
2997
|
+
async function untrackBranch(cwd, options) {
|
|
2998
|
+
const state = await readState(cwd);
|
|
2999
|
+
const stack = findStackForBranch(state, options.branch);
|
|
3000
|
+
if (!stack) {
|
|
3001
|
+
throw new DubError(
|
|
3002
|
+
`Branch '${options.branch}' is not tracked by DubStack.`
|
|
3003
|
+
);
|
|
3004
|
+
}
|
|
3005
|
+
const entry = stack.branches.find((branch) => branch.name === options.branch);
|
|
3006
|
+
if (!entry) {
|
|
3007
|
+
throw new DubError(
|
|
3008
|
+
`Branch '${options.branch}' is missing from tracked stack.`
|
|
3009
|
+
);
|
|
3010
|
+
}
|
|
3011
|
+
const descendants = getDescendants(stack, options.branch);
|
|
3012
|
+
const removedSet = new Set(
|
|
3013
|
+
options.downstack ? [options.branch, ...descendants] : [options.branch]
|
|
3014
|
+
);
|
|
3015
|
+
if (entry.type === "root" && !options.downstack && descendants.length > 0) {
|
|
3016
|
+
throw new DubError(
|
|
3017
|
+
`Branch '${options.branch}' is a root with descendants. Use --downstack to untrack the whole subtree.`
|
|
3018
|
+
);
|
|
3019
|
+
}
|
|
3020
|
+
const reparented = [];
|
|
3021
|
+
if (!options.downstack) {
|
|
3022
|
+
for (const branch of stack.branches) {
|
|
3023
|
+
if (branch.parent !== options.branch) continue;
|
|
3024
|
+
branch.parent = entry.parent;
|
|
3025
|
+
reparented.push({ branch: branch.name, parent: branch.parent });
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
stack.branches = stack.branches.filter(
|
|
3029
|
+
(branch) => !removedSet.has(branch.name)
|
|
3030
|
+
);
|
|
3031
|
+
state.stacks = state.stacks.filter(
|
|
3032
|
+
(candidate) => candidate.branches.length > 0
|
|
3033
|
+
);
|
|
3034
|
+
assertStateInvariants(state.stacks);
|
|
3035
|
+
await writeState(state, cwd);
|
|
3036
|
+
return {
|
|
3037
|
+
removed: [options.branch, ...options.downstack ? descendants : []],
|
|
3038
|
+
reparented
|
|
3039
|
+
};
|
|
3040
|
+
}
|
|
3041
|
+
|
|
3042
|
+
// src/commands/untrack.ts
|
|
3043
|
+
function isInteractiveShell4() {
|
|
3044
|
+
return Boolean(process.stdout.isTTY && process.stdin.isTTY);
|
|
3045
|
+
}
|
|
3046
|
+
async function confirmDownstack(branch, descendants) {
|
|
3047
|
+
const rl = readline4.createInterface({ input: input4, output: output4 });
|
|
3048
|
+
try {
|
|
3049
|
+
const answer = await rl.question(
|
|
3050
|
+
`Branch '${branch}' has descendants (${descendants.join(", ")}). Untrack them too? [y/N] `
|
|
3051
|
+
);
|
|
3052
|
+
const normalized = answer.trim().toLowerCase();
|
|
3053
|
+
return normalized === "y" || normalized === "yes";
|
|
3054
|
+
} finally {
|
|
3055
|
+
rl.close();
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
async function untrack(cwd, branchArg, options = {}) {
|
|
3059
|
+
const branch = branchArg ?? await getCurrentBranch(cwd);
|
|
3060
|
+
const interactive = options.interactive ?? isInteractiveShell4();
|
|
3061
|
+
let downstack = options.downstack ?? false;
|
|
3062
|
+
const context = await getUntrackContext(cwd, branch);
|
|
3063
|
+
if (context.descendants.length > 0 && !downstack) {
|
|
3064
|
+
if (!interactive) {
|
|
3065
|
+
throw new DubError(
|
|
3066
|
+
`Branch '${branch}' has descendants (${context.descendants.join(", ")}). Re-run with --downstack or interactive mode.`
|
|
3067
|
+
);
|
|
3068
|
+
}
|
|
3069
|
+
downstack = await confirmDownstack(branch, context.descendants);
|
|
3070
|
+
}
|
|
3071
|
+
return untrackBranch(cwd, { branch, downstack });
|
|
3072
|
+
}
|
|
3073
|
+
|
|
2387
3074
|
// src/index.ts
|
|
2388
3075
|
init_errors();
|
|
2389
3076
|
var require2 = createRequire(import.meta.url);
|
|
@@ -2436,17 +3123,21 @@ Examples:
|
|
|
2436
3123
|
}
|
|
2437
3124
|
}
|
|
2438
3125
|
);
|
|
2439
|
-
program.command("log").alias("l").description("Display an ASCII tree of the current stack").addHelpText(
|
|
3126
|
+
program.command("log").alias("l").description("Display an ASCII tree of the current stack").option("-s, --stack", "Only show the current stack").option("-a, --all", "Show all stacks (default)").option("-r, --reverse", "Reverse stack/child ordering").addHelpText(
|
|
2440
3127
|
"after",
|
|
2441
3128
|
`
|
|
2442
3129
|
Examples:
|
|
2443
3130
|
$ dub log Show the branch tree with current branch highlighted`
|
|
2444
|
-
).action(
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
3131
|
+
).action(
|
|
3132
|
+
async (options) => {
|
|
3133
|
+
await printLog(process.cwd(), options);
|
|
3134
|
+
}
|
|
3135
|
+
);
|
|
3136
|
+
program.command("ls").description("Display an ASCII tree of the current stack").option("-s, --stack", "Only show the current stack").option("-a, --all", "Show all stacks (default)").option("-r, --reverse", "Reverse stack/child ordering").action(
|
|
3137
|
+
async (options) => {
|
|
3138
|
+
await printLog(process.cwd(), options);
|
|
3139
|
+
}
|
|
3140
|
+
);
|
|
2450
3141
|
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
3142
|
const steps = parseSteps(stepsArg, options.steps);
|
|
2452
3143
|
const result = await upBySteps(process.cwd(), steps);
|
|
@@ -2499,6 +3190,126 @@ program.command("info").argument("[branch]", "Branch to inspect (defaults to cur
|
|
|
2499
3190
|
const info = await branchInfo(process.cwd(), branch);
|
|
2500
3191
|
console.log(formatBranchInfo(info));
|
|
2501
3192
|
});
|
|
3193
|
+
program.command("track").argument("[branch]", "Branch to track (defaults to current branch)").option("-p, --parent <branch>", "Parent branch for tracking").option(
|
|
3194
|
+
"--no-interactive",
|
|
3195
|
+
"Disable parent prompt and require deterministic behavior"
|
|
3196
|
+
).description("Track a branch or update its parent relationship").addHelpText(
|
|
3197
|
+
"after",
|
|
3198
|
+
`
|
|
3199
|
+
Examples:
|
|
3200
|
+
$ dub track
|
|
3201
|
+
$ dub track feat/a --parent main`
|
|
3202
|
+
).action(
|
|
3203
|
+
async (branch, options) => {
|
|
3204
|
+
const result = await track(process.cwd(), branch, {
|
|
3205
|
+
parent: options.parent,
|
|
3206
|
+
interactive: options.interactive
|
|
3207
|
+
});
|
|
3208
|
+
if (result.status === "tracked") {
|
|
3209
|
+
console.log(
|
|
3210
|
+
chalk2.green(`\u2714 Tracking '${result.branch}' on '${result.parent}'`)
|
|
3211
|
+
);
|
|
3212
|
+
return;
|
|
3213
|
+
}
|
|
3214
|
+
if (result.status === "reparented") {
|
|
3215
|
+
console.log(
|
|
3216
|
+
chalk2.green(
|
|
3217
|
+
`\u2714 Re-parented '${result.branch}' onto '${result.parent}'`
|
|
3218
|
+
)
|
|
3219
|
+
);
|
|
3220
|
+
console.log(
|
|
3221
|
+
chalk2.dim(
|
|
3222
|
+
" Run 'dub restack' if descendant branches now need rebasing."
|
|
3223
|
+
)
|
|
3224
|
+
);
|
|
3225
|
+
return;
|
|
3226
|
+
}
|
|
3227
|
+
console.log(
|
|
3228
|
+
chalk2.yellow(
|
|
3229
|
+
`\u26A0 '${result.branch}' is already tracked on '${result.parent}'.`
|
|
3230
|
+
)
|
|
3231
|
+
);
|
|
3232
|
+
}
|
|
3233
|
+
);
|
|
3234
|
+
program.command("untrack").argument("[branch]", "Branch to untrack (defaults to current branch)").option("--downstack", "Also untrack descendants recursively").option("--no-interactive", "Disable prompts and require explicit flags").description(
|
|
3235
|
+
"Remove branch metadata from DubStack without deleting git branches"
|
|
3236
|
+
).addHelpText(
|
|
3237
|
+
"after",
|
|
3238
|
+
`
|
|
3239
|
+
Examples:
|
|
3240
|
+
$ dub untrack
|
|
3241
|
+
$ dub untrack feat/a --downstack`
|
|
3242
|
+
).action(
|
|
3243
|
+
async (branch, options) => {
|
|
3244
|
+
const result = await untrack(process.cwd(), branch, {
|
|
3245
|
+
downstack: options.downstack,
|
|
3246
|
+
interactive: options.interactive
|
|
3247
|
+
});
|
|
3248
|
+
console.log(
|
|
3249
|
+
chalk2.green(
|
|
3250
|
+
`\u2714 Untracked ${result.removed.length} branch(es): ${result.removed.join(", ")}`
|
|
3251
|
+
)
|
|
3252
|
+
);
|
|
3253
|
+
for (const entry of result.reparented) {
|
|
3254
|
+
console.log(
|
|
3255
|
+
chalk2.dim(
|
|
3256
|
+
` \u21B3 Re-parented '${entry.branch}' to '${entry.parent ?? "(none)"}'`
|
|
3257
|
+
)
|
|
3258
|
+
);
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
);
|
|
3262
|
+
program.command("delete").argument("[branch]", "Branch to delete (defaults to current branch)").option("--upstack", "Also delete descendants of the target branch").option("--downstack", "Also delete ancestors toward trunk").option("-f, --force", "Delete branches even when not merged").option("-q, --quiet", "Skip confirmation prompts").option("--no-interactive", "Disable prompts and require explicit flags").description("Delete local branches and update DubStack metadata").addHelpText(
|
|
3263
|
+
"after",
|
|
3264
|
+
`
|
|
3265
|
+
Examples:
|
|
3266
|
+
$ dub delete feat/a
|
|
3267
|
+
$ dub delete feat/a --upstack -f -q`
|
|
3268
|
+
).action(
|
|
3269
|
+
async (branch, options) => {
|
|
3270
|
+
const result = await deleteCommand(process.cwd(), branch, {
|
|
3271
|
+
upstack: options.upstack,
|
|
3272
|
+
downstack: options.downstack,
|
|
3273
|
+
force: options.force,
|
|
3274
|
+
quiet: options.quiet,
|
|
3275
|
+
interactive: options.interactive
|
|
3276
|
+
});
|
|
3277
|
+
if (result.cancelled) {
|
|
3278
|
+
console.log(chalk2.yellow("\u26A0 Delete cancelled."));
|
|
3279
|
+
return;
|
|
3280
|
+
}
|
|
3281
|
+
console.log(
|
|
3282
|
+
chalk2.green(
|
|
3283
|
+
`\u2714 Deleted ${result.deleted.length} branch(es): ${result.deleted.join(", ")}`
|
|
3284
|
+
)
|
|
3285
|
+
);
|
|
3286
|
+
for (const entry of result.reparented) {
|
|
3287
|
+
console.log(
|
|
3288
|
+
chalk2.dim(
|
|
3289
|
+
` \u21B3 Re-parented '${entry.branch}' to '${entry.parent ?? "(none)"}'`
|
|
3290
|
+
)
|
|
3291
|
+
);
|
|
3292
|
+
}
|
|
3293
|
+
}
|
|
3294
|
+
);
|
|
3295
|
+
program.command("parent").argument("[branch]", "Branch to inspect (defaults to current branch)").description("Show the direct parent branch").action(async (branch) => {
|
|
3296
|
+
const result = await parent(process.cwd(), branch);
|
|
3297
|
+
console.log(result.parent);
|
|
3298
|
+
});
|
|
3299
|
+
program.command("children").argument("[branch]", "Branch to inspect (defaults to current branch)").description("Show direct child branches").action(async (branch) => {
|
|
3300
|
+
const result = await children(process.cwd(), branch);
|
|
3301
|
+
if (result.children.length === 0) {
|
|
3302
|
+
console.log("(none)");
|
|
3303
|
+
return;
|
|
3304
|
+
}
|
|
3305
|
+
for (const child of result.children) {
|
|
3306
|
+
console.log(child);
|
|
3307
|
+
}
|
|
3308
|
+
});
|
|
3309
|
+
program.command("trunk").argument("[branch]", "Branch to inspect (defaults to current branch)").description("Show trunk/root branch for the active stack").action(async (branch) => {
|
|
3310
|
+
const result = await trunk(process.cwd(), branch);
|
|
3311
|
+
console.log(result.trunk);
|
|
3312
|
+
});
|
|
2502
3313
|
program.command("sync").description("Sync tracked branches with remote and reconcile divergence").option(
|
|
2503
3314
|
"--restack",
|
|
2504
3315
|
"Restack branches after sync (disable with --no-restack)",
|
|
@@ -2536,6 +3347,37 @@ Examples:
|
|
|
2536
3347
|
}
|
|
2537
3348
|
}
|
|
2538
3349
|
});
|
|
3350
|
+
program.command("continue").description("Continue the active restack or git rebase operation").action(async () => {
|
|
3351
|
+
const result = await continueCommand(process.cwd());
|
|
3352
|
+
if (result.continued === "rebase") {
|
|
3353
|
+
console.log(chalk2.green("\u2714 Continued git rebase."));
|
|
3354
|
+
return;
|
|
3355
|
+
}
|
|
3356
|
+
if (result.restackResult?.status === "conflict") {
|
|
3357
|
+
console.log(
|
|
3358
|
+
chalk2.yellow(
|
|
3359
|
+
`\u26A0 Conflict while restacking '${result.restackResult.conflictBranch}'`
|
|
3360
|
+
)
|
|
3361
|
+
);
|
|
3362
|
+
console.log(
|
|
3363
|
+
chalk2.dim(" Resolve conflicts, stage changes, then run: dub continue")
|
|
3364
|
+
);
|
|
3365
|
+
return;
|
|
3366
|
+
}
|
|
3367
|
+
if (result.restackResult?.status === "up-to-date") {
|
|
3368
|
+
console.log(chalk2.green("\u2714 Stack is already up to date."));
|
|
3369
|
+
return;
|
|
3370
|
+
}
|
|
3371
|
+
console.log(chalk2.green("\u2714 Continued restack."));
|
|
3372
|
+
});
|
|
3373
|
+
program.command("abort").description("Abort the active restack or git rebase operation").action(async () => {
|
|
3374
|
+
const result = await abortCommand(process.cwd());
|
|
3375
|
+
if (result.aborted === "restack") {
|
|
3376
|
+
console.log(chalk2.green("\u2714 Aborted restack and cleared progress."));
|
|
3377
|
+
return;
|
|
3378
|
+
}
|
|
3379
|
+
console.log(chalk2.green("\u2714 Aborted git rebase."));
|
|
3380
|
+
});
|
|
2539
3381
|
program.command("undo").description("Undo the last dub create or dub restack operation").addHelpText(
|
|
2540
3382
|
"after",
|
|
2541
3383
|
`
|
|
@@ -2570,8 +3412,8 @@ program.command("checkout").alias("co").argument("[branch]", "Branch to checkout
|
|
|
2570
3412
|
const result = await checkout(branch, process.cwd());
|
|
2571
3413
|
console.log(chalk2.green(`\u2714 Switched to '${result.branch}'`));
|
|
2572
3414
|
} else if (options.trunk) {
|
|
2573
|
-
const
|
|
2574
|
-
const result = await checkout(
|
|
3415
|
+
const trunk2 = await resolveCheckoutTrunk(process.cwd());
|
|
3416
|
+
const result = await checkout(trunk2, process.cwd());
|
|
2575
3417
|
console.log(chalk2.green(`\u2714 Switched to '${result.branch}'`));
|
|
2576
3418
|
} else {
|
|
2577
3419
|
const result = await interactiveCheckout(process.cwd(), {
|
|
@@ -2635,9 +3477,9 @@ async function runSubmit(options) {
|
|
|
2635
3477
|
}
|
|
2636
3478
|
}
|
|
2637
3479
|
}
|
|
2638
|
-
async function printLog(cwd) {
|
|
2639
|
-
const
|
|
2640
|
-
const styled =
|
|
3480
|
+
async function printLog(cwd, options = {}) {
|
|
3481
|
+
const output5 = await log(cwd, options);
|
|
3482
|
+
const styled = output5.replace(/\*(.+?) \(Current\)\*/g, chalk2.bold.cyan("$1 (Current)")).replace(/⚠ \(missing\)/g, chalk2.yellow("\u26A0 (missing)"));
|
|
2641
3483
|
console.log(styled);
|
|
2642
3484
|
}
|
|
2643
3485
|
function parseSteps(positional, option) {
|