From b4a5b6a3ff94af056e30231b19cbdacbe0566c8f Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sat, 18 Jul 2026 11:49:11 +0200 Subject: [PATCH] Add non-Git projects across filesystem mounts (#2187) * fix(projects): allow non-git folders across mounts Treat failed Git worktree discovery as non-Git so ordinary directories remain addable. Preserve the underlying diagnostic as a structured warning instead of failing project creation. * fix(projects): preserve discovery warnings Propagate checkout context through the remaining worktree discovery calls so fail-open classification retains its structured warning. --- .../server/src/utils/checkout-git.test.ts | 34 +++++++++++++++++++ packages/server/src/utils/checkout-git.ts | 23 +++++-------- 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/packages/server/src/utils/checkout-git.test.ts b/packages/server/src/utils/checkout-git.test.ts index 78369c64a..cf07bfc97 100644 --- a/packages/server/src/utils/checkout-git.test.ts +++ b/packages/server/src/utils/checkout-git.test.ts @@ -8,10 +8,12 @@ import { readFileSync, realpathSync, mkdirSync, + statSync, } from "fs"; import { join } from "path"; import { win32 } from "node:path"; import { tmpdir } from "os"; +import pino from "pino"; import { __resetCheckoutShortstatCacheForTests, __resetPullRequestStatusCacheForTests, @@ -314,6 +316,38 @@ describe("checkout git utilities", () => { await expect(getCheckoutStatus(nonGitDir)).resolves.toEqual({ isGit: false }); }); + it.runIf( + process.platform !== "win32" && + existsSync("/dev/shm") && + statSync("/dev").dev !== statSync("/dev/shm").dev, + )("warns and reports a non-git directory at a filesystem boundary as non-git", async () => { + const nonGitDir = realpathSync.native(mkdtempSync("/dev/shm/checkout-git-boundary-test-")); + const records: unknown[] = []; + const logger = pino( + { level: "warn" }, + { + write(line: string) { + records.push(JSON.parse(line)); + }, + }, + ); + try { + await expect(getCheckoutStatus(nonGitDir, { logger })).resolves.toEqual({ isGit: false }); + expect(records).toEqual([ + expect.objectContaining({ + level: 40, + cwd: nonGitDir, + msg: "Git worktree discovery failed; treating directory as non-Git", + err: expect.objectContaining({ + message: expect.stringContaining("Stopping at filesystem boundary"), + }), + }), + ]); + } finally { + rmSync(nonGitDir, { recursive: true, force: true }); + } + }); + it("returns null for getCurrentBranch in a repo with no commits", async () => { const emptyRepo = join(tempDir, "empty-repo"); mkdirSync(emptyRepo, { recursive: true }); diff --git a/packages/server/src/utils/checkout-git.ts b/packages/server/src/utils/checkout-git.ts index 46a35f66e..331d2413f 100644 --- a/packages/server/src/utils/checkout-git.ts +++ b/packages/server/src/utils/checkout-git.ts @@ -780,7 +780,7 @@ export interface MergeFromBaseOptions { export interface CheckoutContext { paseoHome?: string; worktreesRoot?: string; - logger?: Pick; + logger?: Pick; facts?: CheckoutSnapshotFacts | null; } @@ -805,13 +805,6 @@ export type CheckoutSnapshotFacts = pullRequestLookupTarget: PullRequestStatusLookupTarget | null; }; -function isNotGitRepositoryError(error: unknown): boolean { - if (!(error instanceof Error)) { - return false; - } - return /not a git repository \(or any of the parent directories\): \.git/i.test(error.message); -} - async function requireGitRepo(cwd: string): Promise { try { await runGitCommand(["rev-parse", "--git-dir"], { cwd, envOverlay: READ_ONLY_GIT_ENV }); @@ -867,10 +860,12 @@ async function getWorktreeRoot(cwd: string, context?: CheckoutContext): Promise< }); return parseGitRevParsePath(stdout); } catch (error) { - if (isNotGitRepositoryError(error)) { - return null; - } - throw error; + // Git discovery is fail-open: keep the directory usable as non-Git and retain the diagnostic. + context?.logger?.warn( + { err: error, cwd }, + "Git worktree discovery failed; treating directory as non-Git", + ); + return null; } } @@ -1035,7 +1030,7 @@ async function getPaseoWorktreeForCwd( return { isPaseoOwnedWorktree: true, - worktreeRoot: knownWorktreeRoot ?? (await getWorktreeRoot(cwd)) ?? cwd, + worktreeRoot: knownWorktreeRoot ?? (await getWorktreeRoot(cwd, context)) ?? cwd, }; } @@ -2972,7 +2967,7 @@ export async function mergeToBase( } let normalizedBaseRef = baseRef; normalizedBaseRef = normalizeLocalBranchRefName(normalizedBaseRef); - const currentWorktreeRoot = (await getWorktreeRoot(cwd)) ?? cwd; + const currentWorktreeRoot = (await getWorktreeRoot(cwd, context)) ?? cwd; if (normalizedBaseRef === currentBranch) { return currentWorktreeRoot; }