mirror of
https://github.com/feigeCode/navop.git
synced 2026-09-22 00:01:25 +00:00
367 lines
14 KiB
Python
Executable File
367 lines
14 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Publish a local gpui-pre snapshot to a git fork repository.
|
|
|
|
`gpui-pre-*` is the crate name under which gpui-component republishes Zed's
|
|
gpui crates to crates.io. The publisher (`gpui-component/script/bump-gpui.ts`)
|
|
is the same pipeline Navop's `patch-local-gpui-pre.py` already runs in
|
|
`--stage-only` mode. The two scripts share a single staging area so the
|
|
fork is just a "stage, then commit, then push" extension of the existing
|
|
path-patch workflow.
|
|
|
|
Why bother when a path patch already works locally? A path patch ties
|
|
Navop to a directory on the build host. CI, teammates, and any deploy
|
|
that runs `cargo` from scratch will not have that directory and the build
|
|
breaks. A git fork lives in a normal git remote, and once Navop's
|
|
dependencies point at it (`gpui-pre = { git = "...", tag = "..." }`),
|
|
every build host resolves it the same way it resolves any other dep.
|
|
|
|
Both modes of consumption are valid:
|
|
|
|
- **Path patch** (existing): `script/patch-local-gpui-pre.py`. Devs and CI
|
|
runners that share a workspace layout. No network round-trip; fastest
|
|
edit-build loop.
|
|
- **Git fork** (this script + `migrate-to-git-fork.py`): the long-lived
|
|
self-maintenance mode. CI, teammates, and a future "switch to upstream"
|
|
flip all use the same source.
|
|
|
|
Usage:
|
|
script/publish-gpui-pre-fork.py [options]
|
|
|
|
Options:
|
|
--zed PATH Zed checkout to snapshot (default: the `zed` checkout
|
|
next to Navop)
|
|
--component PATH gpui-component checkout that owns the staging pipeline
|
|
(default: the `gpui-component` checkout next to Navop)
|
|
--patch-dir PATH Where the staging script dropped the snapshot
|
|
(default: `<gpui-component>/../.gpui-pre`)
|
|
--fork-url URL Git URL of the fork. Anything `git push` accepts:
|
|
`https://github.com/feigeCode/gpui-pre.git` (the
|
|
repo is public; pushes need a token), or SSH.
|
|
--branch NAME Branch to push the snapshot to (default: main)
|
|
--tag NAME Tag to apply. Default: `fork-<version>`, e.g.
|
|
`fork-0.3.99`. Bump this to publish a new snapshot.
|
|
--version VERSION The version embedded in the snapshot and the default
|
|
tag. Must match what the staging pipeline stamped.
|
|
--message MSG Commit message; the version is appended automatically.
|
|
--no-stage Reuse the snapshot already produced by
|
|
`patch-local-gpui-pre.py`. Avoids a second
|
|
`bump-gpui.ts` run when the Zed branch has not moved.
|
|
--init-only Initialize the git repo and commit locally, but do not
|
|
push. Useful for inspecting what would go upstream.
|
|
--dry-run Print the commands without running them. Combine with
|
|
`--init-only` for a full preview.
|
|
-h, --help Show this help
|
|
|
|
Workflow:
|
|
|
|
# One-time, on a fresh fork:
|
|
script/publish-gpui-pre-fork.py \\
|
|
--fork-url https://github.com/feigeCode/gpui-pre.git \\
|
|
--init-only # commit only, inspect
|
|
# (push manually, or drop --init-only once you trust the snapshot)
|
|
|
|
# When the local Zed branch changes:
|
|
script/patch-local-gpui-pre.py --no-stage # refreshes the staging
|
|
script/publish-gpui-pre-fork.py \\
|
|
--fork-url https://github.com/feigeCode/gpui-pre.git \\
|
|
--tag fork-0.3.100 # bumped version
|
|
|
|
# In Navop, switch the dependency:
|
|
script/migrate-to-git-fork.py # see its docstring
|
|
|
|
The shipped snapshot is a single commit on `--branch`, tagged `--tag`. All
|
|
25 gpui-pre-* crates share that commit and that tag. Cargo's git
|
|
dependency resolution keys on `package = "..."` for crate identity and on
|
|
the tag/ref for source identity, so a single tag is enough.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
DEFAULT_TAG_PREFIX = "fork"
|
|
DEFAULT_BRANCH = "main"
|
|
PUBLISH_PREFIX = "gpui-pre"
|
|
DEFAULT_VERSION = "0.3.99"
|
|
|
|
|
|
class Error(Exception):
|
|
"""A user-facing failure, printed without a traceback."""
|
|
|
|
|
|
def run(cmd: list[str], cwd: Path | None = None, dry_run: bool = False) -> None:
|
|
print(f"$ {' '.join(cmd)}" + (" (dry-run)" if dry_run else ""))
|
|
if dry_run:
|
|
return
|
|
result = subprocess.run(cmd, cwd=cwd)
|
|
if result.returncode != 0:
|
|
raise Error(f"command failed with exit code {result.returncode}: {' '.join(cmd)}")
|
|
|
|
|
|
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 sibling(root: Path, name: str, marker: str, flag: str) -> Path:
|
|
"""Find a checkout next to the Navop root, at any of the nesting depths."""
|
|
for depth in range(1, 5):
|
|
candidate = root
|
|
for _ in range(depth):
|
|
candidate = candidate.parent
|
|
candidate = candidate / name
|
|
if (candidate / marker).exists():
|
|
return candidate.resolve()
|
|
raise Error(
|
|
f"could not find a {name} checkout next to {root}; pass {flag} PATH"
|
|
)
|
|
|
|
|
|
def find_bun() -> str:
|
|
"""Locate the Bun runtime that runs the staging pipeline."""
|
|
found = shutil.which("bun")
|
|
if found:
|
|
return found
|
|
fallback = Path.home() / "node_modules" / ".bin" / "bun"
|
|
if fallback.is_file():
|
|
return str(fallback)
|
|
raise Error(
|
|
"bun is not installed; install it with `npm install -g bun` (or "
|
|
"`brew install oven-sh/bun/bun`) and re-run"
|
|
)
|
|
|
|
|
|
def read_workspace_version(workspace: Path) -> str:
|
|
"""Pull `version` out of the staged root manifest.
|
|
|
|
The pipeline stamps the version on every member, but a single source of
|
|
truth keeps the publish script and the user from disagreeing about what
|
|
was actually published. The version lives in `[workspace.dependencies]`
|
|
as `version = "=0.3.99"`.
|
|
"""
|
|
manifest = (workspace / "Cargo.toml").read_text()
|
|
section = re.search(
|
|
r"^\[workspace\.dependencies\]\s*$(.*?)(?=^\[|\Z)",
|
|
manifest,
|
|
re.MULTILINE | re.DOTALL,
|
|
)
|
|
if section is None:
|
|
raise Error(f"{workspace / 'Cargo.toml'} has no [workspace.dependencies]")
|
|
match = re.search(r'\bversion\s*=\s*"=([0-9][^"]*)"', section.group(1))
|
|
if match is None:
|
|
raise Error(f"could not find a pinned version in {workspace / 'Cargo.toml'}")
|
|
return match.group(1)
|
|
|
|
|
|
def publish(
|
|
workspace: Path,
|
|
fork_url: str | None,
|
|
branch: str,
|
|
tag: str,
|
|
message: str,
|
|
version: str,
|
|
init_only: bool,
|
|
dry_run: bool,
|
|
component: Path,
|
|
) -> Path:
|
|
"""Materialise a clean, publishable copy of the snapshot and push it.
|
|
|
|
The copy lives next to the existing path-patch mirror (under
|
|
`<component.parent>/.gpui-pre/publish`) so nothing leaks into the
|
|
build output tree. The git history of that copy is the published
|
|
history; rebuilds always start from the staged snapshot, so a
|
|
previous `init` is preserved across runs of this script (bumping the
|
|
tag creates a new commit on the same branch).
|
|
"""
|
|
patch_dir = component.parent / ".gpui-pre"
|
|
publish_root = patch_dir / "publish"
|
|
# Accumulating history: wipe the copied files but keep `.git`, so each
|
|
# publish is a new commit on the same branch and consecutive tags can be
|
|
# diffed against each other.
|
|
if publish_root.is_dir() and not dry_run:
|
|
for child in publish_root.iterdir():
|
|
if child.name == ".git":
|
|
continue
|
|
if child.is_dir():
|
|
shutil.rmtree(child)
|
|
else:
|
|
child.unlink()
|
|
elif not dry_run:
|
|
publish_root.mkdir(parents=True)
|
|
elif not publish_root.exists():
|
|
# In dry-run we still want to point the user at a path, even though
|
|
# the copy does not exist. The real copy is created when the run is
|
|
# not a dry-run.
|
|
publish_root.mkdir(parents=True, exist_ok=True)
|
|
if not dry_run:
|
|
shutil.copytree(
|
|
workspace, publish_root, ignore=shutil.ignore_patterns("target", ".git"), dirs_exist_ok=True
|
|
)
|
|
|
|
git = shutil.which("git")
|
|
if git is None:
|
|
raise Error("git is not on PATH")
|
|
|
|
is_fresh = not (publish_root / ".git").is_dir()
|
|
if is_fresh:
|
|
run([git, "init", "-b", branch, "--quiet"], cwd=publish_root, dry_run=dry_run)
|
|
run(
|
|
[git, "config", "user.email", "gpui-pre-fork@localhost"],
|
|
cwd=publish_root,
|
|
dry_run=dry_run,
|
|
)
|
|
run(
|
|
[git, "config", "user.name", "gpui-pre fork publisher"],
|
|
cwd=publish_root,
|
|
dry_run=dry_run,
|
|
)
|
|
|
|
if fork_url and not init_only and not dry_run:
|
|
has_origin = subprocess.run(
|
|
[git, "remote", "get-url", "origin"], cwd=publish_root, capture_output=True
|
|
).returncode == 0
|
|
if has_origin:
|
|
existing = run_capture([git, "remote", "get-url", "origin"], cwd=publish_root).strip()
|
|
if existing != fork_url:
|
|
raise Error(
|
|
f"{publish_root} already has origin={existing!r}; pass a "
|
|
f"different branch, edit the remote, or delete .git to start over"
|
|
)
|
|
else:
|
|
run([git, "remote", "add", "origin", fork_url], cwd=publish_root, dry_run=dry_run)
|
|
|
|
run([git, "add", "-A"], cwd=publish_root, dry_run=dry_run)
|
|
|
|
status = run_capture([git, "status", "--porcelain"], cwd=publish_root) if not dry_run else ""
|
|
if status.strip() or is_fresh:
|
|
full_message = f"{message}\n\nSnapshot of Zed's gpui crates, renamed to {PUBLISH_PREFIX}-*\n\nVersion: {version}"
|
|
run([git, "commit", "-m", full_message], cwd=publish_root, dry_run=dry_run)
|
|
|
|
existing_tags = (
|
|
run_capture([git, "tag", "--list", tag], cwd=publish_root).strip()
|
|
if not dry_run
|
|
else ""
|
|
)
|
|
if existing_tags:
|
|
# Re-publishing the same version is a no-op on the tag itself, but
|
|
# if the snapshot changed we want the new commit on the branch to
|
|
# carry the tag. Move the tag forward to the new HEAD.
|
|
run([git, "tag", "-f", tag, "HEAD"], cwd=publish_root, dry_run=dry_run)
|
|
action = "moved"
|
|
else:
|
|
run([git, "tag", tag, "HEAD"], cwd=publish_root, dry_run=dry_run)
|
|
action = "created"
|
|
|
|
if fork_url and not init_only and not dry_run:
|
|
run(
|
|
[git, "push", "--force-with-lease", "-u", "origin", f"HEAD:{branch}"],
|
|
cwd=publish_root,
|
|
dry_run=dry_run,
|
|
)
|
|
run([git, "push", "--force", "origin", f"refs/tags/{tag}"], cwd=publish_root, dry_run=dry_run)
|
|
elif fork_url and not init_only and dry_run:
|
|
print(f"# would push branch HEAD:{branch} and tag {tag} to {fork_url}")
|
|
|
|
print(f"publish root: {publish_root}")
|
|
print(f"branch: {branch} ({'updated' if not is_fresh else 'created'})")
|
|
print(f"tag: {tag} ({action})")
|
|
if init_only:
|
|
print("init-only: nothing was pushed; re-run without --init-only to push")
|
|
elif fork_url is None:
|
|
print("no --fork-url given: nothing was pushed; pass --fork-url to push")
|
|
else:
|
|
print(f"pushed to {fork_url}")
|
|
|
|
return publish_root
|
|
|
|
|
|
def main(argv: list[str]) -> int:
|
|
parser = argparse.ArgumentParser(
|
|
description=__doc__,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
)
|
|
parser.add_argument("--zed")
|
|
parser.add_argument("--component")
|
|
parser.add_argument("--patch-dir")
|
|
parser.add_argument("--fork-url")
|
|
parser.add_argument("--branch", default=DEFAULT_BRANCH)
|
|
parser.add_argument("--tag")
|
|
parser.add_argument("--version")
|
|
parser.add_argument("--message", default="Snapshot gpui-pre from local Zed checkout")
|
|
parser.add_argument("--no-stage", action="store_true")
|
|
parser.add_argument("--init-only", action="store_true")
|
|
parser.add_argument("--dry-run", action="store_true")
|
|
args = parser.parse_args(argv)
|
|
|
|
root = Path(__file__).resolve().parent.parent
|
|
component = (
|
|
Path(args.component).expanduser().resolve()
|
|
if args.component
|
|
else sibling(root, "gpui-component", "script/bump-gpui.ts", "--component")
|
|
)
|
|
zed = (
|
|
Path(args.zed).expanduser().resolve()
|
|
if args.zed
|
|
else sibling(root, "zed", "crates/gpui/Cargo.toml", "--zed")
|
|
)
|
|
|
|
# Reuse the staging pipeline directly; the path-patch script also runs
|
|
# this pipeline but then goes on to write the path-patch block and run
|
|
# `cargo update`, which we do not want here.
|
|
if not args.no_stage:
|
|
script = component / "script" / "bump-gpui.ts"
|
|
run(
|
|
[
|
|
find_bun(),
|
|
str(script),
|
|
args.version or DEFAULT_VERSION,
|
|
"--zed",
|
|
str(zed),
|
|
"--stage-only",
|
|
],
|
|
cwd=component,
|
|
)
|
|
|
|
staged = component / "target" / "gpui-pre" / "workspace"
|
|
if not (staged / "Cargo.toml").is_file():
|
|
raise Error(f"no staged snapshot at {staged}; run without --no-stage")
|
|
|
|
version = args.version or read_workspace_version(staged)
|
|
tag = args.tag or f"{DEFAULT_TAG_PREFIX}-{version}"
|
|
|
|
publish(
|
|
workspace=staged,
|
|
fork_url=args.fork_url,
|
|
branch=args.branch,
|
|
tag=tag,
|
|
message=args.message,
|
|
version=version,
|
|
init_only=args.init_only,
|
|
dry_run=args.dry_run,
|
|
component=component,
|
|
)
|
|
|
|
print()
|
|
print("Next, in Navop:")
|
|
print(f" script/migrate-to-git-fork.py --fork-url <url> --tag {tag}")
|
|
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)
|