fix: support minimax large multi-hunk patches

This commit is contained in:
Codex
2026-06-03 10:12:16 +00:00
parent 1d542d82b5
commit d012fe9a5e
2 changed files with 116 additions and 12 deletions
+36 -12
View File
@@ -8,6 +8,7 @@ export type PatchHunk =
export interface UpdateChunk {
changeContext: string | null;
sourceStartLine: number | null;
oldLines: string[];
newLines: string[];
isEndOfFile: boolean;
@@ -119,7 +120,7 @@ export function applyPatchV2HelpPayload() {
rules: [
"Add File has no @@ hunk marker: put content immediately after `*** Add File: <path>` and prefix every content line with +.",
"A blank line in Add File is a line containing only +.",
"Update File uses @@ or @@ context markers, followed by context lines starting with space and changed lines starting with - or +.",
"Update File uses @@ or @@ context markers, followed by context lines starting with space and changed lines starting with - or +; unified-diff line-range headers are accepted with hints for MiniMax compatibility.",
"Prefer `trans <route> apply-patch < /tmp/patch.diff` for long patches, Windows paths, or quoting-sensitive content.",
"MiniMax compatibility: stray @@ or unprefixed content inside Add File, and extra hunk/body lines after Delete File, are accepted with stderr hints."
],
@@ -145,7 +146,7 @@ export function applyPatchV2HelpPayload() {
},
commonPitfalls: [
"Do not put @@ after `*** Add File:`; @@ is only for Update File.",
"Do not paste unified diff headers such as `@@ -1,3 +1,4 @@`.",
"Prefer canonical @@ or @@ context over unified diff headers such as `@@ -1,3 +1,4 @@`; v2 accepts those headers with a hint.",
"Do not use remote Python/Perl/sed heredocs for text patches when `trans <route> apply-patch` is available."
],
note: "apply-patch reads patch text from stdin and uses the v2 engine by default. Use `apply-patch-v1` only for the legacy helper."
@@ -238,7 +239,7 @@ export function parseApplyPatchV2(patchText: string): PatchParseResult {
index += 1;
continue;
}
const parsed = parseUpdateChunk(lines, index, chunks.length === 0);
const parsed = parseUpdateChunk(lines, index, chunks.length === 0, filePath, hints);
chunks.push(parsed.chunk);
index = parsed.nextIndex;
}
@@ -805,17 +806,25 @@ function isFileHeader(line: string): boolean {
return trimmed.startsWith(addFileMarker) || trimmed.startsWith(deleteFileMarker) || trimmed.startsWith(updateFileMarker) || trimmed === endMarker;
}
function parseUpdateChunk(lines: string[], startIndex: number, allowMissingContext: boolean): { chunk: UpdateChunk; nextIndex: number } {
function parseUpdateChunk(lines: string[], startIndex: number, allowMissingContext: boolean, filePath: string, hints: string[]): { chunk: UpdateChunk; nextIndex: number } {
let index = startIndex;
let changeContext: string | null = null;
let sourceStartLine: number | null = null;
const first = lines[index] ?? "";
if (first === emptyChangeContextMarker) {
index += 1;
} else if (first.startsWith(changeContextMarker)) {
changeContext = first.slice(changeContextMarker.length);
index += 1;
} else if (!allowMissingContext) {
throw new ApplyPatchV2Error("expected update chunk to start with @@ context marker", { line: startIndex + 1, text: first });
} else {
const unifiedHeader = parseUnifiedDiffHunkHeader(first);
if (unifiedHeader !== null) {
sourceStartLine = unifiedHeader.oldStart;
hints.push(`apply-patch hint: accepted unified-diff hunk header in ${filePath} on line ${startIndex + 1}; canonical apply_patch uses @@ or @@ context without line ranges.`);
index += 1;
} else if (first.startsWith(changeContextMarker)) {
changeContext = first.slice(changeContextMarker.length);
index += 1;
} else if (!allowMissingContext) {
throw new ApplyPatchV2Error("expected update chunk to start with @@ context marker", { line: startIndex + 1, text: first });
}
}
const oldLines: string[] = [];
@@ -851,7 +860,15 @@ function parseUpdateChunk(lines: string[], startIndex: number, allowMissingConte
index += 1;
}
if (parsed === 0) throw new ApplyPatchV2Error("update chunk does not contain any lines", { line: startIndex + 1 });
return { chunk: { changeContext, oldLines, newLines, isEndOfFile }, nextIndex: index };
return { chunk: { changeContext, sourceStartLine, oldLines, newLines, isEndOfFile }, nextIndex: index };
}
function parseUnifiedDiffHunkHeader(line: string): { oldStart: number } | null {
const match = /^@@\s+-(\d+)(?:,\d+)?\s+\+\d+(?:,\d+)?\s+@@(?:\s+.*)?$/u.exec(line);
if (match === null) return null;
const oldStart = Number(match[1] ?? "0");
if (!Number.isSafeInteger(oldStart) || oldStart < 0) return null;
return { oldStart };
}
function splitContentLines(content: string): string[] {
@@ -883,12 +900,13 @@ function computeReplacements(filePath: string, originalLines: string[], chunks:
}
let pattern = chunk.oldLines;
let found = seekSequence(originalLines, pattern, lineIndex, chunk.isEndOfFile);
const preferredStart = chunk.sourceStartLine === null ? lineIndex : Math.max(lineIndex, chunk.sourceStartLine - 1);
let found = seekSequenceWithFallback(originalLines, pattern, preferredStart, lineIndex, chunk.isEndOfFile);
let newLines = chunk.newLines;
if (found === null && pattern[pattern.length - 1] === "") {
pattern = pattern.slice(0, -1);
newLines = newLines[newLines.length - 1] === "" ? newLines.slice(0, -1) : newLines;
found = seekSequence(originalLines, pattern, lineIndex, chunk.isEndOfFile);
found = seekSequenceWithFallback(originalLines, pattern, preferredStart, lineIndex, chunk.isEndOfFile);
}
if (found === null) {
throw new ApplyPatchV2Error("failed to find expected lines", {
@@ -904,6 +922,12 @@ function computeReplacements(filePath: string, originalLines: string[], chunks:
return replacements;
}
function seekSequenceWithFallback(lines: string[], pattern: string[], preferredStart: number, fallbackStart: number, eof: boolean): number | null {
const preferred = seekSequence(lines, pattern, preferredStart, eof);
if (preferred !== null || preferredStart === fallbackStart) return preferred;
return seekSequence(lines, pattern, fallbackStart, eof);
}
function applyReplacements(lines: string[], replacements: Replacement[]): string[] {
const result = [...lines];
for (const [start, oldLen, newSegment] of [...replacements].reverse()) {