Repository navigation
Expand file tree
/
Copy pathmigrate-to-git-fork.py
More file actions
executable file
·229 lines (194 loc) · 8.68 KB
/
Copy pathmigrate-to-git-fork.py
File metadata and controls
executable file
·229 lines (194 loc) · 8.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
#!/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
`<component>/../.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)