mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-27 11:42:52 +00:00
* fix(server): keep diff highlighting responsive and bounded Tiny patches previously parsed both full source versions synchronously. Yield between parser work units and admit source by UTF-8 bytes, keeping large patches renderable without tokens. Cancel obsolete highlighting when a newer diff supersedes it. * fix(server): keep diff cancellation with its requester Independent commit viewers must not cancel each other. Use request-source cancellation for commit highlighting and cache invalidation for checkout highlighting. Oversized source versions use the existing budgeted hunk reconstruction so large lockfiles retain useful syntax tokens.
347 lines
13 KiB
TypeScript
347 lines
13 KiB
TypeScript
import { execFileSync } from "child_process";
|
|
import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "fs";
|
|
import { tmpdir } from "os";
|
|
import { join } from "path";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import { getCommitFileDiff, listCheckoutCommits } from "./checkout-git.js";
|
|
import { SessionDelivery } from "../server/session/owned-subscriptions/index.js";
|
|
import { startGitCommandMetrics, stopGitCommandMetrics } from "./run-git-command.js";
|
|
|
|
const tempDirs: string[] = [];
|
|
|
|
afterEach(() => {
|
|
for (const dir of tempDirs.splice(0)) {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
function makeTempDir(): string {
|
|
const dir = realpathSync.native(mkdtempSync(join(tmpdir(), "commit-file-diff-test-")));
|
|
tempDirs.push(dir);
|
|
return dir;
|
|
}
|
|
|
|
function git(args: string[], cwd: string): string {
|
|
return execFileSync("git", args, { cwd }).toString();
|
|
}
|
|
|
|
function commitFile(repoDir: string, name: string, content: string, message: string): void {
|
|
writeFileSync(join(repoDir, name), content);
|
|
git(["add", "."], repoDir);
|
|
git(["-c", "commit.gpgsign=false", "commit", "-m", message], repoDir);
|
|
}
|
|
|
|
function initRepo(): string {
|
|
const tempDir = makeTempDir();
|
|
const repoDir = join(tempDir, "repo");
|
|
mkdirSync(repoDir, { recursive: true });
|
|
git(["init", "-b", "main"], repoDir);
|
|
git(["config", "user.email", "test@test.com"], repoDir);
|
|
git(["config", "user.name", "Test User"], repoDir);
|
|
return repoDir;
|
|
}
|
|
|
|
function headSha(repoDir: string): string {
|
|
return git(["rev-parse", "HEAD"], repoDir).trim();
|
|
}
|
|
|
|
describe("getCommitFileDiff", () => {
|
|
it.each(["new.txt", "with spaces.txt"])("preserves a rename-only commit to %s", async (name) => {
|
|
const repoDir = initRepo();
|
|
commitFile(repoDir, "old.txt", "same\n", "initial");
|
|
git(["mv", "old.txt", name], repoDir);
|
|
git(["-c", "commit.gpgsign=false", "commit", "-m", "rename"], repoDir);
|
|
const sha = headSha(repoDir);
|
|
|
|
const history = await listCheckoutCommits({ cwd: repoDir });
|
|
expect(history.commits.find((commit) => commit.sha === sha)?.files).toEqual([
|
|
{ path: name, status: "renamed", additions: 0, deletions: 0 },
|
|
]);
|
|
expect(await getCommitFileDiff({ cwd: repoDir, sha, path: name })).toEqual({
|
|
path: name,
|
|
isNew: false,
|
|
isDeleted: false,
|
|
additions: 0,
|
|
deletions: 0,
|
|
hunks: [],
|
|
});
|
|
});
|
|
|
|
it.each(["new.txt", "目录.txt"])(
|
|
"preserves the edits in a committed rename to %s",
|
|
async (name) => {
|
|
const repoDir = initRepo();
|
|
commitFile(repoDir, "old.txt", "same\nkeep\nlast\n", "initial");
|
|
git(["mv", "old.txt", name], repoDir);
|
|
writeFileSync(join(repoDir, name), "same\nKEEP\nlast\n");
|
|
git(["add", "."], repoDir);
|
|
git(["-c", "commit.gpgsign=false", "commit", "-m", "rename and edit"], repoDir);
|
|
const sha = headSha(repoDir);
|
|
|
|
const history = await listCheckoutCommits({ cwd: repoDir });
|
|
expect(history.commits.find((commit) => commit.sha === sha)?.files).toEqual([
|
|
{ path: name, status: "renamed", additions: 1, deletions: 1 },
|
|
]);
|
|
const file = await getCommitFileDiff({ cwd: repoDir, sha, path: name });
|
|
expect(file).toMatchObject({
|
|
path: name,
|
|
isNew: false,
|
|
isDeleted: false,
|
|
additions: 1,
|
|
deletions: 1,
|
|
});
|
|
expect(file?.hunks.flatMap((hunk) => hunk.lines)).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ type: "remove", content: "keep" }),
|
|
expect.objectContaining({ type: "add", content: "KEEP" }),
|
|
]),
|
|
);
|
|
},
|
|
);
|
|
|
|
it.skipIf(process.platform === "win32")("preserves a rename to a quoted filename", async () => {
|
|
const repoDir = initRepo();
|
|
const name = 'new "draft".txt';
|
|
commitFile(repoDir, "old.txt", "same\n", "initial");
|
|
git(["mv", "old.txt", name], repoDir);
|
|
git(["-c", "commit.gpgsign=false", "commit", "-m", "rename"], repoDir);
|
|
expect(await getCommitFileDiff({ cwd: repoDir, sha: headSha(repoDir), path: name })).toEqual({
|
|
path: name,
|
|
isNew: false,
|
|
isDeleted: false,
|
|
additions: 0,
|
|
deletions: 0,
|
|
hunks: [],
|
|
});
|
|
});
|
|
|
|
it("returns the parsed diff for a modified file in a commit", async () => {
|
|
const repoDir = initRepo();
|
|
commitFile(repoDir, "foo.txt", "a\nb\nc\n", "initial");
|
|
commitFile(repoDir, "foo.txt", "a\nB\nc\nd\n", "edit foo");
|
|
const sha = headSha(repoDir);
|
|
|
|
const file = await getCommitFileDiff({ cwd: repoDir, sha, path: "foo.txt" });
|
|
|
|
expect(file).not.toBeNull();
|
|
expect(file?.path).toBe("foo.txt");
|
|
expect(file?.additions).toBe(2);
|
|
expect(file?.deletions).toBe(1);
|
|
expect(file?.hunks.length).toBeGreaterThan(0);
|
|
|
|
const lines = file?.hunks[0]?.lines ?? [];
|
|
expect(lines.some((line) => line.type === "add" && line.content === "B")).toBe(true);
|
|
expect(lines.some((line) => line.type === "add" && line.content === "d")).toBe(true);
|
|
expect(lines.some((line) => line.type === "remove" && line.content === "b")).toBe(true);
|
|
});
|
|
|
|
it("returns a diff flagged as new for an added file", async () => {
|
|
const repoDir = initRepo();
|
|
commitFile(repoDir, "README.md", "base\n", "initial");
|
|
commitFile(repoDir, "added.txt", "x\ny\n", "add file");
|
|
const sha = headSha(repoDir);
|
|
|
|
const file = await getCommitFileDiff({ cwd: repoDir, sha, path: "added.txt" });
|
|
|
|
expect(file?.path).toBe("added.txt");
|
|
expect(file?.isNew).toBe(true);
|
|
expect(file?.additions).toBe(2);
|
|
expect(file?.deletions).toBe(0);
|
|
});
|
|
|
|
it.each([
|
|
["exact", "line1\nline2\nline3\nline4\nline5\nline6\n"],
|
|
["edited", "line1\nline2\nline3\nline4\nline5\nCHANGED\n"],
|
|
])("shows an added %s copy as a new file", async (_kind, addedContent) => {
|
|
const repoDir = initRepo();
|
|
commitFile(repoDir, "source.txt", "line1\nline2\nline3\nline4\nline5\nline6\n", "initial");
|
|
commitFile(repoDir, "added.txt", addedContent, "add similar file");
|
|
const sha = headSha(repoDir);
|
|
|
|
const history = await listCheckoutCommits({ cwd: repoDir });
|
|
expect(history.commits.find((commit) => commit.sha === sha)?.files).toEqual([
|
|
{ path: "added.txt", status: "added", additions: 6, deletions: 0 },
|
|
]);
|
|
const file = await getCommitFileDiff({ cwd: repoDir, sha, path: "added.txt" });
|
|
expect(file).toMatchObject({
|
|
path: "added.txt",
|
|
isNew: true,
|
|
isDeleted: false,
|
|
additions: 6,
|
|
deletions: 0,
|
|
});
|
|
expect(
|
|
file?.hunks
|
|
.flatMap((hunk) => hunk.lines)
|
|
.filter((line) => line.type !== "header")
|
|
.map(({ type, content }) => ({ type, content })),
|
|
).toEqual(
|
|
addedContent
|
|
.trimEnd()
|
|
.split("\n")
|
|
.map((content) => ({ type: "add", content })),
|
|
);
|
|
});
|
|
|
|
it("shares commit rename discovery across concurrent and later file requests", async () => {
|
|
const repoDir = initRepo();
|
|
commitFile(repoDir, "old.txt", "same\n", "initial");
|
|
git(["mv", "old.txt", "new.txt"], repoDir);
|
|
commitFile(repoDir, "added.txt", "added\n", "rename and add");
|
|
const sha = headSha(repoDir);
|
|
|
|
startGitCommandMetrics();
|
|
try {
|
|
const [renamed, added] = await Promise.all([
|
|
getCommitFileDiff({ cwd: repoDir, sha, path: "new.txt" }),
|
|
getCommitFileDiff({ cwd: repoDir, sha, path: "added.txt" }),
|
|
]);
|
|
expect(renamed).toMatchObject({ isNew: false, additions: 0, deletions: 0 });
|
|
expect(added).toMatchObject({ isNew: true, additions: 1, deletions: 0 });
|
|
expect(await getCommitFileDiff({ cwd: repoDir, sha, path: "new.txt" })).toEqual(renamed);
|
|
} finally {
|
|
const metrics = stopGitCommandMetrics();
|
|
expect(metrics.commands.filter(({ args }) => args.includes("--name-status"))).toHaveLength(1);
|
|
}
|
|
});
|
|
|
|
it("rediscovers renames when a symbolic commit ref moves", async () => {
|
|
const repoDir = initRepo();
|
|
commitFile(repoDir, "old.txt", "same\n", "initial");
|
|
expect(await getCommitFileDiff({ cwd: repoDir, sha: "HEAD", path: "old.txt" })).toMatchObject({
|
|
isNew: true,
|
|
additions: 1,
|
|
});
|
|
git(["mv", "old.txt", "new.txt"], repoDir);
|
|
git(["-c", "commit.gpgsign=false", "commit", "-m", "rename"], repoDir);
|
|
expect(await getCommitFileDiff({ cwd: repoDir, sha: "HEAD", path: "new.txt" })).toMatchObject({
|
|
isNew: false,
|
|
additions: 0,
|
|
deletions: 0,
|
|
});
|
|
});
|
|
|
|
it("returns null for a path not changed in the commit", async () => {
|
|
const repoDir = initRepo();
|
|
commitFile(repoDir, "foo.txt", "a\n", "initial");
|
|
commitFile(repoDir, "foo.txt", "a\nb\n", "edit foo");
|
|
const sha = headSha(repoDir);
|
|
|
|
const file = await getCommitFileDiff({ cwd: repoDir, sha, path: "does-not-exist.txt" });
|
|
|
|
expect(file).toBeNull();
|
|
});
|
|
|
|
it("does not follow an unchanged file back to an earlier commit", async () => {
|
|
const repoDir = initRepo();
|
|
commitFile(repoDir, "foo.txt", "same\n", "initial");
|
|
commitFile(repoDir, "other.txt", "other\n", "unrelated change");
|
|
expect(
|
|
await getCommitFileDiff({ cwd: repoDir, sha: headSha(repoDir), path: "foo.txt" }),
|
|
).toBeNull();
|
|
});
|
|
|
|
it("returns the added file in a root commit and the deleted file in its deletion commit", async () => {
|
|
const repoDir = initRepo();
|
|
commitFile(repoDir, "foo.txt", "same\n", "initial");
|
|
expect(
|
|
await getCommitFileDiff({ cwd: repoDir, sha: headSha(repoDir), path: "foo.txt" }),
|
|
).toMatchObject({
|
|
isNew: true,
|
|
isDeleted: false,
|
|
additions: 1,
|
|
deletions: 0,
|
|
});
|
|
git(["rm", "foo.txt"], repoDir);
|
|
git(["-c", "commit.gpgsign=false", "commit", "-m", "delete"], repoDir);
|
|
expect(
|
|
await getCommitFileDiff({ cwd: repoDir, sha: headSha(repoDir), path: "foo.txt" }),
|
|
).toMatchObject({
|
|
isNew: false,
|
|
isDeleted: true,
|
|
additions: 0,
|
|
deletions: 1,
|
|
});
|
|
});
|
|
|
|
it("preserves a rename introduced by a merge against its first parent", async () => {
|
|
const repoDir = initRepo();
|
|
commitFile(repoDir, "old.txt", "same\n", "initial");
|
|
git(["checkout", "-b", "feature"], repoDir);
|
|
git(["mv", "old.txt", "new.txt"], repoDir);
|
|
git(["-c", "commit.gpgsign=false", "commit", "-m", "rename"], repoDir);
|
|
git(["checkout", "main"], repoDir);
|
|
commitFile(repoDir, "main.txt", "main\n", "advance main");
|
|
git(["merge", "--no-ff", "feature", "-m", "merge feature"], repoDir);
|
|
expect(
|
|
await getCommitFileDiff({ cwd: repoDir, sha: headSha(repoDir), path: "new.txt" }),
|
|
).toMatchObject({
|
|
path: "new.txt",
|
|
isNew: false,
|
|
isDeleted: false,
|
|
additions: 0,
|
|
deletions: 0,
|
|
hunks: [],
|
|
});
|
|
});
|
|
|
|
it("returns a merge commit diff against its first parent", async () => {
|
|
const repoDir = initRepo();
|
|
commitFile(repoDir, "README.md", "base\n", "initial");
|
|
git(["checkout", "-b", "feature"], repoDir);
|
|
commitFile(repoDir, "feature.txt", "feature\n", "add feature");
|
|
git(["checkout", "main"], repoDir);
|
|
commitFile(repoDir, "main.txt", "main\n", "advance main");
|
|
git(["merge", "--no-ff", "feature", "-m", "merge feature"], repoDir);
|
|
const sha = headSha(repoDir);
|
|
|
|
const file = await getCommitFileDiff({ cwd: repoDir, sha, path: "feature.txt" });
|
|
|
|
expect(file?.path).toBe("feature.txt");
|
|
expect(file?.isNew).toBe(true);
|
|
expect(file?.additions).toBe(1);
|
|
expect(file?.deletions).toBe(0);
|
|
});
|
|
});
|
|
|
|
it("identical commit-file requests from two requesters both retain syntax tokens", async () => {
|
|
const repoDir = initRepo();
|
|
const original = "const value = Math.max(42, 7);\n".repeat(3_000);
|
|
commitFile(repoDir, "file.ts", original, "initial");
|
|
commitFile(repoDir, "file.ts", original.replace("42", "43"), "edit");
|
|
const input = { cwd: repoDir, sha: headSha(repoDir), path: "file.ts" };
|
|
const results = await Promise.all([getCommitFileDiff(input), getCommitFileDiff(input)]);
|
|
for (const file of results) {
|
|
expect(
|
|
file?.hunks[0].lines
|
|
.find((line) => line.type === "add")
|
|
?.tokens?.some((token) => token.style !== null),
|
|
).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("a disconnected requester cancels commit highlighting before its sources arrive", async () => {
|
|
const repoDir = initRepo();
|
|
commitFile(repoDir, "file.ts", "const value = 42;\n", "initial");
|
|
commitFile(repoDir, "file.ts", "const value = 43;\n", "edit");
|
|
const delivery = new SessionDelivery(() => {});
|
|
const source = {};
|
|
delivery.attach(source, true);
|
|
const request = {
|
|
type: "checkout.commits.file_diff.request" as const,
|
|
cwd: repoDir,
|
|
sha: headSha(repoDir),
|
|
path: "file.ts",
|
|
requestId: "disconnect",
|
|
};
|
|
const results: Awaited<ReturnType<typeof getCommitFileDiff>>[] = [];
|
|
const pending = delivery.request(source, request, async () => {
|
|
results.push(await getCommitFileDiff({ ...request, signal: delivery.requestSignal }));
|
|
});
|
|
await delivery.detach(source);
|
|
await pending;
|
|
expect(
|
|
results[0]?.hunks.flatMap((hunk) => hunk.lines).every((line) => line.tokens === undefined),
|
|
).toBe(true);
|
|
});
|