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,450 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ import json
5
+ import sys
6
+ import subprocess
7
+ import time
8
+ import tomllib
9
+ from pathlib import Path
10
+
11
+ from .events import EventType, emit, emit_after_outcome
12
+
13
+
14
+ VALID_IF_EXISTS = {"skip", "overwrite", "merge", "error"}
15
+ VALID_ON_FAILURE = {"block", "warn", "skip"}
16
+ VALID_WHEN = {"first_materialize", "always", "dirty", "manual"}
17
+
18
+
19
+ @dataclasses.dataclass(frozen=True)
20
+ class FileProjection:
21
+ kind: str
22
+ src: str
23
+ dest: str
24
+ if_exists: str = "error"
25
+
26
+
27
+ @dataclasses.dataclass(frozen=True)
28
+ class LifecycleHook:
29
+ stage: str
30
+ name: str
31
+ command: str
32
+ cwd: str
33
+ when: str
34
+ on_failure: str
35
+
36
+
37
+ @dataclasses.dataclass(frozen=True)
38
+ class RepoHooks:
39
+ repo_name: str | None
40
+ file_links: list[FileProjection]
41
+ file_copies: list[FileProjection]
42
+ on_materialize: list[LifecycleHook]
43
+ on_enter: list[LifecycleHook]
44
+ on_exit: list[LifecycleHook]
45
+ policy: dict[str, object]
46
+ path: Path
47
+
48
+ def as_dict(self) -> dict[str, object]:
49
+ return {
50
+ "repo_name": self.repo_name,
51
+ "path": str(self.path),
52
+ "files": {
53
+ "link": [dataclasses.asdict(item) for item in self.file_links],
54
+ "copy": [dataclasses.asdict(item) for item in self.file_copies],
55
+ },
56
+ "lifecycle": {
57
+ "on_materialize": [dataclasses.asdict(item) for item in self.on_materialize],
58
+ "on_enter": [dataclasses.asdict(item) for item in self.on_enter],
59
+ "on_exit": [dataclasses.asdict(item) for item in self.on_exit],
60
+ },
61
+ "policy": self.policy,
62
+ }
63
+
64
+
65
+ @dataclasses.dataclass(frozen=True)
66
+ class HookContext:
67
+ workspace_root: Path
68
+ unit_root: Path
69
+ lane_root: Path
70
+ repo_root: Path
71
+ repo_name: str
72
+ lane_owner: str
73
+ lane_subject: str
74
+ lane_name: str
75
+
76
+
77
+ @dataclasses.dataclass(frozen=True)
78
+ class HookResult:
79
+ kind: str
80
+ name: str
81
+ status: str
82
+ detail: str
83
+ cwd: str | None = None
84
+ command: str | None = None
85
+ returncode: int | None = None
86
+ stdout: str | None = None
87
+ stderr: str | None = None
88
+ src: str | None = None
89
+ dest: str | None = None
90
+ if_exists: str | None = None
91
+
92
+ def as_dict(self) -> dict[str, object]:
93
+ return dataclasses.asdict(self)
94
+
95
+
96
+ class HookRuntimeError(SystemExit):
97
+ def __init__(self, payload: dict[str, object]) -> None:
98
+ self.payload = payload
99
+ super().__init__(json.dumps(payload, indent=2))
100
+
101
+
102
+ def hook_file(repo_root: Path) -> Path:
103
+ return repo_root / ".gr2" / "hooks.toml"
104
+
105
+
106
+ def load_repo_hooks(repo_root: Path) -> RepoHooks | None:
107
+ path = hook_file(repo_root)
108
+ if not path.exists():
109
+ return None
110
+ with path.open("rb") as fh:
111
+ raw = tomllib.load(fh)
112
+ return RepoHooks(
113
+ repo_name=raw.get("repo", {}).get("name"),
114
+ file_links=_parse_projections(raw, "link"),
115
+ file_copies=_parse_projections(raw, "copy"),
116
+ on_materialize=_parse_lifecycle(raw, "on_materialize", default_on_failure="block"),
117
+ on_enter=_parse_lifecycle(raw, "on_enter", default_on_failure="warn"),
118
+ on_exit=_parse_lifecycle(raw, "on_exit", default_on_failure="warn"),
119
+ policy=dict(raw.get("policy", {})),
120
+ path=path,
121
+ )
122
+
123
+
124
+ def _parse_projections(raw: dict, kind: str) -> list[FileProjection]:
125
+ items = raw.get("files", {}).get(kind, [])
126
+ results: list[FileProjection] = []
127
+ for item in items:
128
+ if_exists = str(item.get("if_exists", "error"))
129
+ if if_exists not in VALID_IF_EXISTS:
130
+ raise SystemExit(f"invalid if_exists={if_exists} in {kind} projection")
131
+ results.append(
132
+ FileProjection(
133
+ kind=kind,
134
+ src=str(item["src"]),
135
+ dest=str(item["dest"]),
136
+ if_exists=if_exists,
137
+ )
138
+ )
139
+ return results
140
+
141
+
142
+ def _parse_lifecycle(raw: dict, stage: str, default_on_failure: str) -> list[LifecycleHook]:
143
+ items = raw.get("lifecycle", {}).get(stage, [])
144
+ results: list[LifecycleHook] = []
145
+ for item in items:
146
+ when = str(item.get("when", "always"))
147
+ on_failure = str(item.get("on_failure", default_on_failure))
148
+ if when not in VALID_WHEN:
149
+ raise SystemExit(f"invalid when={when} in lifecycle.{stage}")
150
+ if on_failure not in VALID_ON_FAILURE:
151
+ raise SystemExit(f"invalid on_failure={on_failure} in lifecycle.{stage}")
152
+ results.append(
153
+ LifecycleHook(
154
+ stage=stage,
155
+ name=str(item["name"]),
156
+ command=str(item["command"]),
157
+ cwd=str(item.get("cwd", "{repo_root}")),
158
+ when=when,
159
+ on_failure=on_failure,
160
+ )
161
+ )
162
+ return results
163
+
164
+
165
+ def render_path(template: str, ctx: HookContext) -> Path:
166
+ rendered = render_text(template, ctx)
167
+ return Path(rendered)
168
+
169
+
170
+ def render_text(template: str, ctx: HookContext) -> str:
171
+ import re
172
+
173
+ result = (
174
+ template.replace("{workspace_root}", str(ctx.workspace_root))
175
+ .replace("{unit_root}", str(ctx.unit_root))
176
+ .replace("{lane_root}", str(ctx.lane_root))
177
+ .replace("{repo_root}", str(ctx.repo_root))
178
+ .replace("{repo_name}", ctx.repo_name)
179
+ .replace("{lane_owner}", ctx.lane_owner)
180
+ .replace("{lane_subject}", ctx.lane_subject)
181
+ .replace("{lane_name}", ctx.lane_name)
182
+ )
183
+ remaining = re.findall(r"\{(\w+)\}", result)
184
+ if remaining:
185
+ raise ValueError(
186
+ f"undefined template variable(s): {', '.join('{' + v + '}' for v in remaining)}"
187
+ )
188
+ return result
189
+
190
+
191
+ def apply_file_projections(hooks: RepoHooks, ctx: HookContext) -> list[HookResult]:
192
+ results: list[HookResult] = []
193
+ for item in [*hooks.file_links, *hooks.file_copies]:
194
+ rendered_src = render_text(item.src, ctx)
195
+ src = Path(rendered_src)
196
+ if not src.is_absolute():
197
+ src = ctx.repo_root / src
198
+ dest = render_path(item.dest, ctx)
199
+ dest.parent.mkdir(parents=True, exist_ok=True)
200
+
201
+ if not src.exists():
202
+ raise HookRuntimeError(
203
+ {
204
+ "kind": "projection",
205
+ "projection": item.kind,
206
+ "status": "blocked",
207
+ "detail": f"projection source does not exist: {src}",
208
+ "repo_hooks_path": str(hooks.path),
209
+ "src": str(src),
210
+ "dest": str(dest),
211
+ }
212
+ )
213
+
214
+ if dest.exists() or dest.is_symlink():
215
+ if item.if_exists == "skip":
216
+ results.append(
217
+ HookResult(
218
+ kind="projection",
219
+ name=f"{item.kind}:{dest.name}",
220
+ status="skipped",
221
+ detail=f"destination already exists and if_exists=skip: {dest}",
222
+ src=str(src),
223
+ dest=str(dest),
224
+ if_exists=item.if_exists,
225
+ )
226
+ )
227
+ continue
228
+ if item.if_exists == "error":
229
+ raise HookRuntimeError(
230
+ {
231
+ "kind": "projection",
232
+ "projection": item.kind,
233
+ "status": "blocked",
234
+ "detail": f"projection conflict at {dest}",
235
+ "repo_hooks_path": str(hooks.path),
236
+ "src": str(src),
237
+ "dest": str(dest),
238
+ }
239
+ )
240
+ if item.if_exists == "merge":
241
+ raise HookRuntimeError(
242
+ {
243
+ "kind": "projection",
244
+ "projection": item.kind,
245
+ "status": "blocked",
246
+ "detail": f"merge projections not implemented yet for {dest}",
247
+ "repo_hooks_path": str(hooks.path),
248
+ "src": str(src),
249
+ "dest": str(dest),
250
+ }
251
+ )
252
+ if item.if_exists == "overwrite":
253
+ if dest.is_dir() and not dest.is_symlink():
254
+ raise HookRuntimeError(
255
+ {
256
+ "kind": "projection",
257
+ "projection": item.kind,
258
+ "status": "blocked",
259
+ "detail": f"refusing to overwrite directory projection target: {dest}",
260
+ "repo_hooks_path": str(hooks.path),
261
+ "src": str(src),
262
+ "dest": str(dest),
263
+ }
264
+ )
265
+ dest.unlink(missing_ok=True)
266
+
267
+ if item.kind == "link":
268
+ dest.symlink_to(src)
269
+ else:
270
+ dest.write_bytes(src.read_bytes())
271
+ results.append(
272
+ HookResult(
273
+ kind="projection",
274
+ name=f"{item.kind}:{dest.name}",
275
+ status="applied",
276
+ detail=f"{item.kind} {src} -> {dest}",
277
+ src=str(src),
278
+ dest=str(dest),
279
+ if_exists=item.if_exists,
280
+ )
281
+ )
282
+ return results
283
+
284
+
285
+ def run_lifecycle_stage(
286
+ hooks: RepoHooks,
287
+ stage: str,
288
+ ctx: HookContext,
289
+ *,
290
+ repo_dirty: bool,
291
+ first_materialize: bool,
292
+ allow_manual: bool = False,
293
+ ) -> list[HookResult]:
294
+ hooks_for_stage = {
295
+ "on_materialize": hooks.on_materialize,
296
+ "on_enter": hooks.on_enter,
297
+ "on_exit": hooks.on_exit,
298
+ }[stage]
299
+ results: list[HookResult] = []
300
+ for hook in hooks_for_stage:
301
+ if not _should_run(
302
+ hook.when,
303
+ repo_dirty=repo_dirty,
304
+ first_materialize=first_materialize,
305
+ allow_manual=allow_manual,
306
+ ):
307
+ emit(
308
+ event_type=EventType.HOOK_SKIPPED,
309
+ workspace_root=ctx.workspace_root,
310
+ actor="system",
311
+ owner_unit=ctx.lane_owner,
312
+ payload={
313
+ "stage": stage,
314
+ "hook_name": hook.name,
315
+ "repo": ctx.repo_name,
316
+ "reason": f"when={hook.when} did not match current invocation",
317
+ },
318
+ )
319
+ results.append(
320
+ HookResult(
321
+ kind="lifecycle",
322
+ name=hook.name,
323
+ status="skipped",
324
+ detail=f"hook when={hook.when} did not match current invocation",
325
+ )
326
+ )
327
+ continue
328
+ cwd = render_path(hook.cwd, ctx)
329
+ command = render_text(hook.command, ctx)
330
+ emit(
331
+ event_type=EventType.HOOK_STARTED,
332
+ workspace_root=ctx.workspace_root,
333
+ actor="system",
334
+ owner_unit=ctx.lane_owner,
335
+ payload={
336
+ "stage": stage,
337
+ "hook_name": hook.name,
338
+ "repo": ctx.repo_name,
339
+ "command": command,
340
+ "cwd": str(cwd),
341
+ },
342
+ )
343
+ t0 = time.monotonic()
344
+ proc = subprocess.run(
345
+ command,
346
+ cwd=cwd,
347
+ shell=True,
348
+ capture_output=True,
349
+ text=True,
350
+ )
351
+ duration_ms = int((time.monotonic() - t0) * 1000)
352
+ if proc.returncode == 0:
353
+ emit_after_outcome(
354
+ event_type=EventType.HOOK_COMPLETED,
355
+ workspace_root=ctx.workspace_root,
356
+ actor="system",
357
+ owner_unit=ctx.lane_owner,
358
+ payload={
359
+ "stage": stage,
360
+ "hook_name": hook.name,
361
+ "repo": ctx.repo_name,
362
+ "duration_ms": duration_ms,
363
+ "exit_code": 0,
364
+ },
365
+ )
366
+ results.append(
367
+ HookResult(
368
+ kind="lifecycle",
369
+ name=hook.name,
370
+ status="applied",
371
+ detail=f"stage {stage} completed successfully",
372
+ cwd=str(cwd),
373
+ command=command,
374
+ returncode=proc.returncode,
375
+ stdout=proc.stdout,
376
+ stderr=proc.stderr,
377
+ )
378
+ )
379
+ continue
380
+ stderr_tail = proc.stderr[-500:] if proc.stderr else ""
381
+ emit_after_outcome(
382
+ event_type=EventType.HOOK_FAILED,
383
+ workspace_root=ctx.workspace_root,
384
+ actor="system",
385
+ owner_unit=ctx.lane_owner,
386
+ payload={
387
+ "stage": stage,
388
+ "hook_name": hook.name,
389
+ "repo": ctx.repo_name,
390
+ "duration_ms": duration_ms,
391
+ "exit_code": proc.returncode,
392
+ "on_failure": hook.on_failure,
393
+ "stderr_tail": stderr_tail,
394
+ },
395
+ )
396
+ payload = {
397
+ "kind": "lifecycle",
398
+ "stage": stage,
399
+ "hook": hook.name,
400
+ "cwd": str(cwd),
401
+ "command": command,
402
+ "returncode": proc.returncode,
403
+ "stdout": proc.stdout,
404
+ "stderr": proc.stderr,
405
+ "on_failure": hook.on_failure,
406
+ }
407
+ if hook.on_failure == "block":
408
+ raise HookRuntimeError(payload)
409
+ if hook.on_failure == "warn":
410
+ print(json.dumps(payload, indent=2), file=sys.stderr)
411
+ results.append(
412
+ HookResult(
413
+ kind="lifecycle",
414
+ name=hook.name,
415
+ status="warned",
416
+ detail=f"hook failed with on_failure=warn during {stage}",
417
+ cwd=str(cwd),
418
+ command=command,
419
+ returncode=proc.returncode,
420
+ stdout=proc.stdout,
421
+ stderr=proc.stderr,
422
+ )
423
+ )
424
+ continue
425
+ results.append(
426
+ HookResult(
427
+ kind="lifecycle",
428
+ name=hook.name,
429
+ status="skipped",
430
+ detail=f"hook failed with on_failure=skip during {stage}",
431
+ cwd=str(cwd),
432
+ command=command,
433
+ returncode=proc.returncode,
434
+ stdout=proc.stdout,
435
+ stderr=proc.stderr,
436
+ )
437
+ )
438
+ return results
439
+
440
+
441
+ def _should_run(when: str, *, repo_dirty: bool, first_materialize: bool, allow_manual: bool) -> bool:
442
+ if when == "always":
443
+ return True
444
+ if when == "first_materialize":
445
+ return first_materialize
446
+ if when == "dirty":
447
+ return repo_dirty
448
+ if when == "manual":
449
+ return allow_manual
450
+ return False