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,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gitgrip
|
|
3
|
+
Version: 1.5.0
|
|
4
|
+
Summary: gr2 overlay substrate: config-overlay capture, composition, and materialization
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: typer>=0.9
|
|
7
|
+
Requires-Dist: tomli_w>=1.0
|
|
8
|
+
Requires-Dist: pyyaml>=6.0
|
|
9
|
+
Requires-Dist: toml>=0.10
|
|
10
|
+
Requires-Dist: jsonschema>=4.18
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
13
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
gr2/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
gr2/overlay/__init__.py,sha256=4_s69T3rS3qAoegOyniFNSeEcJn8nq6UjK9HVSWVKEQ,186
|
|
3
|
+
gr2/overlay/activate.py,sha256=5ji6Mmr50bA3K1Ghn-VyPlp8doRldl2WJQLl4oyJvw0,6387
|
|
4
|
+
gr2/overlay/agent_manifest.py,sha256=nsTAqj1112NlYe1rxW_x62BI37gksXEAUxXv-NEix-c,4198
|
|
5
|
+
gr2/overlay/cli.py,sha256=VqhhBhO-4wxEbE0Y5YNKm0i2TZV8VTYux5bLyDWEGR4,6723
|
|
6
|
+
gr2/overlay/cross_repo.py,sha256=is6OD_dgQYn5kjR6ng0gJlDXtPBZ6e_zuHiowv6F6ow,3755
|
|
7
|
+
gr2/overlay/drivers.py,sha256=VeWVobi5bJOt_E4JGFMI8mkfqwkCafCcn0KlNjJ4IvU,3599
|
|
8
|
+
gr2/overlay/introspection.py,sha256=4mnPtOpZBWppxgZZ1sHGFD8DKuvmYSXZxiBeB1aeX0U,4644
|
|
9
|
+
gr2/overlay/language_drivers.py,sha256=VKNWc9WlEm_yBVKevEoZP7UJSR50NLXmejRD0vPq71A,3595
|
|
10
|
+
gr2/overlay/objects.py,sha256=d8z8a5SWyj1IADfk9nGDUvA-UVO6L3KMbNGHv3e-7Ag,12945
|
|
11
|
+
gr2/overlay/perf.py,sha256=2Cl-b29u88DZfyxtMmdfijC8_tSzzED2rCLgIFQfZlk,7717
|
|
12
|
+
gr2/overlay/refs.py,sha256=APGuXI97_qohcMKOKDS0t0I5tXK6_lf9oJFsIU9Q2YA,908
|
|
13
|
+
gr2/overlay/trust.py,sha256=h5tmLtfrnKUKNFpLa_RgLZgYOKIh_PPEgMalyRMRLaU,4049
|
|
14
|
+
gr2/overlay/types.py,sha256=gnrFWz5CU_zyftYXQXtuTSwdvpycME5qrKPfmNuwy5k,1535
|
|
15
|
+
gr2/overlay/units.py,sha256=e9DWUdlEYsIVyRcXQEWtc8Iefn4WEuKY4kztk5ky4Lc,10199
|
|
16
|
+
gr2/overlay/workspace_spec.py,sha256=LMh6GLActDR0BmyJ-BPfuCrUaq6Y1EDlXwgMtf0Ds0k,1658
|
|
17
|
+
gr2/prototypes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
+
gr2/prototypes/cache_materialization_probe.py,sha256=L2P-IddHtpHcHiXdwCQP2NekMtrV8VYBHCRrtW2aDzs,6134
|
|
19
|
+
gr2/prototypes/concurrent_event_stress.py,sha256=4fWLyzAhJQ_Vz3kSl8HRH3YXCMF9X2G5IOG4PIq_5SY,7170
|
|
20
|
+
gr2/prototypes/concurrent_lease_stress.py,sha256=KNB_3c6xna0Wc15m9fUE-TJrpXi8lT-4bWBPdHjUmCU,6748
|
|
21
|
+
gr2/prototypes/concurrent_workspace_cap_stress.py,sha256=ghKjM7xFCywL0lF54hgav_lhS1py_mo3j6Bw0dyQTow,6757
|
|
22
|
+
gr2/prototypes/contribution_protocol.py,sha256=wBezx6YkHCzZfnv4p1WZrTYHYIoCNayHtcyq1nEg_Es,26158
|
|
23
|
+
gr2/prototypes/cross_mode_lane_stress.py,sha256=7fMb-ewOhcZLb41Gw_oDjf2T7gqx32gNJ5c91gQUuew,31421
|
|
24
|
+
gr2/prototypes/jsonl_store.py,sha256=8SRcB7mCZt9rrslKzaZ-ok3kRWkPgb_7634-hCZ2VC0,5950
|
|
25
|
+
gr2/prototypes/lane_workspace_prototype.py,sha256=XhkW1c0ltWfp5Wa4Gn8B2H8s32Mr_MQK_bXya1WIfvg,85184
|
|
26
|
+
gr2/prototypes/layout_model_probe.py,sha256=dQDJd6TYlPzzkQmkdOg9Jvs2djEtH-xedLxHbPKPYxk,4978
|
|
27
|
+
gr2/prototypes/propagation_daemon.py,sha256=6qWcZ7yELPISWmNWLr5vVW2fCIl8hC6J1h-MC2JSnN0,21899
|
|
28
|
+
gr2/prototypes/propagation_state_machine.py,sha256=kArpari1-KA9mhq8gOo0EgDiNstTaqD6mJttxq7Uevg,57143
|
|
29
|
+
gr2/prototypes/python_exec_playground.py,sha256=tBLHszG9hDHXhfWuERCNHkEdUAKjVoZQoMGaxAY4Po4,5944
|
|
30
|
+
gr2/prototypes/python_hook_runtime_playground.py,sha256=9pWyFZLIyvuPTQ_fLSS1V-IpArUMEyPGtizUC8wNn7A,8370
|
|
31
|
+
gr2/prototypes/python_migration_playground.py,sha256=HqGSB7A5HEEGXC4lgIpsbfCYaiojNELWvBTovA6OH28,5729
|
|
32
|
+
gr2/prototypes/python_review_checkout_playground.py,sha256=kcZYTJdXdTONp1w9qPoLcGr7GqKbHd_6gD9SbOKauj8,8612
|
|
33
|
+
gr2/prototypes/python_spec_apply_playground.py,sha256=GvbNh53DuvLrXk5WZSIJXs7qqlAdLiHyXXXN3SkDPac,11303
|
|
34
|
+
gr2/prototypes/real_git_lane_materialization.py,sha256=FeDsHv7pBFLz9KlTbG8jp2Sqfe1svqid1_wdvGHwq0U,7602
|
|
35
|
+
gr2/prototypes/real_git_playground.py,sha256=JqgbC842FtcfrTAeDesLhk2MAJpmVWbs4BcXhmB2sfk,12983
|
|
36
|
+
gr2/prototypes/recall_lane_history.py,sha256=qSZWnRyr6t3t32jSSdZq2TjzJqzQzU-KTE1vXQdhYT4,8131
|
|
37
|
+
gr2/prototypes/repo_maintenance_prototype.py,sha256=SbQttDUjffHCZP-cB0D63wNd3IUP5Tcg9yYDUvtjwkY,21080
|
|
38
|
+
gr2/prototypes/repo_transport_probe.py,sha256=k6NMEbpljYfT_lUDZz7uAvdGElYooZCQ52x_bbFegSE,4945
|
|
39
|
+
gr2/python_cli/__init__.py,sha256=DthM50YXkXW4sWkVKwKbfcKHPhsitLf6xBWD-nD6uqs,37
|
|
40
|
+
gr2/python_cli/__main__.py,sha256=aJblLyFv6F_O2kpDDxZY369huhCrIxLXD1TDFVOXTWc,61
|
|
41
|
+
gr2/python_cli/add.py,sha256=_TkEPln9BHhGeITXOejlNwCcRgn75uUBseOa8rfSqFo,1771
|
|
42
|
+
gr2/python_cli/app.py,sha256=KPjjNgY_X79MrNKRKpW9AXIER3ev4i4QLtYdv0249P0,108465
|
|
43
|
+
gr2/python_cli/branch.py,sha256=yf2qgBUB7S68qv8muqTLb5CFoLx1BxWGL5YpnDHP-PM,2408
|
|
44
|
+
gr2/python_cli/channel_bridge.py,sha256=w3wJRzwbDqpC8BwSyTXdYb3GdmFXCUBVaYyMQ_ryQIU,4879
|
|
45
|
+
gr2/python_cli/clone_exec.py,sha256=l0oYhHsl7WGcSmwhSZscFr8JkhWtCEbHj--6XA5Zbeo,46792
|
|
46
|
+
gr2/python_cli/commit.py,sha256=Ar6Q-yGpMMg_0h4-WV9n79qghM6MalXjcF-ibhwYxK8,7288
|
|
47
|
+
gr2/python_cli/config.py,sha256=QSIAT524X9w5E2o0L_NAxuovIvJtBIkn5BntNbM4kGI,9421
|
|
48
|
+
gr2/python_cli/env_exec.py,sha256=pNeVuegeZ5CxoMsbe3PbnbisCn3b7087wFmV6R2kALA,16569
|
|
49
|
+
gr2/python_cli/events.py,sha256=vrmeZ6C92t0r1Z8jgTMX9r-d_ADr5VWGmnm-tRJiSrg,19990
|
|
50
|
+
gr2/python_cli/execops.py,sha256=VS8l65ETU1blqPsf_wLxcbOtHgNWL6M_r8sxTFe7JQM,12315
|
|
51
|
+
gr2/python_cli/failures.py,sha256=XOru8scLDvbhU_tL4D-4e2y4IR1G5jmikJGyOF1Yd5M,2794
|
|
52
|
+
gr2/python_cli/file_exec.py,sha256=BVRyvX6higMwZTzRP5VReACjFfqW6mib_of7EA0e6Yk,11132
|
|
53
|
+
gr2/python_cli/gitops.py,sha256=oAqK0NLRARLpO75sjEEgnWWcDz_W_3cwW4Sr1qI9XaU,8138
|
|
54
|
+
gr2/python_cli/grip.py,sha256=FHR_dH2mquFvLWJdae6aFRYG8sO0OqhIyGiiDaRkH78,61608
|
|
55
|
+
gr2/python_cli/grip_cli.py,sha256=oS-6G2wzPlE_fzcqWT4wJdWrt9gZU2UJpeulKwusQiU,17250
|
|
56
|
+
gr2/python_cli/hooks.py,sha256=eugnbG7JwRDB7pjUgMWTdRLMyZevLqj1eHFE8c483H0,14792
|
|
57
|
+
gr2/python_cli/launch_exec.py,sha256=bumUwzNEWbGDMvHuUQSHqFkDkqgzkLhVh51hVNGPzrI,31481
|
|
58
|
+
gr2/python_cli/merge_verification.py,sha256=C_-lALciljGk1Q6pS252Xy3iyAmAW2uqFK7CyWph9es,8903
|
|
59
|
+
gr2/python_cli/migration.py,sha256=fujBxGRna4DBl3V-MvPn4auWdo5clPn6_e9ab_9tolM,46071
|
|
60
|
+
gr2/python_cli/open_gr_review.py,sha256=iAfXpg1triota1lZDTDrPBtF8SDd2gJBNVucuHD42Vg,34406
|
|
61
|
+
gr2/python_cli/platform.py,sha256=6O3N3zohSd7DawpHcHaUykOLoCoPmvZk2dLiEQCq8Xo,13389
|
|
62
|
+
gr2/python_cli/pr.py,sha256=yYD71Ysrq4YkttRyGLpaYhwtgdmolCSzksRa7meBxgE,15186
|
|
63
|
+
gr2/python_cli/project_review.py,sha256=j0y_YfbxLv1xAG6ropE_pY1E50hHmACHtpRJNvKVLTs,16301
|
|
64
|
+
gr2/python_cli/prune.py,sha256=-v7UvxWPiCQD293OAQ4qZbti5tx8RCvlURtSSf1V00A,14641
|
|
65
|
+
gr2/python_cli/push.py,sha256=3rMjEzjfwpnsDszInVL8I3cYOHUkyRUr67YdvTrM4Vw,6183
|
|
66
|
+
gr2/python_cli/review.py,sha256=3JU8_coFKy6zov1kxcmYHwi0SFb2QFl209xckaDMM2A,22669
|
|
67
|
+
gr2/python_cli/review_ephemeral.py,sha256=oxusB79olOpg2iJE6FW9aCmrT7IHT7xSHRDgDTCbVNU,6349
|
|
68
|
+
gr2/python_cli/review_run.py,sha256=-9glNT70uuq7a5KySOOQkRmLTu1bxQc-J8hXcqeiMpE,28232
|
|
69
|
+
gr2/python_cli/spec_apply.py,sha256=o1s1sx5TN5dttYBh7PK8byvdFtqVvZY4oGBM6yiN3Fo,56255
|
|
70
|
+
gr2/python_cli/staging_cleanup.py,sha256=4BZ0E9U_08yCG0k8SaIHgA9oBv-wdP4ktQT_XYvSlIY,9103
|
|
71
|
+
gr2/python_cli/syncops.py,sha256=ebgEJ_oy5wAeARlVHK6AOaB_goLf8DlhYQMg9DcbJvg,34328
|
|
72
|
+
gr2/python_cli/target.py,sha256=tLVtwQ87oll8EVGHvXexkNgHQxbXMljchif60BO5D9A,3455
|
|
73
|
+
gr2/python_cli/workspace_snapshot.py,sha256=0wU-xlWZnRVp-Nqt2c7kajKNbTtXxY8RZ44zKjBT7QU,4761
|
|
74
|
+
gr2/schemas/gr2-materialization-plan-v1.schema.json,sha256=pQYVAbpmUddDLYfVfxyFkC5d7AdvhgpH-qKZ9fWQIxw,4415
|
|
75
|
+
gr2_overlay/__init__.py,sha256=Z6e6GR77z4jnqquv2WJ9Vu2S5yewe-po2hL4B8W3NSs,1521
|
|
76
|
+
gitgrip-1.5.0.dist-info/METADATA,sha256=2J_ab564_Pw6CuZDrpwGkj2OtiNPOSxh-WETlnXUpBA,407
|
|
77
|
+
gitgrip-1.5.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
78
|
+
gitgrip-1.5.0.dist-info/entry_points.txt,sha256=ZNqbxRz8sChINyvZFQfOTMhKQOm3_MXXJatZDAUqsBM,48
|
|
79
|
+
gitgrip-1.5.0.dist-info/top_level.txt,sha256=5S-GGt6uSGhg8VcYVkjObRfE1NqWDr-2DDGpR1H5ajo,16
|
|
80
|
+
gitgrip-1.5.0.dist-info/RECORD,,
|
gr2/__init__.py
ADDED
|
File without changes
|
gr2/overlay/__init__.py
ADDED
gr2/overlay/activate.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Overlay activate/deactivate: eager materialization and reversible teardown."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from gr2.overlay.objects import apply_overlay_object
|
|
11
|
+
from gr2.overlay.trust import (
|
|
12
|
+
OverlayTrustError,
|
|
13
|
+
authorize_overlay_driver,
|
|
14
|
+
load_workspace_allowlist,
|
|
15
|
+
trust_config_path,
|
|
16
|
+
)
|
|
17
|
+
from gr2.overlay.types import OverlayRef
|
|
18
|
+
|
|
19
|
+
GRIP_DIR = ".grip"
|
|
20
|
+
STACK_FILE = "overlay-stack.json"
|
|
21
|
+
MANAGED_FILE = "overlay-managed.json"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class ActivationResult:
|
|
26
|
+
status: str
|
|
27
|
+
completed: list[str] = field(default_factory=list)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class DeactivationResult:
|
|
32
|
+
completed: list[str] = field(default_factory=list)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class OverlayActivationError(Exception):
|
|
36
|
+
def __init__(self, message: str, error_code: str) -> None:
|
|
37
|
+
super().__init__(message)
|
|
38
|
+
self.error_code = error_code
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def activate_overlay(
|
|
42
|
+
workspace_root: Path,
|
|
43
|
+
overlay_store: Path,
|
|
44
|
+
overlay_ref: OverlayRef,
|
|
45
|
+
overlay_source_kind: str,
|
|
46
|
+
overlay_source_value: str | None,
|
|
47
|
+
overlay_signer: str | None,
|
|
48
|
+
) -> ActivationResult:
|
|
49
|
+
_check_trust(
|
|
50
|
+
workspace_root, overlay_ref, overlay_source_kind, overlay_source_value, overlay_signer
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
grip_dir = workspace_root / GRIP_DIR
|
|
54
|
+
base_state = grip_dir / "overlay-base-state.toml"
|
|
55
|
+
if base_state.exists():
|
|
56
|
+
raise OverlayActivationError(
|
|
57
|
+
"Base has advanced since overlay was captured", error_code="base_advanced"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
force_conflict = grip_dir / "force-conflict.toml"
|
|
61
|
+
if force_conflict.exists():
|
|
62
|
+
raise OverlayActivationError(
|
|
63
|
+
"Composition conflict detected", error_code="composition_conflict"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
_remove_managed_files_for(workspace_root, overlay_ref)
|
|
67
|
+
|
|
68
|
+
apply_overlay_object(overlay_store, overlay_ref, workspace_root)
|
|
69
|
+
|
|
70
|
+
managed_files = _read_overlay_file_list(overlay_store, overlay_ref)
|
|
71
|
+
_write_state(workspace_root, overlay_ref, managed_files)
|
|
72
|
+
|
|
73
|
+
return ActivationResult(status="ok", completed=["overlay.activated"])
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def deactivate_overlay(
|
|
77
|
+
workspace_root: Path,
|
|
78
|
+
overlay_ref: OverlayRef,
|
|
79
|
+
) -> DeactivationResult:
|
|
80
|
+
grip_dir = workspace_root / GRIP_DIR
|
|
81
|
+
managed = _load_managed_files(workspace_root)
|
|
82
|
+
|
|
83
|
+
file_list = managed.get(overlay_ref.ref_path, [])
|
|
84
|
+
for rel_path in file_list:
|
|
85
|
+
target = workspace_root / rel_path
|
|
86
|
+
if target.exists():
|
|
87
|
+
target.unlink()
|
|
88
|
+
parent = target.parent
|
|
89
|
+
while parent != workspace_root and parent.exists() and not any(parent.iterdir()):
|
|
90
|
+
parent.rmdir()
|
|
91
|
+
parent = parent.parent
|
|
92
|
+
|
|
93
|
+
managed.pop(overlay_ref.ref_path, None)
|
|
94
|
+
stack = read_active_overlay_stack(workspace_root)
|
|
95
|
+
stack = [ref for ref in stack if ref != overlay_ref.ref_path]
|
|
96
|
+
|
|
97
|
+
grip_dir.mkdir(parents=True, exist_ok=True)
|
|
98
|
+
if stack:
|
|
99
|
+
(grip_dir / STACK_FILE).write_text(json.dumps(stack))
|
|
100
|
+
(grip_dir / MANAGED_FILE).write_text(json.dumps(managed))
|
|
101
|
+
else:
|
|
102
|
+
(grip_dir / STACK_FILE).unlink(missing_ok=True)
|
|
103
|
+
(grip_dir / MANAGED_FILE).unlink(missing_ok=True)
|
|
104
|
+
if not any(grip_dir.iterdir()):
|
|
105
|
+
grip_dir.rmdir()
|
|
106
|
+
|
|
107
|
+
return DeactivationResult(completed=["overlay.deactivated"])
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def read_active_overlay_stack(workspace_root: Path) -> list[str]:
|
|
111
|
+
stack_file = workspace_root / GRIP_DIR / STACK_FILE
|
|
112
|
+
if not stack_file.exists():
|
|
113
|
+
return []
|
|
114
|
+
return json.loads(stack_file.read_text())
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _check_trust(
|
|
118
|
+
workspace_root: Path,
|
|
119
|
+
overlay_ref: OverlayRef,
|
|
120
|
+
overlay_source_kind: str,
|
|
121
|
+
overlay_source_value: str | None,
|
|
122
|
+
overlay_signer: str | None,
|
|
123
|
+
) -> None:
|
|
124
|
+
config = trust_config_path(workspace_root)
|
|
125
|
+
if not config.exists():
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
allowlist = load_workspace_allowlist(workspace_root)
|
|
129
|
+
try:
|
|
130
|
+
authorize_overlay_driver(
|
|
131
|
+
driver_name="overlay-deep",
|
|
132
|
+
overlay_ref=overlay_ref,
|
|
133
|
+
overlay_source_kind=overlay_source_kind,
|
|
134
|
+
overlay_source_value=overlay_source_value,
|
|
135
|
+
overlay_signer=overlay_signer,
|
|
136
|
+
allowlist=allowlist,
|
|
137
|
+
)
|
|
138
|
+
except OverlayTrustError as e:
|
|
139
|
+
raise OverlayActivationError(str(e), error_code=e.error_code) from e
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _read_overlay_file_list(overlay_store: Path, overlay_ref: OverlayRef) -> list[str]:
|
|
143
|
+
tag_oid = _git_output(overlay_store, "rev-parse", overlay_ref.ref_path)
|
|
144
|
+
structured_tree_oid = _git_output(overlay_store, "rev-parse", f"{tag_oid}^{{tree}}")
|
|
145
|
+
wt_line = _git_output(overlay_store, "ls-tree", structured_tree_oid, "working_tree_tree")
|
|
146
|
+
working_tree_oid = wt_line.split()[2]
|
|
147
|
+
ls_output = _git_output(overlay_store, "ls-tree", "-r", "--name-only", working_tree_oid)
|
|
148
|
+
if not ls_output:
|
|
149
|
+
return []
|
|
150
|
+
return sorted(ls_output.splitlines())
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _write_state(workspace_root: Path, overlay_ref: OverlayRef, managed_files: list[str]) -> None:
|
|
154
|
+
grip_dir = workspace_root / GRIP_DIR
|
|
155
|
+
grip_dir.mkdir(parents=True, exist_ok=True)
|
|
156
|
+
|
|
157
|
+
stack_file = grip_dir / STACK_FILE
|
|
158
|
+
stack = read_active_overlay_stack(workspace_root)
|
|
159
|
+
if overlay_ref.ref_path not in stack:
|
|
160
|
+
stack.append(overlay_ref.ref_path)
|
|
161
|
+
stack_file.write_text(json.dumps(stack))
|
|
162
|
+
|
|
163
|
+
managed_file = grip_dir / MANAGED_FILE
|
|
164
|
+
managed = _load_managed_files(workspace_root)
|
|
165
|
+
managed[overlay_ref.ref_path] = managed_files
|
|
166
|
+
managed_file.write_text(json.dumps(managed))
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _remove_managed_files_for(workspace_root: Path, overlay_ref: OverlayRef) -> None:
|
|
170
|
+
managed = _load_managed_files(workspace_root)
|
|
171
|
+
file_list = managed.get(overlay_ref.ref_path, [])
|
|
172
|
+
for rel_path in file_list:
|
|
173
|
+
target = workspace_root / rel_path
|
|
174
|
+
if target.exists():
|
|
175
|
+
target.unlink()
|
|
176
|
+
parent = target.parent
|
|
177
|
+
while parent != workspace_root and parent.exists() and not any(parent.iterdir()):
|
|
178
|
+
parent.rmdir()
|
|
179
|
+
parent = parent.parent
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _load_managed_files(workspace_root: Path) -> dict[str, list[str]]:
|
|
183
|
+
managed_file = workspace_root / GRIP_DIR / MANAGED_FILE
|
|
184
|
+
if not managed_file.exists():
|
|
185
|
+
return {}
|
|
186
|
+
return json.loads(managed_file.read_text())
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _git_output(git_dir: Path, *args: str) -> str:
|
|
190
|
+
result = subprocess.run(
|
|
191
|
+
["git", f"--git-dir={git_dir}", *args],
|
|
192
|
+
check=True,
|
|
193
|
+
capture_output=True,
|
|
194
|
+
text=True,
|
|
195
|
+
)
|
|
196
|
+
return result.stdout.strip()
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Agent manifest reader: YAML frontmatter from COMPOSE.md with base-wins precedence."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AgentManifestValidationError(Exception):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class AgentManifest:
|
|
19
|
+
description: str
|
|
20
|
+
language: str
|
|
21
|
+
build: str
|
|
22
|
+
test: str
|
|
23
|
+
lint: str | None = None
|
|
24
|
+
format: str | None = None
|
|
25
|
+
source_path: Path | None = None
|
|
26
|
+
source_kind: str = "overlay"
|
|
27
|
+
repo_name: str | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_FRONTMATTER_RE = re.compile(r"\A---\s*\n(.*?)\n---", re.DOTALL)
|
|
31
|
+
|
|
32
|
+
_STRING_FIELDS = ("description", "language", "build", "test", "lint", "format")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def read_effective_agent_manifest(
|
|
36
|
+
*,
|
|
37
|
+
repo_root: Path,
|
|
38
|
+
overlay_root: Path,
|
|
39
|
+
) -> AgentManifest:
|
|
40
|
+
base_compose = repo_root / "COMPOSE.md"
|
|
41
|
+
overlay_compose = overlay_root / "COMPOSE.md"
|
|
42
|
+
|
|
43
|
+
if base_compose.exists():
|
|
44
|
+
raw = _extract_agent_block(base_compose)
|
|
45
|
+
if raw is not None:
|
|
46
|
+
_validate(raw, base_compose)
|
|
47
|
+
return _build_manifest(raw, source_path=base_compose, source_kind="base")
|
|
48
|
+
|
|
49
|
+
if overlay_compose.exists():
|
|
50
|
+
raw = _extract_agent_block(overlay_compose)
|
|
51
|
+
if raw is not None:
|
|
52
|
+
_validate(raw, overlay_compose)
|
|
53
|
+
return _build_manifest(raw, source_path=overlay_compose, source_kind="overlay")
|
|
54
|
+
|
|
55
|
+
raise AgentManifestValidationError("No valid COMPOSE.md with agent frontmatter found")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def read_workspace_repo_agent_manifest(
|
|
59
|
+
*,
|
|
60
|
+
workspace_root: Path,
|
|
61
|
+
repo_name: str,
|
|
62
|
+
overlays_root: Path,
|
|
63
|
+
) -> AgentManifest:
|
|
64
|
+
repo_root = _resolve_repo_root(workspace_root, repo_name)
|
|
65
|
+
overlay_root = overlays_root / repo_name
|
|
66
|
+
|
|
67
|
+
manifest = read_effective_agent_manifest(
|
|
68
|
+
repo_root=repo_root,
|
|
69
|
+
overlay_root=overlay_root,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return AgentManifest(
|
|
73
|
+
description=manifest.description,
|
|
74
|
+
language=manifest.language,
|
|
75
|
+
build=manifest.build,
|
|
76
|
+
test=manifest.test,
|
|
77
|
+
lint=manifest.lint,
|
|
78
|
+
format=manifest.format,
|
|
79
|
+
source_path=manifest.source_path,
|
|
80
|
+
source_kind=manifest.source_kind,
|
|
81
|
+
repo_name=repo_name,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _resolve_repo_root(workspace_root: Path, repo_name: str) -> Path:
|
|
86
|
+
"""Resolve repo root from gripspace.yml manifest, falling back to reference/<name>."""
|
|
87
|
+
manifest_path = workspace_root / ".gitgrip" / "spaces" / "main" / "gripspace.yml"
|
|
88
|
+
if manifest_path.exists():
|
|
89
|
+
manifest = yaml.safe_load(manifest_path.read_text()) or {}
|
|
90
|
+
repos = manifest.get("repos", {}) or {}
|
|
91
|
+
repo_entry = repos.get(repo_name, {}) or {}
|
|
92
|
+
repo_path = str(repo_entry.get("path", "")).strip()
|
|
93
|
+
if repo_path:
|
|
94
|
+
normalized = repo_path[2:] if repo_path.startswith("./") else repo_path
|
|
95
|
+
resolved = workspace_root / normalized
|
|
96
|
+
if resolved.is_dir():
|
|
97
|
+
return resolved
|
|
98
|
+
return workspace_root / "reference" / repo_name
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _extract_agent_block(compose_path: Path) -> dict[str, Any] | None:
|
|
102
|
+
text = compose_path.read_text()
|
|
103
|
+
m = _FRONTMATTER_RE.match(text)
|
|
104
|
+
if m is None:
|
|
105
|
+
return None
|
|
106
|
+
frontmatter = yaml.safe_load(m.group(1))
|
|
107
|
+
if not isinstance(frontmatter, dict):
|
|
108
|
+
return None
|
|
109
|
+
return frontmatter.get("agent")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _validate(raw: Any, source: Path) -> None:
|
|
113
|
+
if not isinstance(raw, dict):
|
|
114
|
+
raise AgentManifestValidationError(f"agent block in {source} is not a mapping")
|
|
115
|
+
for field in _STRING_FIELDS:
|
|
116
|
+
value = raw.get(field)
|
|
117
|
+
if value is not None and not isinstance(value, str):
|
|
118
|
+
raise AgentManifestValidationError(
|
|
119
|
+
f"Field '{field}' in {source} must be a string, got {type(value).__name__}"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _build_manifest(
|
|
124
|
+
raw: dict[str, Any],
|
|
125
|
+
*,
|
|
126
|
+
source_path: Path,
|
|
127
|
+
source_kind: str,
|
|
128
|
+
) -> AgentManifest:
|
|
129
|
+
return AgentManifest(
|
|
130
|
+
description=raw.get("description", ""),
|
|
131
|
+
language=raw.get("language", ""),
|
|
132
|
+
build=raw.get("build", ""),
|
|
133
|
+
test=raw.get("test", ""),
|
|
134
|
+
lint=raw.get("lint"),
|
|
135
|
+
format=raw.get("format"),
|
|
136
|
+
source_path=source_path,
|
|
137
|
+
source_kind=source_kind,
|
|
138
|
+
)
|
gr2/overlay/cli.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""CLI plumbing for `gr overlay` subcommands.
|
|
2
|
+
|
|
3
|
+
Wires typer commands to gr2.overlay module functions.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
|
|
13
|
+
from gr2.overlay import introspection, workspace_spec
|
|
14
|
+
from gr2.overlay.activate import activate_overlay, deactivate_overlay
|
|
15
|
+
from gr2.overlay.types import OverlayRef
|
|
16
|
+
|
|
17
|
+
overlay_app = typer.Typer(
|
|
18
|
+
help="Config overlay capture, composition, and materialization (Tier A).",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _default_overlay_store(workspace_root: Path) -> Path:
|
|
23
|
+
return workspace_root / ".grip" / "overlay-store.git"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _emit(result: str | dict | list, json_output: bool) -> None:
|
|
27
|
+
if json_output:
|
|
28
|
+
if isinstance(result, str):
|
|
29
|
+
typer.echo(result)
|
|
30
|
+
else:
|
|
31
|
+
typer.echo(json.dumps(result, indent=2))
|
|
32
|
+
else:
|
|
33
|
+
typer.echo(result)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@overlay_app.command("activate")
|
|
37
|
+
def overlay_activate_cmd(
|
|
38
|
+
workspace_root: Path,
|
|
39
|
+
ref: str = typer.Argument(..., help="Overlay ref: <author>/<name>"),
|
|
40
|
+
source_kind: str = typer.Option("local", help="Overlay source kind (local, git, registry)"),
|
|
41
|
+
source_value: str | None = typer.Option(None, help="Overlay source value (path or URL)"),
|
|
42
|
+
signer: str | None = typer.Option(None, help="Expected overlay signer"),
|
|
43
|
+
json_output: bool = typer.Option(False, "--json", help="Emit machine-readable JSON"),
|
|
44
|
+
) -> None:
|
|
45
|
+
"""Activate an overlay, eagerly materializing its files into the workspace."""
|
|
46
|
+
overlay_ref = OverlayRef.parse(ref)
|
|
47
|
+
overlay_store = _default_overlay_store(workspace_root)
|
|
48
|
+
result = activate_overlay(
|
|
49
|
+
workspace_root=workspace_root,
|
|
50
|
+
overlay_store=overlay_store,
|
|
51
|
+
overlay_ref=overlay_ref,
|
|
52
|
+
overlay_source_kind=source_kind,
|
|
53
|
+
overlay_source_value=source_value,
|
|
54
|
+
overlay_signer=signer,
|
|
55
|
+
)
|
|
56
|
+
if json_output:
|
|
57
|
+
typer.echo(json.dumps({"status": result.status, "completed": result.completed}))
|
|
58
|
+
else:
|
|
59
|
+
typer.echo(f"Activated {ref}: {result.status}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@overlay_app.command("deactivate")
|
|
63
|
+
def overlay_deactivate_cmd(
|
|
64
|
+
workspace_root: Path,
|
|
65
|
+
ref: str = typer.Argument(..., help="Overlay ref: <author>/<name>"),
|
|
66
|
+
json_output: bool = typer.Option(False, "--json", help="Emit machine-readable JSON"),
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Deactivate an overlay and remove its materialized files."""
|
|
69
|
+
overlay_ref = OverlayRef.parse(ref)
|
|
70
|
+
result = deactivate_overlay(
|
|
71
|
+
workspace_root=workspace_root,
|
|
72
|
+
overlay_ref=overlay_ref,
|
|
73
|
+
)
|
|
74
|
+
if json_output:
|
|
75
|
+
typer.echo(json.dumps({"completed": result.completed}))
|
|
76
|
+
else:
|
|
77
|
+
typer.echo(f"Deactivated {ref}")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@overlay_app.command("diff")
|
|
81
|
+
def overlay_diff_cmd(
|
|
82
|
+
workspace_root: Path,
|
|
83
|
+
ref: str = typer.Argument(..., help="Overlay ref: <author>/<name>"),
|
|
84
|
+
json_output: bool = typer.Option(False, "--json", help="Emit machine-readable JSON"),
|
|
85
|
+
) -> None:
|
|
86
|
+
"""Show the diff an overlay would produce if activated."""
|
|
87
|
+
overlay_ref = OverlayRef.parse(ref)
|
|
88
|
+
overlay_store = _default_overlay_store(workspace_root)
|
|
89
|
+
file_list = introspection._read_overlay_file_list(overlay_store, overlay_ref)
|
|
90
|
+
if json_output:
|
|
91
|
+
typer.echo(json.dumps({"ref": ref, "files": file_list}))
|
|
92
|
+
else:
|
|
93
|
+
typer.echo(f"Files that would change for {ref}:")
|
|
94
|
+
for f in file_list:
|
|
95
|
+
typer.echo(f" {f}")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@overlay_app.command("list")
|
|
99
|
+
def overlay_list_cmd(
|
|
100
|
+
workspace_root: Path,
|
|
101
|
+
json_output: bool = typer.Option(False, "--json", help="Emit machine-readable JSON"),
|
|
102
|
+
) -> None:
|
|
103
|
+
"""List all declared overlays from workspace spec."""
|
|
104
|
+
entries = workspace_spec.load_overlay_spec(workspace_root)
|
|
105
|
+
if json_output:
|
|
106
|
+
overlay_data = [
|
|
107
|
+
{
|
|
108
|
+
"name": e.name,
|
|
109
|
+
"path": e.path,
|
|
110
|
+
"applies_to": e.applies_to,
|
|
111
|
+
"priority": e.priority,
|
|
112
|
+
}
|
|
113
|
+
for e in entries
|
|
114
|
+
]
|
|
115
|
+
typer.echo(json.dumps({"overlays": overlay_data}, indent=2))
|
|
116
|
+
else:
|
|
117
|
+
if not entries:
|
|
118
|
+
typer.echo("No overlays declared.")
|
|
119
|
+
else:
|
|
120
|
+
typer.echo("Declared overlays:")
|
|
121
|
+
for e in entries:
|
|
122
|
+
typer.echo(f" {e.name} (priority={e.priority}, path={e.path})")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@overlay_app.command("stack")
|
|
126
|
+
def overlay_stack_cmd(
|
|
127
|
+
workspace_root: Path,
|
|
128
|
+
json_output: bool = typer.Option(False, "--json", help="Emit machine-readable JSON"),
|
|
129
|
+
) -> None:
|
|
130
|
+
"""Show the current overlay activation stack with priority ordering."""
|
|
131
|
+
overlay_store = _default_overlay_store(workspace_root)
|
|
132
|
+
result = introspection.overlay_stack(workspace_root, overlay_store, json_output)
|
|
133
|
+
_emit(result, json_output)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@overlay_app.command("status")
|
|
137
|
+
def overlay_status_cmd(
|
|
138
|
+
workspace_root: Path,
|
|
139
|
+
json_output: bool = typer.Option(False, "--json", help="Emit machine-readable JSON"),
|
|
140
|
+
) -> None:
|
|
141
|
+
"""Show overlay materialization status: which overlays are active, stale, or conflicting."""
|
|
142
|
+
overlay_store = _default_overlay_store(workspace_root)
|
|
143
|
+
result = introspection.overlay_status(workspace_root, overlay_store, json_output)
|
|
144
|
+
_emit(result, json_output)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@overlay_app.command("trace")
|
|
148
|
+
def overlay_trace_cmd(
|
|
149
|
+
workspace_root: Path,
|
|
150
|
+
file_path: str = typer.Argument(..., help="File path to trace overlay provenance for"),
|
|
151
|
+
json_output: bool = typer.Option(False, "--json", help="Emit machine-readable JSON"),
|
|
152
|
+
) -> None:
|
|
153
|
+
"""Trace which overlay(s) contributed to a specific file's current state."""
|
|
154
|
+
overlay_store = _default_overlay_store(workspace_root)
|
|
155
|
+
result = introspection.overlay_trace(workspace_root, overlay_store, file_path, json_output)
|
|
156
|
+
_emit(result, json_output)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@overlay_app.command("why")
|
|
160
|
+
def overlay_why_cmd(
|
|
161
|
+
workspace_root: Path,
|
|
162
|
+
key: str = typer.Argument(..., help="Config key or file path to explain"),
|
|
163
|
+
json_output: bool = typer.Option(False, "--json", help="Emit machine-readable JSON"),
|
|
164
|
+
) -> None:
|
|
165
|
+
"""Explain why a config key has its current value (which overlay, base, or merge)."""
|
|
166
|
+
overlay_store = _default_overlay_store(workspace_root)
|
|
167
|
+
result = introspection.overlay_why(workspace_root, overlay_store, key, json_output)
|
|
168
|
+
_emit(result, json_output)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@overlay_app.command("impact")
|
|
172
|
+
def overlay_impact_cmd(
|
|
173
|
+
workspace_root: Path,
|
|
174
|
+
ref: str = typer.Argument(..., help="Overlay ref: <author>/<name>"),
|
|
175
|
+
json_output: bool = typer.Option(False, "--json", help="Emit machine-readable JSON"),
|
|
176
|
+
) -> None:
|
|
177
|
+
"""Show what files and keys would change if this overlay were activated or deactivated."""
|
|
178
|
+
overlay_ref = OverlayRef.parse(ref)
|
|
179
|
+
overlay_store = _default_overlay_store(workspace_root)
|
|
180
|
+
result = introspection.overlay_impact(overlay_store, overlay_ref, json_output)
|
|
181
|
+
_emit(result, json_output)
|