gabriel / muse public
repo.py python
66 lines 2.1 KB
d78c6f12 fix: remove last typer references from muse/core/ Gabriel Cardona <gabriel@tellurstori.com> 1d ago
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
9 from __future__ import annotations
10
11 import logging
12 import os
13 import pathlib
14 import sys
15
16 from muse.core.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 if env_root := os.environ.get("MUSE_REPO_ROOT"):
31 p = pathlib.Path(env_root).resolve()
32 logger.debug("⚠️ MUSE_REPO_ROOT override active: %s", p)
33 return p if (p / ".muse").is_dir() else None
34
35 current = (start or pathlib.Path.cwd()).resolve()
36 while True:
37 if (current / ".muse").is_dir():
38 return current
39 parent = current.parent
40 if parent == current:
41 return None
42 current = parent
43
44
45 _NOT_A_REPO_MSG = (
46 'fatal: not a muse repository (or any parent up to mount point /)\n'
47 'Run "muse init" to initialize a new repository.'
48 )
49
50
51 def require_repo(start: pathlib.Path | None = None) -> pathlib.Path:
52 """Return the repo root or exit 2 with a clear error message.
53
54 Wraps ``find_repo_root()`` for command callbacks that must be inside a
55 Muse repository. The error text is written to stderr so the shell always
56 surfaces it; our ``CliRunner`` merges stderr into ``result.output``.
57 """
58 root = find_repo_root(start)
59 if root is None:
60 print(_NOT_A_REPO_MSG, file=sys.stderr)
61 raise SystemExit(ExitCode.REPO_NOT_FOUND)
62 return root
63
64
65 #: Public alias.
66 require_repo_root = require_repo