дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
- Settings › Commit › "Use non-modal commit interface": when off, Local Changes, Shelf and Stash become tabs of the Git tool window and ⌘K opens a Commit dialog; switches live. - Git tool window: Worktrees tab; Show History and Show Git Log for a branch open their own closable tabs instead of changing the Log filters. - Log: toolbar and context menu for a commit's changed files, vertical toolbar, Tags node and hiding for the branches pane, Collapse / Expand Linear Branches as separate actions, and a Version Control › Log page. - Branch popup: collapsible groups, branches grouped by prefix, an options menu behind the gear, incoming / outgoing icons and "Pull into … Using Rebase / Merge"; New Branch on ⌥⌘N outside the editor. - Git main menu regrouped with Uncommitted Changes and Current File submenus; rebase / merge controls enabled only while one is in progress. - Settings: Version Control lists its pages; new options for clearing the initial commit message, showing Update options, confirming dropped commits, revealing created patches, large-file and file-name warnings, the Push dialog after Commit and Push, combined stashes and shelves, and removing applied files from the shelf.
130 рядки
5.6 KiB
TypeScript
130 рядки
5.6 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const vcsLog = vi.fn();
|
|
const fsReadText = 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: async () => ({ status: "ok", data: null }),
|
|
fsReadText: (path: string) => fsReadText(path),
|
|
},
|
|
};
|
|
});
|
|
|
|
import { gitTabs, shownTab, stashTabOf, stashTabs } from "../gitTabs";
|
|
import { pushDialogNeeded } from "../commitUi";
|
|
import { installPreparedMessage, keepMessageAfterCommit, messageAfterCommit, useCommit } from "../commitStore";
|
|
import { closeLogTab, emptyFilters, openLogTab, selectGitTab, setLogFilters, useGitLog } from "../logStore";
|
|
import { useVcs } from "../store";
|
|
|
|
const ids = (tabs: { id: string }[]) => tabs.map((t) => t.id);
|
|
|
|
describe("Git and Commit tool window tabs", () => {
|
|
const base = { combine: false, shelfCount: 0, stashCount: 0, tab: "log", logTabs: [] };
|
|
|
|
it("puts Local Changes, Shelf and Stash into the Git window only with the modal commit interface", () => {
|
|
expect(ids(gitTabs({ ...base, modal: false }))).toEqual(["log", "console", "worktrees"]);
|
|
expect(ids(gitTabs({ ...base, modal: true }))).toEqual(["changes", "stashes", "log", "console", "worktrees"]);
|
|
expect(ids(gitTabs({ ...base, modal: true, shelfCount: 2 }))).toEqual([
|
|
"changes",
|
|
"shelf",
|
|
"stashes",
|
|
"log",
|
|
"console",
|
|
"worktrees",
|
|
]);
|
|
});
|
|
|
|
it("combines stashes and shelves into one tab", () => {
|
|
expect(stashTabs({ ...base, combine: true, shelfCount: 2, stashCount: 1 })).toEqual([
|
|
{ id: "stashes", label: "Stashes and Shelves", count: 3 },
|
|
]);
|
|
expect(stashTabOf("shelf", true)).toBe("stashes");
|
|
expect(stashTabOf("shelf", false)).toBe("shelf");
|
|
});
|
|
|
|
it("lists log tabs as closable after the main Log, and falls back to the Log for a missing tab", () => {
|
|
const logTabs = [{ id: "history:1", kind: "history" as const, target: "/p/src/a.ts" }];
|
|
const tabs = gitTabs({ ...base, modal: false, logTabs });
|
|
expect(tabs.find((t) => t.id === "history:1")).toEqual({ id: "history:1", label: "History: a.ts", closable: true });
|
|
expect(shownTab("changes", tabs, false)).toBe("log");
|
|
expect(shownTab("history:1", tabs, false)).toBe("history:1");
|
|
});
|
|
});
|
|
|
|
describe("Commit and Push… push dialog", () => {
|
|
const git = { showPushDialog: true, pushDialogOnlyProtected: false, protectedBranches: "master;main" };
|
|
|
|
it("shows the dialog unless turned off, or only for protected branches", () => {
|
|
expect(pushDialogNeeded(git, "feature")).toBe(true);
|
|
expect(pushDialogNeeded({ ...git, showPushDialog: false }, "main")).toBe(false);
|
|
expect(pushDialogNeeded({ ...git, pushDialogOnlyProtected: true }, "feature")).toBe(false);
|
|
expect(pushDialogNeeded({ ...git, pushDialogOnlyProtected: true }, "main")).toBe(true);
|
|
// Nothing to tell the target from (detached HEAD): always the dialog.
|
|
expect(pushDialogNeeded({ ...git, showPushDialog: false }, null)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("commit message after a commit", () => {
|
|
beforeEach(() => fsReadText.mockReset());
|
|
|
|
it("keeps the committed message unless Clear initial commit message is on", () => {
|
|
expect(messageAfterCommit(" fix: thing \n", false)).toBe("fix: thing");
|
|
expect(messageAfterCommit("fix: thing", true)).toBe("");
|
|
});
|
|
|
|
it("lets a prepared merge message replace a kept one, but not a typed one", async () => {
|
|
useVcs.setState({ root: "/p", branch: null });
|
|
const dispose = installPreparedMessage();
|
|
keepMessageAfterCommit("fix: thing", false);
|
|
fsReadText.mockResolvedValue({ status: "ok", data: "Merge branch 'x'\n# Conflicts:\n" });
|
|
useVcs.setState({ branch: { operation: "merge" } as never });
|
|
await vi.waitFor(() => expect(useCommit.getState().message).toBe("Merge branch 'x'"));
|
|
useVcs.setState({ branch: null });
|
|
|
|
useCommit.setState({ message: "typed" });
|
|
useVcs.setState({ branch: { operation: "merge" } as never });
|
|
await Promise.resolve();
|
|
expect(useCommit.getState().message).toBe("typed");
|
|
dispose();
|
|
});
|
|
});
|
|
|
|
describe("log tabs of their own", () => {
|
|
beforeEach(() => {
|
|
vcsLog.mockReset().mockResolvedValue({ status: "ok", data: [] });
|
|
useVcs.setState({ root: "/p", repo: "/p", roots: ["/p"] });
|
|
useGitLog.setState({ tab: "log", logTabs: [], shownLog: "log", filters: emptyFilters() });
|
|
});
|
|
|
|
it("opens Show History in a closable tab and leaves the main Log's filters alone", () => {
|
|
setLogFilters({ authors: ["bob"] });
|
|
const id = openLogTab("history", "/p/src/a.ts");
|
|
expect(useGitLog.getState()).toMatchObject({ tab: id, shownLog: id });
|
|
expect(useGitLog.getState().filters).toMatchObject({ path: "/p/src/a.ts", authors: [] });
|
|
// The same file again: the same tab.
|
|
selectGitTab("log");
|
|
expect(useGitLog.getState().filters.authors).toEqual(["bob"]);
|
|
expect(openLogTab("history", "/p/src/a.ts")).toBe(id);
|
|
expect(useGitLog.getState().logTabs).toHaveLength(1);
|
|
|
|
closeLogTab(id);
|
|
expect(useGitLog.getState()).toMatchObject({ tab: "log", shownLog: "log", logTabs: [] });
|
|
expect(useGitLog.getState().filters).toMatchObject({ authors: ["bob"], path: null });
|
|
});
|
|
|
|
it("keeps a log tab's view while another content tab is shown", () => {
|
|
const id = openLogTab("branch", "feature");
|
|
selectGitTab("console");
|
|
expect(useGitLog.getState()).toMatchObject({ tab: "console", shownLog: id });
|
|
selectGitTab(id);
|
|
expect(useGitLog.getState().filters.branches).toEqual(["feature"]);
|
|
closeLogTab(id);
|
|
});
|
|
});
|