@brainervirus/workit-core 8.31.0 → 8.31.1
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/package.json +1 -1
- package/src/core/policy/carry.ts +2 -1
- package/src/ledger.ts +62 -15
package/package.json
CHANGED
package/src/core/policy/carry.ts
CHANGED
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
import { mergeBase, fileAt } from "../../git/rev";
|
|
18
18
|
import { computeLane, driftFiles, kindOf, parseRiskPaths, RISK_PATHS_FILE } from "./lane";
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
/** `rebase`: the same patch on a moved base (ledger.ts exact carry), never a drift rule. */
|
|
21
|
+
export type CarryRule = "lane-none" | "tests-only" | "rebase";
|
|
21
22
|
|
|
22
23
|
export type Drift =
|
|
23
24
|
| { carries: true; rule: CarryRule; since: string; files: string[] }
|
package/src/ledger.ts
CHANGED
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
// `base`, the stable patch-id of `base...head` and an exact diff hash. It is
|
|
34
34
|
// `fresh` (same head; verdicts on a dirty worktree are refused, so the head
|
|
35
35
|
// is the judged tree), `carried` (new head, same patch-id AND same exact
|
|
36
|
-
// diff AND, for a pass, byte-identical own files the base left alone
|
|
36
|
+
// diff AND, for a pass, byte-identical own files the base left alone, or
|
|
37
|
+
// a head that is exactly the judged head cleanly rebased (cleanRebase): a
|
|
37
38
|
// rebase that only moved the base; or a trusted verdict whose
|
|
38
39
|
// follow-up change is lane none, or test-only for a unit verdict, see
|
|
39
40
|
// core/policy/carry.ts) or `stale`.
|
|
@@ -338,6 +339,32 @@ export function diffHash(cwd: string, base: string, head: string): string | null
|
|
|
338
339
|
return `sha256:${hash.digest("hex")}`;
|
|
339
340
|
}
|
|
340
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Whether `to` (on `toBase`) is exactly the judged `from` (on `fromBase`)
|
|
344
|
+
* rebased cleanly onto the moved base: a 3-way merge of `from` onto
|
|
345
|
+
* `merge-base(toBase, to)` with `merge-base(fromBase, from)` as the ancestor
|
|
346
|
+
* has no conflict and yields `to`'s tree. Hunks are anchored to the
|
|
347
|
+
* ancestor, so an edit moved elsewhere in a file, a conflict resolved by
|
|
348
|
+
* hand, or any other change in the branch's files makes the trees differ.
|
|
349
|
+
* False when git cannot tell (no `merge-tree --merge-base`, before 2.40).
|
|
350
|
+
*/
|
|
351
|
+
function cleanRebase(
|
|
352
|
+
cwd: string,
|
|
353
|
+
range: { from: string; to: string; fromBase: string; toBase: string },
|
|
354
|
+
): boolean {
|
|
355
|
+
if (!Object.values(range).every(safeRef)) return false;
|
|
356
|
+
const fromFork = git(cwd, ["merge-base", range.fromBase, range.from]);
|
|
357
|
+
const toFork = git(cwd, ["merge-base", range.toBase, range.to]);
|
|
358
|
+
if (!fromFork || !toFork || fromFork === toFork) return false;
|
|
359
|
+
const merged = gitRun(
|
|
360
|
+
cwd,
|
|
361
|
+
["merge-tree", "--write-tree", "--no-messages", `--merge-base=${fromFork}`, toFork, range.from],
|
|
362
|
+
GIT_TIMEOUTS.worktree,
|
|
363
|
+
);
|
|
364
|
+
const tree = merged.ok ? merged.stdout.split("\n")[0]?.trim() : null;
|
|
365
|
+
return Boolean(tree) && tree === git(cwd, ["rev-parse", "--verify", "-q", `${range.to}^{tree}`]);
|
|
366
|
+
}
|
|
367
|
+
|
|
341
368
|
// ---------------------------------------------------------------------------
|
|
342
369
|
// store root (D13)
|
|
343
370
|
|
|
@@ -1124,7 +1151,9 @@ export type Reverify = { scope: "focused"; since: string; files: string[] };
|
|
|
1124
1151
|
|
|
1125
1152
|
/** `carried (lane none since <head>)`: how a carried verdict reads in `ledger check` and `pr status`. */
|
|
1126
1153
|
export const carryLabel = (carry: CarryNote): string =>
|
|
1127
|
-
|
|
1154
|
+
carry.rule === "rebase"
|
|
1155
|
+
? "carried (rebase: patch unchanged)"
|
|
1156
|
+
: `carried (${carry.rule === "lane-none" ? "lane none" : "tests only"} since ${carry.since.slice(0, 12)})`;
|
|
1128
1157
|
|
|
1129
1158
|
export type VerdictCheck = {
|
|
1130
1159
|
branch: string;
|
|
@@ -1516,31 +1545,49 @@ export function checkVerdicts(
|
|
|
1516
1545
|
};
|
|
1517
1546
|
// The same patch-id and diff hash are not enough to carry a pass (#334):
|
|
1518
1547
|
// both drop hunk line numbers, so an edit moved within a file keeps them.
|
|
1519
|
-
// The branch's own files must be identical blobs too (sameOwnChange)
|
|
1520
|
-
//
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1548
|
+
// The branch's own files must be identical blobs too (sameOwnChange), or
|
|
1549
|
+
// the head must be exactly the judged head cleanly rebased onto the moved
|
|
1550
|
+
// base (cleanRebase): a base edit elsewhere in a file the branch touches.
|
|
1551
|
+
// A failure keeps the looser carry: it fails closed by applying more.
|
|
1552
|
+
// How a pass's own files match: identical blobs, a clean rebase, or neither.
|
|
1553
|
+
const exactCache = new Map<ReadRow, "same" | "rebase" | null>();
|
|
1554
|
+
const exactMatch = (row: ReadRow): "same" | "rebase" | null => {
|
|
1524
1555
|
let exact = exactCache.get(row);
|
|
1525
1556
|
if (exact === undefined) {
|
|
1526
1557
|
const toBase = row.base ?? fallbackBase;
|
|
1527
1558
|
const toBaseSha = toBase ? resolveCommit(cwd, toBase) : null;
|
|
1528
1559
|
const fromBase = row.baseSha ?? toBaseSha;
|
|
1529
|
-
|
|
1530
|
-
row.head &&
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1560
|
+
const range =
|
|
1561
|
+
row.head && head && toBaseSha && fromBase
|
|
1562
|
+
? { from: row.head, to: head, fromBase, toBase: toBaseSha }
|
|
1563
|
+
: null;
|
|
1564
|
+
exact = !range
|
|
1565
|
+
? null
|
|
1566
|
+
: sameOwnChange(cwd, range)
|
|
1567
|
+
? "same"
|
|
1568
|
+
: cleanRebase(cwd, range)
|
|
1569
|
+
? "rebase"
|
|
1570
|
+
: null;
|
|
1536
1571
|
exactCache.set(row, exact);
|
|
1537
1572
|
}
|
|
1538
1573
|
return exact;
|
|
1539
1574
|
};
|
|
1575
|
+
const exactCarry = (row: ReadRow): boolean =>
|
|
1576
|
+
FAILING.has(String(row.result)) || exactMatch(row) !== null;
|
|
1540
1577
|
const keyBasis = (row: ReadRow): Exclude<VerdictBasis, "none"> => {
|
|
1541
1578
|
const basis = verdictBasis(row, { head, ...keyFor(row.base ?? fallbackBase) });
|
|
1542
1579
|
return basis === "carried" && !exactCarry(row) ? "stale" : basis;
|
|
1543
1580
|
};
|
|
1581
|
+
/** A pass the exact keys carry only through a clean rebase (the base also edited a branch file). */
|
|
1582
|
+
const rebaseNote = (row: ReadRow): CarryNote | null =>
|
|
1583
|
+
row.head &&
|
|
1584
|
+
head &&
|
|
1585
|
+
row.head !== head &&
|
|
1586
|
+
!FAILING.has(String(row.result)) &&
|
|
1587
|
+
keyBasis(row) === "carried" &&
|
|
1588
|
+
exactMatch(row) === "rebase"
|
|
1589
|
+
? { rule: "rebase", since: row.head, files: [] }
|
|
1590
|
+
: null;
|
|
1544
1591
|
const restacked = (row: ReadRow): boolean =>
|
|
1545
1592
|
restackCarries(FAILING.has(String(row.result)) ? null : cwd, rows, branch, row.head, head);
|
|
1546
1593
|
/** The follow-up carry of a verdict the exact keys and restacks do not carry. */
|
|
@@ -1646,7 +1693,7 @@ export function checkVerdicts(
|
|
|
1646
1693
|
independent: independence.length === 0,
|
|
1647
1694
|
accepted: reasons.length === 0,
|
|
1648
1695
|
reasons,
|
|
1649
|
-
carry: basis === "carried" ? carryOf(row) : null,
|
|
1696
|
+
carry: basis === "carried" ? (rebaseNote(row) ?? carryOf(row)) : null,
|
|
1650
1697
|
verdict: row,
|
|
1651
1698
|
});
|
|
1652
1699
|
// The author's own failure never displaces an independent pass, but it
|