Skip to content
114 changes: 111 additions & 3 deletions plugins/codex/scripts/lib/workspace.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,116 @@
import { ensureGitRepository } from "./git.mjs";
import fs from "node:fs";
import path from "node:path";

export function resolveWorkspaceRoot(cwd) {
function resolveGitDirectory(candidatePath) {
const candidateStats = fs.statSync(candidatePath);
if (candidateStats.isDirectory()) {
return fs.realpathSync.native(candidatePath);
Comment on lines +6 to +7

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Verify .git directories before accepting them as repositories

When a non-repository child inside a real checkout contains an empty (or otherwise invalid) .git directory, this branch immediately accepts it. Git ignores such a directory and continues discovery to the enclosing repository, so a task invoked below the child now receives a separate job/state namespace and runAppServerTurn is confined to that child; validate directory markers as actual Git directories before returning them.

Useful? React with 👍 / 👎.

}
if (!candidateStats.isFile()) {
return null;
}

const match = /^gitdir: ([^\r\n]+)\r?\n?$/.exec(fs.readFileSync(candidatePath, "utf8"));
if (!match) {
return null;
}
const target = path.resolve(path.dirname(candidatePath), match[1]);
return fs.statSync(target).isDirectory() ? fs.realpathSync.native(target) : null;
}

function readConfiguredWorkTree(gitDirectory) {
const configPath = path.join(gitDirectory, "config");
let section = "";
for (const rawLine of fs.readFileSync(configPath, "utf8").split(/\r?\n/)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Join continued core.worktree values

Git config permits a value to continue across a backslash-newline. For example, a valid core.worktree = ../work\ followed by tree resolves in Git to ../work tree, but splitting the config before parsing reads the incomplete first line, cannot stat that path, and falls back to the metadata checkout. This gives tasks and their app-server sandbox the wrong workspace whenever the configured external worktree uses a continued value.

Useful? React with 👍 / 👎.

const line = rawLine.trim();
if (!line || line.startsWith("#") || line.startsWith(";")) {
continue;
}
const sectionMatch = /^\[([^\]]+)\]$/.exec(line);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Accept comments after core section headers

When a valid config uses [core] # ... or [core] ; ... after another section, this anchored header regexp does not recognize the header and leaves section set to the preceding section. Git treats those as core headers, so the following worktree setting is ignored here and task state plus the workspace-write sandbox fall back to the metadata checkout instead of the configured worktree.

Useful? React with 👍 / 👎.

if (sectionMatch) {
section = sectionMatch[1].trim().toLowerCase();
continue;
}
if (section !== "core") {
continue;
}
const valueMatch = /^worktree\s*=\s*(.+)$/i.exec(line);
if (valueMatch) {
const value = valueMatch[1].trim().replace(/^(?:"(.*)"|'(.*)')$/, "$1$2");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Strip Git config comments before resolving core.worktree

When a valid repository config uses an inline comment after a core.worktree value (for example, worktree = "../work tree" # external tree), Git parses the value as ../work tree, but this code keeps the comment and therefore cannot unquote or stat the intended directory. configuredWorkTree then returns null and resolution falls back to the metadata checkout rather than the configured work tree, splitting task state and giving workspace-write app-server turns the wrong sandbox root.

Useful? React with 👍 / 👎.

return path.resolve(gitDirectory, value);
}
}
return null;
}

function configuredWorkTree(gitDirectory) {
try {
const workTree = readConfiguredWorkTree(gitDirectory);
return workTree && fs.statSync(workTree).isDirectory() ? fs.realpathSync.native(workTree) : null;
} catch {
return null;
}
}

function findMarkerWorkspace(canonicalCwd) {
const cwdStats = fs.statSync(canonicalCwd);
let current = cwdStats.isFile() ? path.dirname(canonicalCwd) : canonicalCwd;

while (true) {
try {
const gitDirectory = resolveGitDirectory(path.join(current, ".git"));
if (gitDirectory) {
return { workspace: fs.realpathSync.native(current), gitDirectory };
}
} catch (error) {
if (error?.code !== "ENOENT" && error?.code !== "ENOTDIR") {
throw error;
}
}

const parent = path.dirname(current);
if (parent === current) {
return null;
}
current = parent;
}
}

export function resolveWorkspaceRoot(cwd, env = process.env) {
try {
return ensureGitRepository(cwd);
const canonicalCwd = fs.realpathSync.native(cwd);
const invocationDirectory = fs.statSync(canonicalCwd).isFile() ? path.dirname(canonicalCwd) : canonicalCwd;
const marker = findMarkerWorkspace(canonicalCwd);

if (env?.GIT_DIR) {
try {
const gitDirectory = resolveGitDirectory(path.resolve(cwd, env.GIT_DIR));
if (gitDirectory) {
const workTree = env.GIT_WORK_TREE
? path.resolve(cwd, env.GIT_WORK_TREE)
: configuredWorkTree(gitDirectory) ?? invocationDirectory;
if (fs.statSync(workTree).isDirectory()) {
return fs.realpathSync.native(workTree);
}
}
} catch {
// Invalid Git environment overrides do not suppress normal marker discovery.
}
} else if (env?.GIT_WORK_TREE && marker) {
try {
const workTree = path.resolve(cwd, env.GIT_WORK_TREE);
if (fs.statSync(workTree).isDirectory()) {
return fs.realpathSync.native(workTree);
}
} catch {
// Invalid Git environment overrides do not suppress normal marker discovery.
}
}

if (marker) {
return configuredWorkTree(marker.gitDirectory) ?? marker.workspace;
}
return cwd;
} catch {
return cwd;
}
Expand Down
203 changes: 203 additions & 0 deletions tests/workspace.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import childProcess from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { syncBuiltinESMExports } from "node:module";
import test from "node:test";
import assert from "node:assert/strict";

import { makeTempDir } from "./helpers.mjs";
import { resolveStateDir } from "../plugins/codex/scripts/lib/state.mjs";
import { resolveWorkspaceRoot } from "../plugins/codex/scripts/lib/workspace.mjs";

function makeNestedWorkspace(gitMarker) {
const workspace = makeTempDir();
const nested = path.join(workspace, "packages", "app");
fs.mkdirSync(nested, { recursive: true });
if (gitMarker === "directory") {
fs.mkdirSync(path.join(workspace, ".git"));
} else {
const gitDirectory = path.join(workspace, "metadata.git");
fs.mkdirSync(gitDirectory);
fs.writeFileSync(path.join(workspace, ".git"), "gitdir: metadata.git\n", "utf8");
}
return { workspace: fs.realpathSync.native(workspace), nested };
}

test("resolveWorkspaceRoot honors an environment-configured work tree without a .git marker", () => {
const worktree = makeTempDir();
const gitDirectory = makeTempDir();
const nested = path.join(worktree, "packages", "app");
fs.mkdirSync(nested, { recursive: true });

assert.equal(
resolveWorkspaceRoot(nested, { GIT_DIR: gitDirectory, GIT_WORK_TREE: "../.." }),
fs.realpathSync.native(worktree)
);
});

test("resolveWorkspaceRoot honors GIT_WORK_TREE alone after finding the current repository", () => {
const outer = makeNestedWorkspace("directory");
const configuredWorktree = makeTempDir();
assert.equal(
resolveWorkspaceRoot(outer.nested, { GIT_WORK_TREE: configuredWorktree }),
fs.realpathSync.native(configuredWorktree)
);
});

test("resolveWorkspaceRoot ignores an environment work tree with a missing GIT_DIR", () => {
const outer = makeNestedWorkspace("directory");
assert.equal(
resolveWorkspaceRoot(outer.nested, { GIT_DIR: "missing.git", GIT_WORK_TREE: "../.." }),
outer.workspace
);
});

test("resolveWorkspaceRoot uses invocation directory for GIT_DIR alone", () => {
const outer = makeNestedWorkspace("directory");
const gitDirectory = makeTempDir();
assert.equal(resolveWorkspaceRoot(outer.nested, { GIT_DIR: gitDirectory }), fs.realpathSync.native(outer.nested));
});

test("resolveWorkspaceRoot honors core.worktree from GIT_DIR config", () => {
const cwd = makeTempDir();
const gitDirectory = makeTempDir();
const worktree = makeTempDir();
fs.writeFileSync(path.join(gitDirectory, "config"), `[core]\n worktree = ${worktree}\n`, "utf8");
assert.equal(resolveWorkspaceRoot(cwd, { GIT_DIR: gitDirectory }), fs.realpathSync.native(worktree));
});

test("resolveWorkspaceRoot honors core.worktree from a marker git directory", () => {
const metadata = makeTempDir();
const gitDirectory = path.join(metadata, ".git");
const nested = path.join(metadata, "sub");
const worktree = makeTempDir();
fs.mkdirSync(gitDirectory);
fs.mkdirSync(nested);
fs.writeFileSync(path.join(gitDirectory, "config"), `[core]\n worktree = ${worktree}\n`, "utf8");
assert.equal(resolveWorkspaceRoot(nested, {}), fs.realpathSync.native(worktree));
});

test("resolveWorkspaceRoot follows a gitfile supplied through GIT_DIR", () => {
const cwd = makeTempDir();
const metadata = makeTempDir();
const gitDirectory = path.join(metadata, "actual.git");
const gitfile = path.join(metadata, "linked.git");
const worktree = makeTempDir();
fs.mkdirSync(gitDirectory);
fs.writeFileSync(gitfile, "gitdir: actual.git\n", "utf8");

assert.equal(
resolveWorkspaceRoot(cwd, { GIT_DIR: gitfile, GIT_WORK_TREE: worktree }),
fs.realpathSync.native(worktree)
);
});

test("resolveWorkspaceRoot ignores GIT_WORK_TREE alone outside a repository", () => {
const cwd = makeTempDir();
const configuredWorktree = makeTempDir();
assert.equal(resolveWorkspaceRoot(cwd, { GIT_WORK_TREE: configuredWorktree }), cwd);
});

test("resolveWorkspaceRoot discovers a checkout without starting a child process", () => {
const { workspace, nested } = makeNestedWorkspace("directory");
const originalSpawnSync = childProcess.spawnSync;
childProcess.spawnSync = () => {
throw new Error("workspace resolution must not start a child process");
};
syncBuiltinESMExports();

try {
assert.equal(resolveWorkspaceRoot(nested), workspace);
} finally {
childProcess.spawnSync = originalSpawnSync;
syncBuiltinESMExports();
}
});

test("resolveWorkspaceRoot accepts a linked-worktree gitfile", () => {
const { workspace, nested } = makeNestedWorkspace("file");

assert.equal(resolveWorkspaceRoot(nested), workspace);
});

test("resolveWorkspaceRoot ignores an ordinary file named .git", () => {
const outer = makeNestedWorkspace("directory");
const nestedWorkspace = path.join(outer.workspace, "vendor", "not-a-repo");
const cwd = path.join(nestedWorkspace, "src");
fs.mkdirSync(cwd, { recursive: true });
fs.writeFileSync(path.join(nestedWorkspace, ".git"), "not a gitfile\n", "utf8");

assert.equal(resolveWorkspaceRoot(cwd), outer.workspace);
});

for (const invalidMarker of ["gitdir:/tmp/metadata\n", "gitdir:\t/tmp/metadata\n", "metadata\ngitdir: /tmp/metadata\n", "gitdir: missing.git\n", "gitdir: metadata.git\ntrailing-data"]) {
test(`resolveWorkspaceRoot rejects malformed gitfile ${JSON.stringify(invalidMarker)}`, () => {
const outer = makeNestedWorkspace("directory");
const nestedWorkspace = path.join(outer.workspace, "vendor", "not-a-repo");
const cwd = path.join(nestedWorkspace, "src");
fs.mkdirSync(cwd, { recursive: true });
fs.writeFileSync(path.join(nestedWorkspace, ".git"), invalidMarker, "utf8");

assert.equal(resolveWorkspaceRoot(cwd), outer.workspace);
});
}

test("resolveWorkspaceRoot follows a symlinked git directory", () => {
const workspace = makeTempDir();
const gitDirectory = makeTempDir();
const nested = path.join(workspace, "packages", "app");
fs.mkdirSync(nested, { recursive: true });
fs.symlinkSync(gitDirectory, path.join(workspace, ".git"), process.platform === "win32" ? "junction" : "dir");

assert.equal(resolveWorkspaceRoot(nested), fs.realpathSync.native(workspace));
});

test("resolveWorkspaceRoot selects the nearest nested working tree", () => {
const { workspace } = makeNestedWorkspace("directory");
const nestedWorkspace = path.join(workspace, "vendor", "nested");
const cwd = path.join(nestedWorkspace, "src");
fs.mkdirSync(path.join(nestedWorkspace, ".git"), { recursive: true });
fs.mkdirSync(cwd);

assert.equal(resolveWorkspaceRoot(cwd), fs.realpathSync.native(nestedWorkspace));
});

test("resolveWorkspaceRoot starts at a file cwd's parent", () => {
const { workspace, nested } = makeNestedWorkspace("directory");
const file = path.join(nested, "index.js");
fs.writeFileSync(file, "export {};\n", "utf8");

assert.equal(resolveWorkspaceRoot(file), workspace);
});

test("workspace aliases resolve to the same canonical root and state identity", (t) => {
const { workspace } = makeNestedWorkspace("directory");
const nested = path.join(workspace, "packages", "app");
const alias = `${workspace}-alias`;
try {
fs.symlinkSync(workspace, alias, process.platform === "win32" ? "junction" : "dir");
} catch (error) {
if (process.platform === "win32" && ["EPERM", "EACCES"].includes(error?.code)) {
t.skip(`junction creation unavailable: ${error.code}`);
return;
}
throw error;
}
const aliasedNested = path.join(alias, "packages", "app");

assert.equal(resolveWorkspaceRoot(aliasedNested), workspace);
assert.equal(resolveWorkspaceRoot(aliasedNested), resolveWorkspaceRoot(nested));
assert.equal(resolveStateDir(aliasedNested), resolveStateDir(nested));
});

test("resolveWorkspaceRoot preserves a non-repository cwd", () => {
const cwd = makeTempDir();

assert.equal(resolveWorkspaceRoot(cwd), cwd);
});

test("resolveWorkspaceRoot preserves an inaccessible or missing cwd", () => {
const cwd = path.join(makeTempDir(), "missing", "directory");

assert.equal(resolveWorkspaceRoot(cwd), cwd);
});