gitgrip 1.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gitgrip-1.5.0.dist-info/METADATA +13 -0
- gitgrip-1.5.0.dist-info/RECORD +80 -0
- gitgrip-1.5.0.dist-info/WHEEL +5 -0
- gitgrip-1.5.0.dist-info/entry_points.txt +2 -0
- gitgrip-1.5.0.dist-info/top_level.txt +2 -0
- gr2/__init__.py +0 -0
- gr2/overlay/__init__.py +6 -0
- gr2/overlay/activate.py +196 -0
- gr2/overlay/agent_manifest.py +138 -0
- gr2/overlay/cli.py +181 -0
- gr2/overlay/cross_repo.py +124 -0
- gr2/overlay/drivers.py +113 -0
- gr2/overlay/introspection.py +155 -0
- gr2/overlay/language_drivers.py +115 -0
- gr2/overlay/objects.py +412 -0
- gr2/overlay/perf.py +251 -0
- gr2/overlay/refs.py +36 -0
- gr2/overlay/trust.py +150 -0
- gr2/overlay/types.py +69 -0
- gr2/overlay/units.py +313 -0
- gr2/overlay/workspace_spec.py +59 -0
- gr2/prototypes/__init__.py +0 -0
- gr2/prototypes/cache_materialization_probe.py +190 -0
- gr2/prototypes/concurrent_event_stress.py +199 -0
- gr2/prototypes/concurrent_lease_stress.py +240 -0
- gr2/prototypes/concurrent_workspace_cap_stress.py +231 -0
- gr2/prototypes/contribution_protocol.py +665 -0
- gr2/prototypes/cross_mode_lane_stress.py +986 -0
- gr2/prototypes/jsonl_store.py +158 -0
- gr2/prototypes/lane_workspace_prototype.py +2088 -0
- gr2/prototypes/layout_model_probe.py +139 -0
- gr2/prototypes/propagation_daemon.py +546 -0
- gr2/prototypes/propagation_state_machine.py +1478 -0
- gr2/prototypes/python_exec_playground.py +194 -0
- gr2/prototypes/python_hook_runtime_playground.py +240 -0
- gr2/prototypes/python_migration_playground.py +144 -0
- gr2/prototypes/python_review_checkout_playground.py +242 -0
- gr2/prototypes/python_spec_apply_playground.py +282 -0
- gr2/prototypes/real_git_lane_materialization.py +248 -0
- gr2/prototypes/real_git_playground.py +334 -0
- gr2/prototypes/recall_lane_history.py +274 -0
- gr2/prototypes/repo_maintenance_prototype.py +659 -0
- gr2/prototypes/repo_transport_probe.py +147 -0
- gr2/python_cli/__init__.py +2 -0
- gr2/python_cli/__main__.py +6 -0
- gr2/python_cli/add.py +51 -0
- gr2/python_cli/app.py +2516 -0
- gr2/python_cli/branch.py +67 -0
- gr2/python_cli/channel_bridge.py +131 -0
- gr2/python_cli/clone_exec.py +1019 -0
- gr2/python_cli/commit.py +199 -0
- gr2/python_cli/config.py +291 -0
- gr2/python_cli/env_exec.py +419 -0
- gr2/python_cli/events.py +529 -0
- gr2/python_cli/execops.py +372 -0
- gr2/python_cli/failures.py +98 -0
- gr2/python_cli/file_exec.py +256 -0
- gr2/python_cli/gitops.py +226 -0
- gr2/python_cli/grip.py +1337 -0
- gr2/python_cli/grip_cli.py +493 -0
- gr2/python_cli/hooks.py +450 -0
- gr2/python_cli/launch_exec.py +786 -0
- gr2/python_cli/merge_verification.py +274 -0
- gr2/python_cli/migration.py +985 -0
- gr2/python_cli/open_gr_review.py +699 -0
- gr2/python_cli/platform.py +441 -0
- gr2/python_cli/pr.py +487 -0
- gr2/python_cli/project_review.py +314 -0
- gr2/python_cli/prune.py +365 -0
- gr2/python_cli/push.py +172 -0
- gr2/python_cli/review.py +462 -0
- gr2/python_cli/review_ephemeral.py +143 -0
- gr2/python_cli/review_run.py +621 -0
- gr2/python_cli/spec_apply.py +1285 -0
- gr2/python_cli/staging_cleanup.py +205 -0
- gr2/python_cli/syncops.py +920 -0
- gr2/python_cli/target.py +100 -0
- gr2/python_cli/workspace_snapshot.py +105 -0
- gr2/schemas/gr2-materialization-plan-v1.schema.json +191 -0
- gr2_overlay/__init__.py +37 -0
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
"""Project-tier open-gr --enter: one review-kind gr commit opens one exact
|
|
2
|
+
multi-repository review, and exit returns the agent to its prior work.
|
|
3
|
+
|
|
4
|
+
Composes the repo-tier review open (``project_review.open_project_review``, which
|
|
5
|
+
materializes each pinned head and enters the review lane) with a project-tier
|
|
6
|
+
receipt naming the gr commit and the per-repo base/head, plus an exit that
|
|
7
|
+
restores the prior lane AND the prior cwd. The review's per-repo ``base`` is the
|
|
8
|
+
recorded fork base (it rode in with the review-kind pins), so one gr commit opens
|
|
9
|
+
the exact review the author bound.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import dataclasses
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
import shutil
|
|
19
|
+
import subprocess
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from gr2.prototypes import lane_workspace_prototype as lane_proto
|
|
23
|
+
|
|
24
|
+
from . import grip, project_review, review
|
|
25
|
+
from .clone_exec import IncompleteRemoval, rmtree_or_refuse
|
|
26
|
+
from .gitops import git
|
|
27
|
+
from .review_run import _OUTPUT_LOG_NAME as _RUN_LOG_NAME
|
|
28
|
+
from .review_run import _RECEIPT_NAME as _RUN_RECEIPT_NAME
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class OpenGrReviewError(Exception):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
_RECEIPT_NAME = ".grip-open-gr.json"
|
|
36
|
+
|
|
37
|
+
# open-gr's teardown marker. Distinct from _RECEIPT_NAME (the open-PROJECT receipt
|
|
38
|
+
# exit-gr reads): open-gr pushes no lane and changes no cwd, so its teardown is only
|
|
39
|
+
# the rm of this disposable tree, and it must NOT be mistaken for an exit-gr receipt.
|
|
40
|
+
_OPEN_GR_MARKER = ".grip-open-gr-reconstruct.json"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def write_open_gr_marker(lane_dir: Path, gr_commit: str, results: dict) -> None:
|
|
44
|
+
"""Record a small teardown marker at the open-gr lane root so ``close_open_gr_lane``
|
|
45
|
+
can verify the directory is an open-gr reconstruction before removing it, rather
|
|
46
|
+
than rm an arbitrary path. ``results`` maps repo key -> the reconstruct dict."""
|
|
47
|
+
marker = {
|
|
48
|
+
"kind": "open-gr-reconstruct",
|
|
49
|
+
"gr_commit": gr_commit,
|
|
50
|
+
"repos": [
|
|
51
|
+
{
|
|
52
|
+
"key": key,
|
|
53
|
+
"reconstructed_head": res.get("reconstructed_head", ""),
|
|
54
|
+
"bound_head": res.get("bound_head", ""),
|
|
55
|
+
"bound_head_tree": res.get("bound_head_tree", ""),
|
|
56
|
+
"reconstructed_tree": res.get("reconstructed_tree", ""),
|
|
57
|
+
"tree_match": res.get("bound_head_tree") == res.get("reconstructed_tree"),
|
|
58
|
+
}
|
|
59
|
+
for key, res in results.items()
|
|
60
|
+
],
|
|
61
|
+
}
|
|
62
|
+
(Path(lane_dir) / _OPEN_GR_MARKER).write_text(json.dumps(marker, indent=2) + "\n")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def close_open_gr_lane(lane_dir: Path) -> dict:
|
|
66
|
+
"""Reclaim an ``open-gr --enter`` reconstruction lane: verify its marker, then rm
|
|
67
|
+
the disposable tree. Refuses a directory with no open-gr marker (so a wrong
|
|
68
|
+
``--lane-dir`` can never remove an arbitrary path). No OWNER_UNIT, no lane pop,
|
|
69
|
+
no cwd restore -- open-gr did none of those, so teardown undoes only the clone."""
|
|
70
|
+
lane_dir = Path(lane_dir)
|
|
71
|
+
marker_path = lane_dir / _OPEN_GR_MARKER
|
|
72
|
+
if not marker_path.exists():
|
|
73
|
+
raise OpenGrReviewError(
|
|
74
|
+
f"no open-gr marker at {marker_path}; not a lane opened by "
|
|
75
|
+
"`review open-gr --enter` (refusing to remove a directory that is not "
|
|
76
|
+
"an open-gr reconstruction)"
|
|
77
|
+
)
|
|
78
|
+
marker = json.loads(marker_path.read_text())
|
|
79
|
+
if marker.get("kind") != "open-gr-reconstruct":
|
|
80
|
+
raise OpenGrReviewError(
|
|
81
|
+
f"marker at {marker_path} is not an open-gr reconstruction "
|
|
82
|
+
f"(kind={marker.get('kind')!r})"
|
|
83
|
+
)
|
|
84
|
+
gr_commit = marker.get("gr_commit", "")
|
|
85
|
+
# review-run door 1: `review run` writes its receipt and pytest-output log INSIDE
|
|
86
|
+
# the lane, so the rmtree below would destroy the only record of what a red run
|
|
87
|
+
# found (35 env failures, in the real review). Carry them OUT first, to a sibling
|
|
88
|
+
# beside the lane that the rmtree cannot reach, and rewrite the preserved receipt's
|
|
89
|
+
# `output_log` to the preserved log's path so the receipt still names its log.
|
|
90
|
+
preserved = _preserve_run_artifacts(lane_dir)
|
|
91
|
+
try:
|
|
92
|
+
rmtree_or_refuse(lane_dir)
|
|
93
|
+
except IncompleteRemoval as cleanup_exc:
|
|
94
|
+
raise OpenGrReviewError(
|
|
95
|
+
f"open-gr lane at {lane_dir} could not be fully reclaimed: {cleanup_exc}"
|
|
96
|
+
) from cleanup_exc
|
|
97
|
+
result = {"reclaimed": str(lane_dir), "gr_commit": gr_commit}
|
|
98
|
+
if preserved:
|
|
99
|
+
result["preserved_run"] = preserved
|
|
100
|
+
return result
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _preserve_run_artifacts(lane_dir: Path) -> dict | None:
|
|
104
|
+
"""Copy a `review run` receipt (and its output log) out of the lane into a sibling
|
|
105
|
+
dir that the lane's rmtree cannot reach, BEFORE the rmtree. Returns
|
|
106
|
+
``{"dir", "receipt", "log"?}`` of the preserved paths, or None when the lane holds
|
|
107
|
+
no run receipt (the run was never done). The preserved receipt's ``output_log`` is
|
|
108
|
+
rewritten to the preserved log path, so the surviving receipt still names its log."""
|
|
109
|
+
receipt_src = lane_dir / _RUN_RECEIPT_NAME
|
|
110
|
+
if not receipt_src.is_file():
|
|
111
|
+
return None
|
|
112
|
+
# Sibling of the lane (NOT under it), `.resolve()` so it is a true sibling even when
|
|
113
|
+
# lane_dir was passed relative.
|
|
114
|
+
lane_dir = lane_dir.resolve()
|
|
115
|
+
receipt = json.loads(receipt_src.read_text())
|
|
116
|
+
# Key the preserved dir by (gr short commit, run timestamp), both taken from the
|
|
117
|
+
# receipt the run wrote. Keying only by lane name let a SECOND close of the same
|
|
118
|
+
# lane name overwrite the first close's receipt and log in place, so door 1's
|
|
119
|
+
# "the evidence survives close" held for one close per lane name only. A distinct
|
|
120
|
+
# run (a reopen + rerun) has a distinct `created`, so it lands beside the first
|
|
121
|
+
# rather than on top of it; the same run closed twice is idempotent.
|
|
122
|
+
gr_short = (receipt.get("gr_commit") or "unknown")[:12]
|
|
123
|
+
created = receipt.get("created") or "no-timestamp"
|
|
124
|
+
key = re.sub(r"[^A-Za-z0-9._-]", "-", f"{gr_short}-{created}")
|
|
125
|
+
dest = lane_dir.parent / f"{lane_dir.name}.review-run" / key
|
|
126
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
127
|
+
out: dict[str, str] = {"dir": str(dest)}
|
|
128
|
+
|
|
129
|
+
log_src = lane_dir / _RUN_LOG_NAME
|
|
130
|
+
log_dest = dest / _RUN_LOG_NAME
|
|
131
|
+
if log_src.is_file():
|
|
132
|
+
shutil.copy2(log_src, log_dest)
|
|
133
|
+
out["log"] = str(log_dest)
|
|
134
|
+
|
|
135
|
+
# Point the surviving receipt at the surviving log (absolute), since the lane-
|
|
136
|
+
# relative name it carried will not exist after the rmtree.
|
|
137
|
+
if "log" in out:
|
|
138
|
+
receipt["output_log"] = out["log"]
|
|
139
|
+
receipt_dest = dest / _RUN_RECEIPT_NAME
|
|
140
|
+
receipt_dest.write_text(json.dumps(receipt, indent=2) + "\n")
|
|
141
|
+
out["receipt"] = str(receipt_dest)
|
|
142
|
+
return out
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _pin_transport_location(pin_repo: str) -> str:
|
|
146
|
+
"""The clone LOCATION named by a pin's recorded remote.
|
|
147
|
+
|
|
148
|
+
A ``local:`` fixture prefix names a filesystem path; every other identity
|
|
149
|
+
(HTTPS/SSH) is its own clone URL. For a ``local:`` checkout we clone from its
|
|
150
|
+
ORIGIN (not the checkout itself), mirroring ``_canonical_repo_identity`` so
|
|
151
|
+
the resolved source's origin canonicalizes to the SAME identity the pin
|
|
152
|
+
carries -- otherwise ``_validate_workspace_repository_boundary`` refuses the
|
|
153
|
+
resolved source as an identity mismatch. This is transport only; that
|
|
154
|
+
boundary check remains the authority regardless of what we clone from.
|
|
155
|
+
"""
|
|
156
|
+
if pin_repo.startswith("local:"):
|
|
157
|
+
local = Path(pin_repo.removeprefix("local:"))
|
|
158
|
+
origin = review.gitops.remote_origin_url(local) if local.is_dir() else None
|
|
159
|
+
return origin or str(local)
|
|
160
|
+
return pin_repo
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def review_cache_root() -> Path:
|
|
164
|
+
"""The persistent per-host review-mirror cache root, SHARED with
|
|
165
|
+
config/scripts/review-clone.sh so there is one bare mirror per repo on this
|
|
166
|
+
host, never a second keyed differently. Honors the same
|
|
167
|
+
``SYNAPT_REVIEW_CACHE_ROOT`` override the script honors."""
|
|
168
|
+
import os
|
|
169
|
+
return Path(os.environ.get("SYNAPT_REVIEW_CACHE_ROOT") or (Path.home() / ".synapt-review-cache"))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _mirror_basename(transport_location: str) -> str:
|
|
173
|
+
"""The repo name the shared cache keys a mirror under: the URL/path basename
|
|
174
|
+
minus a trailing ``.git`` (e.g. …/recall.git -> recall). This matches
|
|
175
|
+
review-clone.sh's ``<repo>.git`` for every repo whose review-clone name is its
|
|
176
|
+
URL basename (all the code repos); ``site`` is the one review-clone name that
|
|
177
|
+
is NOT its URL basename, and is out of scope for multi-repo code review."""
|
|
178
|
+
name = transport_location.rstrip("/").rsplit("/", 1)[-1]
|
|
179
|
+
return name[:-4] if name.endswith(".git") else name
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def resolve_sources_from_pins(
|
|
183
|
+
pins: list[project_review.ProjectReviewPin] | tuple[project_review.ProjectReviewPin, ...],
|
|
184
|
+
staging_dir: Path | str | None = None,
|
|
185
|
+
*,
|
|
186
|
+
allow_local: bool = False,
|
|
187
|
+
cache_root: Path | str | None = None,
|
|
188
|
+
local_sources: dict[str, Path | str] | None = None,
|
|
189
|
+
) -> tuple[dict[str, tuple[Path, str]], dict[str, dict[str, str]]]:
|
|
190
|
+
"""Resolve each source from its RECORDED REMOTE through a PERSISTENT bare mirror.
|
|
191
|
+
|
|
192
|
+
For each pin, ensure a bare mirror at the SHARED per-host cache
|
|
193
|
+
(``<cache_root>/<repo>.git``, cache_root defaulting to review-clone.sh's
|
|
194
|
+
``~/.synapt-review-cache``) via ``ensure_repo_cache`` -- seed with
|
|
195
|
+
``git clone --mirror`` if absent, else ``remote update --prune`` (fetch fresh; a
|
|
196
|
+
stale mirror must never read as current) -- then confirm the pin's base AND head
|
|
197
|
+
are commits IN the mirror. The mirror IS the source handed to the review open:
|
|
198
|
+
the review lane then clones from it blobless (``--filter=blob:none``) and sparse
|
|
199
|
+
(see ``review_ephemeral.materialize_review_ephemeral``), so the lane fetches only
|
|
200
|
+
the blobs and checks out only the paths a review reads instead of copying a full
|
|
201
|
+
clone per review (the measured 978M harm). The mirror's objects are NOT borrowed
|
|
202
|
+
via ``--reference``: the review lane is a self-contained clone, disposable on exit.
|
|
203
|
+
|
|
204
|
+
The returned ``review_branch`` is the head sha itself (a sha resolves to itself
|
|
205
|
+
under ``rev-parse --verify <sha>^{commit}``). A mirror that does not carry the
|
|
206
|
+
pinned head is refused HERE, before any review lane opens. Returns
|
|
207
|
+
``(srcmap, mirror_meta)`` where ``mirror_meta[key]`` names the mirror path and
|
|
208
|
+
its fetched tip so a reviewer can tell a stale mirror from a fresh one.
|
|
209
|
+
|
|
210
|
+
``staging_dir`` is accepted for signature compatibility and no longer used:
|
|
211
|
+
the mirror replaces the per-review staging clone.
|
|
212
|
+
"""
|
|
213
|
+
root = Path(cache_root) if cache_root is not None else review_cache_root()
|
|
214
|
+
srcmap: dict[str, tuple[Path, str]] = {}
|
|
215
|
+
mirror_meta: dict[str, dict[str, str]] = {}
|
|
216
|
+
for pin in pins:
|
|
217
|
+
location = _pin_transport_location(pin.repo)
|
|
218
|
+
mirror = root / f"{_mirror_basename(location)}.git"
|
|
219
|
+
try:
|
|
220
|
+
review.gitops.ensure_repo_cache(location, mirror)
|
|
221
|
+
except SystemExit as exc:
|
|
222
|
+
raise OpenGrReviewError(
|
|
223
|
+
f"cannot ensure review mirror for {pin.key!r} at {mirror} from {location!r}: {exc}"
|
|
224
|
+
) from exc
|
|
225
|
+
# Serve blobless (--filter=blob:none) clones from the mirror, same as
|
|
226
|
+
# review-clone.sh sets on its cache.
|
|
227
|
+
git(mirror, "config", "uploadpack.allowFilter", "true")
|
|
228
|
+
# A pre-push head is ABSENT from the remote-seeded mirror BY DESIGN (a gated
|
|
229
|
+
# review head is never on the remote). If the caller names a LOCAL clone that
|
|
230
|
+
# holds it (the author's own desk has the objects), top the shared mirror up
|
|
231
|
+
# from that clone -- into a namespaced ref so the mirror's own branch names
|
|
232
|
+
# are not clobbered -- so the SAME blobless+sparse ephemeral path runs instead
|
|
233
|
+
# of falling back to a full --sources-json clone (the measured 595M harm).
|
|
234
|
+
local = local_sources.get(pin.key) if local_sources else None
|
|
235
|
+
if local is not None and git(mirror, "cat-file", "-e", f"{pin.head}^{{commit}}").returncode != 0:
|
|
236
|
+
# Fetch ONLY the pinned head (scoped to the sha, not every local branch)
|
|
237
|
+
# from the local clone, then anchor it under refs/localsrc/<key>/ so the
|
|
238
|
+
# object survives gc WITHOUT touching the mirror's own refs/heads/* -- a
|
|
239
|
+
# desk clone's stale branches must never become the shared mirror's
|
|
240
|
+
# branches. These refs/localsrc/* refs persist until gr2's next
|
|
241
|
+
# `remote update --prune` on this mirror.
|
|
242
|
+
fetched = git(mirror, "fetch", "--quiet", str(Path(local)), pin.head)
|
|
243
|
+
if fetched.returncode != 0:
|
|
244
|
+
raise OpenGrReviewError(
|
|
245
|
+
f"cannot top up review mirror for {pin.key!r} from local source "
|
|
246
|
+
f"{local!r}: {fetched.stderr.strip() or fetched.stdout.strip()}"
|
|
247
|
+
)
|
|
248
|
+
git(mirror, "update-ref", f"refs/localsrc/{pin.key}/head", pin.head)
|
|
249
|
+
for label, sha in (("base", pin.base), ("head", pin.head)):
|
|
250
|
+
if git(mirror, "cat-file", "-e", f"{sha}^{{commit}}").returncode != 0:
|
|
251
|
+
raise OpenGrReviewError(
|
|
252
|
+
f"review mirror for {pin.key!r} does not carry {label} {sha}: publish the "
|
|
253
|
+
f"pinned head to the remote, or pass a local source that holds it"
|
|
254
|
+
)
|
|
255
|
+
tip = git(mirror, "rev-parse", "HEAD")
|
|
256
|
+
srcmap[pin.key] = (mirror, pin.head)
|
|
257
|
+
mirror_meta[pin.key] = {"mirror": str(mirror), "fetched_tip": tip.stdout.strip()}
|
|
258
|
+
return srcmap, mirror_meta
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def open_gr_receipt_path(review_root: Path) -> Path:
|
|
262
|
+
"""The project-tier receipt lives at the review lane root (not a repo's .git),
|
|
263
|
+
because it names the whole review, not one member."""
|
|
264
|
+
return Path(review_root) / _RECEIPT_NAME
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _current_lane_name(workspace: Path, owner_unit: str) -> str | None:
|
|
268
|
+
try:
|
|
269
|
+
return lane_proto.require_current_lane(workspace, owner_unit).get("lane_name")
|
|
270
|
+
except SystemExit:
|
|
271
|
+
return None
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def open_gr_enter(
|
|
275
|
+
workspace: Path,
|
|
276
|
+
owner_unit: str,
|
|
277
|
+
lane_name: str,
|
|
278
|
+
gr_commit: str,
|
|
279
|
+
sources: dict[str, tuple[Path, str]] | None = None,
|
|
280
|
+
*,
|
|
281
|
+
prior_cwd: Path | str,
|
|
282
|
+
allow_local: bool = False,
|
|
283
|
+
staging_dir: Path | str | None = None,
|
|
284
|
+
local_sources: dict[str, Path | str] | None = None,
|
|
285
|
+
) -> project_review.ProjectReviewOutcome:
|
|
286
|
+
"""Open a multi-repo review from a review-KIND gr commit and enter its lane.
|
|
287
|
+
|
|
288
|
+
Refuses a non-review-kind commit (``read_project_review_commit`` rejects a
|
|
289
|
+
wrong schema) BEFORE any materialization. When ``sources`` is None the source
|
|
290
|
+
transport map is RESOLVED from each pin's recorded remote through the shared
|
|
291
|
+
mirror and the review lane is blobless + sparse (the production shape). For a
|
|
292
|
+
PRE-PUSH head (absent on the remote by design for a gated review), pass
|
|
293
|
+
``local_sources`` (key -> local clone holding the head): the mirror is topped up
|
|
294
|
+
from that clone and the SAME blobless + sparse path runs, so ``sources`` stays
|
|
295
|
+
None and the lane is NOT a full clone. ``sources`` (an explicit author-clone map)
|
|
296
|
+
is the older escape hatch that clones NORMALLY (full). Delegates materialization
|
|
297
|
+
+ lane enter to
|
|
298
|
+
``open_project_review`` (which refuses a missing pin before it clones
|
|
299
|
+
anything). On success writes the project-tier receipt naming the gr commit,
|
|
300
|
+
the per-repo base/head, the prior lane, and the prior cwd for exit-restore.
|
|
301
|
+
"""
|
|
302
|
+
workspace = Path(workspace).resolve()
|
|
303
|
+
# Control 1: a non-review-kind commit is refused here, before anything is made.
|
|
304
|
+
rows = grip.read_project_review_commit(workspace, gr_commit)
|
|
305
|
+
pins = [
|
|
306
|
+
project_review.ProjectReviewPin(
|
|
307
|
+
key=r["key"], repo=r["repo"], path=r["path"], base=r["base"], head=r["head"]
|
|
308
|
+
)
|
|
309
|
+
for r in rows
|
|
310
|
+
]
|
|
311
|
+
spec = project_review.ProjectReviewSpec("gr2-project-review/v1", gr_commit, tuple(pins))
|
|
312
|
+
|
|
313
|
+
# Carried-range reconstruction (shape (b), self-describing commit): for each pin
|
|
314
|
+
# that carries a range, reconstruct its head from the commit alone into a scratch
|
|
315
|
+
# clone (assert TREE == the recorded head-tree), then feed that scratch as the
|
|
316
|
+
# local source. The reconstructed head is a DIFFERENT sha than the pinned head
|
|
317
|
+
# (git am re-stamps the committer), so it is what the mirror is topped up with and
|
|
318
|
+
# what the review lane materializes at, via materialize_heads -- while the gr
|
|
319
|
+
# commit stays validated against the pinned head. The review lane is still the
|
|
320
|
+
# blobless+sparse ephemeral clone; only the transient scratch is a full clone.
|
|
321
|
+
materialize_heads: dict[str, str] = {}
|
|
322
|
+
reconstructed_heads: dict[str, str] = {}
|
|
323
|
+
scratch_root: Path | None = None
|
|
324
|
+
resolve_pins = pins
|
|
325
|
+
carried = grip.project_review_carried_keys(workspace, gr_commit) if sources is None else set()
|
|
326
|
+
ephemeral = False
|
|
327
|
+
mirror_meta: dict[str, dict[str, str]] = {}
|
|
328
|
+
# The scratch clone is created (mkdtemp) and populated (reconstruct) BEFORE
|
|
329
|
+
# open_project_review runs, so its removal must cover the reconstruction loop
|
|
330
|
+
# and the source resolution too -- not just the open. A range that fails to
|
|
331
|
+
# `git am` raises inside reconstruct_project_review_lane; if only the open
|
|
332
|
+
# were wrapped, that failure would leak the scratch full clone. The finally
|
|
333
|
+
# spans from mkdtemp through open so every exit path removes it.
|
|
334
|
+
try:
|
|
335
|
+
if carried:
|
|
336
|
+
import tempfile
|
|
337
|
+
scratch_root = Path(tempfile.mkdtemp(prefix="gr2-reconstruct-"))
|
|
338
|
+
local_sources = dict(local_sources or {})
|
|
339
|
+
resolve_pins = []
|
|
340
|
+
for pin in pins:
|
|
341
|
+
if pin.key in carried:
|
|
342
|
+
res = grip.reconstruct_project_review_lane(
|
|
343
|
+
workspace, gr_commit, pin.key, scratch_root / pin.key
|
|
344
|
+
)
|
|
345
|
+
rh = res["reconstructed_head"]
|
|
346
|
+
materialize_heads[pin.key] = rh
|
|
347
|
+
reconstructed_heads[pin.key] = rh
|
|
348
|
+
local_sources[pin.key] = scratch_root / pin.key
|
|
349
|
+
resolve_pins.append(dataclasses.replace(pin, head=rh))
|
|
350
|
+
else:
|
|
351
|
+
resolve_pins.append(pin)
|
|
352
|
+
|
|
353
|
+
# When sources are RESOLVED from the recorded remote, each source is the
|
|
354
|
+
# persistent bare mirror and each review lane is a blobless+sparse
|
|
355
|
+
# review-EPHEMERAL clone from it (never the work-lane clone seam). An explicit
|
|
356
|
+
# sources map (pre-push author clones) has no mirror and clones normally.
|
|
357
|
+
if sources is None:
|
|
358
|
+
sources, mirror_meta = resolve_sources_from_pins(
|
|
359
|
+
resolve_pins, staging_dir, allow_local=allow_local, local_sources=local_sources
|
|
360
|
+
)
|
|
361
|
+
ephemeral = True
|
|
362
|
+
|
|
363
|
+
prior_lane = _current_lane_name(workspace, owner_unit)
|
|
364
|
+
outcome = project_review.open_project_review(
|
|
365
|
+
workspace=workspace, owner_unit=owner_unit, lane_name=lane_name,
|
|
366
|
+
spec=spec, sources=sources, allow_local=allow_local, ephemeral=ephemeral,
|
|
367
|
+
materialize_heads=materialize_heads,
|
|
368
|
+
)
|
|
369
|
+
finally:
|
|
370
|
+
if scratch_root is not None:
|
|
371
|
+
# Log-and-continue, deliberately: this finally spans whatever the try
|
|
372
|
+
# block just did (opened a review, or raised) and a leftover scratch
|
|
373
|
+
# clone must never mask either outcome by raising here instead.
|
|
374
|
+
try:
|
|
375
|
+
rmtree_or_refuse(scratch_root)
|
|
376
|
+
except IncompleteRemoval as cleanup_exc:
|
|
377
|
+
import sys
|
|
378
|
+
print(f"warning: {cleanup_exc}", file=sys.stderr)
|
|
379
|
+
if outcome.status != "opened" or outcome.review_root is None:
|
|
380
|
+
# Refusal / partial propagates unchanged; no receipt, no enter to unwind.
|
|
381
|
+
return outcome
|
|
382
|
+
|
|
383
|
+
receipt = {
|
|
384
|
+
"gr_commit": gr_commit,
|
|
385
|
+
"owner_unit": owner_unit,
|
|
386
|
+
"lane_name": lane_name,
|
|
387
|
+
"lane_kind": "review-ephemeral" if ephemeral else "materialized",
|
|
388
|
+
"prior_lane": prior_lane,
|
|
389
|
+
"prior_cwd": str(Path(prior_cwd)),
|
|
390
|
+
"review_root": str(outcome.review_root),
|
|
391
|
+
"repos": [
|
|
392
|
+
{
|
|
393
|
+
"key": r["key"], "base": r["base"], "head": r["head"],
|
|
394
|
+
# For a carried-range pin the lane materializes the RECONSTRUCTED head
|
|
395
|
+
# (tree-equal to head, different sha until the committer-date lane);
|
|
396
|
+
# record it beside the pinned head so that lane has its before/after.
|
|
397
|
+
**({"reconstructed_head": reconstructed_heads[r["key"]]}
|
|
398
|
+
if r["key"] in reconstructed_heads else {}),
|
|
399
|
+
# A reviewer can tell a stale mirror from a fresh one from these.
|
|
400
|
+
**({"mirror": mirror_meta[r["key"]]["mirror"],
|
|
401
|
+
"fetched_tip": mirror_meta[r["key"]]["fetched_tip"]}
|
|
402
|
+
if r["key"] in mirror_meta else {}),
|
|
403
|
+
}
|
|
404
|
+
for r in rows
|
|
405
|
+
],
|
|
406
|
+
}
|
|
407
|
+
open_gr_receipt_path(outcome.review_root).write_text(json.dumps(receipt, indent=2) + "\n")
|
|
408
|
+
return outcome
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def provision_lane_venv(
|
|
412
|
+
lane_dir: Path, bootstrap_python: str, extras: list[str] | None = None
|
|
413
|
+
) -> tuple[str, str]:
|
|
414
|
+
"""Create a fresh ``.venv`` INSIDE the reviewed lane and install the repo into it
|
|
415
|
+
from its ``pyproject`` (editable), so verification runs under the LANE's OWN
|
|
416
|
+
interpreter rather than the desk's. ``bootstrap_python`` only builds the venv; the
|
|
417
|
+
RETURNED python is the one that then runs the tests, and its editable install makes
|
|
418
|
+
``import <pkg>`` resolve to the lane checkout. Returns (venv_python, venv_bin_dir).
|
|
419
|
+
|
|
420
|
+
``extras`` names the repo's own optional-dependency extras to install alongside the
|
|
421
|
+
editable package (``pip install -e ".[e1,e2]"``) -- the mechanism that puts a repo's
|
|
422
|
+
declared TEST dependencies (``pytest`` and what the tests import) into the lane venv,
|
|
423
|
+
so ``python -m pytest`` records a real exit 0 instead of failing on a missing
|
|
424
|
+
dependency. A fresh venv has pip but nothing else; without the extras a declared
|
|
425
|
+
``pytest`` command records non-zero, which is the deferred gap this closes. The
|
|
426
|
+
extras come from the repo's OWN pyproject, so they resolve from wherever the repo
|
|
427
|
+
already declares them (a local path reference resolves offline).
|
|
428
|
+
|
|
429
|
+
Raises ``OpenGrReviewError`` if venv creation or the install fails."""
|
|
430
|
+
lane_dir = Path(lane_dir)
|
|
431
|
+
venv_dir = lane_dir / ".venv"
|
|
432
|
+
bin_dir = venv_dir / ("Scripts" if os.name == "nt" else "bin")
|
|
433
|
+
venv_python = bin_dir / ("python.exe" if os.name == "nt" else "python")
|
|
434
|
+
create = subprocess.run(
|
|
435
|
+
[bootstrap_python, "-m", "venv", str(venv_dir)],
|
|
436
|
+
cwd=lane_dir, text=True, capture_output=True,
|
|
437
|
+
)
|
|
438
|
+
if create.returncode != 0:
|
|
439
|
+
raise OpenGrReviewError(
|
|
440
|
+
f"lane venv creation failed in {lane_dir}: {create.stderr.strip()}"
|
|
441
|
+
)
|
|
442
|
+
# One editable install; when extras are declared they ride the SAME install so the
|
|
443
|
+
# test deps land in the venv, not the desk. A stray extra name pip cannot resolve
|
|
444
|
+
# fails the install loudly rather than silently leaving the dep absent.
|
|
445
|
+
target = "." if not extras else f".[{','.join(extras)}]"
|
|
446
|
+
install = subprocess.run(
|
|
447
|
+
[str(venv_python), "-m", "pip", "install", "-e", target],
|
|
448
|
+
cwd=lane_dir, text=True, capture_output=True,
|
|
449
|
+
)
|
|
450
|
+
if install.returncode != 0:
|
|
451
|
+
raise OpenGrReviewError(
|
|
452
|
+
f"lane venv editable install ({target}) failed in {lane_dir}: "
|
|
453
|
+
f"{install.stderr.strip()[-500:]}"
|
|
454
|
+
)
|
|
455
|
+
return str(venv_python), str(bin_dir)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def record_review_verification(
|
|
459
|
+
review_root: Path,
|
|
460
|
+
key: str,
|
|
461
|
+
*,
|
|
462
|
+
command: list[str],
|
|
463
|
+
interpreter: str,
|
|
464
|
+
import_module: str,
|
|
465
|
+
provision_venv: bool = False,
|
|
466
|
+
extras: list[str] | None = None,
|
|
467
|
+
) -> dict:
|
|
468
|
+
"""Row 4 (run tests inside): run a repo's test COMMAND inside its materialized
|
|
469
|
+
review lane and append a verification record to the project-tier receipt, so
|
|
470
|
+
"the tests ran against the exact reviewed head, in the reviewed checkout, under
|
|
471
|
+
this interpreter and this package" is FRUIT in the receipt rather than a claim
|
|
472
|
+
in prose.
|
|
473
|
+
|
|
474
|
+
The record binds four things a bare "tests pass" cannot:
|
|
475
|
+
|
|
476
|
+
- ``exit_code``: the REAL subprocess return code, so a run that failed (or was
|
|
477
|
+
never invoked) cannot be recorded as green.
|
|
478
|
+
- ``head_tested``: the head the lane actually holds for this key -- the
|
|
479
|
+
reconstructed head for a carried-range pin, else the pinned head -- read from
|
|
480
|
+
the receipt, so a run against the desk worktree or dev tip cannot pass as the
|
|
481
|
+
reviewed head.
|
|
482
|
+
- ``cwd``: the materialized lane dir (``review_root/repos/<key>``), pinning the
|
|
483
|
+
run to the reviewed checkout.
|
|
484
|
+
- ``interpreter`` + ``module_path``: which python ran and which package file it
|
|
485
|
+
imported, MEASURED by a probe under the same ``interpreter`` inside the lane
|
|
486
|
+
(its real ``sys.executable`` and the imported module's ``__file__``). A stale
|
|
487
|
+
editable install can silently import a DIFFERENT desk's package; a receipt
|
|
488
|
+
that cannot say whose code it tested has the same defect one layer up. Pass
|
|
489
|
+
the interpreter that runs ``command`` so the probe measures the tests' python.
|
|
490
|
+
|
|
491
|
+
Returns the appended record. Raises ``OpenGrReviewError`` if there is no open-gr
|
|
492
|
+
receipt, the key was not materialized by this review, its lane dir is missing,
|
|
493
|
+
or the interpreter/module probe itself fails.
|
|
494
|
+
"""
|
|
495
|
+
review_root = Path(review_root)
|
|
496
|
+
receipt_path = open_gr_receipt_path(review_root)
|
|
497
|
+
if not receipt_path.exists():
|
|
498
|
+
raise OpenGrReviewError(
|
|
499
|
+
f"no open-gr receipt at {receipt_path}; not a review opened by open-gr"
|
|
500
|
+
)
|
|
501
|
+
receipt = json.loads(receipt_path.read_text())
|
|
502
|
+
row = next((r for r in receipt.get("repos", []) if r["key"] == key), None)
|
|
503
|
+
if row is None:
|
|
504
|
+
raise OpenGrReviewError(
|
|
505
|
+
f"key {key!r} is not in the review receipt; cannot verify a repo the "
|
|
506
|
+
f"review did not materialize"
|
|
507
|
+
)
|
|
508
|
+
# The lane holds the reconstructed head for a carried-range pin (tree-equal to
|
|
509
|
+
# the pinned head, different sha), else the pinned head. Bind to what is on disk.
|
|
510
|
+
head_tested = row.get("reconstructed_head") or row["head"]
|
|
511
|
+
lane_dir = review_root / "repos" / key
|
|
512
|
+
if not lane_dir.is_dir():
|
|
513
|
+
raise OpenGrReviewError(
|
|
514
|
+
f"materialized lane dir missing for {key!r}: {lane_dir}"
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
# When the spec asks for it, provision the lane's OWN venv and run under it, so the
|
|
518
|
+
# verification interpreter is the reviewed checkout's python -- not the desk's whose
|
|
519
|
+
# editable install could import a different tree's package. `interpreter` is
|
|
520
|
+
# overridden to the lane venv python (the probe then reports IT), and the command is
|
|
521
|
+
# run with the venv's bin on PATH so its `python`/tools resolve to the lane venv too.
|
|
522
|
+
run_env = None
|
|
523
|
+
if provision_venv:
|
|
524
|
+
venv_python, venv_bin = provision_lane_venv(lane_dir, interpreter, extras=extras)
|
|
525
|
+
interpreter = venv_python
|
|
526
|
+
run_env = {
|
|
527
|
+
**os.environ,
|
|
528
|
+
"PATH": venv_bin + os.pathsep + os.environ.get("PATH", ""),
|
|
529
|
+
"VIRTUAL_ENV": str(Path(venv_bin).parent),
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
# Run the tests inside the reviewed checkout; capture the REAL exit code. Both
|
|
533
|
+
# this run and the probe below take the SAME `lane_dir` cwd variable, so the cwd
|
|
534
|
+
# the probe MEASURES is the cwd the tests ran under.
|
|
535
|
+
exit_code = subprocess.run(command, cwd=lane_dir, env=run_env).returncode
|
|
536
|
+
|
|
537
|
+
# Probe, under the SAME interpreter and cwd, for the python, the package file, and
|
|
538
|
+
# the working directory that actually resolve here. `cwd` is recorded from the
|
|
539
|
+
# probe's own os.getcwd() -- MEASURED, not str(lane_dir) -- so the receipt reports
|
|
540
|
+
# where the command ran rather than copying the intended path from the request; and
|
|
541
|
+
# module_path exposes (not hides) a stale editable install that reaches outside the
|
|
542
|
+
# lane.
|
|
543
|
+
probe_args = [interpreter]
|
|
544
|
+
if provision_venv:
|
|
545
|
+
# -P (3.11+) drops the cwd/'' entry from sys.path, so `import <pkg>` must
|
|
546
|
+
# resolve through the lane venv's editable install rather than through the cwd
|
|
547
|
+
# copy that a plain `python -c` puts first. Without it the editable install is
|
|
548
|
+
# untested: module_path reads "lane" even if the install never ran.
|
|
549
|
+
probe_args.append("-P")
|
|
550
|
+
probe_args += [
|
|
551
|
+
"-c",
|
|
552
|
+
"import sys, os, importlib; "
|
|
553
|
+
f"m = importlib.import_module({import_module!r}); "
|
|
554
|
+
"print(sys.executable); print(m.__file__); print(os.getcwd())",
|
|
555
|
+
]
|
|
556
|
+
probe = subprocess.run(probe_args, cwd=lane_dir, text=True, capture_output=True)
|
|
557
|
+
if probe.returncode != 0:
|
|
558
|
+
raise OpenGrReviewError(
|
|
559
|
+
f"interpreter/module probe for {import_module!r} under {interpreter} "
|
|
560
|
+
f"failed (exit {probe.returncode}): {probe.stderr.strip()}"
|
|
561
|
+
)
|
|
562
|
+
probe_lines = probe.stdout.splitlines()
|
|
563
|
+
measured_interpreter = probe_lines[0] if probe_lines else ""
|
|
564
|
+
# DERIVE provisioned from the interpreter that actually ran, not from the request:
|
|
565
|
+
# it is true iff the python that ran is under the lane's own .venv. A copied-from-
|
|
566
|
+
# request flag would read true even if provisioning silently no-op'd.
|
|
567
|
+
provisioned = bool(measured_interpreter) and (
|
|
568
|
+
(lane_dir.resolve() / ".venv") in Path(measured_interpreter).parents
|
|
569
|
+
)
|
|
570
|
+
record = {
|
|
571
|
+
"key": key,
|
|
572
|
+
"command": list(command),
|
|
573
|
+
"exit_code": exit_code,
|
|
574
|
+
"head_tested": head_tested,
|
|
575
|
+
"cwd": probe_lines[2] if len(probe_lines) > 2 else "",
|
|
576
|
+
"interpreter": measured_interpreter,
|
|
577
|
+
"module_path": probe_lines[1] if len(probe_lines) > 1 else "",
|
|
578
|
+
"provisioned": provisioned,
|
|
579
|
+
}
|
|
580
|
+
receipt.setdefault("verification", []).append(record)
|
|
581
|
+
receipt_path.write_text(json.dumps(receipt, indent=2) + "\n")
|
|
582
|
+
return record
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def record_review_verifications(workspace: Path, review_root: Path) -> list[dict]:
|
|
586
|
+
"""Row 4 follow-on (multi-repo): run EVERY reviewed repo's declared test command
|
|
587
|
+
inside its lane and record each in the receipt.
|
|
588
|
+
|
|
589
|
+
The command lives in the workspace spec, per repo, so a review of N repos runs N
|
|
590
|
+
verifications from one call rather than N hand-typed commands::
|
|
591
|
+
|
|
592
|
+
[[repos]]
|
|
593
|
+
name = "app"
|
|
594
|
+
...
|
|
595
|
+
[repos.review_test]
|
|
596
|
+
command = ["python", "-m", "pytest", "-q"]
|
|
597
|
+
interpreter = "python3"
|
|
598
|
+
import_module = "app_pkg"
|
|
599
|
+
|
|
600
|
+
Iterates the repos the review materialized (from the receipt) and, for each that
|
|
601
|
+
declares a ``review_test``, calls ``record_review_verification`` -- which binds the
|
|
602
|
+
real exit code, the reviewed head, the measured cwd, and the interpreter/module
|
|
603
|
+
actually resolved. A repo with no declaration is skipped. Returns the records
|
|
604
|
+
appended this call, in the receipt's repo order.
|
|
605
|
+
|
|
606
|
+
Refuses if the receipt names a repo absent from the spec, if a ``review_test`` is
|
|
607
|
+
missing a required field, or if no reviewed repo declares one at all (a multi-repo
|
|
608
|
+
verify that verified nothing is a silent pass, not a result)."""
|
|
609
|
+
review_root = Path(review_root)
|
|
610
|
+
receipt_path = open_gr_receipt_path(review_root)
|
|
611
|
+
if not receipt_path.exists():
|
|
612
|
+
raise OpenGrReviewError(
|
|
613
|
+
f"no open-gr receipt at {receipt_path}; not a review opened by open-gr"
|
|
614
|
+
)
|
|
615
|
+
receipt = json.loads(receipt_path.read_text())
|
|
616
|
+
keys = [r["key"] for r in receipt.get("repos", [])]
|
|
617
|
+
spec = lane_proto.load_workspace_spec(Path(workspace))
|
|
618
|
+
by_name = {r.get("name"): r for r in spec.get("repos", [])}
|
|
619
|
+
|
|
620
|
+
records: list[dict] = []
|
|
621
|
+
for key in keys:
|
|
622
|
+
repo = by_name.get(key)
|
|
623
|
+
if repo is None:
|
|
624
|
+
raise OpenGrReviewError(
|
|
625
|
+
f"reviewed repo {key!r} is not in the workspace spec; cannot resolve "
|
|
626
|
+
f"its test command"
|
|
627
|
+
)
|
|
628
|
+
review_test = repo.get("review_test")
|
|
629
|
+
if not review_test:
|
|
630
|
+
continue
|
|
631
|
+
for field in ("command", "interpreter", "import_module"):
|
|
632
|
+
if field not in review_test:
|
|
633
|
+
raise OpenGrReviewError(
|
|
634
|
+
f"review_test for {key!r} is missing required field {field!r}"
|
|
635
|
+
)
|
|
636
|
+
records.append(
|
|
637
|
+
record_review_verification(
|
|
638
|
+
review_root,
|
|
639
|
+
key,
|
|
640
|
+
command=list(review_test["command"]),
|
|
641
|
+
interpreter=review_test["interpreter"],
|
|
642
|
+
import_module=review_test["import_module"],
|
|
643
|
+
provision_venv=bool(review_test.get("provision_venv", False)),
|
|
644
|
+
extras=list(review_test["extras"]) if review_test.get("extras") else None,
|
|
645
|
+
)
|
|
646
|
+
)
|
|
647
|
+
if not records:
|
|
648
|
+
raise OpenGrReviewError(
|
|
649
|
+
"no reviewed repo declares a [repos.review_test] command; nothing to verify"
|
|
650
|
+
)
|
|
651
|
+
return records
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
@dataclasses.dataclass(frozen=True)
|
|
655
|
+
class OpenGrExit:
|
|
656
|
+
restored_lane: str | None
|
|
657
|
+
restored_cwd: str
|
|
658
|
+
gr_commit: str
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def exit_gr_review(
|
|
662
|
+
workspace: Path, owner_unit: str, review_root: Path, *, actor: str
|
|
663
|
+
) -> OpenGrExit:
|
|
664
|
+
"""Exit a review opened by ``open_gr_enter``: restore the prior lane and cwd.
|
|
665
|
+
|
|
666
|
+
Reads the project-tier receipt for the prior lane/cwd, then pops the review
|
|
667
|
+
lane off the return stack (``exit_lane`` restores recent[0] as current). The
|
|
668
|
+
restore is the whole point — skipping it leaves the agent stranded in the
|
|
669
|
+
review lane.
|
|
670
|
+
"""
|
|
671
|
+
import argparse
|
|
672
|
+
|
|
673
|
+
workspace = Path(workspace).resolve()
|
|
674
|
+
receipt_path = open_gr_receipt_path(review_root)
|
|
675
|
+
if not receipt_path.exists():
|
|
676
|
+
raise OpenGrReviewError(f"no open-gr receipt at {receipt_path}; not a review opened by open-gr")
|
|
677
|
+
receipt = json.loads(receipt_path.read_text())
|
|
678
|
+
|
|
679
|
+
lane_proto.exit_lane(argparse.Namespace(
|
|
680
|
+
workspace_root=workspace, owner_unit=owner_unit, actor=actor,
|
|
681
|
+
notify_channel=False, recall=False,
|
|
682
|
+
))
|
|
683
|
+
restored_lane = _current_lane_name(workspace, owner_unit)
|
|
684
|
+
# A review-ephemeral review is READ-ONLY and disposable: rm -rf is the ONLY
|
|
685
|
+
# cleanup (no prune verb that could ever touch a work lane). The mirror
|
|
686
|
+
# persists; only the disposable review clones are removed.
|
|
687
|
+
if receipt.get("lane_kind") == "review-ephemeral":
|
|
688
|
+
try:
|
|
689
|
+
rmtree_or_refuse(review_root)
|
|
690
|
+
except IncompleteRemoval as cleanup_exc:
|
|
691
|
+
raise OpenGrReviewError(
|
|
692
|
+
f"review-ephemeral lane at {review_root} could not be fully "
|
|
693
|
+
f"removed on exit: {cleanup_exc}"
|
|
694
|
+
) from cleanup_exc
|
|
695
|
+
return OpenGrExit(
|
|
696
|
+
restored_lane=restored_lane,
|
|
697
|
+
restored_cwd=receipt["prior_cwd"],
|
|
698
|
+
gr_commit=receipt["gr_commit"],
|
|
699
|
+
)
|