Skip to content
Open
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
11 changes: 11 additions & 0 deletions packages/server/review-workspace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { spawnSync } from "node:child_process";
import {
aggregateWorkspacePatch,
buildLocalWorkspaceReview,
isRepoRelative,
prefixPatchPaths,
resolveWorkspaceFilePath,
discoverWorkspaceRepoPaths,
Expand Down Expand Up @@ -1127,6 +1128,16 @@ describe("review-workspace", () => {
expect(workspace.normalizeAnnotationPath(join(api, "src/file.ts"))).toBe("api/src/file.ts");
});

it("treats cross-drive relative() results as escaping the repo", () => {
expect(isRepoRelative("src/file.ts")).toBe(true);
expect(isRepoRelative("nested/deep/file.ts")).toBe(true);
expect(isRepoRelative("../outside.ts")).toBe(false);
expect(isRepoRelative("")).toBe(false);
// On Windows, path.relative returns the target's absolute path when the
// base is on a different drive; after normalization that is "L:/...".
expect(isRepoRelative("L:/repos/project/src/file.ts")).toBe(false);
});

it("keeps requested Git-only workspace modes available when another child repo fails detection", async () => {
const root = makeTempDir("plannotator-workspace-partial-failure-");
const api = join(root, "api");
Expand Down
1 change: 1 addition & 0 deletions packages/server/review-workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {

export {
WorkspaceReviewSession,
isRepoRelative,
mapRepoDiffTypeToWorkspaceMode,
mapWorkspaceModeToRepoDiffType,
resolveWorkspaceInitialDiffType,
Expand Down
14 changes: 11 additions & 3 deletions packages/shared/review-workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,20 +224,28 @@ function aggregateRepos(repos: WorkspaceRepoRuntimeState[]): WorkspaceDiffSnapsh
};
}

// Guards a normalizeWorkspacePath(relative(...)) result: on Windows,
// path.relative cannot relativize across drives and returns the target's
// absolute path instead (after backslash normalization, e.g. "L:/other/..."),
// which must be treated as escaping the repo the same as "..".
export function isRepoRelative(rel: string): boolean {
return Boolean(rel) && !rel.startsWith("..") && !rel.startsWith("/") && !/^[A-Za-z]:/.test(rel);
}

function normalizeAgentPath(root: string, repos: WorkspaceRepoRuntimeState[], filePath: string): string {
const normalized = normalizeWorkspacePath(filePath);
if (resolveWorkspaceFilePath(repos, normalized)) return normalized;

const sorted = [...repos].sort((a, b) => b.cwd.length - a.cwd.length);
for (const repo of sorted) {
const rel = normalizeWorkspacePath(relative(repo.cwd, filePath));
if (rel && !rel.startsWith("..") && !rel.startsWith("/")) {
if (isRepoRelative(rel)) {
return `${normalizeWorkspacePath(repo.label)}/${rel}`;
}
}

const rootRel = normalizeWorkspacePath(relative(root, filePath));
if (rootRel && !rootRel.startsWith("..") && !rootRel.startsWith("/")) {
if (isRepoRelative(rootRel)) {
if (resolveWorkspaceFilePath(repos, rootRel)) return rootRel;
}

Expand All @@ -246,7 +254,7 @@ function normalizeAgentPath(root: string, repos: WorkspaceRepoRuntimeState[], fi
return `${normalizeWorkspacePath(changedRepos[0].label)}/${normalized}`;
}

if (rootRel && !rootRel.startsWith("..") && !rootRel.startsWith("/")) return rootRel;
if (isRepoRelative(rootRel)) return rootRel;
return normalized;
}

Expand Down