Skip to content
Closed
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
107 changes: 101 additions & 6 deletions packages/shared/src/devHome.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,27 @@ import * as Effect from "effect/Effect";

import { resolveGitWorktreePath, resolveWorktreeT3Home } from "./devHome.ts";

/**
* Denying read on the `.git` file only proves anything for a user the mode
* applies to. Root ignores it, and Windows has no equivalent, so the
* unreadable case is skipped rather than silently passing there.
*/
const canDenyReads = typeof process.getuid === "function" && process.getuid() !== 0;

const makeRepo = (
kind:
| "worktree"
| "checkout"
| "bare"
| "no-repo"
| "submodule"
| "unparseable-git-file"
| "unreadable-git-file"
| "bare-repo-worktree"
| "custom-common-dir-worktree",
| "custom-common-dir-worktree"
| "relative-worktree"
| "windows-worktree"
| "shallow-gitdir"
| "nested-checkout",
) =>
Effect.acquireRelease(
Effect.sync(() => {
Expand All @@ -29,18 +41,45 @@ const makeRepo = (
} else if (kind === "custom-common-dir-worktree") {
// $GIT_COMMON_DIR need not be named `.git` at all.
NodeFS.writeFileSync(NodePath.join(root, ".git"), "gitdir: /srv/store/worktrees/x\n");
} else if (kind === "relative-worktree") {
// git >= 2.48 with `worktree.useRelativePaths` writes a relative pointer.
NodeFS.writeFileSync(NodePath.join(root, ".git"), "gitdir: ../.git/worktrees/x\n");
} else if (kind === "windows-worktree") {
NodeFS.writeFileSync(NodePath.join(root, ".git"), "gitdir: C:\\repo\\.git\\worktrees\\x\n");
} else if (kind === "shallow-gitdir") {
// Nothing precedes `worktrees`, so there is no common dir to speak of.
NodeFS.writeFileSync(NodePath.join(root, ".git"), "gitdir: worktrees/x\n");
} else if (kind === "submodule") {
NodeFS.writeFileSync(NodePath.join(root, ".git"), "gitdir: ../.git/modules/sub\n");
} else if (kind === "unreadable-git-file") {
} else if (kind === "unparseable-git-file") {
NodeFS.writeFileSync(NodePath.join(root, ".git"), "not a gitdir pointer\n");
} else if (kind === "unreadable-git-file") {
const gitPath = NodePath.join(root, ".git");
NodeFS.writeFileSync(gitPath, "gitdir: /elsewhere/.git/worktrees/x\n");
NodeFS.chmodSync(gitPath, 0o000);
} else if (kind === "checkout") {
NodeFS.mkdirSync(NodePath.join(root, ".git"));
}
const nested = NodePath.join(root, "apps", "web", "src");
NodeFS.mkdirSync(nested, { recursive: true });
if (kind === "nested-checkout") {
// A worktree that contains a second, unrelated repository. Walking up
// from inside the inner one must stop there, not adopt the outer root.
NodeFS.writeFileSync(NodePath.join(root, ".git"), "gitdir: /elsewhere/.git/worktrees/x\n");
NodeFS.mkdirSync(NodePath.join(root, "apps", "web", ".git"));
}
return { root, nested };
}),
({ root }) => Effect.sync(() => NodeFS.rmSync(root, { recursive: true, force: true })),
({ root }) =>
Effect.sync(() => {
// Restore read permission first: a 0o000 file is still removable, but
// only because the directory is writable — do not depend on that.
const gitPath = NodePath.join(root, ".git");
if (NodeFS.existsSync(gitPath) && NodeFS.statSync(gitPath).isFile()) {
NodeFS.chmodSync(gitPath, 0o600);
}
NodeFS.rmSync(root, { recursive: true, force: true });
}),
);

