diff --git a/src/features/vcs/__tests__/branchOps.test.ts b/src/features/vcs/__tests__/branchOps.test.ts new file mode 100644 index 000000000..e6899b2f1 --- /dev/null +++ b/src/features/vcs/__tests__/branchOps.test.ts @@ -0,0 +1,46 @@ +import { beforeEach, expect, it, vi } from "vitest"; +import type { BranchInfo } from "@/bindings"; +const mocks = vi.hoisted(() => ({ prompt: vi.fn() })); +vi.mock("../dialogs", async (original) => ({ + ...(await original()), + promptText: mocks.prompt, +})); +import { newBranch, newBranchInRepos } from "../branchOps"; +import { useVcs } from "../store"; + +const branch = (head: string | null): BranchInfo => ({ + head, + headCommit: "abc1234567890", + upstream: null, + ahead: 0, + behind: 0, + operation: null, +}); + +beforeEach(() => { + vi.clearAllMocks(); + useVcs.setState({ root: "/p", repo: "/p/front", branch: branch("feature/ui") }); + // Cancel: only what the dialog was offered matters here, not its outcome. + mocks.prompt.mockResolvedValue(null); +}); + +it("prefills a plain New Branch with the current branch name", async () => { + await newBranch(); + expect(mocks.prompt.mock.calls[0]?.[0].initial).toBe("feature/ui"); +}); + +it("prefills nothing when HEAD is detached", async () => { + useVcs.setState({ branch: branch(null) }); + await newBranch(); + expect(mocks.prompt.mock.calls[0]?.[0].initial).toBe(""); +}); + +it("still suggests a remote branch's name without the prefix", async () => { + await newBranch("origin/main"); + expect(mocks.prompt.mock.calls[0]?.[0].initial).toBe("main"); +}); + +it("prefills the current branch in the multi-repository dialog too", async () => { + await newBranchInRepos(["/p/front", "/p/back"]); + expect(mocks.prompt.mock.calls[0]?.[0].initial).toBe("feature/ui"); +}); diff --git a/src/features/vcs/branchOps.ts b/src/features/vcs/branchOps.ts index b14a911b3..b5f4adb09 100644 --- a/src/features/vcs/branchOps.ts +++ b/src/features/vcs/branchOps.ts @@ -145,11 +145,18 @@ export async function checkoutBranch(name: string) { /** New Branch: creates a branch from HEAD (or `from`) and, by default, checks it out. */ export async function newBranch(from?: string) { const repo = currentRepo(); - const suggestion = from ? (from.includes("/") ? shortRemoteName(from) : `${from}-copy`) : ""; + // IDEA opens the dialog with a suggestion instead of an empty field: a + // remote's name without the prefix, the source ref as `-copy`, or the + // current branch for a plain New Branch from HEAD. + const suggestion = from + ? from.includes("/") + ? shortRemoteName(from) + : `${from}-copy` + : (useVcs.getState().branch?.head ?? ""); const r = await promptText({ title: from ? tx`Create New Branch from ${from}` : "Create New Branch", label: "New branch name:", - initial: from && from.includes("/") && !validateBranchName(suggestion) ? suggestion : "", + initial: suggestion && !validateBranchName(suggestion) ? suggestion : "", mono: true, okLabel: "Create", validate: validateBranchName, @@ -360,7 +367,7 @@ export async function newBranchInRepos(roots: readonly string[], from?: string) const r = await promptText({ title: from ? tx`Create New Branch from ${from}` : "Create New Branch", label: tx`New branch name (in ${roots.length} repositories):`, - initial: from?.includes("/") ? shortRemoteName(from) : "", + initial: from ? (from.includes("/") ? shortRemoteName(from) : "") : (useVcs.getState().branch?.head ?? ""), mono: true, okLabel: "Create", validate: validateBranchName,