Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 115 additions & 3 deletions .github/scripts/pr-carry-attribution.cjs

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Security review is required before merge

This changes repository automation under .github/. Repository policy requires explicit security review before merge.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,121 @@ function carryWindow(text, from) {

const TRAILER_RE = /^[ \t]*co-authored-by:[ \t]*(.+)$/gim;

const FENCED_CODE_RE = /^[ \t]*(\u0060{3,}|~{3,})[\s\S]*?^[ \t]*\1[ \t]*$/gm;
const INLINE_CODE_RE = /\u0060[^\u0060\n]*\u0060/g;

/**
* Remove fenced blocks in linear time.
*
* A regex that searches lazily for a closing fence has to retry from every
* opening-looking line when no close exists. Pull request and commit text is
* untrusted workflow input, so that quadratic failure mode is significant
* here. Index the pure fence lines -- the only lines that can close a block --
* then walk the lines once: an opener pairs with the longest closing run no
* longer than its own, and an opener with no such line remains ordinary text,
* so a later opener can still pair with its own close.
*
* Line boundaries follow the same rule the regex's ^ and $ did: CR, LF, and
* the Unicode separators all end a line, with CRLF as one terminator. The
* closing-length rule is what the backreference produced by backtracking: it
* captured the full greedy run first and shortened it one delimiter at a
* time, so the longest close length still ahead wins and the earliest line
* carrying it ends the block.
*/
function stripFencedCode(text) {
const lineStarts = [0];
const lineEnds = [text.length];
for (let i = 0; i < text.length; i++) {
const c = text[i];
if (c === "\n" || c === "\r" || c === "\u2028" || c === "\u2029") {
lineEnds[lineStarts.length - 1] = i;
if (c === "\r" && text[i + 1] === "\n") i++;
lineStarts.push(i + 1);
lineEnds.push(text.length);
}
}

const openRun = new Array(lineStarts.length).fill(null);
const closeLists = { "`": new Map(), "~": new Map() };
for (let i = 0; i < lineStarts.length; i++) {
const line = text.slice(lineStarts[i], lineEnds[i]);
const opening = /^[ \t]*(\u0060{3,}|~{3,})/.exec(line);
if (opening) openRun[i] = { fence: opening[1][0], len: opening[1].length };
const closing = /^[ \t]*(\u0060{3,}|~{3,})[ \t]*$/.exec(line);
if (closing) {
const lists = closeLists[closing[1][0]];
const len = closing[1].length;
const list = lists.get(len);
if (list) list.push(i);
else lists.set(len, [i]);
}
}

// For each delimiter, the distinct close lengths and a disjoint set that
// permanently skips a length once every line carrying it is behind the
// scan. Scanning only moves forward, so each removal is final and total
// work stays near-linear.
const fenceIndex = {};
for (const fence of ["\u0060", "~"]) {
const lengths = [...closeLists[fence].keys()].sort((a, b) => a - b);
fenceIndex[fence] = {
lengths,
lists: lengths.map((len) => closeLists[fence].get(len)),
cursors: new Array(lengths.length).fill(0),
parent: lengths.map((_, index) => index),
};
}

// Largest member of index's set still reachable -- the disjoint-set
// "previous element" trick; a linked root below index is the answer.
function aliveAt(scan, index) {
let root = index;
while (root >= 0 && scan.parent[root] !== root) root = scan.parent[root];
while (index >= 0 && scan.parent[index] !== index) {
const next = scan.parent[index];
scan.parent[index] = root;
index = next;
}
return root;
}

// First pure-fence line after `after` carrying the longest close length
// that is at most openerLen; -1 when no close length qualifies.
function closeFor(fence, openerLen, after) {
const scan = fenceIndex[fence];
let lo = 0;
let hi = scan.lengths.length;
while (lo < hi) {
const mid = (lo + hi) >> 1;
if (scan.lengths[mid] <= openerLen) lo = mid + 1;
else hi = mid;
}
const pos = lo - 1;
while (pos >= 0) {
const index = aliveAt(scan, pos);
if (index < 0) return -1;
const list = scan.lists[index];
let cursor = scan.cursors[index];
while (cursor < list.length && list[cursor] <= after) cursor++;
scan.cursors[index] = cursor;
if (cursor < list.length) return list[cursor];
scan.parent[index] = aliveAt(scan, index - 1);
}
return -1;
Comment on lines +136 to +156

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Previous fence semantics are preserved

The scanner selects the longest available closing run no longer than the opener. Exhaustive and randomized comparisons found no drift from the replaced regex.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

}

let output = "";
let copiedThrough = 0;
for (let i = 0; i < lineStarts.length; i++) {
const run = openRun[i];
if (run === null) continue;
const close = closeFor(run.fence, run.len, i);
if (close === -1) continue;
output += text.slice(copiedThrough, lineStarts[i]);
copiedThrough = lineEnds[close];
i = close;
}
return output + text.slice(copiedThrough);
}
/**
* HTML comments, which GitHub never renders.
*
Expand All @@ -81,8 +194,7 @@ const HTML_COMMENT_RE = /<!--[\s\S]*?(?:-->|$)/g;
*/
function strippedText(text) {
if (typeof text !== "string") return "";
return text
.replace(FENCED_CODE_RE, "")
return stripFencedCode(text)
.replace(HTML_COMMENT_RE, "")
.replace(INLINE_CODE_RE, "");
}
Expand Down
106 changes: 105 additions & 1 deletion .github/scripts/pr-carry-attribution.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

const { describe, it } = require("node:test");
const assert = require("node:assert/strict");
const { assessCarryAttribution } = require("./pr-carry-attribution.cjs");
const {
assessCarryAttribution,
referencedCarryNumbers,
} = require("./pr-carry-attribution.cjs");

const RRMLIMA = {
login: "rrmlima",
Expand Down Expand Up @@ -120,6 +123,107 @@ describe("assessCarryAttribution", () => {
);
});

it("scans many unclosed fence-like lines without repeatedly searching the tail", () => {
const body = "```x\n".repeat(20_000) + "Reimplements #2797.";
const started = performance.now();

assert.deepEqual([...referencedCarryNumbers(body)], [2797]);
assert.ok(performance.now() - started < 2_000, "fence scan should remain linear");
});

it("strips a complete tilde fence that follows an unmatched backtick opener", () => {
// The unclosed opener stays ordinary text, but it must not swallow the
// independent fenced block after it.
assert.deepEqual(
assessCarryAttribution(
base({
body: [
"\u0060\u0060\u0060unclosed",
"~~~",
"Reimplements #2797",
"~~~",
].join("\n"),
}),
),
[],
);
});

it("strips a longer fence that follows an unmatched shorter opener", () => {
assert.deepEqual(
assessCarryAttribution(
base({
body: [
"\u0060\u0060\u0060unclosed",
"\u0060\u0060\u0060\u0060",
"Reimplements #2797",
"\u0060\u0060\u0060\u0060",
].join("\n"),
}),
),
[],
);
});

it("strips a fence whose closing run is shorter than its opening run", () => {
// The backreferenced regex gave back opener delimiters until a close
// matched: a pure ``` line still closes a ```` opener. An exact-length
// lookup would leave "Reimplements #2797" readable as a declaration.
assert.deepEqual(
assessCarryAttribution(
base({
body: [
"\u0060\u0060\u0060\u0060",
"Reimplements #2797",
"\u0060\u0060\u0060",
].join("\n"),
}),
),
[],
);
});

it("prefers the longest closing run, the way the backreference backtracked", () => {
// Greedy capture tries the full opener run first: a pure ```` line
// farther down outranks a nearer ``` line, so the whole span is removed.
assert.deepEqual(
assessCarryAttribution(
base({
body: [
"\u0060\u0060\u0060\u0060",
"\u0060\u0060\u0060",
"Reimplements #2797",
"\u0060\u0060\u0060\u0060",
].join("\n"),
}),
),
[],
);
});

it("strips a fenced block written with CRLF line endings", () => {
assert.deepEqual(
assessCarryAttribution(
base({
body: "\u0060\u0060\u0060\r\nReimplements #2797\r\n\u0060\u0060\u0060\r\n",
}),
),
[],
);
});

it("still reads carry language around an unmatched opener", () => {
// Falling back to ordinary text is not a license to hide a real claim:
// the unmatched opener line itself remains in the scanned text.
const failures = assessCarryAttribution(
base({
body: ["\u0060\u0060\u0060unclosed", "Reimplements #2797."].join("\n"),
}),
);
assert.equal(failures.length, 1);
assert.deepEqual(failures[0].paths, ["#2797"]);
});

it("ignores carry language after an unclosed HTML comment", () => {
// GitHub renders nothing after an unterminated `<!--`, so neither does the
// gate. The closing-delimiter-only pattern used to match nothing here and
Expand Down
Loading