8Provide shared logic for detecting the root of an Option C project.
12- Walk upward from the current directory.
13- Identify the project root by required directories.
18from pathlib
import Path
25MAX_OCTRC_SIZE = 64 * 1024
29 current = start.resolve()
30 workspace_root: Path |
None =
None
31 for parent
in [current] + list(current.parents):
36 if (parent /
".option_c").is_dir():
40 octrc = parent /
".octrc.json"
43 size = octrc.stat().st_size
44 if size > MAX_OCTRC_SIZE:
46 f
"Warning: skipping {octrc} ({size} bytes > "
47 f
"{MAX_OCTRC_SIZE} limit)",
51 cfg = json.loads(octrc.read_text(encoding=
"utf-8"))
52 if cfg.get(
"root_marker",
False):
54 except (json.JSONDecodeError, OSError):
57 has_oc_diag = (parent /
"oc_diagnostics").exists()
58 has_legacy = (parent /
"diagnostics").exists()
59 has_diag = has_oc_diag
or has_legacy
60 if (parent /
"docs").exists()
and (parent /
"tests").exists()
and has_diag:
61 if has_legacy
and not has_oc_diag:
63 "Warning: 'diagnostics/' is deprecated and will be removed in v1.0.0. "
64 "Rename to 'oc_diagnostics/'.",
74 if workspace_root
is None and (
75 parent /
".option_c_workspace.json"
77 workspace_root = parent
79 if workspace_root
is not None:
81 raise click.ClickException(
"Could not locate project root.")
85 """Return project root from --root-dir override or auto-detection.
87 When ``--root-dir`` is supplied, OI-405 requires the target directory to
88 look like an Option C project (contain ``pyproject.toml`` or
89 ``.octrc.json``). If the user intentionally wants to point at an
90 unrelated directory, they must also pass ``--allow-outside-project``,
91 which is recorded in ``ctx.obj``.
93 root_dir = ctx.obj.get(
"root_dir")
if ctx.obj
else None
95 resolved = Path(root_dir).resolve()
96 if not resolved.is_dir():
97 raise click.ClickException(f
"--root-dir path does not exist: {resolved}")
98 allow_outside = bool(ctx.obj.get(
"allow_outside_project"))
if ctx.obj
else False
101 (resolved /
"pyproject.toml").is_file()
102 or (resolved /
".octrc.json").is_file()
103 or (resolved /
".option_c").is_dir()
104 or (resolved /
".option_c_workspace.json").is_file()
107 raise click.ClickException(
108 f
"--root-dir {resolved} does not appear to be an Option C project "
109 f
"(no pyproject.toml, .octrc.json, or .option_c/). Pass "
110 f
"--allow-outside-project to override."
117 """Pure predicate: True if ``path`` resolves inside ``project_root``.
119 Symlink-safe: both sides are resolved before comparison so symlink
120 targets cannot escape the project root. Never raises — any ``OSError``
121 during resolution is treated as "not inside". Intended for library
122 callers (e.g. ``oct.core.git``) that must not depend on Click.
125 resolved = Path(path).resolve()
126 root_resolved = Path(project_root).resolve()
130 resolved.relative_to(root_resolved)
137 path: Path, project_root: Path, allow_outside: bool =
False
139 """Resolve ``path`` and verify it is within ``project_root``.
141 If ``allow_outside`` is True, the resolved path is returned without
142 containment enforcement. Otherwise, a ``click.ClickException`` is raised
143 when the path falls outside ``project_root``. Used by export and clean
144 commands to prevent accidental operations on files outside the project
145 via symlink tricks or bare ``..`` paths (OI-405).
147 resolved = Path(path).resolve()
150 root_resolved = Path(project_root).resolve()
152 raise click.ClickException(
153 f
"Path {resolved} is outside project root {root_resolved}. "
154 f
"Use --allow-outside-project to override."
Path validate_path_containment(Path path, Path project_root, bool allow_outside=False)
Path get_project_root(click.Context ctx)
Path find_project_root(Path start)
bool is_within_project_root(Path path, Path project_root)