diff --git a/src/core/git.test.ts b/src/core/git.test.ts index 88dc0c0..f985f4f 100644 --- a/src/core/git.test.ts +++ b/src/core/git.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, spyOn } from "bun:test"; -import { mkdtemp, realpath, rm, symlink, writeFile } from "node:fs/promises"; +import { mkdtemp, readFile, realpath, rm, symlink, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import * as gitModule from "./git"; @@ -23,6 +23,7 @@ const runGit = async (args: string[], cwd: string): Promise => { const proc = Bun.spawn({ cmd: ["git", ...args], cwd, + env: process.env, stdout: "pipe", stderr: "pipe", }); @@ -44,6 +45,7 @@ const gitSucceeds = async (args: string[], cwd: string): Promise => { const proc = Bun.spawn({ cmd: ["git", ...args], cwd, + env: process.env, stdout: "ignore", stderr: "ignore", }); @@ -67,6 +69,52 @@ const withRepo = async (fn: (repoRoot: string) => Promise) => { }; describe("core/git worktrees", () => { + it("initializes submodules when creating a managed worktree", async () => { + const originalAllowProtocol = process.env.GIT_ALLOW_PROTOCOL; + process.env.GIT_ALLOW_PROTOCOL = "file"; + + try { + const submoduleRoot = await mkdtemp(join(tmpdir(), "workbox-submodule-")); + try { + await runGit(["init"], submoduleRoot); + await runGit(["config", "user.email", "test@example.com"], submoduleRoot); + await runGit(["config", "user.name", "Test"], submoduleRoot); + await writeFile(join(submoduleRoot, "submodule.txt"), "submodule\n"); + await runGit(["add", "submodule.txt"], submoduleRoot); + await runGit(["commit", "-m", "init submodule"], submoduleRoot); + + await withRepo(async (repoRoot) => { + await runGit(["submodule", "add", submoduleRoot, "deps/submodule"], repoRoot); + await runGit(["commit", "-am", "add submodule"], repoRoot); + + const worktreesDir = join(repoRoot, ".workbox", "worktrees"); + const branchPrefix = "wkb/"; + + const created = await createWorktree({ + repoRoot, + worktreesDir, + branchPrefix, + baseRef: "HEAD", + name: "box1", + }); + + expect(await readFile(join(created.path, "deps/submodule/submodule.txt"), "utf8")).toBe( + "submodule\n" + ); + expect(await runGit(["submodule", "status"], created.path)).not.toStartWith("-"); + }); + } finally { + await rm(submoduleRoot, { recursive: true, force: true }); + } + } finally { + if (originalAllowProtocol === undefined) { + delete process.env.GIT_ALLOW_PROTOCOL; + } else { + process.env.GIT_ALLOW_PROTOCOL = originalAllowProtocol; + } + } + }); + it("creates, lists, statuses, and removes a managed worktree without deleting the branch", async () => { await withRepo(async (repoRoot) => { const worktreesDir = join(repoRoot, ".workbox", "worktrees"); diff --git a/src/core/git.ts b/src/core/git.ts index 6ab45fb..c0f4b72 100644 --- a/src/core/git.ts +++ b/src/core/git.ts @@ -1,4 +1,4 @@ -import { mkdir, realpath } from "node:fs/promises"; +import { access, mkdir, realpath } from "node:fs/promises"; import { join, relative, resolve, sep } from "node:path"; import { CliError } from "../ui/errors"; @@ -64,6 +64,15 @@ const normalizePath = async (path: string): Promise => { } }; +const pathExists = async (path: string): Promise => { + try { + await access(path); + return true; + } catch { + return false; + } +}; + const assertWorktreesDirSafe = async (repoRoot: string, worktreesDir: string): Promise => { const within = await checkPathWithinRoot({ rootDir: repoRoot, @@ -255,6 +264,10 @@ export const createWorktree = async (input: { input.repoRoot ); + if (await pathExists(join(worktreePath, ".gitmodules"))) { + await runGit(["submodule", "update", "--init", "--recursive"], worktreePath); + } + return { name: input.name, path: await normalizePath(worktreePath),