_repo.py
python
| 1 | """Repository detection utilities for the Muse CLI. |
| 2 | |
| 3 | Walking up the directory tree to locate a ``.muse/`` directory is the |
| 4 | single most-called internal primitive. Every subcommand uses it. Keeping |
| 5 | the semantics clear (``None`` on miss, never raises) makes callers simpler |
| 6 | and test isolation easier (``MUSE_REPO_ROOT`` env-var override). |
| 7 | """ |
| 8 | from __future__ import annotations |
| 9 | |
| 10 | import logging |
| 11 | import os |
| 12 | import pathlib |
| 13 | |
| 14 | import typer |
| 15 | |
| 16 | from maestro.muse_cli.errors import ExitCode |
| 17 | |
| 18 | logger = logging.getLogger(__name__) |
| 19 | |
| 20 | |
| 21 | def find_repo_root(start: pathlib.Path | None = None) -> pathlib.Path | None: |
| 22 | """Walk up from *start* (default ``Path.cwd()``) looking for ``.muse/``. |
| 23 | |
| 24 | Returns the first directory that contains ``.muse/``, or ``None`` if no |
| 25 | such ancestor exists. Never raises — callers decide what to do on miss. |
| 26 | |
| 27 | The ``MUSE_REPO_ROOT`` environment variable overrides discovery entirely; |
| 28 | set it in tests to avoid ``os.chdir`` calls. |
| 29 | """ |
| 30 | # Env-var override — useful for tests and tooling wrappers. |
| 31 | if env_root := os.environ.get("MUSE_REPO_ROOT"): |
| 32 | p = pathlib.Path(env_root).resolve() |
| 33 | logger.debug("⚠️ MUSE_REPO_ROOT override active: %s", p) |
| 34 | return p if (p / ".muse").is_dir() else None |
| 35 | |
| 36 | current = (start or pathlib.Path.cwd()).resolve() |
| 37 | while True: |
| 38 | if (current / ".muse").is_dir(): |
| 39 | return current |
| 40 | parent = current.parent |
| 41 | if parent == current: |
| 42 | return None |
| 43 | current = parent |
| 44 | |
| 45 | |
| 46 | _NOT_A_REPO_MSG = ( |
| 47 | 'fatal: not a muse repository (or any parent up to mount point /)\n' |
| 48 | 'Run "muse init" to initialize a new repository.' |
| 49 | ) |
| 50 | |
| 51 | |
| 52 | def require_repo(start: pathlib.Path | None = None) -> pathlib.Path: |
| 53 | """Return the repo root or exit 2 with a clear error message. |
| 54 | |
| 55 | Wraps ``find_repo_root()`` for command callbacks that must be inside a |
| 56 | Muse repository. The error text intentionally echoes to stdout so that |
| 57 | ``typer.testing.CliRunner`` captures it in ``result.output`` without |
| 58 | needing ``mix_stderr=True``. |
| 59 | """ |
| 60 | root = find_repo_root(start) |
| 61 | if root is None: |
| 62 | typer.echo(_NOT_A_REPO_MSG) |
| 63 | raise typer.Exit(code=ExitCode.REPO_NOT_FOUND) |
| 64 | return root |
| 65 | |
| 66 | |
| 67 | #: Public alias matching the function name specified. |
| 68 | require_repo_root = require_repo |