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.
Files changed (80) hide show
  1. gitgrip-1.5.0.dist-info/METADATA +13 -0
  2. gitgrip-1.5.0.dist-info/RECORD +80 -0
  3. gitgrip-1.5.0.dist-info/WHEEL +5 -0
  4. gitgrip-1.5.0.dist-info/entry_points.txt +2 -0
  5. gitgrip-1.5.0.dist-info/top_level.txt +2 -0
  6. gr2/__init__.py +0 -0
  7. gr2/overlay/__init__.py +6 -0
  8. gr2/overlay/activate.py +196 -0
  9. gr2/overlay/agent_manifest.py +138 -0
  10. gr2/overlay/cli.py +181 -0
  11. gr2/overlay/cross_repo.py +124 -0
  12. gr2/overlay/drivers.py +113 -0
  13. gr2/overlay/introspection.py +155 -0
  14. gr2/overlay/language_drivers.py +115 -0
  15. gr2/overlay/objects.py +412 -0
  16. gr2/overlay/perf.py +251 -0
  17. gr2/overlay/refs.py +36 -0
  18. gr2/overlay/trust.py +150 -0
  19. gr2/overlay/types.py +69 -0
  20. gr2/overlay/units.py +313 -0
  21. gr2/overlay/workspace_spec.py +59 -0
  22. gr2/prototypes/__init__.py +0 -0
  23. gr2/prototypes/cache_materialization_probe.py +190 -0
  24. gr2/prototypes/concurrent_event_stress.py +199 -0
  25. gr2/prototypes/concurrent_lease_stress.py +240 -0
  26. gr2/prototypes/concurrent_workspace_cap_stress.py +231 -0
  27. gr2/prototypes/contribution_protocol.py +665 -0
  28. gr2/prototypes/cross_mode_lane_stress.py +986 -0
  29. gr2/prototypes/jsonl_store.py +158 -0
  30. gr2/prototypes/lane_workspace_prototype.py +2088 -0
  31. gr2/prototypes/layout_model_probe.py +139 -0
  32. gr2/prototypes/propagation_daemon.py +546 -0
  33. gr2/prototypes/propagation_state_machine.py +1478 -0
  34. gr2/prototypes/python_exec_playground.py +194 -0
  35. gr2/prototypes/python_hook_runtime_playground.py +240 -0
  36. gr2/prototypes/python_migration_playground.py +144 -0
  37. gr2/prototypes/python_review_checkout_playground.py +242 -0
  38. gr2/prototypes/python_spec_apply_playground.py +282 -0
  39. gr2/prototypes/real_git_lane_materialization.py +248 -0
  40. gr2/prototypes/real_git_playground.py +334 -0
  41. gr2/prototypes/recall_lane_history.py +274 -0
  42. gr2/prototypes/repo_maintenance_prototype.py +659 -0
  43. gr2/prototypes/repo_transport_probe.py +147 -0
  44. gr2/python_cli/__init__.py +2 -0
  45. gr2/python_cli/__main__.py +6 -0
  46. gr2/python_cli/add.py +51 -0
  47. gr2/python_cli/app.py +2516 -0
  48. gr2/python_cli/branch.py +67 -0
  49. gr2/python_cli/channel_bridge.py +131 -0
  50. gr2/python_cli/clone_exec.py +1019 -0
  51. gr2/python_cli/commit.py +199 -0
  52. gr2/python_cli/config.py +291 -0
  53. gr2/python_cli/env_exec.py +419 -0
  54. gr2/python_cli/events.py +529 -0
  55. gr2/python_cli/execops.py +372 -0
  56. gr2/python_cli/failures.py +98 -0
  57. gr2/python_cli/file_exec.py +256 -0
  58. gr2/python_cli/gitops.py +226 -0
  59. gr2/python_cli/grip.py +1337 -0
  60. gr2/python_cli/grip_cli.py +493 -0
  61. gr2/python_cli/hooks.py +450 -0
  62. gr2/python_cli/launch_exec.py +786 -0
  63. gr2/python_cli/merge_verification.py +274 -0
  64. gr2/python_cli/migration.py +985 -0
  65. gr2/python_cli/open_gr_review.py +699 -0
  66. gr2/python_cli/platform.py +441 -0
  67. gr2/python_cli/pr.py +487 -0
  68. gr2/python_cli/project_review.py +314 -0
  69. gr2/python_cli/prune.py +365 -0
  70. gr2/python_cli/push.py +172 -0
  71. gr2/python_cli/review.py +462 -0
  72. gr2/python_cli/review_ephemeral.py +143 -0
  73. gr2/python_cli/review_run.py +621 -0
  74. gr2/python_cli/spec_apply.py +1285 -0
  75. gr2/python_cli/staging_cleanup.py +205 -0
  76. gr2/python_cli/syncops.py +920 -0
  77. gr2/python_cli/target.py +100 -0
  78. gr2/python_cli/workspace_snapshot.py +105 -0
  79. gr2/schemas/gr2-materialization-plan-v1.schema.json +191 -0
  80. gr2_overlay/__init__.py +37 -0