describe("resolveGitWorktreePath", () => {
Expand All @@ -60,7 +99,7 @@ describe("resolveGitWorktreePath", () => {

it.effect("reports a directory outside a repository", () =>
Effect.gen(function* () {
const { nested } = yield* makeRepo("bare");
const { nested } = yield* makeRepo("no-repo");
assert.equal(yield* resolveGitWorktreePath(nested), undefined);
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);
Expand All @@ -74,7 +113,7 @@ describe("resolveGitWorktreePath", () => {

it.effect("reports a .git file without a usable gitdir pointer", () =>
Effect.gen(function* () {
const { nested } = yield* makeRepo("unreadable-git-file");
const { nested } = yield* makeRepo("unparseable-git-file");
assert.equal(yield* resolveGitWorktreePath(nested), undefined);
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);
Expand All @@ -92,6 +131,47 @@ describe("resolveGitWorktreePath", () => {
assert.equal(yield* resolveGitWorktreePath(nested), NodePath.resolve(root));
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);

it.effect("finds a worktree from a relative gitdir pointer", () =>
Effect.gen(function* () {
const { root, nested } = yield* makeRepo("relative-worktree");
assert.equal(yield* resolveGitWorktreePath(nested), NodePath.resolve(root));
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);

it.effect("finds a worktree from a Windows gitdir pointer", () =>
Effect.gen(function* () {
const { root, nested } = yield* makeRepo("windows-worktree");
assert.equal(yield* resolveGitWorktreePath(nested), NodePath.resolve(root));
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);

it.effect("rejects a gitdir with nothing before `worktrees`", () =>
Effect.gen(function* () {
const { nested } = yield* makeRepo("shallow-gitdir");
assert.equal(yield* resolveGitWorktreePath(nested), undefined);
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);

it.effect("stops at a repository nested inside a worktree", () =>
Effect.gen(function* () {
const { nested } = yield* makeRepo("nested-checkout");
assert.equal(yield* resolveGitWorktreePath(nested), undefined);
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);

it.effect.skipIf(!canDenyReads)(
"fails instead of reporting `not a worktree` when .git cannot be read",
() =>
Effect.gen(function* () {
const { nested } = yield* makeRepo("unreadable-git-file");
// The whole point: an unreadable pointer is not an answer. Reporting
// `undefined` would send the caller at the shared home and its live
// database, which is the outcome this module exists to prevent.
const error = yield* Effect.flip(resolveGitWorktreePath(nested));
assert.notEqual(error.reason._tag, "NotFound");
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);
});

describe("resolveWorktreeT3Home", () => {
Expand All @@ -103,4 +183,19 @@ describe("resolveWorktreeT3Home", () => {
assert.isFalse(NodeFS.existsSync(home ?? ""));
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);

it.effect("answers with undefined outside a linked worktree", () =>
Effect.gen(function* () {
const { nested } = yield* makeRepo("checkout");
assert.equal(yield* resolveWorktreeT3Home(nested), undefined);
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);

it.effect.skipIf(!canDenyReads)("propagates an unreadable .git rather than falling back", () =>
Effect.gen(function* () {
const { nested } = yield* makeRepo("unreadable-git-file");
const error = yield* Effect.flip(resolveWorktreeT3Home(nested));
assert.notEqual(error.reason._tag, "NotFound");
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);
});
37 changes: 30 additions & 7 deletions packages/shared/src/devHome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Option from "effect/Option";
import * as Path from "effect/Path";
import * as PlatformError from "effect/PlatformError";

/**
* A missing `.git` is the ordinary answer to "is this a worktree root?" — it
* means keep walking up. Any other failure is not evidence of absence: an
* unreadable file, a busy descriptor or a disconnected network mount all say
* "unknown", and answering "not a worktree" to unknown is precisely the
* outcome this module exists to prevent, because the caller then falls through
* to the shared home and its live database. Only absence is absence; the rest
* propagate.
*/
const isAbsent = (error: PlatformError.PlatformError): boolean => error.reason._tag === "NotFound";

/**
* A `.git` file points at the real git directory. A linked worktree's lives at
Expand Down Expand Up @@ -55,26 +67,33 @@ const pointsAtLinkedWorktree = (gitFileContents: string, path: Path.Path): boole
*/
export const resolveGitWorktreePath = (
cwd: string,
): Effect.Effect<string | undefined, never, FileSystem.FileSystem | Path.Path> =>
): Effect.Effect<
string | undefined,
PlatformError.PlatformError,
FileSystem.FileSystem | Path.Path
> =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;

let directory = path.resolve(cwd);
for (;;) {
const gitPath = path.join(directory, ".git");
const info = yield* fileSystem.stat(gitPath).pipe(Effect.option);
const info = yield* fileSystem.stat(gitPath).pipe(
Effect.map(Option.some),
Effect.catchIf(isAbsent, () => Effect.succeed(Option.none())),
);
if (Option.isSome(info)) {
// A directory means the main checkout. Stop either way: nesting one
// repository inside another does not make the outer one this root.
if (info.value.type !== "File") {
return undefined;
}
// A submodule also has a `.git` file, but it is not a worktree of this
// repository and gets no worktree-local home.
const contents = yield* fileSystem
.readFileString(gitPath)
.pipe(Effect.orElseSucceed(() => ""));
// repository and gets no worktree-local home. `stat` has already
// established the file is there, so a read failure here is a real
// fault rather than an answer — let it surface.
const contents = yield* fileSystem.readFileString(gitPath);
return pointsAtLinkedWorktree(contents, path) ? directory : undefined;
}
const parent = path.dirname(directory);
Expand All @@ -92,7 +111,11 @@ export const resolveGitWorktreePath = (
*/
export const resolveWorktreeT3Home = (
cwd: string,
): Effect.Effect<string | undefined, never, FileSystem.FileSystem | Path.Path> =>
): Effect.Effect<
string | undefined,
PlatformError.PlatformError,
FileSystem.FileSystem | Path.Path
> =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Home-dir blocked by .git errors

Medium Severity

resolveWorktreeT3Home can now fail on an unreadable .git, but the sole caller always awaits it before applying --home-dir precedence. An explicit --home-dir therefore cannot override a detection failure, even though that value alone would pick a safe home and never touch the shared database.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9bb4138. Configure here.

Effect.gen(function* () {
const worktreePath = yield* resolveGitWorktreePath(cwd);
if (worktreePath === undefined) {
Expand Down
Loading