fix(cases): skip the cleanup when git never ran, and refuse Docker workspaces

A direct clone whose git-slot wait timed out came back with no spawnError, so
the cleanup emptied a folder git never touched. GitRun now says whether git
started and the cleanup requires it. Cloning into a folder at or inside a
Docker case workspace is refused, as git status, fetch and worktrees do, since
git would run the new repository's hooks and filters on the host.
This commit is contained in:
Devvyn committed 2026-10-31 19:13:56 +08:00
1 parent 086249a386
commit b549a66aa3
5 files changed
+127 -12

No files matched your search

+26
View File
@@ -51,6 +51,8 @@ import {
probeGitRemote,
} from '../../git-clone.js';
import type { GitRemoteProbe, GitUrlParse } from '../../git-clone.js';
import { isPathWithin } from '../../git-worktrees.js';
import { defaultDockerWorkspaces } from './git-status-routes.js';
import { generateClaudeMd } from '../../templates/claude-md.js';
import { prepareNewCasePath } from '../case-path.js';
import {
@@ -147,6 +149,9 @@ const APP_VERSION = (() => {
* would copy another user's workspace into the caller's own (the same escape
* `/api/cases/link` is admin-only for).
*/
const DOCKER_CLONE_REFUSAL =
'Cloning is not available into a folder inside a Docker case workspace: a container can write there, and git would run the new repository’s hooks and filters on this machine. Clone outside the workspace and link it, or clone from inside the container.';
const LOCAL_CLONE_ADMIN_ONLY =
'Cloning from a local path is admin-only in multi-user mode. Use a repository URL instead.';
@@ -790,6 +795,27 @@ export function registerCaseRoutes(
}
}
// git runs the new repository's hooks and filters on THIS machine once the clone finishes, and a container
// with the workspace bind-mounted can write both while it fetches. Like git status, fetch and worktrees,
// never clone at or inside a Docker case workspace. Fails closed when the Docker case list cannot be read.
try {
const roots = await Promise.all(
(await defaultDockerWorkspaces()).map((root) => fs.realpath(root).catch(() => resolve(root)))
);
// A custom folder is already symlink-resolved; the default one is judged through its real cases dir.
const dest =
customPath !== undefined ? casePath : join(await fs.realpath(casesDir).catch(() => casesDir), name);
if (roots.some((root) => isPathWithin(dest, root))) {
return createErrorResponse(ApiErrorCode.INVALID_INPUT, DOCKER_CLONE_REFUSAL);
}
} catch {
reply.code(503);
return createErrorResponse(
ApiErrorCode.OPERATION_FAILED,
'Could not read the Docker case list, so Codeman cannot tell whether that folder is a Docker case workspace. Try again.'
);
}
const clone = await cloneRepository({
repository: parsed.repository,
destination: casePath,