gr2/python_cli/pr.py ADDED
@@ -0,0 +1,487 @@
1
+ """gr2 PR group orchestration.
2
+
3
+ Implements multi-repo PR lifecycle from PR-LIFECYCLE.md:
4
+ - create_pr_group: Create linked PRs across repos with pr_group_id
5
+ - merge_pr_group: Merge all PRs in a group (stops on first failure)
6
+ - check_pr_group_status: Poll status/checks and emit change events
7
+ - record_pr_review: Record an externally-submitted review event
8
+
9
+ The PlatformAdapter is group-unaware. This module assigns pr_group_id,
10
+ persists group metadata, and emits events per HOOK-EVENT-CONTRACT.md
11
+ section 3.2 (PR Lifecycle).
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import os
17
+ import sys
18
+ from collections.abc import Callable, Mapping
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ from .events import EventType, emit, emit_after_outcome
23
+ from .merge_verification import (
24
+ CompletedMerge,
25
+ MergeVerificationTarget,
26
+ consume_merge_receipt,
27
+ )
28
+ from .platform import (
29
+ AdapterError,
30
+ CreatePRRequest,
31
+ MergeEvidenceError,
32
+ MergeMethod,
33
+ MergeReceipt,
34
+ PlatformAdapter,
35
+ )
36
+
37
+ __all__ = [
38
+ "MergeMethod",
39
+ "PRMergeError",
40
+ "PRMergeOutcomeUnknownError",
41
+ "PRMergePostconditionError",
42
+ "UnpermittedMergeMethodError",
43
+ "check_pr_group_status",
44
+ "create_pr_group",
45
+ "merge_pr_group",
46
+ "record_pr_review",
47
+ "resolve_merge_method",
48
+ ]
49
+
50
+
51
+ class UnpermittedMergeMethodError(RuntimeError):
52
+ """The requested method is known, but workspace policy does not permit it."""
53
+
54
+ def __init__(self, requested: MergeMethod, permitted: list[str]) -> None:
55
+ self.requested = requested
56
+ self.permitted = list(permitted)
57
+ allowed = ", ".join(self.permitted) or "none"
58
+ super().__init__(
59
+ f"merge method {requested.value!r} is not permitted here "
60
+ f"(permitted: {allowed}); refusing rather than substituting another method"
61
+ )
62
+
63
+
64
+ def _parse_method(name: str, *, source: str) -> MergeMethod:
65
+ try:
66
+ return MergeMethod(name)
67
+ except ValueError:
68
+ expected = ", ".join(method.value for method in MergeMethod)
69
+ raise ValueError(
70
+ f"unrecognised merge method {name!r} from {source} "
71
+ f"(expected one of: {expected}); refusing rather than falling back"
72
+ ) from None
73
+
74
+
75
+ def resolve_merge_method(
76
+ explicit: str | None = None,
77
+ configured: str | None = None,
78
+ permitted: list[str] | None = None,
79
+ ) -> MergeMethod:
80
+ """Resolve explicit, then configured, then merge-commit strategy."""
81
+
82
+ if explicit is not None:
83
+ chosen = _parse_method(explicit, source="--method")
84
+ elif configured is not None:
85
+ chosen = _parse_method(configured, source="workspace setting")
86
+ else:
87
+ chosen = MergeMethod.MERGE
88
+
89
+ if permitted is not None and chosen.value not in permitted:
90
+ raise UnpermittedMergeMethodError(chosen, permitted)
91
+ return chosen
92
+
93
+
94
+ class PRMergeError(RuntimeError):
95
+ """A merge command failed, carrying prior completed host operations."""
96
+
97
+ operation_acknowledged = False
98
+ outcome_unknown = False
99
+
100
+ def __init__(
101
+ self,
102
+ repo: str,
103
+ pr_number: int,
104
+ reason: str,
105
+ *,
106
+ completed: list[CompletedMerge],
107
+ ) -> None:
108
+ self.repo = repo
109
+ self.pr_number = pr_number
110
+ self.reason = reason
111
+ self.completed = list(completed)
112
+ already = ", ".join(item.receipt.observed.repo for item in self.completed)
113
+ suffix = (
114
+ f" (ALREADY MERGED, do not retry: {already})"
115
+ if self.completed
116
+ else " (nothing had merged yet)"
117
+ )
118
+ super().__init__(f"merge failed for {repo}#{pr_number}: {reason}{suffix}")
119
+
120
+
121
+ class PRMergeOutcomeUnknownError(PRMergeError):
122
+ """The host acknowledged the command but no immutable receipt was available."""
123
+
124
+ operation_acknowledged = True
125
+ outcome_unknown = True
126
+
127
+ def __init__(
128
+ self,
129
+ repo: str,
130
+ pr_number: int,
131
+ reason: str,
132
+ *,
133
+ completed: list[CompletedMerge],
134
+ ) -> None:
135
+ self.repo = repo
136
+ self.pr_number = pr_number
137
+ self.reason = reason
138
+ self.completed = list(completed)
139
+ already = ", ".join(item.receipt.observed.repo for item in self.completed)
140
+ suffix = f"; earlier completed: {already}" if already else ""
141
+ RuntimeError.__init__(
142
+ self,
143
+ f"merge outcome unknown for {repo}#{pr_number}: {reason}; "
144
+ f"the command was acknowledged, so do not retry{suffix}",
145
+ )
146
+
147
+
148
+ class PRMergePostconditionError(PRMergeError):
149
+ """The merge is known to have happened but its evidence was not consumed."""
150
+
151
+ operation_acknowledged = True
152
+ outcome_unknown = False
153
+
154
+ def __init__(
155
+ self,
156
+ receipt: MergeReceipt,
157
+ reason: str,
158
+ *,
159
+ completed: list[CompletedMerge],
160
+ ) -> None:
161
+ observed = receipt.observed
162
+ self.receipt = receipt
163
+ self.repo = str(observed.repo)
164
+ self.pr_number = int(observed.number)
165
+ self.reason = reason
166
+ self.completed = list(completed)
167
+ already = ", ".join(item.receipt.observed.repo for item in self.completed)
168
+ suffix = f"; earlier completed: {already}" if already else ""
169
+ RuntimeError.__init__(
170
+ self,
171
+ f"merge postcondition failed for {self.repo}#{self.pr_number}: {reason}; "
172
+ f"the host merge is acknowledged, so do not retry{suffix}",
173
+ )
174
+
175
+
176
+ def _pr_groups_dir(workspace_root: Path) -> Path:
177
+ return workspace_root / ".grip" / "pr_groups"
178
+
179
+
180
+ def _generate_group_id() -> str:
181
+ return "pg_" + os.urandom(4).hex()
182
+
183
+
184
+ def _load_group(workspace_root: Path, pr_group_id: str) -> dict:
185
+ path = _pr_groups_dir(workspace_root) / f"{pr_group_id}.json"
186
+ return json.loads(path.read_text())
187
+
188
+
189
+ def _save_group(workspace_root: Path, group: dict) -> Path:
190
+ d = _pr_groups_dir(workspace_root)
191
+ d.mkdir(parents=True, exist_ok=True)
192
+ path = d / f"{group['pr_group_id']}.json"
193
+ path.write_text(json.dumps(group, indent=2))
194
+ return path
195
+
196
+
197
+ def create_pr_group(
198
+ workspace_root: Path,
199
+ owner_unit: str,
200
+ lane_name: str,
201
+ title: str,
202
+ base_branch: str,
203
+ head_branch: str,
204
+ repos: list[str],
205
+ adapter: PlatformAdapter,
206
+ actor: str,
207
+ *,
208
+ body: str = "",
209
+ draft: bool = False,
210
+ ) -> dict:
211
+ """Create linked PRs across repos and emit pr.created."""
212
+ pr_group_id = _generate_group_id()
213
+ prs: list[dict] = []
214
+
215
+ for repo in repos:
216
+ request = CreatePRRequest(
217
+ repo=repo,
218
+ title=title,
219
+ body=body,
220
+ head_branch=head_branch,
221
+ base_branch=base_branch,
222
+ draft=draft,
223
+ )
224
+ ref = adapter.create_pr(request)
225
+ prs.append({"repo": repo, "pr_number": ref.number, "url": ref.url})
226
+
227
+ group = {
228
+ "pr_group_id": pr_group_id,
229
+ "owner_unit": owner_unit,
230
+ "lane_name": lane_name,
231
+ "title": title,
232
+ "base_branch": base_branch,
233
+ "head_branch": head_branch,
234
+ "platform": getattr(adapter, "name", "github"),
235
+ "prs": prs,
236
+ "status": {repo: "OPEN" for repo in repos},
237
+ }
238
+ path = _save_group(workspace_root, group)
239
+
240
+ emit_after_outcome(
241
+ event_type=EventType.PR_CREATED,
242
+ workspace_root=workspace_root,
243
+ actor=actor,
244
+ owner_unit=owner_unit,
245
+ payload={"pr_group_id": pr_group_id, "lane_name": lane_name, "repos": prs},
246
+ )
247
+
248
+ group["state_path"] = str(path)
249
+ return group
250
+
251
+
252
+ def merge_pr_group(
253
+ workspace_root: Path,
254
+ pr_group_id: str,
255
+ adapter: PlatformAdapter,
256
+ actor: str,
257
+ *,
258
+ method: MergeMethod,
259
+ verification_targets: Mapping[str, MergeVerificationTarget],
260
+ report: Callable[[str], Any],
261
+ ) -> dict:
262
+ """Merge all PRs, consuming host evidence before recording completion."""
263
+ group = _load_group(workspace_root, pr_group_id)
264
+ merged: list[CompletedMerge] = []
265
+ targets = dict(verification_targets)
266
+
267
+ missing_targets = [
268
+ str(item["repo"]) for item in group["prs"] if str(item["repo"]) not in targets
269
+ ]
270
+ if missing_targets:
271
+ raise ValueError(
272
+ "merge verification has no explicit local target for: "
273
+ + ", ".join(missing_targets)
274
+ )
275
+
276
+ for pr_info in group["prs"]:
277
+ repo = str(pr_info["repo"])
278
+ number = int(pr_info["pr_number"])
279
+ try:
280
+ receipt = adapter.merge_pr(repo, number, method=method)
281
+ except MergeEvidenceError as exc:
282
+ _record_merge_failure(
283
+ workspace_root=workspace_root,
284
+ group=group,
285
+ actor=actor,
286
+ repo=repo,
287
+ number=number,
288
+ reason=str(exc),
289
+ completed=merged,
290
+ operation_acknowledged=True,
291
+ )
292
+ raise PRMergeOutcomeUnknownError(
293
+ repo,
294
+ number,
295
+ str(exc),
296
+ completed=merged,
297
+ ) from exc
298
+ except AdapterError as exc:
299
+ _record_merge_failure(
300
+ workspace_root=workspace_root,
301
+ group=group,
302
+ actor=actor,
303
+ repo=repo,
304
+ number=number,
305
+ reason=str(exc),
306
+ completed=merged,
307
+ operation_acknowledged=False,
308
+ )
309
+ raise PRMergeError(repo, number, str(exc), completed=merged) from exc
310
+
311
+ target = targets[repo]
312
+ try:
313
+ completed = consume_merge_receipt(
314
+ repo_root=target.repo_root,
315
+ receipt=receipt,
316
+ remote=target.remote,
317
+ report=report,
318
+ )
319
+ except Exception as exc: # noqa: BLE001 - host operation already happened
320
+ reason = f"could not consume merge evidence: {exc}"
321
+ _record_merge_failure(
322
+ workspace_root=workspace_root,
323
+ group=group,
324
+ actor=actor,
325
+ repo=repo,
326
+ number=number,
327
+ reason=reason,
328
+ completed=merged,
329
+ operation_acknowledged=True,
330
+ )
331
+ raise PRMergePostconditionError(
332
+ receipt,
333
+ reason,
334
+ completed=merged,
335
+ ) from exc
336
+ merged.append(completed)
337
+
338
+ records = _completed_records(merged)
339
+
340
+ group["completed"] = records
341
+ group["group_state"] = "merged"
342
+ _save_group(workspace_root, group)
343
+
344
+ emit_after_outcome(
345
+ event_type=EventType.PR_MERGED,
346
+ workspace_root=workspace_root,
347
+ actor=actor,
348
+ owner_unit=group.get("owner_unit", actor),
349
+ payload={"pr_group_id": pr_group_id, "repos": records},
350
+ )
351
+
352
+ return group
353
+
354
+
355
+ def _record_merge_failure(
356
+ *,
357
+ workspace_root: Path,
358
+ group: dict[str, object],
359
+ actor: str,
360
+ repo: str,
361
+ number: int,
362
+ reason: str,
363
+ completed: list[CompletedMerge],
364
+ operation_acknowledged: bool,
365
+ ) -> None:
366
+ """Best-effort durable layer; never replaces the in-process error."""
367
+
368
+ try:
369
+ emit(
370
+ event_type=EventType.PR_MERGE_FAILED,
371
+ workspace_root=workspace_root,
372
+ actor=actor,
373
+ owner_unit=str(group.get("owner_unit", actor)),
374
+ payload={
375
+ "pr_group_id": group["pr_group_id"],
376
+ "repo": repo,
377
+ "pr_number": number,
378
+ "reason": reason,
379
+ "completed": _completed_records(completed),
380
+ "operation_acknowledged": operation_acknowledged,
381
+ },
382
+ )
383
+ except Exception as emit_exc: # noqa: BLE001 - event logging is best effort
384
+ print(
385
+ f"gr2: could not record partial merge ({emit_exc}); "
386
+ "the completed list remains on the raised error",
387
+ file=sys.stderr,
388
+ )
389
+
390
+
391
+ def _completed_records(completed: list[CompletedMerge]) -> list[dict[str, object]]:
392
+ """Flatten earned evidence only at a JSON serialization boundary."""
393
+
394
+ return [item.as_dict() for item in completed]
395
+
396
+
397
+ def check_pr_group_status(
398
+ workspace_root: Path,
399
+ pr_group_id: str,
400
+ adapter: PlatformAdapter,
401
+ actor: str,
402
+ ) -> dict:
403
+ """Poll PR status/checks for all repos in a group. Emit change events."""
404
+ group = _load_group(workspace_root, pr_group_id)
405
+ cached_status = group.get("status", {})
406
+
407
+ for pr_info in group["prs"]:
408
+ repo = pr_info["repo"]
409
+ number = pr_info["pr_number"]
410
+ status = adapter.pr_status(repo, number)
411
+ old_state = cached_status.get(repo, "OPEN")
412
+
413
+ if status.state != old_state:
414
+ emit(
415
+ event_type=EventType.PR_STATUS_CHANGED,
416
+ workspace_root=workspace_root,
417
+ actor=actor,
418
+ owner_unit=group.get("owner_unit", actor),
419
+ payload={
420
+ "pr_group_id": pr_group_id,
421
+ "repo": repo,
422
+ "pr_number": number,
423
+ "old_status": old_state,
424
+ "new_status": status.state,
425
+ },
426
+ )
427
+ cached_status[repo] = status.state
428
+
429
+ if status.checks:
430
+ completed = [c for c in status.checks if c.status == "COMPLETED"]
431
+ if completed and len(completed) == len(status.checks):
432
+ failed = [c.name for c in completed if c.conclusion != "SUCCESS"]
433
+ if failed:
434
+ emit(
435
+ event_type=EventType.PR_CHECKS_FAILED,
436
+ workspace_root=workspace_root,
437
+ actor=actor,
438
+ owner_unit=group.get("owner_unit", actor),
439
+ payload={
440
+ "pr_group_id": pr_group_id,
441
+ "repo": repo,
442
+ "pr_number": number,
443
+ "failed_checks": failed,
444
+ },
445
+ )
446
+ else:
447
+ emit(
448
+ event_type=EventType.PR_CHECKS_PASSED,
449
+ workspace_root=workspace_root,
450
+ actor=actor,
451
+ owner_unit=group.get("owner_unit", actor),
452
+ payload={
453
+ "pr_group_id": pr_group_id,
454
+ "repo": repo,
455
+ "pr_number": number,
456
+ "passed_checks": [c.name for c in completed],
457
+ },
458
+ )
459
+
460
+ group["status"] = cached_status
461
+ _save_group(workspace_root, group)
462
+ return group
463
+
464
+
465
+ def record_pr_review(
466
+ workspace_root: Path,
467
+ pr_group_id: str,
468
+ repo: str,
469
+ pr_number: int,
470
+ reviewer: str,
471
+ state: str,
472
+ actor: str,
473
+ ) -> None:
474
+ """Record an externally-submitted PR review and emit pr.review_submitted."""
475
+ emit(
476
+ event_type=EventType.PR_REVIEW_SUBMITTED,
477
+ workspace_root=workspace_root,
478
+ actor=actor,
479
+ owner_unit=actor,
480
+ payload={
481
+ "pr_group_id": pr_group_id,
482
+ "repo": repo,
483
+ "pr_number": pr_number,
484
+ "reviewer": reviewer,
485
+ "state": state,
486
+ },
487
+ )