Files
openidea/src/features/vcs/__tests__/diffChain.test.ts
T
l0ng-ai b2fec4ae30 feat(vcs): Show combined changes for multiple selected log commits (#52)
Root cause: the Git Log already supported multi-selection, but the Changes pane only read the last clicked commit and the backend had no "changes across several commits" query.
Fix (IDEA): ide-vcs Repo::commits_changes merges per-file changes across selected commits (before the first touching commit ↔ after the last, renames chained, add-then-delete dropped); the details pane shows "N files in M commits" with the commit list; exactly two commits get "Compare Versions" (version ↔ version dialog, also fixing added/deleted sides).
Siblings covered: Show History for file/folder (same log), Local History multi-select (oldest ↔ newest diff).
Verified: ide-vcs tests incl. temp-repo commits_changes, vcs vitest (143), in-app on macOS: 3 contiguous commits → "2 files in 3 commits"; Cmd-click 2 commits → Compare Versions lists the right files.

Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
2026-10-27 10:39:01 +08:00

50 lines
2.3 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { chainNeighbour, revisionChangeDiffData, withChain, type DiffData } from "../diffInput";
const diff = (path: string): DiffData => ({ path, left: { type: "rev", rev: "HEAD" }, right: { type: "worktree" } });
describe("diff chain", () => {
const items = [diff("/r/a"), diff("/r/b"), diff("/r/c")];
it("moves to the neighbouring file, revealing the nearest difference", () => {
const b = withChain(items, 1);
expect(b.path).toBe("/r/b");
const next = chainNeighbour(b, 1)!;
expect(next).toMatchObject({ path: "/r/c", reveal: "first" });
expect(chainNeighbour(b, -1)).toMatchObject({ path: "/r/a", reveal: "last" });
// The neighbour keeps the chain, so F7 can continue from it.
expect(chainNeighbour(next, -1)).toMatchObject({ path: "/r/b" });
});
it("stops at the ends and without a chain", () => {
expect(chainNeighbour(withChain(items, 2), 1)).toBeNull();
expect(chainNeighbour(withChain(items, 0), -1)).toBeNull();
expect(chainNeighbour(withChain([diff("/r/a")], 0), 1)).toBeNull();
expect(chainNeighbour(diff("/r/a"), 1)).toBeNull();
});
});
describe("combined changes of several commits", () => {
const base = { oldPath: null, before: "b".repeat(40), after: "a".repeat(40) };
it("diffs the state before the first commit with the state after the last", () => {
expect(revisionChangeDiffData({ ...base, path: "src/x.ts", kind: "modified" })).toMatchObject({
path: "src/x.ts",
left: { type: "rev", rev: base.before, label: "bbbbbbbb" },
right: { type: "rev", rev: base.after, label: "aaaaaaaa" },
});
});
it("reads a renamed file's old content at its old path", () => {
const d = revisionChangeDiffData({ ...base, path: "new.ts", oldPath: "old.ts", kind: "renamed" });
expect(d.left).toMatchObject({ type: "rev", rev: base.before, path: "old.ts" });
});
it("shows an empty side for added and deleted files", () => {
const added = revisionChangeDiffData({ ...base, path: "n.ts", kind: "added", before: null });
expect(added.left).toMatchObject({ type: "text", text: "" });
const deleted = revisionChangeDiffData({ ...base, path: "d.ts", kind: "deleted", after: null });
expect(deleted.right).toMatchObject({ type: "text", text: "" });
});
});