fix(changes): a huge line no longer cuts the diff short; preview pages up to 64 MB

A page that inlines a video as base64 has rows of megabytes. Its patch was read
at the 200 KB per-file cap, so the first such row used the whole budget and
every hunk after it was lost ("Diff cut short"). The patch is now read up to
16 M characters, rows over 4,000 characters are cut to 1,000 with an `elided`
count the diff shows, and only then is the cap applied. Hunk actions re-read
git's own diff by header and are unchanged.

The HTML preview refused a page over 10 MB and showed only the file icon; a
self-contained 18 MB slide deck now opens (limit 64 MB).
This commit is contained in:
2026-10-30 10:28:53 +07:00
parent c41114faf8
commit d97c403aab
10 changed files with 166 additions and 15 deletions
+55 -5
View File
@@ -61,6 +61,19 @@ async function readCapped(dir, args, capChars) {
const MAX_FILES = 300;
/** Most patch bytes read for ONE file before its hunks are cut short. */
const MAX_FILE_BYTES = 200_000;
/** Characters of one file's raw patch read before its long lines are cut down
* (`elideLongLines`) and `MAX_FILE_BYTES` applied. A page that embeds a video
* or a font as base64 is a few lines of megabytes each: read at
* `MAX_FILE_BYTES`, its first such line used the whole budget and every hunk
* after it was lost. Past this the patch is still truncated. */
const MAX_PATCH_READ_CHARS = 16 * 1024 * 1024;
/** A diff row longer than this is shown cut down to `LONG_LINE_KEEP`
* characters, with how many it hides (`elided` on the parsed line). */
const LONG_LINE_CHARS = 4000;
const LONG_LINE_KEEP = 1000;
/** Ends a cut-down row in the raw patch: a private-use character, then the
* count of characters hidden. `parseUnifiedDiff` turns it into `elided`. */
const ELIDED_MARK = "\uE000";
/** Most patch bytes across all files in a `full` (review-mode) response. */
const MAX_TOTAL_BYTES = 1_000_000;
/** An untracked file larger than this is reported without content. */
@@ -259,15 +272,23 @@ function parseUnifiedDiff(patch) {
// row of its own and carries no line number.
if (raw.startsWith("\\")) continue;
const marker = raw[0];
const text = raw.slice(1);
let text = raw.slice(1);
// A row `elideLongLines` cut down: the text it kept, and how much it hid.
let elided = 0;
const cut = text.lastIndexOf(ELIDED_MARK);
if (cut >= 0 && /^\d+$/.test(text.slice(cut + 1))) {
elided = Number(text.slice(cut + 1));
text = text.slice(0, cut);
}
const extra = elided ? { elided } : {};
if (marker === "+") {
current.lines.push({ type: "add", oldLine: null, newLine: newLine++, text });
current.lines.push({ type: "add", oldLine: null, newLine: newLine++, text, ...extra });
} else if (marker === "-") {
current.lines.push({ type: "del", oldLine: oldLine++, newLine: null, text });
current.lines.push({ type: "del", oldLine: oldLine++, newLine: null, text, ...extra });
} else if (marker === " ") {
// An unchanged EMPTY line is " " — one space, no text. A truly empty
// string is the tail of the final "\n", not a row.
current.lines.push({ type: "ctx", oldLine: oldLine++, newLine: newLine++, text });
current.lines.push({ type: "ctx", oldLine: oldLine++, newLine: newLine++, text, ...extra });
}
}
return hunks;
@@ -532,7 +553,27 @@ async function patchFor(dir, { range, baseRef, sha, file, untracked, context })
: range === "branch"
? ["diff", u, `${baseRef}...HEAD`, "--", file]
: ["diff", u, "HEAD", "--", file];
return readCapped(dir, [...args, "--no-color", "--no-ext-diff"], MAX_FILE_BYTES).catch(() => "");
return readCapped(dir, [...args, "--no-color", "--no-ext-diff"], MAX_PATCH_READ_CHARS).catch(
() => ""
);
}
/**
* The patch with every row longer than `LONG_LINE_CHARS` cut down to its first
* `LONG_LINE_KEEP` characters plus `ELIDED_MARK` and the count it hides. Only
* what the panel SHOWS is cut: the hunk actions (`hunkPatch`, `selectionPatch`)
* re-read git's own diff and match hunks by header, which this never changes.
*/
function elideLongLines(patch) {
if (patch.length <= LONG_LINE_CHARS) return patch;
return patch
.split("\n")
.map((row) =>
row.length > LONG_LINE_CHARS && !row.startsWith("@@")
? `${row.slice(0, LONG_LINE_KEEP + 1)}${ELIDED_MARK}${row.length - LONG_LINE_KEEP - 1}`
: row
)
.join("\n");
}
/**
@@ -648,7 +689,15 @@ async function collectChanges(
untracked: entry.untracked,
context: ctx,
});
// A read that hit its own ceiling came back partial: still cut short,
// however small eliding its long rows makes it.
const partial = patch.length >= MAX_PATCH_READ_CHARS;
patch = elideLongLines(patch);
const cap = Math.min(MAX_FILE_BYTES, budget);
if (partial) {
entry.truncated = true;
truncated = true;
}
if (patch.length > cap) {
// Cut on a line boundary so the last row is a whole line — but only
// when that boundary is near the cap. A minified file is one enormous
@@ -1334,6 +1383,7 @@ async function plainTreeAll(dir) {
}
module.exports = {
elideLongLines,
collectChanges,
CONFLICT_CODES,
contentSha,