fix(migrate): preserve checkout and shell semantics

This commit is contained in:
Mohamed Boudra
2026-07-19 00:48:41 +02:00
parent ddaaaa27be
commit c54ddfd07f
7 changed files with 167 additions and 22 deletions
@@ -94,6 +94,75 @@ test("a different live checkout continues to protect its branch", async () => {
expect(realpathSync(existingPath)).toBe(existingPath);
});
test("ensureCheckout repairs only the requested missing registration", async () => {
const repoRoot = createRepository();
execFileSync("git", ["branch", "unrelated"], { cwd: repoRoot });
const staleFeature = path.join(path.dirname(repoRoot), "stale-feature");
const staleUnrelated = path.join(path.dirname(repoRoot), "stale-unrelated");
execFileSync("git", ["worktree", "add", staleFeature, "feature"], { cwd: repoRoot });
execFileSync("git", ["worktree", "add", staleUnrelated, "unrelated"], { cwd: repoRoot });
rmSync(staleFeature, { recursive: true, force: true });
rmSync(staleUnrelated, { recursive: true, force: true });
const daemon = await createTestPaseoDaemon();
cleanupDaemons.add(daemon);
const paseo = await connectHostAutomation({
appVersion: "0.1.110",
clientId: "migration-stale-registration-e2e",
env: {},
host: `127.0.0.1:${daemon.port}`,
});
cleanupConnections.add(paseo);
const ensured = await paseo.ensureCheckout({
rootPath: repoRoot,
refName: "feature",
directoryName: "imported-feature",
});
expect(path.basename(ensured.path)).toBe("imported-feature");
const registrations = execFileSync("git", ["worktree", "list", "--porcelain"], {
cwd: repoRoot,
encoding: "utf8",
});
expect(registrations).not.toContain(staleFeature);
expect(registrations).toContain(staleUnrelated);
});
test("normalized checkout-name collisions cannot reuse another branch", async () => {
const repoRoot = createRepository();
execFileSync("git", ["branch", "other-feature"], { cwd: repoRoot });
const daemon = await createTestPaseoDaemon();
cleanupDaemons.add(daemon);
const paseo = await connectHostAutomation({
appVersion: "0.1.110",
clientId: "migration-slug-collision-e2e",
env: {},
host: `127.0.0.1:${daemon.port}`,
});
cleanupConnections.add(paseo);
const first = await paseo.ensureCheckout({
rootPath: repoRoot,
refName: "feature",
directoryName: "Foo.Bar",
});
await expect(
paseo.ensureCheckout({
rootPath: repoRoot,
refName: "other-feature",
directoryName: "foo-bar",
}),
).rejects.toThrow(/already used by branch feature/);
expect(
execFileSync("git", ["branch", "--show-current"], {
cwd: first.path,
encoding: "utf8",
}).trim(),
).toBe("feature");
});
test("the public connector authenticates to a real password-protected daemon and closes cleanly", async () => {
const daemon = await createTestPaseoDaemon({
auth: { password: hashDaemonPassword("connector-secret") },
@@ -117,6 +117,7 @@ export async function createWorktreeCore(
worktreesRoot: input.worktreesRoot,
});
if (existingWorktree) {
assertExistingWorktreeMatchesIntent(existingWorktree, intent, normalizedSlug);
return { worktree: existingWorktree, intent, repoRoot, created: false };
}
@@ -135,6 +136,22 @@ export async function createWorktreeCore(
};
}
function assertExistingWorktreeMatchesIntent(
existingWorktree: WorktreeConfig,
intent: WorktreeCreationIntent,
slug: string,
): void {
const intendedBranchName =
intent.kind === "checkout-change-request" || intent.kind === "checkout-github-pr"
? (intent.localBranchName ?? intent.headRef)
: intent.branchName;
if (existingWorktree.branchName !== intendedBranchName) {
throw new Error(
`Worktree name ${slug} is already used by branch ${existingWorktree.branchName}.`,
);
}
}
async function resolveForge(
repoRoot: string,
deps: CreateWorktreeCoreDeps,
-11
View File
@@ -1226,7 +1226,6 @@ export const createWorktree = async ({
paseoHome,
worktreesRoot,
}: CreateWorktreeOptions): Promise<WorktreeConfig> => {
await pruneVerifiedStaleWorktrees(cwd);
const sourcePlan = await resolveWorktreeSourcePlan({ cwd, source, desiredSlug: worktreeSlug });
let worktreePath = join(await getPaseoWorktreesRoot(cwd, paseoHome, worktreesRoot), worktreeSlug);
mkdirSync(dirname(worktreePath), { recursive: true });
@@ -1284,16 +1283,6 @@ export const createWorktree = async ({
};
};
async function pruneVerifiedStaleWorktrees(cwd: string): Promise<void> {
const { stdout } = await runGitCommand(["worktree", "list", "--porcelain"], {
cwd,
envOverlay: READ_ONLY_GIT_ENV,
});
const hasStaleRegistration = parseWorktreeList(stdout).some((entry) => !existsSync(entry.path));
if (!hasStaleRegistration) return;
await runGitCommand(["worktree", "prune"], { cwd, timeout: 30_000 });
}
interface ResolveWorktreeSourcePlanOptions {
cwd: string;
source: WorktreeSource;