state.py
python
sha256:c0eba5ad689cec79f4a3fcdc4f5da78556cb4b8cb7b330f944634356c10379ed
chore: pivot to nightly channel — bump version to 0.2.0.dev…
Sonnet 5
patch
13 days ago
| 1 | """Bridge state persistence — ``.muse/git-bridge.toml``. |
| 2 | |
| 3 | Owns the :class:`BridgeState` TypedDict family, the thread-safe |
| 4 | :func:`read_bridge_state` / :func:`write_bridge_state` pair, and the |
| 5 | minimal TOML serialiser :func:`_dict_to_toml`. |
| 6 | |
| 7 | All Muse commit IDs stored in bridge state must carry the canonical |
| 8 | ``sha256:`` prefix. :func:`write_bridge_state` enforces this at the |
| 9 | persistence boundary. |
| 10 | """ |
| 11 | |
| 12 | from __future__ import annotations |
| 13 | |
| 14 | import pathlib |
| 15 | import threading |
| 16 | from typing import TypedDict |
| 17 | |
| 18 | from muse.core.paths import git_bridge_state_path |
| 19 | from muse.core.types import split_id |
| 20 | |
| 21 | |
| 22 | # --------------------------------------------------------------------------- |
| 23 | # Type aliases |
| 24 | # --------------------------------------------------------------------------- |
| 25 | |
| 26 | SnapshotManifest = dict[str, str] |
| 27 | |
| 28 | # --------------------------------------------------------------------------- |
| 29 | # Bridge state — TypedDicts |
| 30 | # --------------------------------------------------------------------------- |
| 31 | |
| 32 | class _LastImportState(TypedDict, total=False): |
| 33 | """State recorded after a successful ``muse bridge git-import`` run. |
| 34 | |
| 35 | Fields |
| 36 | ------ |
| 37 | git_sha: 40-char SHA-1 of the last git commit imported. |
| 38 | git_ref: Git branch or tag that was imported (e.g. ``"main"``). |
| 39 | git_remote: Name of the git remote (e.g. ``"origin"``). |
| 40 | muse_branch: Muse branch the commits were written to. |
| 41 | muse_commit_id: ``sha256:`` prefixed ID of the last Muse commit written. |
| 42 | imported_at: ISO-8601 UTC timestamp of the import run. |
| 43 | commits_written: Number of Muse commits created in the last import run. |
| 44 | """ |
| 45 | |
| 46 | git_sha: str |
| 47 | git_ref: str |
| 48 | git_remote: str |
| 49 | muse_branch: str |
| 50 | muse_commit_id: str |
| 51 | imported_at: str |
| 52 | commits_written: int |
| 53 | |
| 54 | |
| 55 | class _LastExportState(TypedDict, total=False): |
| 56 | """State recorded after a successful ``muse bridge git-export`` run. |
| 57 | |
| 58 | Fields |
| 59 | ------ |
| 60 | muse_branch: Muse branch that was exported. |
| 61 | muse_commit_id: ``sha256:`` prefixed ID of the exported Muse commit. |
| 62 | git_remote: Name of the git remote pushed to. |
| 63 | git_ref: Git branch written to (e.g. ``"muse-mirror"``). |
| 64 | git_sha: 40-char SHA-1 of the git commit created by the export. |
| 65 | exported_at: ISO-8601 UTC timestamp of the export run. |
| 66 | """ |
| 67 | |
| 68 | muse_branch: str |
| 69 | muse_commit_id: str |
| 70 | git_remote: str |
| 71 | git_ref: str |
| 72 | git_sha: str |
| 73 | exported_at: str |
| 74 | |
| 75 | |
| 76 | class BridgeState(TypedDict): |
| 77 | """Persisted bridge synchronisation state. |
| 78 | |
| 79 | Written to ``<repo_root>/.muse/git-bridge.toml`` by both ``git-import`` |
| 80 | and ``git-export``. Never committed to the Muse object store — add the |
| 81 | path to ``.museignore``. |
| 82 | |
| 83 | Both sub-dicts use :class:`total=False` keys so callers that only perform |
| 84 | one direction of sync can omit the unused section. |
| 85 | |
| 86 | Fields |
| 87 | ------ |
| 88 | last_import: Most recent git-import run details (may be empty dict). |
| 89 | last_export: Most recent git-export run details (may be empty dict). |
| 90 | """ |
| 91 | |
| 92 | last_import: _LastImportState |
| 93 | last_export: _LastExportState |
| 94 | |
| 95 | |
| 96 | class _SidecarData(TypedDict, total=False): |
| 97 | """Phase-8 sidecar JSON — tracks imported stashes and exported shelf IDs.""" |
| 98 | |
| 99 | imported_stashes: list[str] |
| 100 | exported_shelf_ids: list[str] |
| 101 | |
| 102 | |
| 103 | class DriftInfo(TypedDict): |
| 104 | """Commit drift between git and Muse since last bridge sync.""" |
| 105 | |
| 106 | git_commits_since_import: int | None |
| 107 | muse_commits_since_export: int | None |
| 108 | |
| 109 | |
| 110 | # --------------------------------------------------------------------------- |
| 111 | # Internal lock |
| 112 | # --------------------------------------------------------------------------- |
| 113 | |
| 114 | _BRIDGE_STATE_LOCK = threading.Lock() |
| 115 | |
| 116 | |
| 117 | # --------------------------------------------------------------------------- |
| 118 | # read_bridge_state / write_bridge_state |
| 119 | # --------------------------------------------------------------------------- |
| 120 | |
| 121 | def read_bridge_state(repo_root: pathlib.Path) -> BridgeState: |
| 122 | """Read bridge state from ``<repo_root>/.muse/git-bridge.toml``. |
| 123 | |
| 124 | Returns a :class:`BridgeState` with both ``last_import`` and |
| 125 | ``last_export`` set to empty dicts when the file does not exist or when |
| 126 | the corresponding TOML section is absent. Unknown keys are passed through |
| 127 | unchanged so that future fields survive a round-trip through older CLI |
| 128 | versions. |
| 129 | |
| 130 | Args: |
| 131 | repo_root: Root directory of the Muse repository (must contain ``.muse/``). |
| 132 | |
| 133 | Returns: |
| 134 | A :class:`BridgeState` dict, always with both top-level keys present. |
| 135 | |
| 136 | Raises: |
| 137 | Nothing — missing or empty files return an empty default state. |
| 138 | """ |
| 139 | import tomllib |
| 140 | |
| 141 | path = git_bridge_state_path(repo_root) |
| 142 | raw = {} |
| 143 | if path.exists(): |
| 144 | try: |
| 145 | with _BRIDGE_STATE_LOCK: |
| 146 | raw = tomllib.loads(path.read_text(encoding="utf-8")) |
| 147 | except Exception: # noqa: BLE001 — tolerate malformed TOML |
| 148 | raw = {} |
| 149 | |
| 150 | state: BridgeState = { |
| 151 | "last_import": raw.get("last_import", {}), |
| 152 | "last_export": raw.get("last_export", {}), |
| 153 | } |
| 154 | return state |
| 155 | |
| 156 | |
| 157 | def write_bridge_state(repo_root: pathlib.Path, state: BridgeState) -> None: |
| 158 | """Write bridge state to ``<repo_root>/.muse/git-bridge.toml``. |
| 159 | |
| 160 | Validates that any ``muse_commit_id`` value in *state* carries the |
| 161 | canonical ``sha256:`` prefix. Raises :class:`ValueError` for bare hex |
| 162 | strings to enforce the prefix invariant at the persistence boundary. |
| 163 | |
| 164 | Thread-safe: uses a module-level lock so concurrent writes do not |
| 165 | interleave partial TOML output. |
| 166 | |
| 167 | Args: |
| 168 | repo_root: Root directory of the Muse repository. |
| 169 | state: A :class:`BridgeState`-compatible dict to persist. |
| 170 | |
| 171 | Raises: |
| 172 | ValueError: If any ``muse_commit_id`` value lacks the ``sha256:`` prefix. |
| 173 | """ |
| 174 | for section_key in ("last_import", "last_export"): |
| 175 | section = state.get(section_key, {}) |
| 176 | cid = section.get("muse_commit_id", "") |
| 177 | if cid: |
| 178 | if not cid.startswith("sha256:"): |
| 179 | raise ValueError( |
| 180 | f"bridge state muse_commit_id {cid!r} in section {section_key!r} " |
| 181 | "must carry a 'sha256:' prefix (e.g. sha256:<64-hex>). " |
| 182 | "Never store bare hex strings in bridge state." |
| 183 | ) |
| 184 | try: |
| 185 | split_id(cid) |
| 186 | except ValueError as exc: |
| 187 | raise ValueError( |
| 188 | f"bridge state muse_commit_id {cid!r} in section {section_key!r} " |
| 189 | "is not a valid sha256: object ID." |
| 190 | ) from exc |
| 191 | |
| 192 | path = git_bridge_state_path(repo_root) |
| 193 | path.parent.mkdir(parents=True, exist_ok=True) |
| 194 | |
| 195 | toml_text = _dict_to_toml(state) |
| 196 | |
| 197 | with _BRIDGE_STATE_LOCK: |
| 198 | tmp = path.with_suffix(".toml.tmp") |
| 199 | tmp.write_text(toml_text, encoding="utf-8") |
| 200 | tmp.replace(path) |
| 201 | |
| 202 | |
| 203 | def _dict_to_toml(d: BridgeState) -> str: |
| 204 | """Serialise a shallow dict-of-dicts to TOML. |
| 205 | |
| 206 | Handles one level of nesting (``[section]`` headers) and the following |
| 207 | value types: ``str``, ``int``, ``bool``, ``float``. Sufficient for the |
| 208 | :class:`BridgeState` structure. Not a general-purpose TOML serialiser. |
| 209 | """ |
| 210 | lines: list[str] = [] |
| 211 | for section, values in d.items(): |
| 212 | if not isinstance(values, dict): |
| 213 | continue |
| 214 | lines.append(f"[{section}]") |
| 215 | for key, val in values.items(): |
| 216 | if isinstance(val, bool): |
| 217 | lines.append(f"{key} = {str(val).lower()}") |
| 218 | elif isinstance(val, int): |
| 219 | lines.append(f"{key} = {val}") |
| 220 | elif isinstance(val, float): |
| 221 | lines.append(f"{key} = {val!r}") |
| 222 | elif isinstance(val, str): |
| 223 | escaped = val.replace("\\", "\\\\").replace('"', '\\"') |
| 224 | lines.append(f'{key} = "{escaped}"') |
| 225 | lines.append("") |
| 226 | return "\n".join(lines) |
File History
8 commits
sha256:c287f599c5429903a139eadf3c5db5d930520e57cb0c3c575d9570e953c3b2d6
chore: bump version to 0.2.0.dev2 — nightly.2
Sonnet 4.6
patch
12 days ago
sha256:8de4334a98c945aace420969d389ad678aa926d4ab4e886b2ac4c4241cb3bf2b
revert: keep pyproject.toml in canonical PEP 440 form
Sonnet 4.6
patch
15 days ago
sha256:50bb615c573aa4b928fca75b60f82ba2a659910066507cec6a95c412ae22ccb9
docs: add issue docs for push have-negotiation bug (#55) an…
Sonnet 4.6
18 days ago
sha256:d90d175cded68aae1d4ffcf4858917854195d0cd8ce1fe73cee4dbc02541cb74
chore: trigger push to surface null-OID paths
Sonnet 4.6
24 days ago
sha256:e452ad9a6ace6ccc6d875a35e06caf9da5576a970c1c36133b69a891ce5fefa8
chore: prebuild timing test
Sonnet 4.6
34 days ago
sha256:0008ab6695e3e064b3e236b24fd19e538fef6a588eb0d211622f4466d919c0b1
merge: pull staging/dev — advance to 0.2.0rc12
Sonnet 4.6
patch
36 days ago
sha256:9c33d61749fff814c5226d5386aa2af7064c2c02788594a25fdd709358132eea
fix: _PROPOSAL_PREFIX_RESOLVE_LIMIT 200 → 100 to match hub …
Sonnet 4.6
47 days ago
sha256:36c3cb3e76619d4c30a6d9bf81b5ec4ff148e30dcfed913e3114ca7b43b81c7e
fix: rename objects→blobs in push client and all stale test…
Sonnet 4.6
patch
50 days ago