#!/usr/bin/env python3 """Rewrite the local `gpui-pre` patch block from path to git dependencies. `script/patch-local-gpui-pre.py` adds a `[patch.crates-io]` block to Navop's root `Cargo.toml` so the upstream `gpui-pre-*` crates resolve to a directory on the build host. That works for the dev who runs both scripts, but it ties every other build host (CI, teammates, deploy machines) to the same directory layout. The fix is to keep the patch block but switch each `path = "..."` entry to `git = "..." + tag = "..."`, pointing at the fork that `script/publish-gpui-pre-fork.py` populates. The patch block stays because `gpui-pre` reaches Navop only transitively through `gpui-component`; adding `gpui-pre` to Navop's own `[dependencies]` would be unused and rejected. Patching crates.io from path or from git is the only mechanism that lets a transitive dep be re-pointed without touching the upstream crate's manifest. This script only edits the existing block produced by the path-patch script. Both scripts share the same `>>> local gpui-pre patch >>>` / `<<< local gpui-pre patch <<<` markers, so they can be interleaved freely: script/patch-local-gpui-pre.py # path patch (dev workstation) script/migrate-to-git-fork.py --tag X # switch to git (CI, ship) script/migrate-to-git-fork.py --path # switch back to path (dev) Usage: script/migrate-to-git-fork.py [options] Options: --fork-url URL Git URL of the fork. Required for `--git` (default). --tag TAG The tag the fork exposes for the snapshot. Required for `--git` (default). --path Rewrite the block back to a local path patch. This is the inverse of `--git`; the path defaults to the `/../.gpui-pre/workspace` layout produced by the path-patch script. --no-update-lock Print the change but do not run `cargo update`. The lock will keep the old path-based entries until you run it. --dry-run Print what would be written, do not touch files. -h, --help Show this help The script refuses to write a git-fork block without a non-empty `--fork-url` and `--tag`, and refuses to write a path block when the path mirror does not exist. Both refusals are deliberate: a half-written patch is worse than a working path patch, and "the path is wrong" is a runtime error that only shows up on `cargo check` two minutes later. """ from __future__ import annotations import argparse import json import re import subprocess import sys from pathlib import Path BEGIN_MARKER = "# >>> local gpui-pre patch (generated by script/patch-local-gpui-pre.py) >>>" END_MARKER = "# <<< local gpui-pre patch <<<" PUBLISH_PREFIX = "gpui-pre" class Error(Exception): """A user-facing failure, printed without a traceback.""" def run_capture(cmd: list[str], cwd: Path | None = None) -> str: result = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True) if result.returncode != 0: raise Error( f"command failed with exit code {result.returncode}: {' '.join(cmd)}\n" f"{result.stderr}" ) return result.stdout def read_block(manifest: Path) -> tuple[str, int, int, str]: """Return (text, start_offset, end_offset, prefix_before_block).""" text = manifest.read_text() pattern = re.compile( rf"^{re.escape(BEGIN_MARKER)}$.*?^{re.escape(END_MARKER)}\n?", re.MULTILINE | re.DOTALL, ) match = pattern.search(text) if match is None: raise Error(f"{manifest} has no local gpui-pre patch; run patch-local-gpui-pre.py first") return match.group(0), match.start(), match.end(), text[: match.start()] def parse_block_entries(block: str) -> list[str]: """Extract the patched crate names from an existing patch block. Accepts both the path form (`{ path = "..." }`, written by the path-patch script) and the git form (`{ git = "...", tag = "..." }`, written by this script) so the two modes can be toggled repeatedly. Only the crate names matter: for `--git` the path values are replaced outright, and for `--path` the authoritative paths are recomputed by `patch-local-gpui-pre.py`. """ names: list[str] = [] for line in block.splitlines(): entry = re.match(r"^\s*([\w-]+)\s*=\s*\{(.*)\}\s*$", line) if entry is None: continue name, payload = entry.group(1), entry.group(2) if not name.startswith(PUBLISH_PREFIX): continue if re.search(r'\b(path|git)\s*=\s*"', payload) is None: continue names.append(name) if not names: raise Error( f"the patch block in this manifest has no {PUBLISH_PREFIX}-* entries" ) return names def build_git_block(names: list[str], fork_url: str, tag: str) -> str: width = max(len(name) for name in names) lines = [ BEGIN_MARKER, "#", "# SELF-MAINTAINED: gpui-pre-* is a git fork of the snapshot produced", "# by script/patch-local-gpui-pre.py. The fork is owned by us, the tag", "# below is bumped by script/publish-gpui-pre-fork.py. To return to a", "# path patch (dev workstation only), re-run", "# `script/migrate-to-git-fork.py --path`.", "[patch.crates-io]", ] for name in names: lines.append( f'{name.ljust(width)} = {{ git = "{fork_url}", tag = "{tag}" }}' ) lines.append(END_MARKER) return "\n".join(lines) + "\n" def graph_crates(manifest: Path) -> list[str]: """Crates that Navop's graph resolves; only those can be `cargo update -p`.""" metadata = json.loads( run_capture(["cargo", "metadata", "--format-version", "1"], cwd=manifest.parent) ) return sorted( { package["name"] for package in metadata["packages"] if package["name"].startswith(PUBLISH_PREFIX) } ) def main(argv: list[str]) -> int: parser = argparse.ArgumentParser( description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument("--fork-url") parser.add_argument("--tag") parser.add_argument("--path", action="store_true") parser.add_argument("--no-update-lock", action="store_true") parser.add_argument("--dry-run", action="store_true") args = parser.parse_args(argv) root = Path(__file__).resolve().parent.parent manifest = root / "Cargo.toml" if not manifest.is_file(): raise Error(f"{manifest} not found") block, start, end, prefix = read_block(manifest) names = parse_block_entries(block) if args.path: # The path-patch script is the authoritative writer of the path # block: it recomputes the relative paths from its own mirror, so # delegate instead of trying to reconstruct them here (a git block # does not carry the path values at all). It leaves Cargo.lock # alone unless told to update, which matches --no-update-lock. patcher = root / "script" / "patch-local-gpui-pre.py" if not args.dry_run: run( [ sys.executable, str(patcher), "--no-stage", ], cwd=root, ) else: print(f"# would run {patcher} --no-stage") print(f"# ({len(names)} crates currently in the git block)") return 0 if not args.fork_url: raise Error("--fork-url is required (or pass --path to revert)") if not args.tag: raise Error("--tag is required (or pass --path to revert)") new_block = build_git_block(names, args.fork_url, args.tag) kind = "git" if args.dry_run: print(f"# would replace {end - start} bytes in {manifest}:") print("--- new block ---") print(new_block) return 0 manifest.write_text(prefix + new_block + manifest.read_text()[end:]) print(f"wrote {kind}-patch block to {manifest} ({len(names)} entries: {', '.join(names)})") if args.no_update_lock: print("--no-update-lock: Cargo.lock still resolves to the old source; run") print(" cargo update " + " ".join(f"-p {n}" for n in graph_crates(manifest))) return 0 reachable = graph_crates(manifest) if not reachable: print("no gpui-pre-* packages reached by Navop's graph; nothing to update") return 0 packages = [arg for n in reachable for arg in ("-p", n)] subprocess.run(["cargo", "update", *packages], cwd=root) return 0 if __name__ == "__main__": try: sys.exit(main(sys.argv[1:])) except Error as error: print(f"error: {error}", file=sys.stderr) sys.exit(1)