дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
* feat(vcs): Preview the selected file's diff in the Git Log Selecting a single changed file in the Git Log's changes pane shows its diff in the editor's preview tab without taking focus, so arrow keys walk through the files. View Options › Show Diff Preview turns it off; double-click and Show Diff still open a regular diff tab. * fix(vcs): Skip the Git Log diff preview when it cannot open
183 рядки
8.2 KiB
TypeScript
183 рядки
8.2 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const vcsLog = vi.fn();
|
|
const vcsConfigGet = vi.fn();
|
|
|
|
vi.mock("@/platform/ipc", async (importOriginal) => {
|
|
const real = await importOriginal<typeof import("@/platform/ipc")>();
|
|
return {
|
|
...real,
|
|
commands: {
|
|
...real.commands,
|
|
vcsLog: (repo: unknown, query: unknown) => vcsLog(repo, query),
|
|
vcsConfigGet: (repo: unknown, key: string) => vcsConfigGet(repo, key),
|
|
},
|
|
};
|
|
});
|
|
|
|
import { collapseLinear, computeGraph, hideLongEdges, reachableFrom } from "../graph";
|
|
import { DEFAULT_LOG_OPTIONS, parseLogOptions, refLabels, setLogOptions, useLogOptions } from "../logOptions";
|
|
import { emptyFilters, goToRef, isMine, reloadLog, updateLogOptions, useGitLog } from "../logStore";
|
|
import { defaultWorktreePath, disabledMergeFlags, remoteBranchesOf, validateRemoteName } from "../gitMenuDialogs";
|
|
import type { Branch, Commit } from "@/bindings";
|
|
|
|
const c = (id: string, parents: string[] = [], refs: string[] = []) => ({ id, parents, refs });
|
|
|
|
describe("Collapse Linear Branches", () => {
|
|
it("hides linear commits and links the remaining ones with dashed edges", () => {
|
|
// m merges f3 into main; f1..f3 is a linear branch from base, b a commit on main.
|
|
const commits = [
|
|
c("t", ["m"], ["main"]),
|
|
c("m", ["b", "f3"]),
|
|
c("f3", ["f2"]),
|
|
c("f2", ["f1"]),
|
|
c("f1", ["base"]),
|
|
c("b", ["base"]),
|
|
c("base"),
|
|
];
|
|
const { commits: shown, graph } = collapseLinear(commits, new Set());
|
|
expect(shown.map((x) => x.id)).toEqual(["t", "m", "base"]);
|
|
expect(graph.find((g) => g.id === "m")).toEqual({ id: "m", parents: ["base"], dashed: [true] });
|
|
expect(graph.find((g) => g.id === "t")).toEqual({ id: "t", parents: ["m"] });
|
|
const { rows } = computeGraph(graph);
|
|
expect(rows[1]!.down.some((s) => s.dashed)).toBe(true);
|
|
expect(rows[2]!.up.some((s) => s.dashed)).toBe(true);
|
|
});
|
|
|
|
it("keeps referenced and kept commits", () => {
|
|
const commits = [c("d", ["c"]), c("c", ["b"], ["tag: v1"]), c("b", ["a"]), c("a")];
|
|
expect(collapseLinear(commits, new Set()).commits.map((x) => x.id)).toEqual(["d", "c", "a"]);
|
|
const chain = [c("e", ["d"]), c("d", ["c"]), c("c", ["b"]), c("b", ["a"]), c("a")];
|
|
expect(collapseLinear(chain, new Set(["c"])).commits.map((x) => x.id)).toEqual(["e", "c", "a"]);
|
|
expect(collapseLinear(chain, new Set()).commits.map((x) => x.id)).toEqual(["e", "a"]);
|
|
});
|
|
});
|
|
|
|
describe("long edges", () => {
|
|
it("hides the middle of a long straight lane and puts arrows on the stubs", () => {
|
|
// t's parent p is 40 rows below; the commits in between sit in lane 0.
|
|
const between = Array.from({ length: 40 }, (_, i) => c(`x${i}`, i < 39 ? [`x${i + 1}`] : []));
|
|
const { rows } = computeGraph([c("t", ["p"]), ...between, c("p")]);
|
|
const hidden = hideLongEdges(rows);
|
|
// The x chain sits in lane 1; the edge t → p passes through lane 0.
|
|
const lane0 = (r: (typeof rows)[number]) => r.up.concat(r.down).filter((s) => s.from === 0 && s.to === 0);
|
|
expect(lane0(rows[20]!)).toHaveLength(2);
|
|
expect(lane0(hidden[20]!)).toHaveLength(0);
|
|
expect(hidden[20]!.lane).toBe(1);
|
|
expect(hidden.some((r) => r.down.some((s) => s.arrow === "down"))).toBe(true);
|
|
expect(hidden.some((r) => r.up.some((s) => s.arrow === "up"))).toBe(true);
|
|
expect(hideLongEdges(rows.slice(0, 10))).toEqual(rows.slice(0, 10));
|
|
});
|
|
});
|
|
|
|
describe("highlighting", () => {
|
|
it("finds the loaded commits of the current branch", () => {
|
|
const commits = [c("s", ["a"]), c("h", ["a"]), c("a", ["r"]), c("r")];
|
|
expect([...reachableFrom(commits, "h")].sort()).toEqual(["a", "h", "r"]);
|
|
expect(reachableFrom(commits, null).size).toBe(0);
|
|
});
|
|
|
|
it("matches my commits by email or name", () => {
|
|
const commit = { email: "[email protected]", author: "Me" } as Commit;
|
|
expect(isMine(commit, "[email protected]")).toBe(true);
|
|
expect(isMine(commit, "me")).toBe(true);
|
|
expect(isMine(commit, null)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("reference labels", () => {
|
|
const remotes = new Set(["origin/main", "origin/feat/x"]);
|
|
const refs = ["HEAD", "main", "origin/main", "feat/x", "tag: v1", "tag: v2"];
|
|
|
|
it("merges a branch with its remote-tracking branches in the compact view", () => {
|
|
const labels = refLabels(refs, { compact: true, tagNames: false }, remotes);
|
|
expect(labels.map((l) => [l.kind, l.text])).toEqual([
|
|
["local", "origin & main"],
|
|
["local", "feat/x"],
|
|
["tag", ""],
|
|
]);
|
|
expect(labels[2]!.title).toBe("v1, v2");
|
|
});
|
|
|
|
it("lists every reference otherwise", () => {
|
|
const labels = refLabels(refs, { compact: false, tagNames: true }, remotes);
|
|
expect(labels.map((l) => l.text)).toEqual(["HEAD", "main", "feat/x", "origin/main", "v1", "v2"]);
|
|
});
|
|
});
|
|
|
|
describe("log options", () => {
|
|
beforeEach(() => {
|
|
vcsLog.mockReset().mockResolvedValue({ status: "ok", data: [] });
|
|
vcsConfigGet.mockReset().mockResolvedValue({ status: "ok", data: null });
|
|
useLogOptions.setState(DEFAULT_LOG_OPTIONS);
|
|
});
|
|
|
|
it("parses stored options with per-field defaults", () => {
|
|
expect(parseLogOptions(null)).toEqual(DEFAULT_LOG_OPTIONS);
|
|
expect(parseLogOptions({ sort: "topo", longEdges: true, showHash: "yes" })).toEqual({
|
|
...DEFAULT_LOG_OPTIONS,
|
|
sort: "topo",
|
|
longEdges: true,
|
|
});
|
|
});
|
|
|
|
it("previews the selected file's diff by default and remembers turning it off", () => {
|
|
expect(DEFAULT_LOG_OPTIONS.diffPreview).toBe(true);
|
|
expect(parseLogOptions({ diffPreview: false }).diffPreview).toBe(false);
|
|
expect(setLogOptions({ diffPreview: false })).toBe(false);
|
|
});
|
|
|
|
it("reloads only for options that change the query", () => {
|
|
expect(setLogOptions({ compactRefs: false })).toBe(false);
|
|
expect(setLogOptions({ noMerges: true })).toBe(true);
|
|
expect(JSON.parse(localStorage.getItem("openidea.vcs.logOptions")!).noMerges).toBe(true);
|
|
});
|
|
|
|
it("sends the graph options and the date range to the backend", async () => {
|
|
useGitLog.setState({ filters: { ...emptyFilters(), date: { after: 1000, before: 5000, label: "x" } } });
|
|
updateLogOptions({ sort: "topo", firstParent: true });
|
|
await reloadLog();
|
|
const q = vcsLog.mock.calls.map((x) => x[1] as Record<string, unknown>).find((x) => x.range === null)!;
|
|
expect(q).toMatchObject({ order: "topo", firstParent: true, noMerges: false, sinceMs: 1000, untilMs: 5000 });
|
|
});
|
|
|
|
it("goes to a reference by resolving it and selecting the commit", async () => {
|
|
const commit = { id: "abc123", parents: [], refs: [], summary: "s", message: "s" } as unknown as Commit;
|
|
vcsLog.mockImplementation(async (_r: unknown, q: { branch: string | null }) => ({
|
|
status: "ok",
|
|
data: q.branch === "v1" || q.branch === null ? [commit] : [],
|
|
}));
|
|
useGitLog.setState({ filters: emptyFilters(), commits: [], done: false });
|
|
expect(await goToRef("v1")).toBe(true);
|
|
expect(useGitLog.getState().selected).toBe("abc123");
|
|
expect(await goToRef("nope")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("Git menu dialogs", () => {
|
|
it("disables merge flags git refuses together", () => {
|
|
expect([...disabledMergeFlags(new Set(["ffOnly"]))].sort()).toEqual(["noCommit", "noFf", "squash"]);
|
|
expect(disabledMergeFlags(new Set(), true).size).toBe(4);
|
|
expect(disabledMergeFlags(new Set()).size).toBe(0);
|
|
});
|
|
|
|
it("lists a remote's branches without its prefix", () => {
|
|
const b = (name: string, remote: boolean) => ({ name, remote, current: false }) as Branch;
|
|
const branches = [b("main", false), b("origin/main", true), b("origin/HEAD", true), b("fork/dev", true)];
|
|
expect(remoteBranchesOf(branches, "origin")).toEqual(["main"]);
|
|
});
|
|
|
|
it("validates remote names", () => {
|
|
expect(validateRemoteName("origin", ["origin"])).toMatch(/already exists/);
|
|
expect(validateRemoteName("my remote", [])).toBe("Invalid remote name");
|
|
expect(validateRemoteName("", [])).toBe("Enter a remote name");
|
|
expect(validateRemoteName("upstream", ["origin"])).toBeNull();
|
|
});
|
|
|
|
it("suggests a worktree folder next to the repository", () => {
|
|
expect(defaultWorktreePath("/src/app", "feature/x")).toBe("/src/app-feature-x");
|
|
expect(defaultWorktreePath("/src/app/", "main")).toBe("/src/app-main");
|
|
expect(defaultWorktreePath("C:\\src\\app", "fix")).toBe("C:\\src\\app-fix");
|
|
});
|
|
});
|