@brainervirus/workit-claude-code 2.8.0 → 2.9.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/.claude-plugin/plugin.json +1 -1
- package/dist/workit.js +1502 -72
- package/package.json +3 -3
package/dist/workit.js
CHANGED
|
@@ -65,7 +65,7 @@ var package_default;
|
|
|
65
65
|
var init_package = __esm(() => {
|
|
66
66
|
package_default = {
|
|
67
67
|
name: "@brainervirus/workit-cli",
|
|
68
|
-
version: "2.
|
|
68
|
+
version: "2.8.0",
|
|
69
69
|
private: false,
|
|
70
70
|
description: "Workit CLI — setup wizard, doctor, and task control for agentic coding workflows",
|
|
71
71
|
keywords: [
|
|
@@ -13539,6 +13539,11 @@ function worktreeSignal(cwd, options = {}) {
|
|
|
13539
13539
|
}
|
|
13540
13540
|
return `sig:${hash.digest("hex")}`;
|
|
13541
13541
|
}
|
|
13542
|
+
function mergeBase(cwd, a, b) {
|
|
13543
|
+
if (!safeArg(a) || !safeArg(b))
|
|
13544
|
+
return null;
|
|
13545
|
+
return line(git2(cwd, ["merge-base", a, b]));
|
|
13546
|
+
}
|
|
13542
13547
|
function patchId(cwd, base, head) {
|
|
13543
13548
|
if (!safeArg(base) || !safeArg(head))
|
|
13544
13549
|
return null;
|
|
@@ -20350,6 +20355,27 @@ function effectiveOf(rows, authors) {
|
|
|
20350
20355
|
}
|
|
20351
20356
|
return effective;
|
|
20352
20357
|
}
|
|
20358
|
+
function restackCarries(rows, branch, from, to) {
|
|
20359
|
+
if (!from || !to)
|
|
20360
|
+
return false;
|
|
20361
|
+
const edges = new Map;
|
|
20362
|
+
for (const row of rows)
|
|
20363
|
+
if (row.type === "stack.restacked" && row.observer === "workit_cli" && row.branch === branch && row.patchEqual === true && typeof row.fromHead === "string" && row.head)
|
|
20364
|
+
edges.set(row.fromHead, [...edges.get(row.fromHead) ?? [], row.head]);
|
|
20365
|
+
const seen = new Set([from]);
|
|
20366
|
+
const queue = [from];
|
|
20367
|
+
while (queue.length) {
|
|
20368
|
+
const next = queue.shift();
|
|
20369
|
+
if (next === to)
|
|
20370
|
+
return true;
|
|
20371
|
+
for (const target of edges.get(next) ?? [])
|
|
20372
|
+
if (!seen.has(target)) {
|
|
20373
|
+
seen.add(target);
|
|
20374
|
+
queue.push(target);
|
|
20375
|
+
}
|
|
20376
|
+
}
|
|
20377
|
+
return false;
|
|
20378
|
+
}
|
|
20353
20379
|
function checkVerdicts(cwd, branch, rows) {
|
|
20354
20380
|
const head = branchHead(cwd, branch);
|
|
20355
20381
|
const fallbackBase = defaultBase2(cwd);
|
|
@@ -20376,7 +20402,9 @@ function checkVerdicts(cwd, branch, rows) {
|
|
|
20376
20402
|
const row = effectiveOf(list, authors);
|
|
20377
20403
|
if (!row)
|
|
20378
20404
|
continue;
|
|
20379
|
-
|
|
20405
|
+
let basis = verdictBasis(row, { head, ...keyFor(row.base ?? fallbackBase) });
|
|
20406
|
+
if (basis === "stale" && restackCarries(rows, branch, row.head, head))
|
|
20407
|
+
basis = "carried";
|
|
20380
20408
|
const current = basis !== "stale";
|
|
20381
20409
|
const independence = independenceReasons(row, authors);
|
|
20382
20410
|
const reasons = [
|
|
@@ -76857,6 +76885,8 @@ async function mergePullRequest(cwd, resolved, input, sleep) {
|
|
|
76857
76885
|
});
|
|
76858
76886
|
if (doc.next !== "READY")
|
|
76859
76887
|
return refuse(`not_ready: ${label} is ${doc.next} (${doc.blockers.join(", ") || doc.next.toLowerCase()})`, NEXT_HINTS[doc.next] ?? "workit pr status", { reason: "not_ready", next: doc.next, blockers: doc.blockers });
|
|
76888
|
+
if (input.expect && (doc.base !== input.expect.base || doc.head.branch !== input.expect.branch))
|
|
76889
|
+
return refuse(`base_mismatch: ${label} is ${doc.head.branch} -> ${doc.base}, expected ${input.expect.branch} -> ${input.expect.base}`, "workit stack sync # retarget it, then land again", { reason: "base_mismatch" });
|
|
76860
76890
|
const branch = doc.head.branch;
|
|
76861
76891
|
const head = doc.head.sha;
|
|
76862
76892
|
if (input.deleteBranch) {
|
|
@@ -77951,7 +77981,25 @@ async function status(argv, io) {
|
|
|
77951
77981
|
});
|
|
77952
77982
|
if (!report.ok)
|
|
77953
77983
|
return forgeFail(io, report);
|
|
77954
|
-
|
|
77984
|
+
const doc = report.data.doc;
|
|
77985
|
+
return emit(io, ok({ ...doc, verdict: verdictBlock(io.cwd, doc.head.branch) }), (data) => {
|
|
77986
|
+
const lines = renderStatus(data);
|
|
77987
|
+
lines.splice(lines.length - 1, 0, data.verdict ? `verdict: ${data.verdict.accepted ? `accepted (${data.verdict.basis})` : `not accepted (${data.verdict.reasons.join(", ") || data.verdict.basis})`}` : "verdict: unknown (ledger unreadable)");
|
|
77988
|
+
return lines;
|
|
77989
|
+
});
|
|
77990
|
+
}
|
|
77991
|
+
function verdictBlock(cwd, branch) {
|
|
77992
|
+
const ledger = readLedger(cwd);
|
|
77993
|
+
if (!ledger.ok)
|
|
77994
|
+
return null;
|
|
77995
|
+
const check = checkVerdicts(cwd, branch, ledger.value.rows);
|
|
77996
|
+
return {
|
|
77997
|
+
accepted: check.accepted.accepted,
|
|
77998
|
+
basis: check.current.basis,
|
|
77999
|
+
reasons: check.accepted.reasons,
|
|
78000
|
+
head: check.head,
|
|
78001
|
+
verdictId: check.accepted.accepted && typeof check.accepted.verdict?.id === "string" ? check.accepted.verdict.id : null
|
|
78002
|
+
};
|
|
77955
78003
|
}
|
|
77956
78004
|
async function create4(argv, io) {
|
|
77957
78005
|
const flags = parseFlags(argv, {
|
|
@@ -78732,12 +78780,1206 @@ var init_verify_delivery = __esm(() => {
|
|
|
78732
78780
|
};
|
|
78733
78781
|
});
|
|
78734
78782
|
|
|
78735
|
-
// packages/workit-
|
|
78736
|
-
|
|
78737
|
-
|
|
78738
|
-
|
|
78739
|
-
|
|
78740
|
-
|
|
78783
|
+
// packages/workit-core/src/stack.ts
|
|
78784
|
+
import { spawnSync as spawnSync21 } from "node:child_process";
|
|
78785
|
+
import { createHash as createHash15, randomBytes as randomBytes3 } from "node:crypto";
|
|
78786
|
+
import fs23 from "node:fs";
|
|
78787
|
+
import path42 from "node:path";
|
|
78788
|
+
function worktreeOf(cwd, branch) {
|
|
78789
|
+
const list = gitRun2(cwd, ["worktree", "list", "--porcelain"]);
|
|
78790
|
+
if (!list.ok)
|
|
78791
|
+
return null;
|
|
78792
|
+
let current = null;
|
|
78793
|
+
for (const line of list.stdout.split(`
|
|
78794
|
+
`)) {
|
|
78795
|
+
if (line.startsWith("worktree "))
|
|
78796
|
+
current = line.slice("worktree ".length);
|
|
78797
|
+
else if (line === `branch refs/heads/${branch}` && current)
|
|
78798
|
+
return current;
|
|
78799
|
+
else if (line === "detached" && current && rebasingBranch(current) === `refs/heads/${branch}`)
|
|
78800
|
+
return current;
|
|
78801
|
+
}
|
|
78802
|
+
return null;
|
|
78803
|
+
}
|
|
78804
|
+
function rebasingBranch(dir) {
|
|
78805
|
+
for (const name of ["rebase-merge/head-name", "rebase-apply/head-name"]) {
|
|
78806
|
+
const found = gitRun2(dir, ["rev-parse", "--path-format=absolute", "--git-path", name]);
|
|
78807
|
+
if (!found.ok)
|
|
78808
|
+
continue;
|
|
78809
|
+
try {
|
|
78810
|
+
return fs23.readFileSync(found.stdout.trim(), "utf8").trim();
|
|
78811
|
+
} catch {}
|
|
78812
|
+
}
|
|
78813
|
+
return null;
|
|
78814
|
+
}
|
|
78815
|
+
function rebaseInProgress(dir) {
|
|
78816
|
+
for (const name of ["rebase-merge", "rebase-apply"]) {
|
|
78817
|
+
const found = gitRun2(dir, ["rev-parse", "--path-format=absolute", "--git-path", name]);
|
|
78818
|
+
if (found.ok && fs23.existsSync(found.stdout.trim()))
|
|
78819
|
+
return true;
|
|
78820
|
+
}
|
|
78821
|
+
return false;
|
|
78822
|
+
}
|
|
78823
|
+
function gitRestack(cwd, input) {
|
|
78824
|
+
const owner = worktreeOf(cwd, input.branch);
|
|
78825
|
+
let dir = owner;
|
|
78826
|
+
if (!dir) {
|
|
78827
|
+
const scratch = scratchDir(cwd);
|
|
78828
|
+
if (!scratch)
|
|
78829
|
+
return { ok: false, conflict: false, worktree: cwd, error: "no store root" };
|
|
78830
|
+
fs23.mkdirSync(scratch, { recursive: true });
|
|
78831
|
+
dir = path42.join(scratch, `${input.branch.replace(/[^A-Za-z0-9._-]+/gu, "_").slice(0, 40)}-${randomBytes3(4).toString("hex")}`);
|
|
78832
|
+
const added = gitRun2(cwd, ["worktree", "add", "-q", dir, input.branch], GIT_TIMEOUTS.worktree);
|
|
78833
|
+
if (!added.ok)
|
|
78834
|
+
return { ok: false, conflict: false, worktree: cwd, error: firstLine(added) };
|
|
78835
|
+
}
|
|
78836
|
+
const run = gitRun2(dir, [
|
|
78837
|
+
"-c",
|
|
78838
|
+
"rebase.autoStash=false",
|
|
78839
|
+
"-c",
|
|
78840
|
+
"rebase.updateRefs=false",
|
|
78841
|
+
"rebase",
|
|
78842
|
+
"--no-autosquash",
|
|
78843
|
+
"--onto",
|
|
78844
|
+
input.onto,
|
|
78845
|
+
input.from
|
|
78846
|
+
], stackDeps.rebaseTimeoutMs);
|
|
78847
|
+
if (!run.ok) {
|
|
78848
|
+
if (run.timedOut)
|
|
78849
|
+
return {
|
|
78850
|
+
ok: false,
|
|
78851
|
+
conflict: false,
|
|
78852
|
+
timedOut: true,
|
|
78853
|
+
worktree: dir,
|
|
78854
|
+
error: `git rebase did not finish within ${stackDeps.rebaseTimeoutMs} ms`
|
|
78855
|
+
};
|
|
78856
|
+
if (rebaseInProgress(dir))
|
|
78857
|
+
return { ok: false, conflict: true, worktree: dir, error: firstLine(run) };
|
|
78858
|
+
if (!owner)
|
|
78859
|
+
gitRun2(cwd, ["worktree", "remove", "--force", dir]);
|
|
78860
|
+
return { ok: false, conflict: false, worktree: dir, error: firstLine(run) };
|
|
78861
|
+
}
|
|
78862
|
+
if (!owner)
|
|
78863
|
+
gitRun2(cwd, ["worktree", "remove", "--force", dir]);
|
|
78864
|
+
const head = branchTip(cwd, input.branch);
|
|
78865
|
+
return head ? { ok: true, head } : { ok: false, conflict: false, worktree: dir, error: `${input.branch} vanished` };
|
|
78866
|
+
}
|
|
78867
|
+
function pruneScratch(cwd) {
|
|
78868
|
+
const scratch = scratchDir(cwd);
|
|
78869
|
+
if (!scratch || !fs23.existsSync(scratch))
|
|
78870
|
+
return;
|
|
78871
|
+
for (const name of fs23.readdirSync(scratch)) {
|
|
78872
|
+
const dir = path42.join(scratch, name);
|
|
78873
|
+
if (!rebaseInProgress(dir) && !trackedDirt(dir))
|
|
78874
|
+
gitRun2(cwd, ["worktree", "remove", "--force", dir]);
|
|
78875
|
+
}
|
|
78876
|
+
gitRun2(cwd, ["worktree", "prune"]);
|
|
78877
|
+
}
|
|
78878
|
+
function stacksDir(cwd) {
|
|
78879
|
+
const root = storeRoot(cwd);
|
|
78880
|
+
if (!root.ok)
|
|
78881
|
+
return stackFail(root.code, root.error, root.unblock);
|
|
78882
|
+
return { ok: true, data: path42.join(root.value.root, "stacks") };
|
|
78883
|
+
}
|
|
78884
|
+
function parseStack(raw) {
|
|
78885
|
+
if (!isRecord7(raw) || !Array.isArray(raw.branches))
|
|
78886
|
+
return null;
|
|
78887
|
+
const name = str2(raw.name);
|
|
78888
|
+
const trunk = str2(raw.trunk);
|
|
78889
|
+
if (!name || !trunk)
|
|
78890
|
+
return null;
|
|
78891
|
+
const branches = [];
|
|
78892
|
+
for (const item of raw.branches) {
|
|
78893
|
+
if (!isRecord7(item))
|
|
78894
|
+
return null;
|
|
78895
|
+
const branch = str2(item.branch);
|
|
78896
|
+
if (!branch)
|
|
78897
|
+
return null;
|
|
78898
|
+
const merged = isRecord7(item.merged) ? {
|
|
78899
|
+
pr: typeof item.merged.pr === "number" ? item.merged.pr : null,
|
|
78900
|
+
mergeSha: str2(item.merged.mergeSha),
|
|
78901
|
+
at: str2(item.merged.at) ?? ""
|
|
78902
|
+
} : null;
|
|
78903
|
+
branches.push({
|
|
78904
|
+
branch,
|
|
78905
|
+
parent: str2(item.parent) ?? trunk,
|
|
78906
|
+
pr: typeof item.pr === "number" ? item.pr : null,
|
|
78907
|
+
lastHead: str2(item.lastHead),
|
|
78908
|
+
lastParentHead: str2(item.lastParentHead),
|
|
78909
|
+
patchId: str2(item.patchId),
|
|
78910
|
+
merged,
|
|
78911
|
+
...isRecord7(item.pendingRestack) && str2(item.pendingRestack.from) && str2(item.pendingRestack.tip) && str2(item.pendingRestack.onto) ? {
|
|
78912
|
+
pendingRestack: {
|
|
78913
|
+
from: str2(item.pendingRestack.from),
|
|
78914
|
+
tip: str2(item.pendingRestack.tip),
|
|
78915
|
+
onto: str2(item.pendingRestack.onto)
|
|
78916
|
+
}
|
|
78917
|
+
} : {}
|
|
78918
|
+
});
|
|
78919
|
+
}
|
|
78920
|
+
return {
|
|
78921
|
+
v: typeof raw.v === "number" ? raw.v : STACK_VERSION,
|
|
78922
|
+
name,
|
|
78923
|
+
trunk,
|
|
78924
|
+
forge: raw.forge === "github" || raw.forge === "gitlab" ? raw.forge : null,
|
|
78925
|
+
repo: str2(raw.repo),
|
|
78926
|
+
branches,
|
|
78927
|
+
updatedAt: str2(raw.updatedAt) ?? ""
|
|
78928
|
+
};
|
|
78929
|
+
}
|
|
78930
|
+
function readStack(cwd, name) {
|
|
78931
|
+
const dir = stacksDir(cwd);
|
|
78932
|
+
if (!dir.ok)
|
|
78933
|
+
return dir;
|
|
78934
|
+
const file = path42.join(dir.data, fileFor(name));
|
|
78935
|
+
let text;
|
|
78936
|
+
try {
|
|
78937
|
+
text = fs23.readFileSync(file, "utf8");
|
|
78938
|
+
} catch (error) {
|
|
78939
|
+
if (error.code === "ENOENT")
|
|
78940
|
+
return { ok: true, data: null };
|
|
78941
|
+
return stackFail("unavailable", `cannot read ${file}: ${error.message}`);
|
|
78942
|
+
}
|
|
78943
|
+
let parsed = null;
|
|
78944
|
+
try {
|
|
78945
|
+
parsed = parseStack(JSON.parse(text));
|
|
78946
|
+
} catch {
|
|
78947
|
+
parsed = null;
|
|
78948
|
+
}
|
|
78949
|
+
return parsed ? { ok: true, data: parsed } : stackFail("failed", `stack file ${file} is not a valid stack`, `workit stack plan --name ${name} <branch…> # rebuild it`);
|
|
78950
|
+
}
|
|
78951
|
+
function writeStack(cwd, stack, now = new Date) {
|
|
78952
|
+
const dir = stacksDir(cwd);
|
|
78953
|
+
if (!dir.ok)
|
|
78954
|
+
return dir;
|
|
78955
|
+
const file = path42.join(dir.data, fileFor(stack.name));
|
|
78956
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
78957
|
+
try {
|
|
78958
|
+
fs23.mkdirSync(dir.data, { recursive: true });
|
|
78959
|
+
stack.updatedAt = now.toISOString();
|
|
78960
|
+
fs23.writeFileSync(temp, `${JSON.stringify(stack, null, 2)}
|
|
78961
|
+
`);
|
|
78962
|
+
fs23.renameSync(temp, file);
|
|
78963
|
+
return { ok: true, data: undefined };
|
|
78964
|
+
} catch (error) {
|
|
78965
|
+
fs23.rmSync(temp, { force: true });
|
|
78966
|
+
return stackFail("unavailable", `cannot write ${file}: ${error.message}`);
|
|
78967
|
+
}
|
|
78968
|
+
}
|
|
78969
|
+
function listStacks(cwd) {
|
|
78970
|
+
const dir = stacksDir(cwd);
|
|
78971
|
+
if (!dir.ok)
|
|
78972
|
+
return dir;
|
|
78973
|
+
let names = [];
|
|
78974
|
+
try {
|
|
78975
|
+
names = fs23.readdirSync(dir.data).filter((name) => name.endsWith(".json"));
|
|
78976
|
+
} catch {
|
|
78977
|
+
return { ok: true, data: [] };
|
|
78978
|
+
}
|
|
78979
|
+
const out = [];
|
|
78980
|
+
for (const name of names) {
|
|
78981
|
+
let parsed = null;
|
|
78982
|
+
try {
|
|
78983
|
+
parsed = parseStack(JSON.parse(fs23.readFileSync(path42.join(dir.data, name), "utf8")));
|
|
78984
|
+
} catch {
|
|
78985
|
+
parsed = null;
|
|
78986
|
+
}
|
|
78987
|
+
if (parsed && fileFor(parsed.name) === name)
|
|
78988
|
+
out.push(parsed);
|
|
78989
|
+
}
|
|
78990
|
+
return { ok: true, data: out.toSorted((a, b) => a.name.localeCompare(b.name)) };
|
|
78991
|
+
}
|
|
78992
|
+
function selectStack(cwd, name) {
|
|
78993
|
+
if (name !== null && !validName(name))
|
|
78994
|
+
return stackFail("invalid_input", "a stack name must be 1-200 printable characters");
|
|
78995
|
+
if (name) {
|
|
78996
|
+
const read = readStack(cwd, name);
|
|
78997
|
+
if (!read.ok)
|
|
78998
|
+
return read;
|
|
78999
|
+
return read.data ? { ok: true, data: read.data } : stackFail("not_found", `no stack named ${name}`, `workit stack plan --name ${name} <branch…>`);
|
|
79000
|
+
}
|
|
79001
|
+
const all = listStacks(cwd);
|
|
79002
|
+
if (!all.ok)
|
|
79003
|
+
return all;
|
|
79004
|
+
const branch = currentBranch(cwd);
|
|
79005
|
+
const holding = all.data.filter((stack) => stack.branches.some((entry) => entry.branch === branch));
|
|
79006
|
+
if (holding.length === 1)
|
|
79007
|
+
return { ok: true, data: holding[0] };
|
|
79008
|
+
if (holding.length === 0 && all.data.length === 1)
|
|
79009
|
+
return { ok: true, data: all.data[0] };
|
|
79010
|
+
return stackFail("not_found", holding.length > 1 || all.data.length > 1 ? `several stacks match; pick one (${(holding.length > 1 ? holding : all.data).map((stack) => stack.name).join(", ")})` : "no stack is planned here", holding.length > 1 || all.data.length > 1 ? "pass --name <stack>" : "workit stack plan [<branch>…] # from the current branch chain by default");
|
|
79011
|
+
}
|
|
79012
|
+
function stackLockStale(raw, age) {
|
|
79013
|
+
let payload = null;
|
|
79014
|
+
try {
|
|
79015
|
+
payload = raw ? JSON.parse(raw) : null;
|
|
79016
|
+
} catch {
|
|
79017
|
+
payload = null;
|
|
79018
|
+
}
|
|
79019
|
+
if (payload === null)
|
|
79020
|
+
return age !== null && age > OWNERLESS_STALE_MS;
|
|
79021
|
+
const owner = classifyLockOwner(payload, age);
|
|
79022
|
+
if (owner.state === "live")
|
|
79023
|
+
return false;
|
|
79024
|
+
if (owner.state === "stale")
|
|
79025
|
+
return true;
|
|
79026
|
+
return age !== null && age > HEARTBEAT_STALE_MS;
|
|
79027
|
+
}
|
|
79028
|
+
function takeLock(lock) {
|
|
79029
|
+
try {
|
|
79030
|
+
fs23.mkdirSync(lock);
|
|
79031
|
+
} catch (error) {
|
|
79032
|
+
if (error.code === "EEXIST")
|
|
79033
|
+
return null;
|
|
79034
|
+
throw error;
|
|
79035
|
+
}
|
|
79036
|
+
const owner = {
|
|
79037
|
+
pid: process.pid,
|
|
79038
|
+
processStart: processStartOf(process.pid),
|
|
79039
|
+
host: localLockHost(),
|
|
79040
|
+
nonce: randomBytes3(8).toString("hex")
|
|
79041
|
+
};
|
|
79042
|
+
fs23.writeFileSync(path42.join(lock, OWNER), JSON.stringify(owner));
|
|
79043
|
+
return owner;
|
|
79044
|
+
}
|
|
79045
|
+
function reapLock(lock, observed) {
|
|
79046
|
+
const tomb = `${lock}.reap-${randomBytes3(6).toString("hex")}`;
|
|
79047
|
+
try {
|
|
79048
|
+
fs23.renameSync(lock, tomb);
|
|
79049
|
+
} catch {
|
|
79050
|
+
return false;
|
|
79051
|
+
}
|
|
79052
|
+
if (readOwner(tomb).raw === observed) {
|
|
79053
|
+
fs23.rmSync(tomb, { recursive: true, force: true });
|
|
79054
|
+
return true;
|
|
79055
|
+
}
|
|
79056
|
+
try {
|
|
79057
|
+
fs23.renameSync(tomb, lock);
|
|
79058
|
+
} catch {
|
|
79059
|
+
fs23.rmSync(tomb, { recursive: true, force: true });
|
|
79060
|
+
}
|
|
79061
|
+
return false;
|
|
79062
|
+
}
|
|
79063
|
+
function releaseLock(lock, owner) {
|
|
79064
|
+
const { raw } = readOwner(lock);
|
|
79065
|
+
if (raw !== JSON.stringify(owner))
|
|
79066
|
+
return;
|
|
79067
|
+
const tomb = `${lock}.release-${randomBytes3(6).toString("hex")}`;
|
|
79068
|
+
try {
|
|
79069
|
+
fs23.renameSync(lock, tomb);
|
|
79070
|
+
fs23.rmSync(tomb, { recursive: true, force: true });
|
|
79071
|
+
} catch {}
|
|
79072
|
+
}
|
|
79073
|
+
async function withStackLock(cwd, name, fn, waitMs = LOCK_WAIT_MS3) {
|
|
79074
|
+
const dir = stacksDir(cwd);
|
|
79075
|
+
if (!dir.ok)
|
|
79076
|
+
return dir;
|
|
79077
|
+
if (!validName(name))
|
|
79078
|
+
return stackFail("invalid_input", "a stack name must be 1-200 printable characters");
|
|
79079
|
+
const lock = path42.join(dir.data, `${fileFor(name)}.lock`);
|
|
79080
|
+
try {
|
|
79081
|
+
fs23.mkdirSync(dir.data, { recursive: true });
|
|
79082
|
+
} catch (error) {
|
|
79083
|
+
return stackFail("unavailable", `cannot create ${dir.data}: ${error.message}`);
|
|
79084
|
+
}
|
|
79085
|
+
const deadline = Date.now() + waitMs;
|
|
79086
|
+
let owner = null;
|
|
79087
|
+
for (;; ) {
|
|
79088
|
+
owner = takeLock(lock);
|
|
79089
|
+
if (owner)
|
|
79090
|
+
break;
|
|
79091
|
+
const seen = readOwner(lock);
|
|
79092
|
+
if (stackLockStale(seen.raw, seen.age) && reapLock(lock, seen.raw))
|
|
79093
|
+
continue;
|
|
79094
|
+
if (Date.now() >= deadline) {
|
|
79095
|
+
let holder = {};
|
|
79096
|
+
try {
|
|
79097
|
+
holder = seen.raw ? JSON.parse(seen.raw) : {};
|
|
79098
|
+
} catch {
|
|
79099
|
+
holder = {};
|
|
79100
|
+
}
|
|
79101
|
+
return stackFail("busy", `stack ${name} is busy: another workit stack command${holder.pid ? ` (pid ${holder.pid} on ${holder.host ?? "?"})` : ""} holds ${lock}`, "retry when it finishes; a lock whose holder stopped heartbeating is reclaimed automatically", { lock, pid: holder.pid ?? null, host: holder.host ?? null });
|
|
79102
|
+
}
|
|
79103
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
79104
|
+
}
|
|
79105
|
+
const held = owner;
|
|
79106
|
+
const touch = () => {
|
|
79107
|
+
try {
|
|
79108
|
+
const now = new Date;
|
|
79109
|
+
fs23.utimesSync(path42.join(lock, OWNER), now, now);
|
|
79110
|
+
} catch {}
|
|
79111
|
+
};
|
|
79112
|
+
const beat = setInterval(touch, HEARTBEAT_MS);
|
|
79113
|
+
beat.unref?.();
|
|
79114
|
+
const previous = activeBeat;
|
|
79115
|
+
activeBeat = touch;
|
|
79116
|
+
try {
|
|
79117
|
+
return await fn();
|
|
79118
|
+
} finally {
|
|
79119
|
+
activeBeat = previous;
|
|
79120
|
+
clearInterval(beat);
|
|
79121
|
+
releaseLock(lock, held);
|
|
79122
|
+
}
|
|
79123
|
+
}
|
|
79124
|
+
function deriveChain(cwd, trunk, base) {
|
|
79125
|
+
const branch = currentBranch(cwd);
|
|
79126
|
+
if (!branch || branch === trunk)
|
|
79127
|
+
return stackFail("invalid_input", branch ? `you are on the trunk ${trunk}` : "HEAD is detached", "switch to the top branch of the stack, or name the branches: workit stack plan <bottom> … <top>");
|
|
79128
|
+
const refs = gitRun2(cwd, [
|
|
79129
|
+
"for-each-ref",
|
|
79130
|
+
"--format=%(refname:short) %(objectname)",
|
|
79131
|
+
"refs/heads"
|
|
79132
|
+
]);
|
|
79133
|
+
const head = branchTip(cwd, branch);
|
|
79134
|
+
if (!refs.ok || !head)
|
|
79135
|
+
return stackFail("failed", "cannot list local branches");
|
|
79136
|
+
const below = [];
|
|
79137
|
+
for (const line of refs.stdout.split(`
|
|
79138
|
+
`)) {
|
|
79139
|
+
const [name, sha] = line.trim().split(" ");
|
|
79140
|
+
if (!name || !sha || name === branch || name === trunk)
|
|
79141
|
+
continue;
|
|
79142
|
+
if (sha === head || isAncestor(cwd, sha, base) || !isAncestor(cwd, sha, head))
|
|
79143
|
+
continue;
|
|
79144
|
+
const count = gitRun2(cwd, ["rev-list", "--count", `${base}..${sha}`]);
|
|
79145
|
+
below.push({ branch: name, depth: Number(count.stdout.trim()) || 0 });
|
|
79146
|
+
}
|
|
79147
|
+
below.sort((a, b) => a.depth - b.depth || a.branch.localeCompare(b.branch));
|
|
79148
|
+
return { ok: true, data: [...below.map((entry) => entry.branch), branch] };
|
|
79149
|
+
}
|
|
79150
|
+
function planStack(cwd, resolved, input, existing) {
|
|
79151
|
+
const remote = remoteFor(cwd, resolved, input.trunk);
|
|
79152
|
+
const notes = [];
|
|
79153
|
+
const trunkSha = trunkTip(cwd, remote, input.trunk);
|
|
79154
|
+
if (!trunkSha)
|
|
79155
|
+
return stackFail("not_found", `trunk ${input.trunk} is not available locally`, `git fetch ${remote} ${input.trunk} # or pass --trunk <branch>`);
|
|
79156
|
+
let branches = input.branches;
|
|
79157
|
+
if (branches.length === 0) {
|
|
79158
|
+
if (existing)
|
|
79159
|
+
branches = existing.branches.map((entry) => entry.branch);
|
|
79160
|
+
else {
|
|
79161
|
+
const derived = deriveChain(cwd, input.trunk, trunkSha);
|
|
79162
|
+
if (!derived.ok)
|
|
79163
|
+
return derived;
|
|
79164
|
+
branches = derived.data;
|
|
79165
|
+
}
|
|
79166
|
+
}
|
|
79167
|
+
if (new Set(branches).size !== branches.length)
|
|
79168
|
+
return stackFail("invalid_input", "a branch is listed twice");
|
|
79169
|
+
if (branches.includes(input.trunk))
|
|
79170
|
+
return stackFail("invalid_input", `the trunk ${input.trunk} cannot be in the stack`);
|
|
79171
|
+
for (const branch of branches)
|
|
79172
|
+
if (!safeRef2(branch) || !branchTip(cwd, branch))
|
|
79173
|
+
return stackFail("not_found", `branch ${branch} does not exist locally`, `git switch ${branch} # or git branch ${branch} ${remote}/${branch}`);
|
|
79174
|
+
const previous = new Map((existing?.branches ?? []).map((entry) => [entry.branch, entry]));
|
|
79175
|
+
const entries = [];
|
|
79176
|
+
const checks = [];
|
|
79177
|
+
let parent = input.trunk;
|
|
79178
|
+
let parentSha = trunkSha;
|
|
79179
|
+
for (const branch of branches) {
|
|
79180
|
+
const tip = branchTip(cwd, branch);
|
|
79181
|
+
const before = previous.get(branch);
|
|
79182
|
+
if (before?.merged) {
|
|
79183
|
+
let still = false;
|
|
79184
|
+
if (before.pr && resolved) {
|
|
79185
|
+
const status = resolved.forge.prStatus(before.pr);
|
|
79186
|
+
still = status.ok && status.data.state === "merged";
|
|
79187
|
+
} else
|
|
79188
|
+
still = hasOwnCommits(cwd, tip, before.lastParentHead) && isAncestor(cwd, tip, trunkSha);
|
|
79189
|
+
if (still) {
|
|
79190
|
+
entries.push(before);
|
|
79191
|
+
continue;
|
|
79192
|
+
}
|
|
79193
|
+
notes.push(`${branch} was recorded as merged but is not; it is back in the stack`);
|
|
79194
|
+
}
|
|
79195
|
+
const onParent = isAncestor(cwd, parentSha, tip);
|
|
79196
|
+
let builtOn = parentSha;
|
|
79197
|
+
if (!onParent) {
|
|
79198
|
+
if (before?.lastParentHead && isAncestor(cwd, before.lastParentHead, tip))
|
|
79199
|
+
builtOn = before.lastParentHead;
|
|
79200
|
+
else {
|
|
79201
|
+
const old = oldParentVersion(cwd, parent, parentSha, tip) ?? oldParentLookalike(cwd, parentSha, tip);
|
|
79202
|
+
if (old)
|
|
79203
|
+
return stackFail("blocked", `parent_rewritten: ${branch} is built on an older version of ${parent} (${old.slice(0, 12)}); replaying it from the merge base would bring the old ${parent} commits back`, `git rebase --onto ${parent} ${old} ${branch} # moves only ${branch}'s own commits; then workit stack plan`, { branch, parent, oldParentTip: old });
|
|
79204
|
+
builtOn = mergeBase(cwd, parentSha, tip) ?? parentSha;
|
|
79205
|
+
notes.push(`${branch} is not on ${parent} and has no recorded base; sync will replay every commit since their merge base ${builtOn.slice(0, 12)}`);
|
|
79206
|
+
}
|
|
79207
|
+
}
|
|
79208
|
+
let pr = before?.pr ?? null;
|
|
79209
|
+
let prBase = null;
|
|
79210
|
+
if (resolved) {
|
|
79211
|
+
const found = resolved.forge.findPr(branch, {
|
|
79212
|
+
owner: resolved.headRepo.split("/")[0] ?? null,
|
|
79213
|
+
projectId: resolved.headProjectId,
|
|
79214
|
+
sha: tip
|
|
79215
|
+
});
|
|
79216
|
+
if (found.ok && found.data && found.data.state === "open") {
|
|
79217
|
+
pr = found.data.number;
|
|
79218
|
+
const status = resolved.forge.prStatus(pr);
|
|
79219
|
+
if (status.ok)
|
|
79220
|
+
prBase = status.data.base;
|
|
79221
|
+
} else if (!found.ok)
|
|
79222
|
+
notes.push(`${branch}: PR lookup failed (${found.error})`);
|
|
79223
|
+
}
|
|
79224
|
+
entries.push({
|
|
79225
|
+
...before?.pendingRestack ? { pendingRestack: before.pendingRestack } : {},
|
|
79226
|
+
branch,
|
|
79227
|
+
parent,
|
|
79228
|
+
pr,
|
|
79229
|
+
lastHead: tip,
|
|
79230
|
+
lastParentHead: builtOn,
|
|
79231
|
+
patchId: patchId(cwd, builtOn, tip),
|
|
79232
|
+
merged: null
|
|
79233
|
+
});
|
|
79234
|
+
checks.push({
|
|
79235
|
+
branch,
|
|
79236
|
+
parent,
|
|
79237
|
+
onParent,
|
|
79238
|
+
pr,
|
|
79239
|
+
prBase,
|
|
79240
|
+
baseMatches: prBase === null ? null : prBase === parent
|
|
79241
|
+
});
|
|
79242
|
+
if (!onParent)
|
|
79243
|
+
notes.push(`${branch} is not on ${parent}; workit stack sync rebases it`);
|
|
79244
|
+
if (prBase !== null && prBase !== parent)
|
|
79245
|
+
notes.push(`${branch}: PR base is ${prBase}, the stack says ${parent}; workit stack sync retargets it`);
|
|
79246
|
+
parent = branch;
|
|
79247
|
+
parentSha = tip;
|
|
79248
|
+
}
|
|
79249
|
+
if (!resolved)
|
|
79250
|
+
notes.push("forge not resolved: PR numbers were not looked up");
|
|
79251
|
+
const name = input.name ?? existing?.name ?? branches[0];
|
|
79252
|
+
return {
|
|
79253
|
+
ok: true,
|
|
79254
|
+
data: {
|
|
79255
|
+
stack: {
|
|
79256
|
+
v: STACK_VERSION,
|
|
79257
|
+
name,
|
|
79258
|
+
trunk: input.trunk,
|
|
79259
|
+
forge: resolved?.forge.kind ?? existing?.forge ?? null,
|
|
79260
|
+
repo: resolved?.forge.repo ?? existing?.repo ?? null,
|
|
79261
|
+
branches: entries,
|
|
79262
|
+
updatedAt: ""
|
|
79263
|
+
},
|
|
79264
|
+
created: existing === null,
|
|
79265
|
+
checks,
|
|
79266
|
+
notes
|
|
79267
|
+
}
|
|
79268
|
+
};
|
|
79269
|
+
}
|
|
79270
|
+
function hasOwnCommits(cwd, tip, base) {
|
|
79271
|
+
if (!base)
|
|
79272
|
+
return false;
|
|
79273
|
+
return tip !== base && !isAncestor(cwd, tip, base);
|
|
79274
|
+
}
|
|
79275
|
+
function oldParentVersion(cwd, parent, parentSha, child) {
|
|
79276
|
+
if (!safeRef2(parent))
|
|
79277
|
+
return null;
|
|
79278
|
+
const log = gitRun2(cwd, ["reflog", "show", "--format=%H", `refs/heads/${parent}`, "--"]);
|
|
79279
|
+
if (!log.ok)
|
|
79280
|
+
return null;
|
|
79281
|
+
for (const sha of log.stdout.split(`
|
|
79282
|
+
`).map((line) => line.trim())) {
|
|
79283
|
+
if (!sha || sha === parentSha)
|
|
79284
|
+
continue;
|
|
79285
|
+
if (isAncestor(cwd, sha, child) && !isAncestor(cwd, sha, parentSha))
|
|
79286
|
+
return sha;
|
|
79287
|
+
}
|
|
79288
|
+
return null;
|
|
79289
|
+
}
|
|
79290
|
+
function commitKeys(cwd, range) {
|
|
79291
|
+
const log = gitRun2(cwd, ["log", "--reverse", "--no-merges", "--format=%H%x00%s", range]);
|
|
79292
|
+
if (!log.ok)
|
|
79293
|
+
return [];
|
|
79294
|
+
const patches = new Map;
|
|
79295
|
+
const diff = gitRun2(cwd, ["log", "--no-merges", "-p", "--format=commit %H", range]);
|
|
79296
|
+
if (diff.ok && diff.stdout.trim()) {
|
|
79297
|
+
const ids = spawnSync21("git", ["patch-id", "--stable"], {
|
|
79298
|
+
cwd,
|
|
79299
|
+
input: diff.stdout,
|
|
79300
|
+
encoding: "utf8",
|
|
79301
|
+
maxBuffer: 64 * 1024 * 1024
|
|
79302
|
+
});
|
|
79303
|
+
for (const line of (ids.stdout ?? "").split(`
|
|
79304
|
+
`)) {
|
|
79305
|
+
const [patch, sha] = line.trim().split(/\s+/u);
|
|
79306
|
+
if (patch && sha)
|
|
79307
|
+
patches.set(sha, patch);
|
|
79308
|
+
}
|
|
79309
|
+
}
|
|
79310
|
+
return log.stdout.split(`
|
|
79311
|
+
`).filter(Boolean).map((line) => {
|
|
79312
|
+
const [sha, subject = ""] = line.split("\x00");
|
|
79313
|
+
return { sha, subject, patch: patches.get(sha) ?? null };
|
|
79314
|
+
});
|
|
79315
|
+
}
|
|
79316
|
+
function oldParentLookalike(cwd, parentSha, child) {
|
|
79317
|
+
const base = mergeBase(cwd, parentSha, child);
|
|
79318
|
+
if (!base)
|
|
79319
|
+
return null;
|
|
79320
|
+
const parentCommits = commitKeys(cwd, `${base}..${parentSha}`);
|
|
79321
|
+
if (parentCommits.length === 0)
|
|
79322
|
+
return null;
|
|
79323
|
+
const patches = new Set(parentCommits.map((commit) => commit.patch).filter(Boolean));
|
|
79324
|
+
const subjects = new Set(parentCommits.map((commit) => commit.subject).filter(Boolean));
|
|
79325
|
+
let last = null;
|
|
79326
|
+
for (const commit of commitKeys(cwd, `${base}..${child}`))
|
|
79327
|
+
if (commit.patch && patches.has(commit.patch) || subjects.has(commit.subject))
|
|
79328
|
+
last = commit.sha;
|
|
79329
|
+
return last;
|
|
79330
|
+
}
|
|
79331
|
+
function statusDoc(ctx, pr) {
|
|
79332
|
+
const status = ctx.resolved.forge.prStatus(pr);
|
|
79333
|
+
if (!status.ok)
|
|
79334
|
+
return status;
|
|
79335
|
+
return buildStatusDoc(ctx.cwd, ctx.resolved, status.data, { logLines: 0, behind: false });
|
|
79336
|
+
}
|
|
79337
|
+
function prBinding(ctx, entry, doc) {
|
|
79338
|
+
const label = noun2(ctx.resolved, entry.pr);
|
|
79339
|
+
if (doc.head.branch !== entry.branch)
|
|
79340
|
+
return `${label} is for ${doc.head.branch}, not ${entry.branch}`;
|
|
79341
|
+
const found = ctx.resolved.forge.findPr(entry.branch, {
|
|
79342
|
+
owner: ctx.resolved.headRepo.split("/")[0] ?? null,
|
|
79343
|
+
projectId: ctx.resolved.headProjectId,
|
|
79344
|
+
sha: null
|
|
79345
|
+
});
|
|
79346
|
+
if (!found.ok)
|
|
79347
|
+
return `cannot confirm ${label} belongs to ${entry.branch}: ${found.error}`;
|
|
79348
|
+
if (found.data?.number !== entry.pr)
|
|
79349
|
+
return `${entry.branch} from ${ctx.resolved.headRepo} is ${found.data ? noun2(ctx.resolved, found.data.number) : "without a PR"}, not ${label}`;
|
|
79350
|
+
return null;
|
|
79351
|
+
}
|
|
79352
|
+
function repoMismatch(ctx, stack) {
|
|
79353
|
+
const { kind, repo } = ctx.resolved.forge;
|
|
79354
|
+
if (stack.repo && stack.repo !== repo || stack.forge && stack.forge !== kind)
|
|
79355
|
+
return stackFail("blocked", `repo_mismatch: stack ${stack.name} was planned on ${stack.forge ?? "?"} ${stack.repo ?? "?"}, but this checkout resolves to ${kind} ${repo}`, `workit stack plan --name ${stack.name} <branch…> # re-plan it here, or run from the right checkout`, { planned: { forge: stack.forge, repo: stack.repo }, resolved: { forge: kind, repo } });
|
|
79356
|
+
return null;
|
|
79357
|
+
}
|
|
79358
|
+
function qualify(ctx, stack, entry, doc, verdict, grant, binding = null) {
|
|
79359
|
+
const label = entry.pr ? noun2(ctx.resolved, entry.pr) : entry.branch;
|
|
79360
|
+
const stop = (reason, detail, unblock, ready = false) => ({
|
|
79361
|
+
ok: false,
|
|
79362
|
+
reason,
|
|
79363
|
+
detail,
|
|
79364
|
+
unblock,
|
|
79365
|
+
ready
|
|
79366
|
+
});
|
|
79367
|
+
if (!entry.pr || !doc)
|
|
79368
|
+
return stop("no_pr", `${entry.branch} has no open PR`, `git switch ${entry.branch} && workit pr create --fill --base ${entry.parent} # then workit stack plan`);
|
|
79369
|
+
if (binding)
|
|
79370
|
+
return stop("pr_mismatch", binding, `workit stack plan --name ${stack.name} # re-resolve the PRs`);
|
|
79371
|
+
if (doc.state !== "open")
|
|
79372
|
+
return stop("pr_closed", `${label} is ${doc.state}`, "reopen it, or drop the branch from the stack: workit stack plan <branches…>");
|
|
79373
|
+
if (doc.base !== stack.trunk)
|
|
79374
|
+
return stop("base_not_trunk", `${label} targets ${doc.base}, not ${stack.trunk}`, "workit stack sync");
|
|
79375
|
+
if (doc.next !== "READY")
|
|
79376
|
+
return stop("not_ready", `${label} is ${doc.next}${doc.blockers.length ? ` (${doc.blockers.join(", ")})` : ""}`, doc.next === "WAITING_CI" ? `workit ci wait --pr ${entry.pr}` : `workit pr status --pr ${entry.pr}`);
|
|
79377
|
+
const requireVerdict = grant.allowed ? grant.requireVerdict : true;
|
|
79378
|
+
if (requireVerdict && !verdict.accepted)
|
|
79379
|
+
return stop(verdict.reasons.includes("no_verdict") ? "no_verdict" : "needs_verdict", `${label} has no accepted independent verdict for ${doc.head.sha.slice(0, 12)} (${verdict.reasons.join(", ") || "none"})`, `an independent session verifies ${entry.branch} and runs: workit ledger verdict verified --how "<what was exercised>" --branch ${entry.branch}`);
|
|
79380
|
+
if (!grant.allowed)
|
|
79381
|
+
return stop("grant_required", `${label} is verified and ready, but ${grant.error}`, grant.unblock, true);
|
|
79382
|
+
return { ok: true };
|
|
79383
|
+
}
|
|
79384
|
+
function stackStatus(ctx, stack) {
|
|
79385
|
+
const mismatch = repoMismatch(ctx, stack);
|
|
79386
|
+
if (mismatch)
|
|
79387
|
+
return mismatch;
|
|
79388
|
+
const ledger = readLedger(ctx.cwd);
|
|
79389
|
+
if (!ledger.ok)
|
|
79390
|
+
return stackFail(ledger.code, ledger.error, ledger.unblock);
|
|
79391
|
+
const remote = remoteFor(ctx.cwd, ctx.resolved, stack.trunk);
|
|
79392
|
+
const trunkSha = trunkTip(ctx.cwd, remote, stack.trunk);
|
|
79393
|
+
const grant = requireGrant(ctx.cwd, "merge");
|
|
79394
|
+
const branches = [];
|
|
79395
|
+
let verdict = null;
|
|
79396
|
+
let reason = null;
|
|
79397
|
+
let next = null;
|
|
79398
|
+
let parentName = stack.trunk;
|
|
79399
|
+
let parentSha = trunkSha;
|
|
79400
|
+
let mergedSeen = false;
|
|
79401
|
+
for (const entry of stack.branches) {
|
|
79402
|
+
let doc = null;
|
|
79403
|
+
if (entry.pr) {
|
|
79404
|
+
const read = statusDoc(ctx, entry.pr);
|
|
79405
|
+
if (!read.ok)
|
|
79406
|
+
return fromForge(read);
|
|
79407
|
+
doc = read.data;
|
|
79408
|
+
}
|
|
79409
|
+
const merged = entry.merged !== null || doc?.state === "merged";
|
|
79410
|
+
const head = branchTip(ctx.cwd, entry.branch);
|
|
79411
|
+
const row = {
|
|
79412
|
+
branch: entry.branch,
|
|
79413
|
+
parent: parentName,
|
|
79414
|
+
pr: entry.pr,
|
|
79415
|
+
merged,
|
|
79416
|
+
head,
|
|
79417
|
+
onParent: merged || !head || !parentSha ? null : isAncestor(ctx.cwd, parentSha, head),
|
|
79418
|
+
prState: doc?.state ?? null,
|
|
79419
|
+
prBase: doc?.base ?? null,
|
|
79420
|
+
baseMatches: doc && !merged ? doc.base === parentName : null,
|
|
79421
|
+
next: doc?.next ?? null,
|
|
79422
|
+
checks: doc?.checks.state ?? null,
|
|
79423
|
+
blockers: doc?.blockers ?? [],
|
|
79424
|
+
verdict: merged ? { accepted: true, basis: "none", reasons: [], verdictId: null } : verdictOf(ctx.cwd, entry.branch, ledger.value.rows)
|
|
79425
|
+
};
|
|
79426
|
+
branches.push(row);
|
|
79427
|
+
if (merged) {
|
|
79428
|
+
mergedSeen = true;
|
|
79429
|
+
continue;
|
|
79430
|
+
}
|
|
79431
|
+
if (verdict === null) {
|
|
79432
|
+
const stale = row.onParent === false || row.baseMatches === false;
|
|
79433
|
+
if (mergedSeen && stale) {
|
|
79434
|
+
verdict = "ADVANCE";
|
|
79435
|
+
reason = "a parent merged; the rest is not restacked or retargeted yet";
|
|
79436
|
+
next = "workit stack sync";
|
|
79437
|
+
} else {
|
|
79438
|
+
const q = qualify(ctx, stack, entry, doc, row.verdict, grant, doc ? prBinding(ctx, entry, doc) : null);
|
|
79439
|
+
if (q.ok) {
|
|
79440
|
+
verdict = "READY";
|
|
79441
|
+
next = "workit stack land";
|
|
79442
|
+
} else {
|
|
79443
|
+
verdict = "WAITING";
|
|
79444
|
+
reason = `${q.reason}: ${q.detail}`;
|
|
79445
|
+
next = q.unblock;
|
|
79446
|
+
}
|
|
79447
|
+
}
|
|
79448
|
+
}
|
|
79449
|
+
parentName = entry.branch;
|
|
79450
|
+
parentSha = head;
|
|
79451
|
+
}
|
|
79452
|
+
return {
|
|
79453
|
+
ok: true,
|
|
79454
|
+
data: {
|
|
79455
|
+
name: stack.name,
|
|
79456
|
+
trunk: stack.trunk,
|
|
79457
|
+
verdict: verdict ?? "COMPLETE",
|
|
79458
|
+
reason,
|
|
79459
|
+
next,
|
|
79460
|
+
branches
|
|
79461
|
+
}
|
|
79462
|
+
};
|
|
79463
|
+
}
|
|
79464
|
+
function detectMerged(ctx, cwd, stack, trunkSha, state) {
|
|
79465
|
+
const newly = [];
|
|
79466
|
+
for (const entry of stack.branches) {
|
|
79467
|
+
if (entry.merged)
|
|
79468
|
+
continue;
|
|
79469
|
+
let merged = false;
|
|
79470
|
+
if (entry.pr && ctx) {
|
|
79471
|
+
const read = statusDoc(ctx, entry.pr);
|
|
79472
|
+
if (!read.ok)
|
|
79473
|
+
return fromForge(read);
|
|
79474
|
+
if (read.data.head.branch !== entry.branch)
|
|
79475
|
+
return stackFail("blocked", `pr_mismatch: ${noun2(ctx.resolved, entry.pr)} is for ${read.data.head.branch}, not ${entry.branch}`, `workit stack plan --name ${stack.name} # re-resolve the PRs`, { branch: entry.branch, pr: entry.pr });
|
|
79476
|
+
state.docs.set(entry.pr, read.data);
|
|
79477
|
+
merged = read.data.state === "merged";
|
|
79478
|
+
} else {
|
|
79479
|
+
const tip = branchTip(cwd, entry.branch);
|
|
79480
|
+
merged = tip !== null && hasOwnCommits(cwd, tip, entry.lastParentHead) && isAncestor(cwd, tip, trunkSha);
|
|
79481
|
+
}
|
|
79482
|
+
if (merged) {
|
|
79483
|
+
entry.merged = { pr: entry.pr, mergeSha: null, at: new Date().toISOString() };
|
|
79484
|
+
newly.push({ branch: entry.branch, pr: entry.pr });
|
|
79485
|
+
}
|
|
79486
|
+
}
|
|
79487
|
+
return { ok: true, data: newly };
|
|
79488
|
+
}
|
|
79489
|
+
function pushBranch(cwd, actor, branch, priorTip) {
|
|
79490
|
+
const plan = stackDeps.pushPlan(cwd, branch);
|
|
79491
|
+
if (!plan.ok)
|
|
79492
|
+
return fromForge(plan);
|
|
79493
|
+
const remote = remoteRefTip(cwd, plan.data.rawUrl, `refs/heads/${branch}`);
|
|
79494
|
+
if (!remote.ok)
|
|
79495
|
+
return stackFail(remote.code, remote.error);
|
|
79496
|
+
if (remote.sha === plan.data.sha)
|
|
79497
|
+
return { ok: true, data: { pushed: false, forced: false, sha: plan.data.sha } };
|
|
79498
|
+
const grant = requireGrant(cwd, "push", { forge: !plan.data.local });
|
|
79499
|
+
if (!grant.allowed)
|
|
79500
|
+
return stackFail("blocked", grant.error, grant.unblock, { reason: grant.reason });
|
|
79501
|
+
const force = remote.sha !== null && !isAncestor(cwd, remote.sha, plan.data.sha);
|
|
79502
|
+
const expect = !force ? null : remote.sha === priorTip ? priorTip : recordedRemoteTip(cwd, plan.data) ?? null;
|
|
79503
|
+
const pushed = executePush(cwd, plan.data, { forceWithLease: force, expect, actor });
|
|
79504
|
+
if (!pushed.ok)
|
|
79505
|
+
return { ...fromForge(pushed), data: { branch } };
|
|
79506
|
+
return {
|
|
79507
|
+
ok: true,
|
|
79508
|
+
data: { pushed: pushed.data.pushed, forced: pushed.data.forced, sha: plan.data.sha }
|
|
79509
|
+
};
|
|
79510
|
+
}
|
|
79511
|
+
function syncStack(cwd, ctx, stack, options, actor) {
|
|
79512
|
+
const remote = remoteFor(cwd, ctx?.resolved ?? null, stack.trunk);
|
|
79513
|
+
let observedTrunk = null;
|
|
79514
|
+
if (!options.dryRun && safeRef2(stack.trunk)) {
|
|
79515
|
+
const fetched = fetchRefs(cwd, remote, [
|
|
79516
|
+
`+refs/heads/${stack.trunk}:refs/remotes/${remote}/${stack.trunk}`
|
|
79517
|
+
]);
|
|
79518
|
+
if (!fetched.ok)
|
|
79519
|
+
return stackFail(fetched.code, fetched.error, `git fetch ${remote} ${stack.trunk}`);
|
|
79520
|
+
} else if (safeRef2(stack.trunk)) {
|
|
79521
|
+
const remoteTrunk = remoteRefTip(cwd, remote, `refs/heads/${stack.trunk}`);
|
|
79522
|
+
if (remoteTrunk.ok && remoteTrunk.sha) {
|
|
79523
|
+
if (!hasCommit(cwd, remoteTrunk.sha))
|
|
79524
|
+
fetchRefs(cwd, remote, [remoteTrunk.sha]);
|
|
79525
|
+
if (hasCommit(cwd, remoteTrunk.sha))
|
|
79526
|
+
observedTrunk = remoteTrunk.sha;
|
|
79527
|
+
}
|
|
79528
|
+
}
|
|
79529
|
+
const trunkSha = observedTrunk ?? trunkTip(cwd, remote, stack.trunk);
|
|
79530
|
+
if (!trunkSha)
|
|
79531
|
+
return stackFail("not_found", `trunk ${stack.trunk} is not available`, `git fetch ${remote} ${stack.trunk}`);
|
|
79532
|
+
if (ctx) {
|
|
79533
|
+
const mismatch = repoMismatch(ctx, stack);
|
|
79534
|
+
if (mismatch)
|
|
79535
|
+
return mismatch;
|
|
79536
|
+
}
|
|
79537
|
+
const state = { docs: new Map };
|
|
79538
|
+
const merged = detectMerged(ctx, cwd, stack, trunkSha, state);
|
|
79539
|
+
if (!merged.ok)
|
|
79540
|
+
return merged;
|
|
79541
|
+
const outcome = {
|
|
79542
|
+
name: stack.name,
|
|
79543
|
+
dryRun: options.dryRun,
|
|
79544
|
+
merged: merged.data,
|
|
79545
|
+
steps: []
|
|
79546
|
+
};
|
|
79547
|
+
pruneScratch(cwd);
|
|
79548
|
+
let parent = { name: stack.trunk, tip: trunkSha };
|
|
79549
|
+
let processed = 0;
|
|
79550
|
+
for (const entry of stack.branches) {
|
|
79551
|
+
if (entry.merged)
|
|
79552
|
+
continue;
|
|
79553
|
+
if (options.limit !== undefined && processed >= options.limit)
|
|
79554
|
+
break;
|
|
79555
|
+
processed += 1;
|
|
79556
|
+
touchStackLock();
|
|
79557
|
+
const tip = branchTip(cwd, entry.branch);
|
|
79558
|
+
if (!tip)
|
|
79559
|
+
return stackFail("not_found", `stack branch ${entry.branch} does not exist locally`, `git branch ${entry.branch} ${remote}/${entry.branch} # then workit stack sync`, { progress: outcome });
|
|
79560
|
+
const step = {
|
|
79561
|
+
branch: entry.branch,
|
|
79562
|
+
pr: entry.pr,
|
|
79563
|
+
parent: parent.name,
|
|
79564
|
+
restack: null,
|
|
79565
|
+
push: null,
|
|
79566
|
+
retarget: null
|
|
79567
|
+
};
|
|
79568
|
+
let newTip = tip;
|
|
79569
|
+
const parentTip = parent.tip;
|
|
79570
|
+
if (parentTip === null) {
|
|
79571
|
+
step.restack = {
|
|
79572
|
+
from: entry.lastParentHead ?? "?",
|
|
79573
|
+
onto: `${parent.name} (restacked)`,
|
|
79574
|
+
head: null,
|
|
79575
|
+
carried: null
|
|
79576
|
+
};
|
|
79577
|
+
newTip = null;
|
|
79578
|
+
} else if (!isAncestor(cwd, parentTip, tip)) {
|
|
79579
|
+
const pending = entry.pendingRestack ?? null;
|
|
79580
|
+
const from = pending && isAncestor(cwd, pending.onto, tip) ? pending.onto : entry.lastParentHead && isAncestor(cwd, entry.lastParentHead, tip) ? entry.lastParentHead : mergeBase(cwd, entry.lastParentHead ?? parentTip, tip) ?? parentTip;
|
|
79581
|
+
if (options.dryRun) {
|
|
79582
|
+
step.restack = { from, onto: parentTip, head: null, carried: null };
|
|
79583
|
+
newTip = null;
|
|
79584
|
+
} else {
|
|
79585
|
+
touchStackLock();
|
|
79586
|
+
const merges = gitRun2(cwd, ["rev-list", "--merges", `${from}..${tip}`]);
|
|
79587
|
+
if (!merges.ok || merges.stdout.trim())
|
|
79588
|
+
return stackFail("blocked", `merge_commits: ${entry.branch} has merge commits since ${from.slice(0, 12)}; restacking would drop what they resolved`, `git rebase --rebase-merges --onto ${parentTip} ${from} ${entry.branch} # check the result, then: workit stack sync`, {
|
|
79589
|
+
branch: entry.branch,
|
|
79590
|
+
merges: merges.stdout.trim().split(`
|
|
79591
|
+
`).filter(Boolean),
|
|
79592
|
+
progress: outcome
|
|
79593
|
+
});
|
|
79594
|
+
const owner = worktreeOf(cwd, entry.branch);
|
|
79595
|
+
if (owner && rebaseInProgress(owner))
|
|
79596
|
+
return stackFail("blocked", `a rebase is in progress in ${owner}`, `cd ${owner} && git rebase --continue # or --abort; then workit stack sync`, { conflict: entry.branch, worktree: owner, progress: outcome });
|
|
79597
|
+
if (owner && trackedDirt(owner))
|
|
79598
|
+
return stackFail("blocked", `dirty_worktree: ${owner} has ${entry.branch} checked out with uncommitted changes; restacking needs it clean`, `commit or stash the changes in ${owner}, then workit stack sync`, { branch: entry.branch, worktree: owner, progress: outcome });
|
|
79599
|
+
entry.pendingRestack = pending ? { ...pending, onto: parentTip } : { from, tip, onto: parentTip };
|
|
79600
|
+
const recorded = writeStack(cwd, stack);
|
|
79601
|
+
if (!recorded.ok)
|
|
79602
|
+
return recorded;
|
|
79603
|
+
const rebased = stackDeps.adapter.restack(cwd, {
|
|
79604
|
+
branch: entry.branch,
|
|
79605
|
+
onto: parentTip,
|
|
79606
|
+
from
|
|
79607
|
+
});
|
|
79608
|
+
touchStackLock();
|
|
79609
|
+
if (!rebased.ok) {
|
|
79610
|
+
outcome.steps.push(step);
|
|
79611
|
+
if (rebased.timedOut)
|
|
79612
|
+
return stackFail("failed", `timed_out: restacking ${entry.branch} onto ${parent.name}: ${rebased.error}`, `finish or abort the rebase in ${rebased.worktree} (git rebase --continue | --abort), then workit stack sync # slow hooks: raise WORKIT_STACK_REBASE_TIMEOUT_MS`, {
|
|
79613
|
+
reason: "timed_out",
|
|
79614
|
+
branch: entry.branch,
|
|
79615
|
+
worktree: rebased.worktree,
|
|
79616
|
+
progress: outcome
|
|
79617
|
+
});
|
|
79618
|
+
return rebased.conflict ? stackFail("blocked", `conflict restacking ${entry.branch} onto ${parent.name}: ${rebased.error}`, `resolve the conflicts in ${rebased.worktree}, git rebase --continue, then workit stack sync # or git rebase --abort`, { conflict: entry.branch, worktree: rebased.worktree, progress: outcome }) : stackFail("failed", `restacking ${entry.branch} failed: ${rebased.error}`, undefined, {
|
|
79619
|
+
branch: entry.branch,
|
|
79620
|
+
progress: outcome
|
|
79621
|
+
});
|
|
79622
|
+
}
|
|
79623
|
+
newTip = rebased.head;
|
|
79624
|
+
step.restack = { from, onto: parentTip, head: newTip, carried: null };
|
|
79625
|
+
}
|
|
79626
|
+
}
|
|
79627
|
+
if (!options.dryRun && newTip) {
|
|
79628
|
+
const check = entry.pendingRestack && parentTip && isAncestor(cwd, parentTip, newTip) ? entry.pendingRestack : null;
|
|
79629
|
+
let forced = false;
|
|
79630
|
+
if (check && parentTip) {
|
|
79631
|
+
const before = {
|
|
79632
|
+
d: diffHash(cwd, check.from, check.tip),
|
|
79633
|
+
commits: commitCount(cwd, check.from, check.tip)
|
|
79634
|
+
};
|
|
79635
|
+
const after = {
|
|
79636
|
+
d: diffHash(cwd, parentTip, newTip),
|
|
79637
|
+
commits: commitCount(cwd, parentTip, newTip)
|
|
79638
|
+
};
|
|
79639
|
+
const sameContent = before.d !== null && before.d === after.d;
|
|
79640
|
+
const dropped = Math.max(0, before.commits - after.commits);
|
|
79641
|
+
if (!sameContent || dropped > 0) {
|
|
79642
|
+
if (!options.force?.has(entry.branch)) {
|
|
79643
|
+
outcome.steps.push(step);
|
|
79644
|
+
const written = writeStack(cwd, stack);
|
|
79645
|
+
if (!written.ok)
|
|
79646
|
+
return written;
|
|
79647
|
+
return stackFail("blocked", `content_changed: ${entry.branch} on ${parent.name} is not the same change it was on ${check.from.slice(0, 12)}${dropped ? ` (${dropped} commit${dropped === 1 ? "" : "s"} dropped as empty or already applied)` : ""}; nothing was pushed`, `inspect: git range-diff ${check.from}..${check.tip} ${parentTip}..${newTip} # if intended: workit stack sync --force ${entry.branch}; to undo: git branch -f ${entry.branch} ${check.tip}`, {
|
|
79648
|
+
reason: "content_changed",
|
|
79649
|
+
branch: entry.branch,
|
|
79650
|
+
before: {
|
|
79651
|
+
base: check.from,
|
|
79652
|
+
head: check.tip,
|
|
79653
|
+
commits: before.commits,
|
|
79654
|
+
stat: shortStat(cwd, check.from, check.tip)
|
|
79655
|
+
},
|
|
79656
|
+
after: {
|
|
79657
|
+
base: parentTip,
|
|
79658
|
+
head: newTip,
|
|
79659
|
+
commits: after.commits,
|
|
79660
|
+
stat: shortStat(cwd, parentTip, newTip)
|
|
79661
|
+
},
|
|
79662
|
+
dropped,
|
|
79663
|
+
progress: outcome
|
|
79664
|
+
});
|
|
79665
|
+
}
|
|
79666
|
+
forced = true;
|
|
79667
|
+
}
|
|
79668
|
+
entry.pendingRestack = null;
|
|
79669
|
+
}
|
|
79670
|
+
const moved = entry.lastHead !== null && entry.lastHead !== newTip && entry.lastParentHead !== null && parentTip !== null && parentTip !== entry.lastParentHead;
|
|
79671
|
+
if (moved && parentTip) {
|
|
79672
|
+
const fromBase = entry.lastParentHead;
|
|
79673
|
+
const fromHead = entry.lastHead;
|
|
79674
|
+
const before = {
|
|
79675
|
+
p: patchId(cwd, fromBase, fromHead),
|
|
79676
|
+
d: diffHash(cwd, fromBase, fromHead)
|
|
79677
|
+
};
|
|
79678
|
+
const after = { p: patchId(cwd, parentTip, newTip), d: diffHash(cwd, parentTip, newTip) };
|
|
79679
|
+
const equal = before.d !== null && before.d === after.d && before.p !== null && before.p === after.p;
|
|
79680
|
+
appendObserved(cwd, {
|
|
79681
|
+
type: "stack.restacked",
|
|
79682
|
+
actor,
|
|
79683
|
+
stack: stack.name,
|
|
79684
|
+
branch: entry.branch,
|
|
79685
|
+
head: newTip,
|
|
79686
|
+
fromHead,
|
|
79687
|
+
fromBase,
|
|
79688
|
+
toBase: parentTip,
|
|
79689
|
+
patchId: after.p,
|
|
79690
|
+
diffHash: after.d,
|
|
79691
|
+
patchEqual: equal,
|
|
79692
|
+
forced
|
|
79693
|
+
});
|
|
79694
|
+
if (step.restack)
|
|
79695
|
+
step.restack.carried = equal;
|
|
79696
|
+
else
|
|
79697
|
+
step.restack = { from: fromBase, onto: parentTip, head: newTip, carried: equal };
|
|
79698
|
+
}
|
|
79699
|
+
if (options.publish) {
|
|
79700
|
+
touchStackLock();
|
|
79701
|
+
const pushed = pushBranch(cwd, actor, entry.branch, entry.lastHead ?? tip);
|
|
79702
|
+
touchStackLock();
|
|
79703
|
+
if (!pushed.ok) {
|
|
79704
|
+
outcome.steps.push(step);
|
|
79705
|
+
return { ...pushed, data: { ...pushed.data, progress: outcome } };
|
|
79706
|
+
}
|
|
79707
|
+
step.push = pushed.data;
|
|
79708
|
+
}
|
|
79709
|
+
entry.lastHead = newTip;
|
|
79710
|
+
entry.lastParentHead = parentTip;
|
|
79711
|
+
entry.patchId = parentTip ? patchId(cwd, parentTip, newTip) : null;
|
|
79712
|
+
}
|
|
79713
|
+
if (options.publish && ctx && entry.pr) {
|
|
79714
|
+
const doc = state.docs.get(entry.pr);
|
|
79715
|
+
if (doc && doc.state === "open" && doc.base !== parent.name) {
|
|
79716
|
+
step.retarget = { from: doc.base, to: parent.name };
|
|
79717
|
+
if (!options.dryRun) {
|
|
79718
|
+
const grant = requireGrant(cwd, "pr");
|
|
79719
|
+
if (!grant.allowed)
|
|
79720
|
+
return stackFail("blocked", grant.error, grant.unblock, {
|
|
79721
|
+
reason: grant.reason,
|
|
79722
|
+
progress: outcome
|
|
79723
|
+
});
|
|
79724
|
+
const updated = ctx.resolved.forge.updateBase(entry.pr, parent.name);
|
|
79725
|
+
if (!updated.ok)
|
|
79726
|
+
return { ...fromForge(updated), data: { branch: entry.branch, progress: outcome } };
|
|
79727
|
+
appendObserved(cwd, {
|
|
79728
|
+
type: "pr.retargeted",
|
|
79729
|
+
actor,
|
|
79730
|
+
stack: stack.name,
|
|
79731
|
+
branch: entry.branch,
|
|
79732
|
+
head: newTip,
|
|
79733
|
+
pr: entry.pr,
|
|
79734
|
+
base: parent.name,
|
|
79735
|
+
previousBase: doc.base
|
|
79736
|
+
});
|
|
79737
|
+
}
|
|
79738
|
+
}
|
|
79739
|
+
}
|
|
79740
|
+
entry.parent = parent.name;
|
|
79741
|
+
outcome.steps.push(step);
|
|
79742
|
+
if (!options.dryRun) {
|
|
79743
|
+
const written = writeStack(cwd, stack);
|
|
79744
|
+
if (!written.ok)
|
|
79745
|
+
return written;
|
|
79746
|
+
}
|
|
79747
|
+
parent = { name: entry.branch, tip: newTip };
|
|
79748
|
+
}
|
|
79749
|
+
if (!options.dryRun) {
|
|
79750
|
+
const written = writeStack(cwd, stack);
|
|
79751
|
+
if (!written.ok)
|
|
79752
|
+
return written;
|
|
79753
|
+
}
|
|
79754
|
+
return { ok: true, data: outcome };
|
|
79755
|
+
}
|
|
79756
|
+
async function awaitChecks(ctx, pr, head, options) {
|
|
79757
|
+
let waited = 0;
|
|
79758
|
+
for (let poll = 0;; poll += 1) {
|
|
79759
|
+
touchStackLock();
|
|
79760
|
+
const doc = statusDoc(ctx, pr);
|
|
79761
|
+
if (!doc.ok)
|
|
79762
|
+
return doc;
|
|
79763
|
+
const verdict = waitVerdict(doc.data, { head, elapsedMs: waited });
|
|
79764
|
+
if (verdict.state !== "waiting" || waited >= options.timeoutMs)
|
|
79765
|
+
return doc;
|
|
79766
|
+
const delay = Math.min(pollDelay(options.intervalMs, poll), Math.max(1, options.timeoutMs - waited));
|
|
79767
|
+
await ctx.sleep(delay);
|
|
79768
|
+
waited += delay;
|
|
79769
|
+
}
|
|
79770
|
+
}
|
|
79771
|
+
async function landStack(ctx, stack, options) {
|
|
79772
|
+
const outcome = {
|
|
79773
|
+
name: stack.name,
|
|
79774
|
+
dryRun: options.dryRun,
|
|
79775
|
+
landed: [],
|
|
79776
|
+
wouldLand: [],
|
|
79777
|
+
stoppedAt: null,
|
|
79778
|
+
retargeted: [],
|
|
79779
|
+
restacked: [],
|
|
79780
|
+
complete: false
|
|
79781
|
+
};
|
|
79782
|
+
const absorb = (sync) => {
|
|
79783
|
+
for (const step of sync.steps) {
|
|
79784
|
+
if (step.retarget && step.pr && !outcome.retargeted.includes(step.pr))
|
|
79785
|
+
outcome.retargeted.push(step.pr);
|
|
79786
|
+
if (step.restack?.head && !outcome.restacked.includes(step.branch))
|
|
79787
|
+
outcome.restacked.push(step.branch);
|
|
79788
|
+
}
|
|
79789
|
+
};
|
|
79790
|
+
const failWith = (error) => ({
|
|
79791
|
+
...error,
|
|
79792
|
+
data: { ...error.data, land: outcome }
|
|
79793
|
+
});
|
|
79794
|
+
const mismatch = repoMismatch(ctx, stack);
|
|
79795
|
+
if (mismatch)
|
|
79796
|
+
return failWith(mismatch);
|
|
79797
|
+
if (!options.dryRun) {
|
|
79798
|
+
const synced = syncStack(ctx.cwd, ctx, stack, { publish: true, dryRun: false, limit: 1 }, ctx.actor);
|
|
79799
|
+
if (!synced.ok)
|
|
79800
|
+
return failWith(synced);
|
|
79801
|
+
absorb(synced.data);
|
|
79802
|
+
}
|
|
79803
|
+
const ledgerRows = () => {
|
|
79804
|
+
const ledger = readLedger(ctx.cwd);
|
|
79805
|
+
return ledger.ok ? ledger.value.rows : [];
|
|
79806
|
+
};
|
|
79807
|
+
const simulated = new Set;
|
|
79808
|
+
let justRestacked = new Set(outcome.restacked);
|
|
79809
|
+
for (;; ) {
|
|
79810
|
+
touchStackLock();
|
|
79811
|
+
const entry = stack.branches.find((candidate) => !candidate.merged && !simulated.has(candidate.branch));
|
|
79812
|
+
if (!entry) {
|
|
79813
|
+
outcome.complete = !options.dryRun || stack.branches.every((candidate) => candidate.merged || simulated.has(candidate.branch));
|
|
79814
|
+
break;
|
|
79815
|
+
}
|
|
79816
|
+
if (options.max !== null && outcome.landed.length + outcome.wouldLand.length >= options.max) {
|
|
79817
|
+
outcome.stoppedAt = {
|
|
79818
|
+
pr: entry.pr,
|
|
79819
|
+
branch: entry.branch,
|
|
79820
|
+
reason: "max_reached",
|
|
79821
|
+
detail: `--max ${options.max} reached`,
|
|
79822
|
+
unblock: "workit stack land",
|
|
79823
|
+
ready: false
|
|
79824
|
+
};
|
|
79825
|
+
break;
|
|
79826
|
+
}
|
|
79827
|
+
let doc = null;
|
|
79828
|
+
if (entry.pr) {
|
|
79829
|
+
const head = branchTip(ctx.cwd, entry.branch);
|
|
79830
|
+
const read = justRestacked.has(entry.branch) && head ? await awaitChecks(ctx, entry.pr, head, options) : statusDoc(ctx, entry.pr);
|
|
79831
|
+
if (!read.ok)
|
|
79832
|
+
return failWith(fromForge(read));
|
|
79833
|
+
doc = read.data;
|
|
79834
|
+
if (options.dryRun && doc.state === "merged") {
|
|
79835
|
+
simulated.add(entry.branch);
|
|
79836
|
+
continue;
|
|
79837
|
+
}
|
|
79838
|
+
}
|
|
79839
|
+
const grant = requireGrant(ctx.cwd, "merge");
|
|
79840
|
+
const verdict = verdictOf(ctx.cwd, entry.branch, ledgerRows());
|
|
79841
|
+
const effective = options.dryRun && simulated.size > 0 && doc ? { ...doc, base: stack.trunk } : doc;
|
|
79842
|
+
const q = qualify(ctx, stack, entry, effective, verdict, grant, doc ? prBinding(ctx, entry, doc) : null);
|
|
79843
|
+
if (!q.ok) {
|
|
79844
|
+
outcome.stoppedAt = {
|
|
79845
|
+
pr: entry.pr,
|
|
79846
|
+
branch: entry.branch,
|
|
79847
|
+
reason: q.reason,
|
|
79848
|
+
detail: q.detail,
|
|
79849
|
+
unblock: q.unblock,
|
|
79850
|
+
ready: q.ready
|
|
79851
|
+
};
|
|
79852
|
+
break;
|
|
79853
|
+
}
|
|
79854
|
+
const pr = entry.pr;
|
|
79855
|
+
if (options.dryRun) {
|
|
79856
|
+
outcome.wouldLand.push({ pr, branch: entry.branch });
|
|
79857
|
+
simulated.add(entry.branch);
|
|
79858
|
+
continue;
|
|
79859
|
+
}
|
|
79860
|
+
const merged = await mergePullRequest(ctx.cwd, ctx.resolved, {
|
|
79861
|
+
pr,
|
|
79862
|
+
method: options.method,
|
|
79863
|
+
deleteBranch: false,
|
|
79864
|
+
actor: ctx.actor,
|
|
79865
|
+
expect: { base: stack.trunk, branch: entry.branch }
|
|
79866
|
+
}, ctx.sleep);
|
|
79867
|
+
if (!merged.ok) {
|
|
79868
|
+
if (merged.code === "blocked" && "refusal" in merged && merged.refusal) {
|
|
79869
|
+
outcome.stoppedAt = {
|
|
79870
|
+
pr,
|
|
79871
|
+
branch: entry.branch,
|
|
79872
|
+
reason: "merge_refused",
|
|
79873
|
+
detail: merged.error,
|
|
79874
|
+
unblock: merged.unblock ?? `workit pr status --pr ${pr}`,
|
|
79875
|
+
ready: false
|
|
79876
|
+
};
|
|
79877
|
+
break;
|
|
79878
|
+
}
|
|
79879
|
+
return failWith(stackFail(merged.code, merged.error, merged.unblock));
|
|
79880
|
+
}
|
|
79881
|
+
outcome.landed.push({ pr, branch: entry.branch, mergeSha: merged.data.mergeSha });
|
|
79882
|
+
entry.merged = { pr, mergeSha: merged.data.mergeSha, at: new Date().toISOString() };
|
|
79883
|
+
const written = writeStack(ctx.cwd, stack);
|
|
79884
|
+
if (!written.ok)
|
|
79885
|
+
return failWith(written);
|
|
79886
|
+
const before = new Set(outcome.restacked);
|
|
79887
|
+
const synced = syncStack(ctx.cwd, ctx, stack, { publish: true, dryRun: false, limit: 1 }, ctx.actor);
|
|
79888
|
+
if (!synced.ok)
|
|
79889
|
+
return failWith(synced);
|
|
79890
|
+
absorb(synced.data);
|
|
79891
|
+
justRestacked = new Set(outcome.restacked.filter((branch) => !before.has(branch)));
|
|
79892
|
+
for (const step of synced.data.steps)
|
|
79893
|
+
if (step.restack?.head)
|
|
79894
|
+
justRestacked.add(step.branch);
|
|
79895
|
+
}
|
|
79896
|
+
return { ok: true, data: outcome };
|
|
79897
|
+
}
|
|
79898
|
+
var STACK_VERSION = 1, stackFail = (code, error, unblock, data) => ({
|
|
79899
|
+
ok: false,
|
|
79900
|
+
code,
|
|
79901
|
+
error,
|
|
79902
|
+
...unblock ? { unblock } : {},
|
|
79903
|
+
...data ? { data } : {}
|
|
79904
|
+
}), fromForge = (result) => stackFail(result.code, result.error, result.unblock), stackDeps, gitRun2 = (cwd, args, timeoutMs = GIT_TIMEOUTS.local) => {
|
|
79905
|
+
const run = spawnSync21("git", args, {
|
|
79906
|
+
cwd,
|
|
79907
|
+
encoding: "utf8",
|
|
79908
|
+
timeout: timeoutMs,
|
|
79909
|
+
killSignal: "SIGKILL",
|
|
79910
|
+
windowsHide: true,
|
|
79911
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
79912
|
+
env: {
|
|
79913
|
+
...process.env,
|
|
79914
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
79915
|
+
GIT_EDITOR: "true",
|
|
79916
|
+
GIT_SEQUENCE_EDITOR: "true",
|
|
79917
|
+
LC_ALL: "C",
|
|
79918
|
+
LANGUAGE: "C"
|
|
79919
|
+
}
|
|
79920
|
+
});
|
|
79921
|
+
const failure = run.error;
|
|
79922
|
+
return {
|
|
79923
|
+
ok: run.status === 0,
|
|
79924
|
+
stdout: run.stdout ?? "",
|
|
79925
|
+
stderr: run.error ? String(run.error.message) : run.stderr ?? "",
|
|
79926
|
+
timedOut: failure?.code === "ETIMEDOUT"
|
|
79927
|
+
};
|
|
79928
|
+
}, safeRef2 = (value) => value.length > 0 && !value.startsWith("-"), isAncestor = (cwd, ancestor, descendant) => safeRef2(ancestor) && safeRef2(descendant) && gitRun2(cwd, ["merge-base", "--is-ancestor", ancestor, descendant]).ok, branchTip = (cwd, branch) => resolveRef(cwd, `refs/heads/${branch}`), trackedDirt = (dir) => {
|
|
79929
|
+
const status = gitRun2(dir, ["status", "--porcelain", "--untracked-files=no"]);
|
|
79930
|
+
return !status.ok || status.stdout.trim() !== "";
|
|
79931
|
+
}, scratchDir = (cwd) => {
|
|
79932
|
+
const dir = stacksDir(cwd);
|
|
79933
|
+
return dir.ok ? path42.join(dir.data, "worktrees") : null;
|
|
79934
|
+
}, firstLine = (run) => (`${run.stderr}
|
|
79935
|
+
${run.stdout}`.split(`
|
|
79936
|
+
`).find((line) => line.trim()) ?? "git failed").trim().slice(0, 300), stackFileName = (name) => `${name.replace(/[^A-Za-z0-9._-]+/gu, "_").replace(/^[._]+/u, "").slice(0, 48).toLowerCase()}-${createHash15("sha256").update(name).digest("hex").slice(0, 12)}.json`, fileFor, validName = (name) => {
|
|
79937
|
+
if (name.length === 0 || name.length > 200)
|
|
79938
|
+
return false;
|
|
79939
|
+
for (let index = 0;index < name.length; index += 1) {
|
|
79940
|
+
const code = name.charCodeAt(index);
|
|
79941
|
+
if (code < 32 || code === 127)
|
|
79942
|
+
return false;
|
|
79943
|
+
}
|
|
79944
|
+
return true;
|
|
79945
|
+
}, isRecord7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), str2 = (value) => typeof value === "string" && value ? value : null, LOCK_WAIT_MS3 = 2000, HEARTBEAT_MS = 20000, HEARTBEAT_STALE_MS, OWNERLESS_STALE_MS = 30000, OWNER = "owner", readOwner = (lock) => {
|
|
79946
|
+
let raw = null;
|
|
79947
|
+
try {
|
|
79948
|
+
raw = fs23.readFileSync(path42.join(lock, OWNER), "utf8");
|
|
79949
|
+
} catch {
|
|
79950
|
+
raw = null;
|
|
79951
|
+
}
|
|
79952
|
+
let age = null;
|
|
79953
|
+
for (const target of [path42.join(lock, OWNER), lock])
|
|
79954
|
+
try {
|
|
79955
|
+
age = Date.now() - fs23.statSync(target).mtimeMs;
|
|
79956
|
+
break;
|
|
79957
|
+
} catch {}
|
|
79958
|
+
return { raw, age };
|
|
79959
|
+
}, activeBeat = null, touchStackLock = () => activeBeat?.(), remoteFor = (cwd, resolved, trunk) => resolved?.baseRemote ?? pushRemoteName(cwd, trunk) ?? "origin", trunkTip = (cwd, remote, trunk) => resolveRef(cwd, `refs/remotes/${remote}/${trunk}`) ?? branchTip(cwd, trunk), noun2 = (resolved, pr) => `${resolved.forge.kind === "github" ? "PR #" : "MR !"}${pr}`, verdictOf = (cwd, branch, rows) => {
|
|
79960
|
+
const check = checkVerdicts(cwd, branch, rows);
|
|
79961
|
+
return {
|
|
79962
|
+
accepted: check.accepted.accepted,
|
|
79963
|
+
basis: check.current.basis,
|
|
79964
|
+
reasons: check.accepted.reasons,
|
|
79965
|
+
verdictId: typeof check.accepted.verdict?.id === "string" && check.accepted.accepted ? check.accepted.verdict.id : null
|
|
79966
|
+
};
|
|
79967
|
+
}, commitCount = (cwd, base, head) => Number(gitRun2(cwd, ["rev-list", "--count", "--no-merges", `${base}..${head}`]).stdout.trim()) || 0, shortStat = (cwd, base, head) => gitRun2(cwd, ["diff", "--shortstat", `${base}...${head}`]).stdout.trim();
|
|
79968
|
+
var init_stack = __esm(() => {
|
|
79969
|
+
init_autonomy();
|
|
79970
|
+
init_store_lock();
|
|
79971
|
+
init_pr_ops();
|
|
79972
|
+
init_report();
|
|
79973
|
+
init_ops();
|
|
79974
|
+
init_rev();
|
|
79975
|
+
init_ledger();
|
|
79976
|
+
stackDeps = {
|
|
79977
|
+
pushPlan: (cwd, branch) => pushPreflight(cwd, { branch }),
|
|
79978
|
+
adapter: { name: "git", restack: (cwd, input) => gitRestack(cwd, input) },
|
|
79979
|
+
rebaseTimeoutMs: Number(process.env.WORKIT_STACK_REBASE_TIMEOUT_MS) || 10 * 60000
|
|
79980
|
+
};
|
|
79981
|
+
fileFor = stackFileName;
|
|
79982
|
+
HEARTBEAT_STALE_MS = 2 * 60000;
|
|
78741
79983
|
});
|
|
78742
79984
|
|
|
78743
79985
|
// packages/workit-cli/src/verbs/stack.ts
|
|
@@ -78745,10 +79987,199 @@ var exports_stack = {};
|
|
|
78745
79987
|
__export(exports_stack, {
|
|
78746
79988
|
run: () => run17
|
|
78747
79989
|
});
|
|
78748
|
-
|
|
78749
|
-
|
|
78750
|
-
|
|
78751
|
-
|
|
79990
|
+
async function plan(argv, io) {
|
|
79991
|
+
const flags = parseFlags(argv, { name: "value", trunk: "value" });
|
|
79992
|
+
if (typeof flags === "string")
|
|
79993
|
+
return usage3(io, flags, PLAN_USAGE);
|
|
79994
|
+
const name = flags.values.name ?? null;
|
|
79995
|
+
const existingByName = name ? readStack(io.cwd, name) : null;
|
|
79996
|
+
if (existingByName && !existingByName.ok)
|
|
79997
|
+
return stackFailed(io, existingByName);
|
|
79998
|
+
let existing = existingByName?.ok ? existingByName.data : null;
|
|
79999
|
+
if (!name && flags.positionals.length === 0) {
|
|
80000
|
+
const selected = selectStack(io.cwd, null);
|
|
80001
|
+
if (selected.ok)
|
|
80002
|
+
existing = selected.data;
|
|
80003
|
+
}
|
|
80004
|
+
const trunk = flags.values.trunk ?? existing?.trunk ?? defaultTrunk(io) ?? "main";
|
|
80005
|
+
const connected = connect(io, flags.positionals[0] ?? null);
|
|
80006
|
+
const resolved = connected.ok ? connected.data : null;
|
|
80007
|
+
const planned = planStack(io.cwd, resolved, { name, trunk, branches: flags.positionals }, existing);
|
|
80008
|
+
if (!planned.ok)
|
|
80009
|
+
return stackFailed(io, planned);
|
|
80010
|
+
if (!connected.ok)
|
|
80011
|
+
planned.data.notes.push(`forge: ${connected.error}${connected.unblock ? ` (${connected.unblock})` : ""}`);
|
|
80012
|
+
const result = await withStackLock(io.cwd, planned.data.stack.name, async () => {
|
|
80013
|
+
const written = writeStack(io.cwd, planned.data.stack);
|
|
80014
|
+
return written.ok ? planned : written;
|
|
80015
|
+
});
|
|
80016
|
+
if (!result.ok)
|
|
80017
|
+
return stackFailed(io, result);
|
|
80018
|
+
return emit(io, ok(result.data), (data) => [
|
|
80019
|
+
`${data.created ? "planned" : "re-planned"} stack ${data.stack.name} on ${data.stack.trunk}:`,
|
|
80020
|
+
...data.stack.branches.map((entry, index) => {
|
|
80021
|
+
const check = data.checks.find((item) => item.branch === entry.branch);
|
|
80022
|
+
return ` ${index + 1}. ${entry.branch} <- ${entry.parent} ${entry.merged ? "merged" : label(data.stack.forge, entry.pr)}${check && !check.onParent ? " (not on its parent)" : ""}${check?.baseMatches === false ? ` (PR base ${check.prBase})` : ""}`;
|
|
80023
|
+
}),
|
|
80024
|
+
...data.notes.map((note) => `note: ${note}`)
|
|
80025
|
+
]);
|
|
80026
|
+
}
|
|
80027
|
+
async function status2(argv, io) {
|
|
80028
|
+
const flags = parseFlags(argv, { name: "value" });
|
|
80029
|
+
if (typeof flags === "string")
|
|
80030
|
+
return usage3(io, flags, STATUS_USAGE2);
|
|
80031
|
+
if (flags.positionals.length)
|
|
80032
|
+
return usage3(io, `unexpected argument ${flags.positionals[0]}`, STATUS_USAGE2);
|
|
80033
|
+
const selected = selectStack(io.cwd, flags.values.name ?? null);
|
|
80034
|
+
if (!selected.ok)
|
|
80035
|
+
return stackFailed(io, selected);
|
|
80036
|
+
const connected = connect(io, selected.data.branches[0]?.branch ?? null);
|
|
80037
|
+
if (!connected.ok)
|
|
80038
|
+
return forgeFail(io, connected);
|
|
80039
|
+
const result = stackStatus({ cwd: io.cwd, resolved: connected.data, actor: actorFromEnv(io.env), sleep: forgeDeps.sleep }, selected.data);
|
|
80040
|
+
if (!result.ok)
|
|
80041
|
+
return stackFailed(io, result);
|
|
80042
|
+
return emit(io, ok(result.data), (data) => [
|
|
80043
|
+
`stack ${data.name} on ${data.trunk}: ${data.verdict}${data.reason ? ` (${data.reason})` : ""}`,
|
|
80044
|
+
...data.branches.map((row, index) => ` ${index + 1}. ${row.branch} ${label(connected.data.forge.kind, row.pr)} ${row.merged ? "merged" : `${row.next ?? "?"} · checks ${row.checks ?? "?"} · verdict ${row.verdict.accepted ? `accepted (${row.verdict.basis})` : `missing (${row.verdict.reasons.join(", ") || row.verdict.basis})`} · ${row.onParent === false ? `not on ${row.parent}` : `on ${row.parent}`}${row.baseMatches === false ? ` · PR base ${row.prBase}` : ""}`}`),
|
|
80045
|
+
...data.next ? [`next: ${data.next}`] : []
|
|
80046
|
+
]);
|
|
80047
|
+
}
|
|
80048
|
+
async function sync(argv, io) {
|
|
80049
|
+
const flags = parseFlags(argv, {
|
|
80050
|
+
name: "value",
|
|
80051
|
+
local: "boolean",
|
|
80052
|
+
"dry-run": "boolean",
|
|
80053
|
+
force: "list"
|
|
80054
|
+
});
|
|
80055
|
+
if (typeof flags === "string")
|
|
80056
|
+
return usage3(io, flags, SYNC_USAGE);
|
|
80057
|
+
if (flags.positionals.length)
|
|
80058
|
+
return usage3(io, `unexpected argument ${flags.positionals[0]}`, SYNC_USAGE);
|
|
80059
|
+
const selected = selectStack(io.cwd, flags.values.name ?? null);
|
|
80060
|
+
if (!selected.ok)
|
|
80061
|
+
return stackFailed(io, selected);
|
|
80062
|
+
const force = new Set(flags.lists.force ?? []);
|
|
80063
|
+
const unknown = [...force].filter((branch) => !selected.data.branches.some((entry) => entry.branch === branch));
|
|
80064
|
+
if (unknown.length)
|
|
80065
|
+
return usage3(io, `--force ${unknown[0]}: not a branch of stack ${selected.data.name}`, SYNC_USAGE);
|
|
80066
|
+
const local = flags.booleans.has("local");
|
|
80067
|
+
let ctx = null;
|
|
80068
|
+
if (!local) {
|
|
80069
|
+
const connected = connect(io, selected.data.branches[0]?.branch ?? null);
|
|
80070
|
+
if (!connected.ok)
|
|
80071
|
+
return forgeFail(io, connected);
|
|
80072
|
+
ctx = {
|
|
80073
|
+
cwd: io.cwd,
|
|
80074
|
+
resolved: connected.data,
|
|
80075
|
+
actor: actorFromEnv(io.env),
|
|
80076
|
+
sleep: forgeDeps.sleep
|
|
80077
|
+
};
|
|
80078
|
+
}
|
|
80079
|
+
const name = selected.data.name;
|
|
80080
|
+
const result = await withStackLock(io.cwd, name, async () => {
|
|
80081
|
+
const fresh = readStack(io.cwd, name);
|
|
80082
|
+
if (!fresh.ok)
|
|
80083
|
+
return fresh;
|
|
80084
|
+
return syncStack(io.cwd, ctx, fresh.data ?? selected.data, {
|
|
80085
|
+
publish: !local,
|
|
80086
|
+
dryRun: flags.booleans.has("dry-run"),
|
|
80087
|
+
force
|
|
80088
|
+
}, actorFromEnv(io.env));
|
|
80089
|
+
});
|
|
80090
|
+
if (!result.ok)
|
|
80091
|
+
return stackFailed(io, result);
|
|
80092
|
+
return emit(io, ok(result.data), renderSync);
|
|
80093
|
+
}
|
|
80094
|
+
async function land(argv, io) {
|
|
80095
|
+
const flags = parseFlags(argv, {
|
|
80096
|
+
name: "value",
|
|
80097
|
+
"dry-run": "boolean",
|
|
80098
|
+
max: "value",
|
|
80099
|
+
method: "value",
|
|
80100
|
+
timeout: "value",
|
|
80101
|
+
interval: "value"
|
|
80102
|
+
});
|
|
80103
|
+
if (typeof flags === "string")
|
|
80104
|
+
return usage3(io, flags, LAND_USAGE);
|
|
80105
|
+
if (flags.positionals.length)
|
|
80106
|
+
return usage3(io, `unexpected argument ${flags.positionals[0]}`, LAND_USAGE);
|
|
80107
|
+
const max = positiveInt(flags.values.max, "--max");
|
|
80108
|
+
if (typeof max === "string")
|
|
80109
|
+
return usage3(io, max, LAND_USAGE);
|
|
80110
|
+
const method = flags.values.method ?? "squash";
|
|
80111
|
+
if (!METHODS3.has(method))
|
|
80112
|
+
return usage3(io, "--method must be squash, merge or rebase", LAND_USAGE);
|
|
80113
|
+
const timeoutMs = parseDuration2(flags.values.timeout ?? "20m");
|
|
80114
|
+
const intervalMs = parseDuration2(flags.values.interval ?? "30s");
|
|
80115
|
+
if (timeoutMs === null || timeoutMs > 6 * 3600000)
|
|
80116
|
+
return usage3(io, "--timeout must be a duration up to 6h (20m, 90s)", LAND_USAGE);
|
|
80117
|
+
if (intervalMs === null || intervalMs < 1000 || intervalMs > 600000)
|
|
80118
|
+
return usage3(io, "--interval must be between 1s and 10m", LAND_USAGE);
|
|
80119
|
+
const selected = selectStack(io.cwd, flags.values.name ?? null);
|
|
80120
|
+
if (!selected.ok)
|
|
80121
|
+
return stackFailed(io, selected);
|
|
80122
|
+
const connected = connect(io, selected.data.branches[0]?.branch ?? null);
|
|
80123
|
+
if (!connected.ok)
|
|
80124
|
+
return forgeFail(io, connected);
|
|
80125
|
+
const name = selected.data.name;
|
|
80126
|
+
const result = await withStackLock(io.cwd, name, async () => {
|
|
80127
|
+
const fresh = readStack(io.cwd, name);
|
|
80128
|
+
if (!fresh.ok)
|
|
80129
|
+
return fresh;
|
|
80130
|
+
return landStack({
|
|
80131
|
+
cwd: io.cwd,
|
|
80132
|
+
resolved: connected.data,
|
|
80133
|
+
actor: actorFromEnv(io.env),
|
|
80134
|
+
sleep: forgeDeps.sleep
|
|
80135
|
+
}, fresh.data ?? selected.data, { dryRun: flags.booleans.has("dry-run"), max, method, timeoutMs, intervalMs });
|
|
80136
|
+
});
|
|
80137
|
+
if (!result.ok)
|
|
80138
|
+
return stackFailed(io, result);
|
|
80139
|
+
const kind = connected.data.forge.kind;
|
|
80140
|
+
return emit(io, ok(result.data), (data) => [
|
|
80141
|
+
...data.dryRun ? [
|
|
80142
|
+
data.wouldLand.length ? `would land: ${data.wouldLand.map((item) => `${label(kind, item.pr)} (${item.branch})`).join(", ")}` : "would land nothing"
|
|
80143
|
+
] : [
|
|
80144
|
+
data.landed.length ? `landed: ${data.landed.map((item) => `${label(kind, item.pr)} (${item.branch}) -> ${short4(item.mergeSha)}`).join(", ")}` : "landed nothing"
|
|
80145
|
+
],
|
|
80146
|
+
...data.retargeted.length ? [`retargeted: ${data.retargeted.map((pr) => label(kind, pr)).join(", ")}`] : [],
|
|
80147
|
+
...data.restacked.length ? [`restacked: ${data.restacked.join(", ")}`] : [],
|
|
80148
|
+
data.stoppedAt ? `stopped at ${label(kind, data.stoppedAt.pr)} (${data.stoppedAt.branch}): ${data.stoppedAt.reason}${data.stoppedAt.ready ? " (verified, ready)" : ""} - ${data.stoppedAt.detail}
|
|
80149
|
+
unblock: ${data.stoppedAt.unblock}` : data.complete ? "stack complete" : ""
|
|
80150
|
+
].filter(Boolean));
|
|
80151
|
+
}
|
|
80152
|
+
async function run17(argv, io) {
|
|
80153
|
+
const [sub, ...rest] = argv.filter((arg) => arg !== "--json");
|
|
80154
|
+
if (sub === "plan")
|
|
80155
|
+
return plan(rest, io);
|
|
80156
|
+
if (sub === "status")
|
|
80157
|
+
return status2(rest, io);
|
|
80158
|
+
if (sub === "sync")
|
|
80159
|
+
return sync(rest, io);
|
|
80160
|
+
if (sub === "land")
|
|
80161
|
+
return land(rest, io);
|
|
80162
|
+
return usage3(io, sub && !sub.startsWith("-") ? `unknown stack subcommand "${sub}"` : "missing stack subcommand", USAGE5);
|
|
80163
|
+
}
|
|
80164
|
+
var PLAN_USAGE = "workit stack plan [--name <n>] [--trunk <b>] [<bottom> … <top>] [--json]", STATUS_USAGE2 = "workit stack status [--name <n>] [--json]", SYNC_USAGE = "workit stack sync [--name <n>] [--local] [--dry-run] [--force <branch>]… [--json]", LAND_USAGE = "workit stack land [--name <n>] [--dry-run] [--max <n>] [--method squash|merge|rebase] [--timeout 20m] [--interval 30s] [--json]", USAGE5 = "workit stack plan|status|sync|land ... (workit help stack)", short4 = (sha) => sha ? sha.slice(0, 12) : "?", stackFailed = (io, result) => emit(io, fail(result.code, result.error, { unblock: result.unblock, data: result.data ?? {} })), defaultTrunk = (io) => {
|
|
80165
|
+
const resolved = vcsConfig("resolve", io.cwd);
|
|
80166
|
+
return resolved.ok === false ? null : String(resolved.defaultTargetBranch ?? "") || null;
|
|
80167
|
+
}, label = (forge, pr) => pr === null ? "no PR" : `${forge === "gitlab" ? "MR !" : "PR #"}${pr}`, renderSync = (data) => [
|
|
80168
|
+
`${data.dryRun ? "would sync" : "synced"} stack ${data.name}${data.merged.length ? `; merged: ${data.merged.map((item) => item.branch).join(", ")}` : ""}`,
|
|
80169
|
+
...data.steps.map((step) => [
|
|
80170
|
+
` ${step.branch} on ${step.parent}:`,
|
|
80171
|
+
step.restack ? `restacked ${short4(step.restack.from)} -> ${short4(step.restack.onto)}${step.restack.head ? ` = ${short4(step.restack.head)}` : ""}${step.restack.carried === true ? " (same change; verdict carries)" : step.restack.carried === false ? " (change differs; verdict stale)" : ""}` : "up to date",
|
|
80172
|
+
step.push ? step.push.pushed ? `pushed${step.push.forced ? " (lease)" : ""}` : "remote current" : "",
|
|
80173
|
+
step.retarget ? `retargeted ${step.retarget.from} -> ${step.retarget.to}` : ""
|
|
80174
|
+
].filter(Boolean).join(" "))
|
|
80175
|
+
], METHODS3;
|
|
80176
|
+
var init_stack2 = __esm(() => {
|
|
80177
|
+
init_ledger();
|
|
80178
|
+
init_stack();
|
|
80179
|
+
init_vcs_config();
|
|
80180
|
+
init_output();
|
|
80181
|
+
init_forge_common();
|
|
80182
|
+
METHODS3 = new Set(["squash", "merge", "rebase"]);
|
|
78752
80183
|
});
|
|
78753
80184
|
|
|
78754
80185
|
// packages/workit-cli/src/verbs/ledger.ts
|
|
@@ -78866,12 +80297,12 @@ async function run18(argv, io) {
|
|
|
78866
80297
|
evidenceRefs: values.evidence
|
|
78867
80298
|
}), (row) => `recorded verdict ${row.id}: ${row.result} [${row.kind}] for ${row.branch} @ ${(row.head ?? "").slice(0, 12)}${row.self ? ` (self${row.selfReason === "no_session" ? ": WORKIT_SESSION_ID unset" : ""}; never accepted)` : ""}`);
|
|
78868
80299
|
}
|
|
78869
|
-
var
|
|
80300
|
+
var USAGE6 = "workit ledger decision|ruling|verdict|list|check ... (workit help ledger for the grammar)", OPTIONS, positiveInt2 = (value, flag) => {
|
|
78870
80301
|
if (value === undefined)
|
|
78871
80302
|
return;
|
|
78872
80303
|
const parsed = Number(value);
|
|
78873
80304
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : new Error(`${flag} must be a positive integer`);
|
|
78874
|
-
}, failed = (io, result) => emit(io, fail(result.code, result.error, result.unblock ? { unblock: result.unblock } : {})), fromResult = (io, result, human) => result.ok ? emit(io, ok(result.value), human) : failed(io, result), usage4 = (io, error) => emit(io, fail("invalid_input", error, { unblock:
|
|
80305
|
+
}, failed = (io, result) => emit(io, fail(result.code, result.error, result.unblock ? { unblock: result.unblock } : {})), fromResult = (io, result, human) => result.ok ? emit(io, ok(result.value), human) : failed(io, result), usage4 = (io, error) => emit(io, fail("invalid_input", error, { unblock: USAGE6 })), verdictLines = (check) => {
|
|
78875
80306
|
const lines = [
|
|
78876
80307
|
`${check.branch} @ ${(check.head ?? "no commit").slice(0, 12)}: current: ${check.current.basis}${typeof check.current.verdict?.result === "string" ? ` (${check.current.verdict.result})` : ""}; ${check.accepted.accepted ? "accepted" : `not accepted (${check.accepted.reasons.join(", ")})`}`
|
|
78877
80308
|
];
|
|
@@ -93863,7 +95294,7 @@ var init_ast = __esm(() => {
|
|
|
93863
95294
|
|
|
93864
95295
|
// packages/workit-cli/src/test-audit/rules.ts
|
|
93865
95296
|
import { builtinModules } from "node:module";
|
|
93866
|
-
import
|
|
95297
|
+
import path43 from "node:path";
|
|
93867
95298
|
function testCallee(call) {
|
|
93868
95299
|
let callee = call.callee;
|
|
93869
95300
|
if (callee.type === "CallExpression")
|
|
@@ -94356,12 +95787,12 @@ function checkConstantsOnly(ctx, emit) {
|
|
|
94356
95787
|
}
|
|
94357
95788
|
function checkOverMocking(ctx, emit) {
|
|
94358
95789
|
const { program } = ctx.parsed;
|
|
94359
|
-
const unit =
|
|
95790
|
+
const unit = path43.basename(ctx.file).replace(/\.(?:[cm]?[jt]sx?)$/, "").replace(/\.(?:test|spec)$/, "");
|
|
94360
95791
|
const mocked = find2(program, (node) => node.type === "CallExpression" && MODULE_MOCK.test(calleeName(node.callee) ?? "") && node.arguments[0]?.type === "StringLiteral");
|
|
94361
95792
|
const internal = mocked.filter((call) => /^(?:\.|@\/|~\/)/.test(call.arguments[0].value));
|
|
94362
95793
|
for (const call of mocked) {
|
|
94363
95794
|
const spec = call.arguments[0].value;
|
|
94364
|
-
const base =
|
|
95795
|
+
const base = path43.basename(spec.replace(/\/index(?:\.[cm]?[jt]sx?)?$/, "")).replace(/\.(?:[cm]?[jt]sx?)$/, "");
|
|
94365
95796
|
if (base === unit)
|
|
94366
95797
|
emit({
|
|
94367
95798
|
rule: "over-mocking",
|
|
@@ -94749,11 +96180,11 @@ var init_rules = __esm(() => {
|
|
|
94749
96180
|
});
|
|
94750
96181
|
|
|
94751
96182
|
// packages/workit-cli/src/test-audit/audit.ts
|
|
94752
|
-
import { spawnSync as
|
|
96183
|
+
import { spawnSync as spawnSync22 } from "node:child_process";
|
|
94753
96184
|
import { existsSync as existsSync20, readdirSync as readdirSync10, readFileSync as readFileSync23, statSync as statSync7 } from "node:fs";
|
|
94754
|
-
import
|
|
96185
|
+
import path44 from "node:path";
|
|
94755
96186
|
function git5(cwd, args) {
|
|
94756
|
-
const run =
|
|
96187
|
+
const run = spawnSync22("git", ["-c", "core.quotepath=off", ...args], {
|
|
94757
96188
|
cwd,
|
|
94758
96189
|
encoding: "utf8",
|
|
94759
96190
|
maxBuffer: 64 * 1024 * 1024
|
|
@@ -94764,7 +96195,7 @@ function walkTests(dir, out) {
|
|
|
94764
96195
|
for (const entry of readdirSync10(dir, { withFileTypes: true })) {
|
|
94765
96196
|
if (entry.name.startsWith(".") && entry.name !== "." && entry.isDirectory())
|
|
94766
96197
|
continue;
|
|
94767
|
-
const full =
|
|
96198
|
+
const full = path44.join(dir, entry.name);
|
|
94768
96199
|
if (entry.isDirectory()) {
|
|
94769
96200
|
if (!SKIP_DIRS.has(entry.name))
|
|
94770
96201
|
walkTests(full, out);
|
|
@@ -94776,7 +96207,7 @@ function expandPaths(cwd, inputs) {
|
|
|
94776
96207
|
const files = [];
|
|
94777
96208
|
const missing = [];
|
|
94778
96209
|
for (const input of inputs.length > 0 ? inputs : ["."]) {
|
|
94779
|
-
const full =
|
|
96210
|
+
const full = path44.resolve(cwd, input);
|
|
94780
96211
|
if (!existsSync20(full))
|
|
94781
96212
|
missing.push(input);
|
|
94782
96213
|
else if (statSync7(full).isDirectory())
|
|
@@ -94813,11 +96244,11 @@ function changedLines(cwd, base) {
|
|
|
94813
96244
|
for (let offset = 0;offset < count; offset += 1)
|
|
94814
96245
|
lines.add(start + offset);
|
|
94815
96246
|
}
|
|
94816
|
-
changed.set(
|
|
96247
|
+
changed.set(path44.join(root, file), lines);
|
|
94817
96248
|
}
|
|
94818
96249
|
const untracked = git5(root, ["ls-files", "--others", "--exclude-standard", "-z"]);
|
|
94819
96250
|
for (const file of untracked.stdout.split("\x00").filter(Boolean))
|
|
94820
|
-
changed.set(
|
|
96251
|
+
changed.set(path44.join(root, file), "all");
|
|
94821
96252
|
return changed;
|
|
94822
96253
|
}
|
|
94823
96254
|
function auditFiles(files, display) {
|
|
@@ -94879,7 +96310,7 @@ function auditFiles(files, display) {
|
|
|
94879
96310
|
durationMs: Math.round(performance.now() - started)
|
|
94880
96311
|
};
|
|
94881
96312
|
}
|
|
94882
|
-
var TEST_FILE, CODE_FILE, SKIP_DIRS, toPosix = (file) => file.split(
|
|
96313
|
+
var TEST_FILE, CODE_FILE, SKIP_DIRS, toPosix = (file) => file.split(path44.sep).join("/"), SEVERITY_ORDER, ruleIds;
|
|
94883
96314
|
var init_audit = __esm(() => {
|
|
94884
96315
|
init_ast();
|
|
94885
96316
|
init_rules();
|
|
@@ -94909,7 +96340,7 @@ import {
|
|
|
94909
96340
|
copyFileSync as copyFileSync4
|
|
94910
96341
|
} from "node:fs";
|
|
94911
96342
|
import os14 from "node:os";
|
|
94912
|
-
import
|
|
96343
|
+
import path45 from "node:path";
|
|
94913
96344
|
function mutantsFor(source, file, lines) {
|
|
94914
96345
|
const parsed = parseSource(source, file);
|
|
94915
96346
|
const out = [];
|
|
@@ -94982,7 +96413,7 @@ function relatedTests(source, testFiles) {
|
|
|
94982
96413
|
for (const match of text.matchAll(SPECIFIER)) {
|
|
94983
96414
|
const spec = match[1];
|
|
94984
96415
|
if (spec.startsWith(".")) {
|
|
94985
|
-
if (stripExt(
|
|
96416
|
+
if (stripExt(path45.posix.join(path45.posix.dirname(file), spec)) === target)
|
|
94986
96417
|
return true;
|
|
94987
96418
|
} else if (spec.includes("/") && stripExt(spec).split("/").slice(-2).join("/") === tail)
|
|
94988
96419
|
return true;
|
|
@@ -95002,12 +96433,12 @@ function removeStaleCopies(tmp = os14.tmpdir()) {
|
|
|
95002
96433
|
for (const name of names) {
|
|
95003
96434
|
if (!name.startsWith(TEMP_PREFIX))
|
|
95004
96435
|
continue;
|
|
95005
|
-
const dir =
|
|
96436
|
+
const dir = path45.join(tmp, name);
|
|
95006
96437
|
try {
|
|
95007
96438
|
const stat = lstatSync9(dir);
|
|
95008
96439
|
if (!stat.isDirectory() || uid !== undefined && stat.uid !== uid)
|
|
95009
96440
|
continue;
|
|
95010
|
-
const pid = Number(readFileSync24(
|
|
96441
|
+
const pid = Number(readFileSync24(path45.join(dir, PID_FILE), "utf8"));
|
|
95011
96442
|
if (Number.isInteger(pid) && pid > 0 && alive(pid))
|
|
95012
96443
|
continue;
|
|
95013
96444
|
} catch (error) {
|
|
@@ -95024,8 +96455,8 @@ function removeStaleCopies(tmp = os14.tmpdir()) {
|
|
|
95024
96455
|
function linkNodeModules(origin, target, repo, copy) {
|
|
95025
96456
|
mkdirSync10(target, { recursive: true });
|
|
95026
96457
|
for (const entry of readdirSync11(origin, { withFileTypes: true })) {
|
|
95027
|
-
const from =
|
|
95028
|
-
const to =
|
|
96458
|
+
const from = path45.join(origin, entry.name);
|
|
96459
|
+
const to = path45.join(target, entry.name);
|
|
95029
96460
|
if (entry.name.startsWith("@") && entry.isDirectory()) {
|
|
95030
96461
|
linkNodeModules(from, to, repo, copy);
|
|
95031
96462
|
continue;
|
|
@@ -95041,9 +96472,9 @@ function linkNodeModules(origin, target, repo, copy) {
|
|
|
95041
96472
|
} catch {
|
|
95042
96473
|
continue;
|
|
95043
96474
|
}
|
|
95044
|
-
const relative =
|
|
95045
|
-
if (!relative.startsWith("..") && !
|
|
95046
|
-
symlinkSync(
|
|
96475
|
+
const relative = path45.relative(repo, real);
|
|
96476
|
+
if (!relative.startsWith("..") && !path45.isAbsolute(relative) && !relative.split(path45.sep).includes("node_modules")) {
|
|
96477
|
+
symlinkSync(path45.join(copy, relative), to, "junction");
|
|
95047
96478
|
continue;
|
|
95048
96479
|
}
|
|
95049
96480
|
}
|
|
@@ -95059,7 +96490,7 @@ function copySymlink(repo, tree, from, to) {
|
|
|
95059
96490
|
}
|
|
95060
96491
|
const realRepo = realpathSync12(repo);
|
|
95061
96492
|
if (real && inside2(realRepo, real)) {
|
|
95062
|
-
symlinkSync(
|
|
96493
|
+
symlinkSync(path45.relative(path45.dirname(to), path45.join(tree, path45.relative(realRepo, real))), to);
|
|
95063
96494
|
return;
|
|
95064
96495
|
}
|
|
95065
96496
|
if (real && statSync8(real).isFile())
|
|
@@ -95068,10 +96499,10 @@ function copySymlink(repo, tree, from, to) {
|
|
|
95068
96499
|
symlinkSync(readlinkSync3(from), to);
|
|
95069
96500
|
}
|
|
95070
96501
|
function writeInside(copy, file, content) {
|
|
95071
|
-
const target =
|
|
96502
|
+
const target = path45.join(copy, file);
|
|
95072
96503
|
try {
|
|
95073
96504
|
const stat = lstatSync9(target);
|
|
95074
|
-
if (!stat.isFile() || !inside2(realpathSync12(copy), realpathSync12(
|
|
96505
|
+
if (!stat.isFile() || !inside2(realpathSync12(copy), realpathSync12(path45.dirname(target))))
|
|
95075
96506
|
return false;
|
|
95076
96507
|
} catch {
|
|
95077
96508
|
return false;
|
|
@@ -95082,9 +96513,9 @@ function writeInside(copy, file, content) {
|
|
|
95082
96513
|
return true;
|
|
95083
96514
|
}
|
|
95084
96515
|
function copyWorkingTree(repo) {
|
|
95085
|
-
const dir = mkdtempSync2(
|
|
95086
|
-
writeFileSync9(
|
|
95087
|
-
const tree =
|
|
96516
|
+
const dir = mkdtempSync2(path45.join(os14.tmpdir(), TEMP_PREFIX));
|
|
96517
|
+
writeFileSync9(path45.join(dir, PID_FILE), String(process.pid));
|
|
96518
|
+
const tree = path45.join(dir, "tree");
|
|
95088
96519
|
const head = git5(repo, ["rev-parse", "--verify", "--quiet", "HEAD"]).stdout.trim();
|
|
95089
96520
|
if (head && git5(dir, ["clone", "-q", "--shared", "--no-checkout", repo, tree]).ok) {
|
|
95090
96521
|
const branch = git5(repo, ["symbolic-ref", "--quiet", "--short", "HEAD"]).stdout.trim();
|
|
@@ -95101,29 +96532,29 @@ function copyWorkingTree(repo) {
|
|
|
95101
96532
|
for (const relative of listed) {
|
|
95102
96533
|
if (!relative)
|
|
95103
96534
|
continue;
|
|
95104
|
-
const from =
|
|
96535
|
+
const from = path45.join(repo, relative);
|
|
95105
96536
|
let stat;
|
|
95106
96537
|
try {
|
|
95107
96538
|
stat = lstatSync9(from);
|
|
95108
96539
|
} catch {
|
|
95109
96540
|
continue;
|
|
95110
96541
|
}
|
|
95111
|
-
const to =
|
|
95112
|
-
mkdirSync10(
|
|
96542
|
+
const to = path45.join(tree, relative);
|
|
96543
|
+
mkdirSync10(path45.dirname(to), { recursive: true });
|
|
95113
96544
|
if (stat.isSymbolicLink())
|
|
95114
96545
|
copySymlink(repo, tree, from, to);
|
|
95115
96546
|
else if (stat.isFile())
|
|
95116
96547
|
copyFileSync4(from, to);
|
|
95117
|
-
let parent =
|
|
96548
|
+
let parent = path45.dirname(relative);
|
|
95118
96549
|
while (parent !== "." && !dirs.has(parent)) {
|
|
95119
96550
|
dirs.add(parent);
|
|
95120
|
-
parent =
|
|
96551
|
+
parent = path45.dirname(parent);
|
|
95121
96552
|
}
|
|
95122
96553
|
}
|
|
95123
96554
|
for (const parent of dirs) {
|
|
95124
|
-
const modules =
|
|
95125
|
-
if (existsSync21(modules) && !existsSync21(
|
|
95126
|
-
linkNodeModules(modules,
|
|
96555
|
+
const modules = path45.join(repo, parent, "node_modules");
|
|
96556
|
+
if (existsSync21(modules) && !existsSync21(path45.join(tree, parent, "node_modules")))
|
|
96557
|
+
linkNodeModules(modules, path45.join(tree, parent, "node_modules"), repo, tree);
|
|
95127
96558
|
}
|
|
95128
96559
|
return { dir, tree };
|
|
95129
96560
|
}
|
|
@@ -95162,14 +96593,14 @@ async function runMutation(options) {
|
|
|
95162
96593
|
const tracked = git5(repo, ["ls-files", "-co", "--exclude-standard", "-z"]).stdout.split("\x00").filter(Boolean);
|
|
95163
96594
|
const testFiles = tracked.filter((file) => TEST_FILE.test(file)).map((file) => {
|
|
95164
96595
|
try {
|
|
95165
|
-
return { file, text: readFileSync24(
|
|
96596
|
+
return { file, text: readFileSync24(path45.join(repo, file), "utf8") };
|
|
95166
96597
|
} catch {
|
|
95167
96598
|
return { file, text: "" };
|
|
95168
96599
|
}
|
|
95169
96600
|
});
|
|
95170
96601
|
const candidates = [];
|
|
95171
96602
|
for (const [absolute, lines] of [...options.changed].toSorted(([a], [b]) => a.localeCompare(b))) {
|
|
95172
|
-
const relative = toPosix(
|
|
96603
|
+
const relative = toPosix(path45.relative(repo, absolute));
|
|
95173
96604
|
if (!CODE_FILE.test(relative) || TEST_FILE.test(relative) || relative.endsWith(".d.ts"))
|
|
95174
96605
|
continue;
|
|
95175
96606
|
if (NOT_SOURCE.test(relative) || !existsSync21(absolute))
|
|
@@ -95183,9 +96614,9 @@ async function runMutation(options) {
|
|
|
95183
96614
|
for (const mutant of mutants)
|
|
95184
96615
|
if (!related.has(mutant.file))
|
|
95185
96616
|
related.set(mutant.file, relatedTests(mutant.file, testFiles));
|
|
95186
|
-
const cwdRelative =
|
|
96617
|
+
const cwdRelative = path45.relative(repo, options.cwd);
|
|
95187
96618
|
const asArg = (file) => {
|
|
95188
|
-
const relative = toPosix(
|
|
96619
|
+
const relative = toPosix(path45.relative(cwdRelative, file));
|
|
95189
96620
|
return shellQuote(relative.startsWith(".") ? relative : `./${relative}`);
|
|
95190
96621
|
};
|
|
95191
96622
|
const commandFor = (tests) => options.command.replaceAll("{files}", () => tests.map(asArg).join(" "));
|
|
@@ -95220,7 +96651,7 @@ async function runMutation(options) {
|
|
|
95220
96651
|
return finish2(options, 0, results, counts, started);
|
|
95221
96652
|
removeStaleCopies();
|
|
95222
96653
|
const { dir: copyDir, tree: copy } = copyWorkingTree(repo);
|
|
95223
|
-
const copyCwd =
|
|
96654
|
+
const copyCwd = path45.join(copy, cwdRelative);
|
|
95224
96655
|
let current = null;
|
|
95225
96656
|
const restore = () => {
|
|
95226
96657
|
if (current)
|
|
@@ -95255,7 +96686,7 @@ async function runMutation(options) {
|
|
|
95255
96686
|
record({ ...mutant, status: "skipped", durationMs: 0, tests, reason: "budget" });
|
|
95256
96687
|
continue;
|
|
95257
96688
|
}
|
|
95258
|
-
const original = readFileSync24(
|
|
96689
|
+
const original = readFileSync24(path45.join(copy, mutant.file), "utf8");
|
|
95259
96690
|
const mutated = original.slice(0, mutant.start) + mutant.replacement + original.slice(mutant.end);
|
|
95260
96691
|
current = { file: mutant.file, original };
|
|
95261
96692
|
if (!writeInside(copy, mutant.file, mutated)) {
|
|
@@ -95312,8 +96743,8 @@ var NOT_SOURCE, FLIPS, isStringy = (node) => node.type === "StringLiteral" || no
|
|
|
95312
96743
|
return error.code === "EPERM";
|
|
95313
96744
|
}
|
|
95314
96745
|
}, inside2 = (root, file) => {
|
|
95315
|
-
const relative =
|
|
95316
|
-
return relative === "" || !relative.startsWith("..") && !
|
|
96746
|
+
const relative = path45.relative(root, file);
|
|
96747
|
+
return relative === "" || !relative.startsWith("..") && !path45.isAbsolute(relative);
|
|
95317
96748
|
}, active, killTree = (child) => {
|
|
95318
96749
|
try {
|
|
95319
96750
|
if (child.pid && process.platform !== "win32")
|
|
@@ -95362,7 +96793,7 @@ __export(exports_test_audit, {
|
|
|
95362
96793
|
run: () => run19
|
|
95363
96794
|
});
|
|
95364
96795
|
import { existsSync as existsSync22 } from "node:fs";
|
|
95365
|
-
import
|
|
96796
|
+
import path46 from "node:path";
|
|
95366
96797
|
function parse6(argv) {
|
|
95367
96798
|
const options = {
|
|
95368
96799
|
paths: [],
|
|
@@ -95460,10 +96891,10 @@ async function run19(argv, io) {
|
|
|
95460
96891
|
const options = parse6(argv);
|
|
95461
96892
|
if (typeof options === "string")
|
|
95462
96893
|
return emit(io, fail("invalid_input", `${options}
|
|
95463
|
-
usage: ${
|
|
96894
|
+
usage: ${USAGE7}`));
|
|
95464
96895
|
const top = git5(io.cwd, ["rev-parse", "--show-toplevel"]);
|
|
95465
|
-
const repo = top.ok ?
|
|
95466
|
-
const display = (file) => toPosix(
|
|
96896
|
+
const repo = top.ok ? path46.resolve(top.stdout.trim()) : null;
|
|
96897
|
+
const display = (file) => toPosix(path46.relative(io.cwd, file) || file);
|
|
95467
96898
|
let base = null;
|
|
95468
96899
|
let changed = null;
|
|
95469
96900
|
if (options.diff || options.mutate) {
|
|
@@ -95546,7 +96977,7 @@ usage: ${USAGE6}`));
|
|
|
95546
96977
|
}
|
|
95547
96978
|
return emit(io, ok(data), human);
|
|
95548
96979
|
}
|
|
95549
|
-
var
|
|
96980
|
+
var USAGE7 = "workit test-audit [paths…] [--diff [base]] [--rule <ids>] [--min-severity <level>] [--fail-on <level>] [--mutate [--test-cmd <cmd>] [--max-mutants <n>] [--budget <s>] [--timeout <s>]] [--json]", LEVELS, VALUE_FLAGS, quote = (arg) => /^[\w./@:=+-]+$/.test(arg) ? arg : `'${arg.replace(/'/g, "'\\''")}'`;
|
|
95550
96981
|
var init_test_audit = __esm(() => {
|
|
95551
96982
|
init_audit();
|
|
95552
96983
|
init_mutate();
|
|
@@ -95572,7 +97003,7 @@ __export(exports_handoff, {
|
|
|
95572
97003
|
import { parseArgs as parseArgs2 } from "node:util";
|
|
95573
97004
|
function human2(brief) {
|
|
95574
97005
|
const lines = [
|
|
95575
|
-
`branch: ${brief.branch ?? "(detached)"} @ ${
|
|
97006
|
+
`branch: ${brief.branch ?? "(detached)"} @ ${short5(brief.head)}${brief.subject ? ` ${brief.subject}` : ""}`
|
|
95576
97007
|
];
|
|
95577
97008
|
if (brief.base)
|
|
95578
97009
|
lines.push(`base: ${brief.base}${brief.ahead === null ? "" : ` (ahead ${brief.ahead}, behind ${brief.behind})`}`);
|
|
@@ -95615,11 +97046,11 @@ async function resumeBrief(argv, io) {
|
|
|
95615
97046
|
strict: true
|
|
95616
97047
|
}));
|
|
95617
97048
|
} catch (error) {
|
|
95618
|
-
return emit(io, fail("invalid_input", error.message, { unblock:
|
|
97049
|
+
return emit(io, fail("invalid_input", error.message, { unblock: USAGE8 }));
|
|
95619
97050
|
}
|
|
95620
97051
|
const last = values.last === undefined ? 10 : Number(values.last);
|
|
95621
97052
|
if (!Number.isInteger(last) || last < 1)
|
|
95622
|
-
return emit(io, fail("invalid_input", "--last must be a positive integer", { unblock:
|
|
97053
|
+
return emit(io, fail("invalid_input", "--last must be a positive integer", { unblock: USAGE8 }));
|
|
95623
97054
|
const built = buildHandoff(io.cwd, {
|
|
95624
97055
|
last,
|
|
95625
97056
|
note: values.note ?? null,
|
|
@@ -95643,7 +97074,7 @@ async function run20(argv, io) {
|
|
|
95643
97074
|
await Promise.resolve().then(() => init_task());
|
|
95644
97075
|
return runTaskCommand(["handoff", ...argv]);
|
|
95645
97076
|
}
|
|
95646
|
-
var
|
|
97077
|
+
var USAGE8 = "workit handoff [--note <text>] [--next <text>] [--record] [--last <n>] [--json]", short5 = (sha) => sha ? sha.slice(0, 12) : "no commit";
|
|
95647
97078
|
var init_handoff = __esm(() => {
|
|
95648
97079
|
init_ledger();
|
|
95649
97080
|
init_output();
|
|
@@ -95652,7 +97083,7 @@ var init_handoff = __esm(() => {
|
|
|
95652
97083
|
// packages/workit-cli/src/main.ts
|
|
95653
97084
|
init_package();
|
|
95654
97085
|
init_output();
|
|
95655
|
-
import
|
|
97086
|
+
import path47 from "node:path";
|
|
95656
97087
|
|
|
95657
97088
|
// packages/workit-cli/src/verbs/registry.ts
|
|
95658
97089
|
var TASK_FAMILY_NAMES = [
|
|
@@ -95768,10 +97199,9 @@ var VERBS = [
|
|
|
95768
97199
|
{
|
|
95769
97200
|
name: "stack",
|
|
95770
97201
|
group: "delivery",
|
|
95771
|
-
usage: "workit stack plan|sync|land [--json]",
|
|
95772
|
-
summary: "Plan, restack and land a stack of PRs",
|
|
95773
|
-
|
|
95774
|
-
load: () => Promise.resolve().then(() => (init_stack(), exports_stack))
|
|
97202
|
+
usage: "workit stack plan [<bottom> … <top>] | status | sync [--local] | land [--dry-run] [--max <n>] [--json]",
|
|
97203
|
+
summary: "Plan, restack (lease push + retarget) and land a base-branch stack of PRs, root first",
|
|
97204
|
+
load: () => Promise.resolve().then(() => (init_stack2(), exports_stack))
|
|
95775
97205
|
},
|
|
95776
97206
|
{
|
|
95777
97207
|
name: "ledger",
|
|
@@ -95877,7 +97307,7 @@ var defaultIo = (json) => ({
|
|
|
95877
97307
|
});
|
|
95878
97308
|
async function main(argv, overrides = {}, options = {}) {
|
|
95879
97309
|
const parsed = parseGlobals(argv);
|
|
95880
|
-
const cwd =
|
|
97310
|
+
const cwd = path47.resolve(overrides.cwd ?? process.cwd(), parsed.cwd ?? ".");
|
|
95881
97311
|
const io = { ...defaultIo(parsed.json), ...overrides, json: parsed.json, cwd };
|
|
95882
97312
|
if (parsed.error)
|
|
95883
97313
|
return emit(io, fail("invalid_input", parsed.error));
|