mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix(site): strip SVN-style Index headers from diffs before parsing (#23179)
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import {
|
import {
|
||||||
BORDER_BG_STYLE,
|
BORDER_BG_STYLE,
|
||||||
buildEditDiff,
|
buildEditDiff,
|
||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
parseArgs,
|
parseArgs,
|
||||||
parseEditFilesArgs,
|
parseEditFilesArgs,
|
||||||
shortDurationMs,
|
shortDurationMs,
|
||||||
|
stripSvnIndexHeaders,
|
||||||
toProviderLabel,
|
toProviderLabel,
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
|
|
||||||
@@ -420,6 +421,20 @@ describe("buildWriteFileDiff", () => {
|
|||||||
// That's 1 empty-string line, which is still a valid line.
|
// That's 1 empty-string line, which is still a valid line.
|
||||||
expect(diff).not.toBeNull();
|
expect(diff).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not emit console errors from SVN-style Index headers", () => {
|
||||||
|
const spy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
try {
|
||||||
|
const diff = buildWriteFileDiff(
|
||||||
|
"src/components/Example.tsx",
|
||||||
|
"export default function Example() {\n return <div />;\n}\n",
|
||||||
|
);
|
||||||
|
expect(diff).not.toBeNull();
|
||||||
|
expect(spy).not.toHaveBeenCalled();
|
||||||
|
} finally {
|
||||||
|
spy.mockRestore();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getWriteFileDiff", () => {
|
describe("getWriteFileDiff", () => {
|
||||||
@@ -520,6 +535,26 @@ describe("buildEditDiff", () => {
|
|||||||
expect(diff).not.toBeNull();
|
expect(diff).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not emit console errors for multi-edit diffs", () => {
|
||||||
|
const spy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
try {
|
||||||
|
const diff = buildEditDiff(
|
||||||
|
"/home/coder/coder/site/src/pages/AgentsPage/AgentsSidebar.tsx",
|
||||||
|
[
|
||||||
|
{ search: "const a = 1;", replace: "const a = 2;" },
|
||||||
|
{ search: "const b = 3;", replace: "const b = 4;" },
|
||||||
|
],
|
||||||
|
);
|
||||||
|
expect(diff).not.toBeNull();
|
||||||
|
// Before the fix, @pierre/diffs logged:
|
||||||
|
// parseLineType: Invalid firstChar: "I"
|
||||||
|
// processFile: invalid rawLine: Index: ...
|
||||||
|
expect(spy).not.toHaveBeenCalled();
|
||||||
|
} finally {
|
||||||
|
spy.mockRestore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("strips leading slash from path", () => {
|
it("strips leading slash from path", () => {
|
||||||
const diff = buildEditDiff("/src/index.ts", [
|
const diff = buildEditDiff("/src/index.ts", [
|
||||||
{ search: "old", replace: "new" },
|
{ search: "old", replace: "new" },
|
||||||
@@ -570,6 +605,68 @@ describe("buildEditDiff", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("stripSvnIndexHeaders", () => {
|
||||||
|
it("removes Index: headers from SVN-style patches", () => {
|
||||||
|
const input = [
|
||||||
|
"Index: src/file.ts",
|
||||||
|
"===================================================================",
|
||||||
|
"--- src/file.ts",
|
||||||
|
"+++ src/file.ts",
|
||||||
|
"@@ -1,1 +1,1 @@",
|
||||||
|
"-old",
|
||||||
|
"+new",
|
||||||
|
"",
|
||||||
|
].join("\n");
|
||||||
|
const result = stripSvnIndexHeaders(input);
|
||||||
|
expect(result).not.toContain("Index:");
|
||||||
|
expect(result).not.toContain("===");
|
||||||
|
expect(result).toContain("--- src/file.ts");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles multiple Index: headers in concatenated patches", () => {
|
||||||
|
const input = [
|
||||||
|
"Index: a.ts",
|
||||||
|
"===================================================================",
|
||||||
|
"--- a.ts",
|
||||||
|
"+++ a.ts",
|
||||||
|
"@@ -1,1 +1,1 @@",
|
||||||
|
"-old1",
|
||||||
|
"+new1",
|
||||||
|
"Index: b.ts",
|
||||||
|
"===================================================================",
|
||||||
|
"--- b.ts",
|
||||||
|
"+++ b.ts",
|
||||||
|
"@@ -1,1 +1,1 @@",
|
||||||
|
"-old2",
|
||||||
|
"+new2",
|
||||||
|
"",
|
||||||
|
].join("\n");
|
||||||
|
const result = stripSvnIndexHeaders(input);
|
||||||
|
// Both Index headers removed.
|
||||||
|
expect(result.match(/Index:/g)).toBeNull();
|
||||||
|
// Diff content preserved.
|
||||||
|
expect(result).toContain("-old1");
|
||||||
|
expect(result).toContain("+new2");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is a no-op for git-style diffs", () => {
|
||||||
|
const gitDiff = [
|
||||||
|
"diff --git a/file.ts b/file.ts",
|
||||||
|
"--- a/file.ts",
|
||||||
|
"+++ b/file.ts",
|
||||||
|
"@@ -1,1 +1,1 @@",
|
||||||
|
"-old",
|
||||||
|
"+new",
|
||||||
|
"",
|
||||||
|
].join("\n");
|
||||||
|
expect(stripSvnIndexHeaders(gitDiff)).toBe(gitDiff);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is a no-op for empty strings", () => {
|
||||||
|
expect(stripSvnIndexHeaders("")).toBe("");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("constants", () => {
|
describe("constants", () => {
|
||||||
it("COLLAPSED_OUTPUT_HEIGHT is 54", () => {
|
it("COLLAPSED_OUTPUT_HEIGHT is 54", () => {
|
||||||
expect(COLLAPSED_OUTPUT_HEIGHT).toBe(54);
|
expect(COLLAPSED_OUTPUT_HEIGHT).toBe(54);
|
||||||
|
|||||||
@@ -189,6 +189,15 @@ export function getFileViewerOptionsMinimal(isDark: boolean) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strips SVN-style "Index:" headers that `Diff.createPatch()`
|
||||||
|
* emits but `@pierre/diffs` does not recognize as file
|
||||||
|
* boundaries. Left in place they leak into hunk bodies and
|
||||||
|
* trigger console errors.
|
||||||
|
*/
|
||||||
|
export const stripSvnIndexHeaders = (patch: string): string =>
|
||||||
|
patch.replace(/^Index: .*\n={3,}\n/gm, "");
|
||||||
|
|
||||||
export const DIFFS_FONT_STYLE = {
|
export const DIFFS_FONT_STYLE = {
|
||||||
"--diffs-font-family": '"Geist Mono Variable", monospace, monospace',
|
"--diffs-font-family": '"Geist Mono Variable", monospace, monospace',
|
||||||
"--diffs-header-font-family": '"Geist Variable", system-ui, sans-serif',
|
"--diffs-header-font-family": '"Geist Variable", system-ui, sans-serif',
|
||||||
@@ -261,7 +270,7 @@ export const buildWriteFileDiff = (
|
|||||||
): FileDiffMetadata | null => {
|
): FileDiffMetadata | null => {
|
||||||
if (!content) return null;
|
if (!content) return null;
|
||||||
const patch = Diff.createPatch(path, "", content, "", "");
|
const patch = Diff.createPatch(path, "", content, "", "");
|
||||||
const parsed = parsePatchFiles(patch);
|
const parsed = parsePatchFiles(stripSvnIndexHeaders(patch));
|
||||||
if (!parsed.length || !parsed[0].files.length) return null;
|
if (!parsed.length || !parsed[0].files.length) return null;
|
||||||
return parsed[0].files[0];
|
return parsed[0].files[0];
|
||||||
};
|
};
|
||||||
@@ -338,12 +347,10 @@ export const buildEditDiff = (
|
|||||||
// All edits were skipped (empty search). Produce a
|
// All edits were skipped (empty search). Produce a
|
||||||
// header-only patch so the parser still returns a file
|
// header-only patch so the parser still returns a file
|
||||||
// entry with zero hunks.
|
// entry with zero hunks.
|
||||||
patches.push(
|
patches.push(`--- ${diffPath}\n+++ ${diffPath}\n`);
|
||||||
`Index: ${diffPath}\n===================================================================\n--- ${diffPath}\n+++ ${diffPath}\n`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsed = parsePatchFiles(patches.join(""));
|
const parsed = parsePatchFiles(stripSvnIndexHeaders(patches.join("")));
|
||||||
if (!parsed.length || !parsed[0].files.length) return null;
|
if (!parsed.length || !parsed[0].files.length) return null;
|
||||||
return parsed[0].files[0];
|
return parsed[0].files[0];
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user