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
@@ -0,0 +1,372 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import subprocess
5
+ from concurrent.futures import ThreadPoolExecutor, as_completed
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+ from gr2.prototypes import lane_workspace_prototype as lane_proto
10
+ from gr2.python_cli.events import EventType, emit_after_outcome
11
+
12
+
13
+ @dataclass
14
+ class ExecResult:
15
+ repo: str
16
+ cwd: str
17
+ status: str
18
+ returncode: int | None
19
+ stdout: str
20
+ stderr: str
21
+
22
+ @property
23
+ def succeeded(self) -> bool:
24
+ return self.status == "ok"
25
+
26
+ def to_dict(self) -> dict[str, object]:
27
+ return {
28
+ "repo": self.repo,
29
+ "cwd": self.cwd,
30
+ "status": self.status,
31
+ "returncode": self.returncode,
32
+ "stdout": self.stdout,
33
+ "stderr": self.stderr,
34
+ }
35
+
36
+
37
+ def _exec_one(command: list[str], repo: str, cwd: str) -> ExecResult:
38
+ cwd_path = Path(cwd)
39
+ if not cwd_path.exists():
40
+ return ExecResult(
41
+ repo=repo, cwd=cwd, status="missing",
42
+ returncode=None, stdout="",
43
+ stderr=f"lane repo checkout missing: {cwd}",
44
+ )
45
+ proc = subprocess.run(command, cwd=cwd_path, capture_output=True, text=True, check=False)
46
+ return ExecResult(
47
+ repo=repo, cwd=cwd,
48
+ status="ok" if proc.returncode == 0 else "failed",
49
+ returncode=proc.returncode,
50
+ stdout=proc.stdout, stderr=proc.stderr,
51
+ )
52
+
53
+
54
+ def run_exec_parallel(
55
+ command: list[str],
56
+ repo_rows: list[dict[str, str]],
57
+ max_workers: int = 4,
58
+ ) -> list[ExecResult]:
59
+ futures_map: dict[int, object] = {}
60
+ results: list[ExecResult | None] = [None] * len(repo_rows)
61
+
62
+ with ThreadPoolExecutor(max_workers=max_workers) as pool:
63
+ for idx, row in enumerate(repo_rows):
64
+ future = pool.submit(_exec_one, command, row["repo"], row["cwd"])
65
+ futures_map[id(future)] = (idx, future)
66
+
67
+ for _, (idx, future) in futures_map.items():
68
+ results[idx] = future.result()
69
+
70
+ return [r for r in results if r is not None]
71
+
72
+
73
+ def _resolve_lane_name(workspace_root: Path, owner_unit: str, lane_name: str | None) -> str:
74
+ if lane_name:
75
+ return lane_name
76
+ current = lane_proto.require_current_lane(workspace_root, owner_unit)
77
+ return str(current["lane_name"])
78
+
79
+
80
+ def _selected_repos(lane_doc: dict[str, object], repos: str | None) -> list[str]:
81
+ selected = [str(repo) for repo in lane_doc.get("repos", [])]
82
+ if repos:
83
+ requested = set(lane_proto.parse_repo_list(repos))
84
+ selected = [repo for repo in selected if repo in requested]
85
+ return selected
86
+
87
+
88
+ def _blocked_payload(
89
+ *,
90
+ reason: str,
91
+ lane_doc: dict[str, object],
92
+ owner_unit: str,
93
+ extra: dict[str, object] | None = None,
94
+ ) -> dict[str, object]:
95
+ payload = {
96
+ "status": "blocked",
97
+ "reason": reason,
98
+ "lane": str(lane_doc["lane_name"]),
99
+ "owner_unit": owner_unit,
100
+ }
101
+ if extra:
102
+ payload.update(extra)
103
+ return payload
104
+
105
+
106
+ def exec_status_payload(
107
+ workspace_root: Path,
108
+ owner_unit: str,
109
+ lane_name: str | None,
110
+ *,
111
+ repos: str | None = None,
112
+ actor: str = "agent:exec-status",
113
+ ) -> dict[str, object]:
114
+ owner_unit = str(owner_unit)
115
+ lane_name = _resolve_lane_name(workspace_root, owner_unit, lane_name)
116
+ lane_doc = lane_proto.load_lane_doc(workspace_root, owner_unit, lane_name)
117
+
118
+ leases = lane_proto.load_lane_leases(workspace_root, owner_unit, lane_name)
119
+ active_conflicts, stale_conflicts = lane_proto.conflicting_leases(leases, actor, "exec")
120
+ if active_conflicts:
121
+ return _blocked_payload(
122
+ reason="conflicting-active-lease",
123
+ lane_doc=lane_doc,
124
+ owner_unit=owner_unit,
125
+ extra={"requested_mode": "exec", "conflicting_leases": active_conflicts},
126
+ )
127
+ if stale_conflicts:
128
+ return _blocked_payload(
129
+ reason="stale-conflicting-lease",
130
+ lane_doc=lane_doc,
131
+ owner_unit=owner_unit,
132
+ extra={
133
+ "requested_mode": "exec",
134
+ "conflicting_leases": stale_conflicts,
135
+ "hint": "break stale leases with gr2 lane lease acquire --force or clean them up first",
136
+ },
137
+ )
138
+
139
+ selected_repos = _selected_repos(lane_doc, repos)
140
+ rows: list[dict[str, object]] = []
141
+ for repo in selected_repos:
142
+ cwd = lane_proto.lane_dir(workspace_root, owner_unit, lane_name) / "repos" / repo
143
+ rows.append(
144
+ {
145
+ "lane": lane_name,
146
+ "owner_unit": owner_unit,
147
+ "repo": repo,
148
+ "branch": dict(lane_doc.get("branch_map", {})).get(repo),
149
+ "cwd": str(cwd),
150
+ "exists": cwd.exists(),
151
+ "shared_context_roots": lane_doc.get("context", {}).get("shared_roots", []),
152
+ "private_context_roots": lane_doc.get("context", {}).get("private_roots", []),
153
+ "parallelism": lane_doc["exec_defaults"].get("parallelism"),
154
+ "fail_fast": bool(lane_doc["exec_defaults"].get("fail_fast", True)),
155
+ "default_command_family": lane_doc["exec_defaults"].get("default_command_family", []),
156
+ "commands": lane_doc["exec_defaults"].get("commands", []),
157
+ }
158
+ )
159
+
160
+ return {
161
+ "status": "ready",
162
+ "lane": lane_name,
163
+ "owner_unit": owner_unit,
164
+ "lane_type": lane_doc["lane_type"],
165
+ "rows": rows,
166
+ }
167
+
168
+
169
+ def render_exec_status(payload: dict[str, object]) -> str:
170
+ if payload["status"] != "ready":
171
+ lines = [
172
+ "ExecStatus",
173
+ f"status = {payload['status']}",
174
+ f"reason = {payload['reason']}",
175
+ f"owner_unit = {payload['owner_unit']}",
176
+ f"lane = {payload['lane']}",
177
+ ]
178
+ if "hint" in payload:
179
+ lines.append(f"hint = {payload['hint']}")
180
+ return "\n".join(lines)
181
+
182
+ lines = [
183
+ "ExecStatus",
184
+ f"status = {payload['status']}",
185
+ f"owner_unit = {payload['owner_unit']}",
186
+ f"lane = {payload['lane']}",
187
+ f"lane_type = {payload['lane_type']}",
188
+ "REPO\tBRANCH\tEXISTS\tCWD",
189
+ ]
190
+ for row in payload["rows"]:
191
+ lines.append(f"{row['repo']}\t{row['branch']}\t{row['exists']}\t{row['cwd']}")
192
+ return "\n".join(lines)
193
+
194
+
195
+ def _emit_lease_event(
196
+ workspace_root: Path,
197
+ *,
198
+ owner_unit: str,
199
+ lane_name: str,
200
+ actor: str,
201
+ event_type: str,
202
+ ttl_seconds: int | None = None,
203
+ ) -> None:
204
+ lane_doc = lane_proto.load_lane_doc(workspace_root, owner_unit, lane_name)
205
+ payload = {
206
+ "type": event_type,
207
+ "agent": actor,
208
+ "owner_unit": owner_unit,
209
+ "lane": lane_name,
210
+ "lane_type": lane_doc["lane_type"],
211
+ "repos": lane_doc.get("repos", []),
212
+ "timestamp": lane_proto.now_utc(),
213
+ }
214
+ if ttl_seconds is not None:
215
+ payload["lease_mode"] = "exec"
216
+ payload["ttl_seconds"] = ttl_seconds
217
+ lane_proto.emit_lane_event(workspace_root, payload)
218
+
219
+
220
+ def acquire_exec_lease(workspace_root: Path, owner_unit: str, lane_name: str, actor: str, ttl_seconds: int) -> None:
221
+ def mutator(leases: list[dict]) -> dict:
222
+ retained = [lease for lease in leases if lease["actor"] != actor]
223
+ active_conflicts, stale_conflicts = lane_proto.conflicting_leases(retained, actor, "exec")
224
+ if active_conflicts:
225
+ return {"status": "blocked", "payload": {"reason": "conflicting-active-lease", "conflicting_leases": active_conflicts}, "write": False}
226
+ if stale_conflicts:
227
+ return {"status": "blocked", "payload": {"reason": "stale-conflicting-lease", "conflicting_leases": stale_conflicts}, "write": False}
228
+ retained.append(lane_proto.build_lease(actor, "exec", ttl_seconds))
229
+ return {"status": "ok", "leases": retained, "write": True}
230
+
231
+ result = lane_proto.mutate_lane_leases(workspace_root, owner_unit, lane_name, mutator)
232
+ if result["status"] == "blocked":
233
+ raise SystemExit(json.dumps(result["payload"], indent=2))
234
+ _emit_lease_event(
235
+ workspace_root,
236
+ owner_unit=owner_unit,
237
+ lane_name=lane_name,
238
+ actor=actor,
239
+ event_type="lease_acquire",
240
+ ttl_seconds=ttl_seconds,
241
+ )
242
+
243
+
244
+ def release_exec_lease(workspace_root: Path, owner_unit: str, lane_name: str, actor: str) -> None:
245
+ lane_proto.mutate_lane_leases(
246
+ workspace_root,
247
+ owner_unit,
248
+ lane_name,
249
+ lambda leases: {
250
+ "status": "ok",
251
+ "leases": [lease for lease in leases if lease["actor"] != actor],
252
+ "write": True,
253
+ },
254
+ )
255
+ _emit_lease_event(
256
+ workspace_root,
257
+ owner_unit=owner_unit,
258
+ lane_name=lane_name,
259
+ actor=actor,
260
+ event_type="lease_release",
261
+ )
262
+
263
+
264
+ def run_exec(
265
+ workspace_root: Path,
266
+ owner_unit: str,
267
+ lane_name: str | None,
268
+ *,
269
+ actor: str,
270
+ command: list[str],
271
+ repos: str | None = None,
272
+ ttl_seconds: int = 900,
273
+ ) -> dict[str, object]:
274
+ status = exec_status_payload(workspace_root, owner_unit, lane_name, repos=repos, actor=actor)
275
+ if status["status"] != "ready":
276
+ return status
277
+
278
+ resolved_lane = str(status["lane"])
279
+ rows = list(status["rows"])
280
+ fail_fast = bool(rows[0]["fail_fast"]) if rows else True
281
+ parallelism = str(rows[0]["parallelism"]) if rows else "sequential"
282
+ acquire_exec_lease(workspace_root, owner_unit, resolved_lane, actor, ttl_seconds)
283
+
284
+ emit_after_outcome(
285
+ event_type=EventType.EXEC_STARTED,
286
+ workspace_root=workspace_root,
287
+ actor=actor,
288
+ owner_unit=owner_unit,
289
+ payload={
290
+ "lane": resolved_lane,
291
+ "command": command,
292
+ "repos": [r["repo"] for r in rows],
293
+ "parallelism": parallelism,
294
+ },
295
+ )
296
+
297
+ exec_results: list[ExecResult] = []
298
+ overall = "success"
299
+
300
+ try:
301
+ if parallelism == "parallel":
302
+ repo_rows = [{"repo": r["repo"], "cwd": str(r["cwd"])} for r in rows]
303
+ exec_results = run_exec_parallel(command, repo_rows)
304
+ if any(not r.succeeded for r in exec_results):
305
+ overall = "failed"
306
+ else:
307
+ for row in rows:
308
+ r = _exec_one(command, str(row["repo"]), str(row["cwd"]))
309
+ exec_results.append(r)
310
+ if not r.succeeded:
311
+ overall = "failed"
312
+ if fail_fast:
313
+ break
314
+ finally:
315
+ release_exec_lease(workspace_root, owner_unit, resolved_lane, actor)
316
+
317
+ result_dicts = [r.to_dict() for r in exec_results]
318
+ failed_repos = [r.repo for r in exec_results if not r.succeeded]
319
+
320
+ if overall == "success":
321
+ emit_after_outcome(
322
+ event_type=EventType.EXEC_COMPLETED,
323
+ workspace_root=workspace_root,
324
+ actor=actor,
325
+ owner_unit=owner_unit,
326
+ payload={
327
+ "lane": resolved_lane,
328
+ "command": command,
329
+ "overall_status": "success",
330
+ "repo_count": len(exec_results),
331
+ },
332
+ )
333
+ else:
334
+ emit_after_outcome(
335
+ event_type=EventType.EXEC_FAILED,
336
+ workspace_root=workspace_root,
337
+ actor=actor,
338
+ owner_unit=owner_unit,
339
+ payload={
340
+ "lane": resolved_lane,
341
+ "command": command,
342
+ "overall_status": "failed",
343
+ "failed_repos": failed_repos,
344
+ "repo_count": len(exec_results),
345
+ },
346
+ )
347
+
348
+ return {
349
+ "status": overall,
350
+ "owner_unit": owner_unit,
351
+ "lane": resolved_lane,
352
+ "command": command,
353
+ "results": result_dicts,
354
+ }
355
+
356
+
357
+ def render_exec_run(payload: dict[str, object]) -> str:
358
+ if payload.get("status") == "blocked":
359
+ return render_exec_status(payload)
360
+ lines = [
361
+ "ExecRun",
362
+ f"status = {payload['status']}",
363
+ f"owner_unit = {payload['owner_unit']}",
364
+ f"lane = {payload['lane']}",
365
+ f"command = {' '.join(payload['command'])}",
366
+ "REPO\tSTATUS\tRETURNCODE\tCWD",
367
+ ]
368
+ for result in payload["results"]:
369
+ lines.append(
370
+ f"{result['repo']}\t{result['status']}\t{result['returncode']}\t{result['cwd']}"
371
+ )
372
+ return "\n".join(lines)
@@ -0,0 +1,98 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from datetime import UTC, datetime
6
+ from pathlib import Path
7
+
8
+ from .events import EventType, emit_after_outcome
9
+
10
+
11
+ def _now_utc() -> str:
12
+ return datetime.now(UTC).isoformat()
13
+
14
+
15
+ def failures_root(workspace_root: Path) -> Path:
16
+ return workspace_root / ".grip" / "state" / "failures"
17
+
18
+
19
+ def failure_marker_path(workspace_root: Path, operation_id: str) -> Path:
20
+ return failures_root(workspace_root) / f"{operation_id}.json"
21
+
22
+
23
+ def unresolved_lane_failure(workspace_root: Path, owner_unit: str, lane_name: str) -> dict[str, object] | None:
24
+ root = failures_root(workspace_root)
25
+ if not root.exists():
26
+ return None
27
+ for path in sorted(root.glob("*.json")):
28
+ doc = json.loads(path.read_text())
29
+ if doc.get("resolved") is True:
30
+ continue
31
+ if doc.get("owner_unit") == owner_unit and doc.get("lane_name") == lane_name:
32
+ return doc
33
+ return None
34
+
35
+
36
+ def write_failure_marker(
37
+ workspace_root: Path,
38
+ *,
39
+ operation: str,
40
+ stage: str,
41
+ hook_name: str,
42
+ repo: str,
43
+ owner_unit: str,
44
+ lane_name: str,
45
+ partial_state: dict[str, object] | None = None,
46
+ event_id: str | None = None,
47
+ ) -> dict[str, object]:
48
+ operation_id = f"op_{os.urandom(4).hex()}"
49
+ marker = {
50
+ "operation_id": operation_id,
51
+ "operation": operation,
52
+ "stage": stage,
53
+ "hook_name": hook_name,
54
+ "repo": repo,
55
+ "owner_unit": owner_unit,
56
+ "lane_name": lane_name,
57
+ "failed_at": _now_utc(),
58
+ "event_id": event_id,
59
+ "partial_state": partial_state or {},
60
+ "resolved": False,
61
+ }
62
+ path = failure_marker_path(workspace_root, operation_id)
63
+ path.parent.mkdir(parents=True, exist_ok=True)
64
+ path.write_text(json.dumps(marker, indent=2) + "\n")
65
+ return marker
66
+
67
+
68
+ def resolve_failure_marker(
69
+ workspace_root: Path,
70
+ *,
71
+ operation_id: str,
72
+ resolved_by: str,
73
+ resolution: str,
74
+ owner_unit: str,
75
+ ) -> dict[str, object]:
76
+ path = failure_marker_path(workspace_root, operation_id)
77
+ if not path.exists():
78
+ raise SystemExit(f"failure marker not found: {operation_id}")
79
+ marker = json.loads(path.read_text())
80
+ path.unlink()
81
+ emit_after_outcome(
82
+ event_type=EventType.FAILURE_RESOLVED,
83
+ workspace_root=workspace_root,
84
+ actor=resolved_by,
85
+ owner_unit=owner_unit,
86
+ payload={
87
+ "operation_id": operation_id,
88
+ "resolved_by": resolved_by,
89
+ "resolution": resolution,
90
+ "lane_name": marker.get("lane_name", ""),
91
+ },
92
+ )
93
+ return {
94
+ "operation_id": operation_id,
95
+ "resolved_by": resolved_by,
96
+ "resolution": resolution,
97
+ "lane_name": marker.get("lane_name", ""),
98
+ }