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.
This commit is contained in:
@@ -8,10 +8,12 @@ import {
|
|||||||
readFileSync,
|
readFileSync,
|
||||||
realpathSync,
|
realpathSync,
|
||||||
mkdirSync,
|
mkdirSync,
|
||||||
|
statSync,
|
||||||
} from "fs";
|
} from "fs";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import { win32 } from "node:path";
|
import { win32 } from "node:path";
|
||||||
import { tmpdir } from "os";
|
import { tmpdir } from "os";
|
||||||
|
import pino from "pino";
|
||||||
import {
|
import {
|
||||||
__resetCheckoutShortstatCacheForTests,
|
__resetCheckoutShortstatCacheForTests,
|
||||||
__resetPullRequestStatusCacheForTests,
|
__resetPullRequestStatusCacheForTests,
|
||||||
@@ -314,6 +316,38 @@ describe("checkout git utilities", () => {
|
|||||||
await expect(getCheckoutStatus(nonGitDir)).resolves.toEqual({ isGit: false });
|
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 () => {
|
it("returns null for getCurrentBranch in a repo with no commits", async () => {
|
||||||
const emptyRepo = join(tempDir, "empty-repo");
|
const emptyRepo = join(tempDir, "empty-repo");
|
||||||
mkdirSync(emptyRepo, { recursive: true });
|
mkdirSync(emptyRepo, { recursive: true });
|
||||||
|
|||||||
@@ -780,7 +780,7 @@ export interface MergeFromBaseOptions {
|
|||||||
export interface CheckoutContext {
|
export interface CheckoutContext {
|
||||||
paseoHome?: string;
|
paseoHome?: string;
|
||||||
worktreesRoot?: string;
|
worktreesRoot?: string;
|
||||||
logger?: Pick<Logger, "trace">;
|
logger?: Pick<Logger, "trace" | "warn">;
|
||||||
facts?: CheckoutSnapshotFacts | null;
|
facts?: CheckoutSnapshotFacts | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -805,13 +805,6 @@ export type CheckoutSnapshotFacts =
|
|||||||
pullRequestLookupTarget: PullRequestStatusLookupTarget | null;
|
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<void> {
|
async function requireGitRepo(cwd: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await runGitCommand(["rev-parse", "--git-dir"], { cwd, envOverlay: READ_ONLY_GIT_ENV });
|
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);
|
return parseGitRevParsePath(stdout);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isNotGitRepositoryError(error)) {
|
// Git discovery is fail-open: keep the directory usable as non-Git and retain the diagnostic.
|
||||||
return null;
|
context?.logger?.warn(
|
||||||
}
|
{ err: error, cwd },
|
||||||
throw error;
|
"Git worktree discovery failed; treating directory as non-Git",
|
||||||
|
);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1035,7 +1030,7 @@ async function getPaseoWorktreeForCwd(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
isPaseoOwnedWorktree: true,
|
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;
|
let normalizedBaseRef = baseRef;
|
||||||
normalizedBaseRef = normalizeLocalBranchRefName(normalizedBaseRef);
|
normalizedBaseRef = normalizeLocalBranchRefName(normalizedBaseRef);
|
||||||
const currentWorktreeRoot = (await getWorktreeRoot(cwd)) ?? cwd;
|
const currentWorktreeRoot = (await getWorktreeRoot(cwd, context)) ?? cwd;
|
||||||
if (normalizedBaseRef === currentBranch) {
|
if (normalizedBaseRef === currentBranch) {
|
||||||
return currentWorktreeRoot;
|
return currentWorktreeRoot;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user