Skip to content

Commit 36cedf3

Browse files
committed
fix: allow canonical git worktree roots
1 parent 680b3d7 commit 36cedf3

2 files changed

Lines changed: 41 additions & 5 deletions

File tree

‎src/git-worktrees.ts‎

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import { randomBytes } from "node:crypto";
22
import { execFile } from "node:child_process";
33
import { promisify } from "node:util";
44
import { mkdir, realpath, rm, stat } from "node:fs/promises";
5-
import { basename, join } from "node:path";
5+
import { basename, join, relative, resolve } from "node:path";
66
import type { ServerConfig } from "./config.js";
7-
import { assertAllowedPath } from "./roots.js";
7+
import { assertAllowedPath, isPathInsideRoot } from "./roots.js";
88

99
const execFileAsync = promisify(execFile);
1010

@@ -93,7 +93,7 @@ export async function createManagedWorktree(input: {
9393
async function resolveGitRoot(path: string, allowedRoots: string[]): Promise<string> {
9494
try {
9595
const output = await git(["rev-parse", "--show-toplevel"], path);
96-
return assertAllowedPath(await realpath(output.trim()), allowedRoots);
96+
return await assertGitRootAllowed(output.trim(), allowedRoots);
9797
} catch (error) {
9898
if (isGitUnavailable(error)) {
9999
throw new GitWorktreeError(
@@ -109,6 +109,25 @@ async function resolveGitRoot(path: string, allowedRoots: string[]): Promise<str
109109
}
110110
}
111111

112+
async function assertGitRootAllowed(gitRoot: string, allowedRoots: string[]): Promise<string> {
113+
try {
114+
return assertAllowedPath(gitRoot, allowedRoots);
115+
} catch {
116+
const canonicalGitRoot = await realpath(gitRoot);
117+
for (const allowedRoot of allowedRoots) {
118+
const canonicalAllowedRoot = await realpath(allowedRoot).catch(() => undefined);
119+
if (!canonicalAllowedRoot || !isPathInsideRoot(canonicalGitRoot, canonicalAllowedRoot)) {
120+
continue;
121+
}
122+
123+
const logicalGitRoot = resolve(allowedRoot, relative(canonicalAllowedRoot, canonicalGitRoot));
124+
return assertAllowedPath(logicalGitRoot, allowedRoots);
125+
}
126+
127+
return assertAllowedPath(canonicalGitRoot, allowedRoots);
128+
}
129+
}
130+
112131
async function resolveBaseCommit(sourceRoot: string, baseRef: string): Promise<string> {
113132
try {
114133
return (await git(["rev-parse", "--verify", `${baseRef}^{commit}`], sourceRoot)).trim();

‎src/workspaces.test.ts‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { execFile } from "node:child_process";
2-
import { mkdtemp, mkdir, rm, stat, writeFile } from "node:fs/promises";
3-
import { tmpdir } from "node:os";
2+
import { mkdtemp, mkdir, rm, stat, symlink, writeFile } from "node:fs/promises";
3+
import { platform, tmpdir } from "node:os";
44
import { join } from "node:path";
55
import { promisify } from "node:util";
66
import assert from "node:assert/strict";
@@ -104,6 +104,23 @@ try {
104104
assert.equal(restoredWorktree.root, persistentWorktree.workspace.root);
105105
assert.equal(restoredWorktree.worktree?.managed, true);
106106
secondStore.close();
107+
108+
if (platform() !== "win32") {
109+
const aliasRoot = join(root, "alias-root");
110+
await symlink(root, aliasRoot, "dir");
111+
const aliasConfig = loadConfig({
112+
DEVSPACE_ALLOWED_ROOTS: aliasRoot,
113+
DEVSPACE_WORKTREE_ROOT: join(aliasRoot, ".devspace", "alias-worktrees"),
114+
DEVSPACE_AGENT_DIR: agentDir,
115+
DEVSPACE_OAUTH_OWNER_TOKEN: "test-owner-token-that-is-long-enough",
116+
PORT: "1",
117+
});
118+
const aliasWorkspace = await new WorkspaceRegistry(aliasConfig).openWorkspace({
119+
path: join(aliasRoot, "git-project"),
120+
mode: "worktree",
121+
});
122+
assert.equal(aliasWorkspace.workspace.sourceRoot, join(aliasRoot, "git-project"));
123+
}
107124
} finally {
108125
await rm(root, { recursive: true, force: true });
109126
}

0 commit comments

Comments
 (0)