vde-worktree 0.0.13 → 0.0.15
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/dist/index.mjs +69 -9
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1200,7 +1200,11 @@ const RESERVED_FZF_ARGS = new Set([
|
|
|
1200
1200
|
"height",
|
|
1201
1201
|
"border"
|
|
1202
1202
|
]);
|
|
1203
|
+
const ANSI_ESCAPE_SEQUENCE_PATTERN = String.raw`\u001B\[[0-?]*[ -/]*[@-~]`;
|
|
1204
|
+
const ANSI_ESCAPE_SEQUENCE_REGEX = new RegExp(ANSI_ESCAPE_SEQUENCE_PATTERN, "g");
|
|
1203
1205
|
const sanitizeCandidate = (value) => value.replace(/[\r\n]+/g, " ").trim();
|
|
1206
|
+
const stripAnsi = (value) => value.replace(ANSI_ESCAPE_SEQUENCE_REGEX, "");
|
|
1207
|
+
const stripTrailingNewlines = (value) => value.replace(/[\r\n]+$/g, "");
|
|
1204
1208
|
const buildFzfInput = (candidates) => {
|
|
1205
1209
|
return candidates.map((candidate) => sanitizeCandidate(candidate)).filter((candidate) => candidate.length > 0).join("\n");
|
|
1206
1210
|
};
|
|
@@ -1256,14 +1260,14 @@ const selectPathWithFzf = async ({ candidates, prompt = "worktree> ", fzfExtraAr
|
|
|
1256
1260
|
});
|
|
1257
1261
|
const input = buildFzfInput(candidates);
|
|
1258
1262
|
if (input.length === 0) throw new Error("All candidates are empty after sanitization");
|
|
1259
|
-
const candidateSet = new Set(input.split("\n"));
|
|
1263
|
+
const candidateSet = new Set(input.split("\n").map((candidate) => stripAnsi(candidate)));
|
|
1260
1264
|
try {
|
|
1261
|
-
const selectedPath = (await runFzf({
|
|
1265
|
+
const selectedPath = stripAnsi(stripTrailingNewlines((await runFzf({
|
|
1262
1266
|
args,
|
|
1263
1267
|
input,
|
|
1264
1268
|
cwd,
|
|
1265
1269
|
env
|
|
1266
|
-
})).stdout
|
|
1270
|
+
})).stdout));
|
|
1267
1271
|
if (selectedPath.length === 0) return { status: "cancelled" };
|
|
1268
1272
|
if (!candidateSet.has(selectedPath)) throw new Error("fzf returned a value that is not in the candidate list");
|
|
1269
1273
|
return {
|
|
@@ -1436,9 +1440,9 @@ const colorizeListTableLine = ({ line, theme }) => {
|
|
|
1436
1440
|
const segments = line.split("│");
|
|
1437
1441
|
if (segments.length < 3) return line;
|
|
1438
1442
|
const cells = segments.slice(1, -1);
|
|
1439
|
-
if (cells.length !==
|
|
1443
|
+
if (cells.length !== 7) return line;
|
|
1440
1444
|
const headers = cells.map((cell) => cell.trim());
|
|
1441
|
-
if (headers[0] === "branch" && headers[1] === "dirty" && headers[2] === "merged" && headers[3] === "locked" && headers[4] === "path") {
|
|
1445
|
+
if (headers[0] === "branch" && headers[1] === "dirty" && headers[2] === "merged" && headers[3] === "locked" && headers[4] === "ahead" && headers[5] === "behind" && headers[6] === "path") {
|
|
1442
1446
|
const nextCells = cells.map((cell) => colorizeCellContent({
|
|
1443
1447
|
cell,
|
|
1444
1448
|
color: theme.header
|
|
@@ -1453,13 +1457,21 @@ const colorizeListTableLine = ({ line, theme }) => {
|
|
|
1453
1457
|
const dirtyCell = cells[1];
|
|
1454
1458
|
const mergedCell = cells[2];
|
|
1455
1459
|
const lockedCell = cells[3];
|
|
1456
|
-
const
|
|
1460
|
+
const aheadCell = cells[4];
|
|
1461
|
+
const behindCell = cells[5];
|
|
1462
|
+
const pathCell = cells[6];
|
|
1457
1463
|
const branchColor = branchCell.includes("(detached)") === true ? theme.branchDetached : branchCell.trimStart().startsWith("*") ? theme.branchCurrent : theme.branch;
|
|
1458
1464
|
const dirtyTrimmed = dirtyCell.trim();
|
|
1459
1465
|
const dirtyColor = dirtyTrimmed === "dirty" ? theme.dirty : dirtyTrimmed === "clean" ? theme.clean : theme.value;
|
|
1460
1466
|
const mergedTrimmed = mergedCell.trim();
|
|
1461
1467
|
const mergedColor = mergedTrimmed === "merged" ? theme.merged : mergedTrimmed === "unmerged" ? theme.unmerged : mergedTrimmed === "-" ? theme.base : theme.unknown;
|
|
1462
1468
|
const lockedColor = lockedCell.trim() === "locked" ? theme.locked : theme.muted;
|
|
1469
|
+
const aheadTrimmed = aheadCell.trim();
|
|
1470
|
+
const aheadValue = Number.parseInt(aheadTrimmed, 10);
|
|
1471
|
+
const aheadColor = aheadTrimmed === "-" ? theme.muted : Number.isNaN(aheadValue) ? theme.value : aheadValue > 0 ? theme.unmerged : aheadValue === 0 ? theme.merged : theme.unknown;
|
|
1472
|
+
const behindTrimmed = behindCell.trim();
|
|
1473
|
+
const behindValue = Number.parseInt(behindTrimmed, 10);
|
|
1474
|
+
const behindColor = behindTrimmed === "-" ? theme.muted : Number.isNaN(behindValue) ? theme.value : behindValue > 0 ? theme.unknown : behindValue === 0 ? theme.merged : theme.unknown;
|
|
1463
1475
|
const nextCells = [
|
|
1464
1476
|
colorizeCellContent({
|
|
1465
1477
|
cell: branchCell,
|
|
@@ -1477,6 +1489,14 @@ const colorizeListTableLine = ({ line, theme }) => {
|
|
|
1477
1489
|
cell: lockedCell,
|
|
1478
1490
|
color: lockedColor
|
|
1479
1491
|
}),
|
|
1492
|
+
colorizeCellContent({
|
|
1493
|
+
cell: aheadCell,
|
|
1494
|
+
color: aheadColor
|
|
1495
|
+
}),
|
|
1496
|
+
colorizeCellContent({
|
|
1497
|
+
cell: behindCell,
|
|
1498
|
+
color: behindColor
|
|
1499
|
+
}),
|
|
1480
1500
|
colorizeCellContent({
|
|
1481
1501
|
cell: pathCell,
|
|
1482
1502
|
color: theme.path
|
|
@@ -1505,7 +1525,7 @@ const commandHelpEntries = [
|
|
|
1505
1525
|
name: "list",
|
|
1506
1526
|
usage: "vw list [--json]",
|
|
1507
1527
|
summary: "List worktrees with status metadata.",
|
|
1508
|
-
details: ["
|
|
1528
|
+
details: ["Table output includes branch, path, dirty, lock, merged, and ahead/behind vs base branch.", "JSON output includes upstream metadata fields."]
|
|
1509
1529
|
},
|
|
1510
1530
|
{
|
|
1511
1531
|
name: "status",
|
|
@@ -2338,6 +2358,37 @@ const formatMergedColor = ({ mergedState, theme }) => {
|
|
|
2338
2358
|
if (normalized === "base") return theme.base(mergedState);
|
|
2339
2359
|
return theme.unknown(mergedState);
|
|
2340
2360
|
};
|
|
2361
|
+
const formatListUpstreamCount = (value) => {
|
|
2362
|
+
if (value === null) return "-";
|
|
2363
|
+
return String(value);
|
|
2364
|
+
};
|
|
2365
|
+
const resolveAheadBehindAgainstBaseBranch = async ({ repoRoot, baseBranch, worktree }) => {
|
|
2366
|
+
if (baseBranch === null) return {
|
|
2367
|
+
ahead: null,
|
|
2368
|
+
behind: null
|
|
2369
|
+
};
|
|
2370
|
+
const distance = await runGitCommand({
|
|
2371
|
+
cwd: repoRoot,
|
|
2372
|
+
args: [
|
|
2373
|
+
"rev-list",
|
|
2374
|
+
"--left-right",
|
|
2375
|
+
"--count",
|
|
2376
|
+
`${baseBranch}...${worktree.branch ?? worktree.head}`
|
|
2377
|
+
],
|
|
2378
|
+
reject: false
|
|
2379
|
+
});
|
|
2380
|
+
if (distance.exitCode !== 0) return {
|
|
2381
|
+
ahead: null,
|
|
2382
|
+
behind: null
|
|
2383
|
+
};
|
|
2384
|
+
const [behindRaw, aheadRaw] = distance.stdout.trim().split(/\s+/);
|
|
2385
|
+
const behind = Number.parseInt(behindRaw ?? "", 10);
|
|
2386
|
+
const ahead = Number.parseInt(aheadRaw ?? "", 10);
|
|
2387
|
+
return {
|
|
2388
|
+
ahead: Number.isNaN(ahead) ? null : ahead,
|
|
2389
|
+
behind: Number.isNaN(behind) ? null : behind
|
|
2390
|
+
};
|
|
2391
|
+
};
|
|
2341
2392
|
const padToDisplayWidth = ({ value, width }) => {
|
|
2342
2393
|
const visibleLength = stringWidth(value);
|
|
2343
2394
|
if (visibleLength >= width) return value;
|
|
@@ -2906,17 +2957,26 @@ const createCli = (options = {}) => {
|
|
|
2906
2957
|
"dirty",
|
|
2907
2958
|
"merged",
|
|
2908
2959
|
"locked",
|
|
2960
|
+
"ahead",
|
|
2961
|
+
"behind",
|
|
2909
2962
|
"path"
|
|
2910
|
-
], ...snapshot.worktrees.map((worktree) => {
|
|
2963
|
+
], ...await Promise.all(snapshot.worktrees.map(async (worktree) => {
|
|
2964
|
+
const distanceFromBase = await resolveAheadBehindAgainstBaseBranch({
|
|
2965
|
+
repoRoot,
|
|
2966
|
+
baseBranch: snapshot.baseBranch,
|
|
2967
|
+
worktree
|
|
2968
|
+
});
|
|
2911
2969
|
const mergedState = (worktree.branch !== null && snapshot.baseBranch !== null && worktree.branch === snapshot.baseBranch) === true ? "-" : worktree.merged.overall === true ? "merged" : worktree.merged.overall === false ? "unmerged" : "unknown";
|
|
2912
2970
|
return [
|
|
2913
2971
|
`${worktree.path === repoContext.currentWorktreeRoot ? "*" : " "} ${worktree.branch ?? "(detached)"}`,
|
|
2914
2972
|
worktree.dirty ? "dirty" : "clean",
|
|
2915
2973
|
mergedState,
|
|
2916
2974
|
worktree.locked.value ? "locked" : "-",
|
|
2975
|
+
formatListUpstreamCount(distanceFromBase.ahead),
|
|
2976
|
+
formatListUpstreamCount(distanceFromBase.behind),
|
|
2917
2977
|
formatDisplayPath(worktree.path)
|
|
2918
2978
|
];
|
|
2919
|
-
})], {
|
|
2979
|
+
}))], {
|
|
2920
2980
|
border: getBorderCharacters("norc"),
|
|
2921
2981
|
drawHorizontalLine: (lineIndex, rowCount) => {
|
|
2922
2982
|
return lineIndex === 0 || lineIndex === 1 || lineIndex === rowCount;
|