Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion src/core/git.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -23,6 +23,7 @@ const runGit = async (args: string[], cwd: string): Promise<string> => {
const proc = Bun.spawn({
cmd: ["git", ...args],
cwd,
env: process.env,
stdout: "pipe",
stderr: "pipe",
});
Expand All @@ -44,6 +45,7 @@ const gitSucceeds = async (args: string[], cwd: string): Promise<boolean> => {
const proc = Bun.spawn({
cmd: ["git", ...args],
cwd,
env: process.env,
stdout: "ignore",
stderr: "ignore",
});
Expand All @@ -67,6 +69,52 @@ const withRepo = async (fn: (repoRoot: string) => Promise<void>) => {
};

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");
Expand Down
15 changes: 14 additions & 1 deletion src/core/git.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -64,6 +64,15 @@ const normalizePath = async (path: string): Promise<string> => {
}
};

const pathExists = async (path: string): Promise<boolean> => {
try {
await access(path);
return true;
} catch {
return false;
}
};

const assertWorktreesDirSafe = async (repoRoot: string, worktreesDir: string): Promise<void> => {
const within = await checkPathWithinRoot({
rootDir: repoRoot,
Expand Down Expand Up @@ -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),
Expand Down