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,124 @@
1
+ """Cross-repo atomic overlay activation with rollback."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ from dataclasses import dataclass, field
7
+ from pathlib import Path
8
+
9
+ from gr2.overlay.activate import (
10
+ OverlayActivationError,
11
+ activate_overlay,
12
+ deactivate_overlay,
13
+ )
14
+ from gr2.overlay.types import OverlayRef
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class RepoOverlayTarget:
19
+ repo_name: str
20
+ checkout_root: Path
21
+ overlay_store: Path
22
+ overlay_ref: OverlayRef
23
+ overlay_source_kind: str
24
+ overlay_source_value: str | None
25
+ overlay_signer: str | None
26
+
27
+
28
+ @dataclass
29
+ class CrossRepoActivationResult:
30
+ status: str
31
+ completed_repos: list[str] = field(default_factory=list)
32
+ rolled_back_repos: list[str] = field(default_factory=list)
33
+
34
+
35
+ class CrossRepoActivationError(Exception):
36
+ def __init__(
37
+ self,
38
+ message: str,
39
+ *,
40
+ error_code: str,
41
+ failing_repo: str,
42
+ rolled_back_repos: list[str],
43
+ ) -> None:
44
+ super().__init__(message)
45
+ self.error_code = error_code
46
+ self.failing_repo = failing_repo
47
+ self.rolled_back_repos = rolled_back_repos
48
+
49
+
50
+ def activate_overlays_atomically(
51
+ targets: list[RepoOverlayTarget],
52
+ ) -> CrossRepoActivationResult:
53
+ snapshots: dict[str, dict[str, str]] = {}
54
+ applied: list[RepoOverlayTarget] = []
55
+
56
+ for target in targets:
57
+ snapshots[target.repo_name] = _snapshot(target.checkout_root)
58
+
59
+ try:
60
+ for target in targets:
61
+ activate_overlay(
62
+ workspace_root=target.checkout_root,
63
+ overlay_store=target.overlay_store,
64
+ overlay_ref=target.overlay_ref,
65
+ overlay_source_kind=target.overlay_source_kind,
66
+ overlay_source_value=target.overlay_source_value,
67
+ overlay_signer=target.overlay_signer,
68
+ )
69
+ applied.append(target)
70
+ except OverlayActivationError as e:
71
+ failing_target = target
72
+ rolled_back: list[str] = []
73
+
74
+ for prev in reversed(applied):
75
+ _restore_snapshot(prev.checkout_root, snapshots[prev.repo_name])
76
+ rolled_back.append(prev.repo_name)
77
+ rolled_back.reverse()
78
+
79
+ if _snapshot(failing_target.checkout_root) != snapshots[failing_target.repo_name]:
80
+ _restore_snapshot(failing_target.checkout_root, snapshots[failing_target.repo_name])
81
+ rolled_back.append(failing_target.repo_name)
82
+
83
+ raise CrossRepoActivationError(
84
+ str(e),
85
+ error_code=e.error_code,
86
+ failing_repo=failing_target.repo_name,
87
+ rolled_back_repos=rolled_back,
88
+ ) from e
89
+
90
+ return CrossRepoActivationResult(
91
+ status="ok",
92
+ completed_repos=[t.repo_name for t in applied],
93
+ )
94
+
95
+
96
+ def _snapshot(root: Path) -> dict[str, bytes]:
97
+ result: dict[str, bytes] = {}
98
+ for path in sorted(root.rglob("*")):
99
+ if path.is_file():
100
+ result[str(path.relative_to(root))] = path.read_bytes()
101
+ return result
102
+
103
+
104
+ def _restore_snapshot(root: Path, snapshot: dict[str, bytes | str]) -> None:
105
+ current_files = set()
106
+ for path in root.rglob("*"):
107
+ if path.is_file():
108
+ current_files.add(str(path.relative_to(root)))
109
+
110
+ for rel_path in current_files - set(snapshot.keys()):
111
+ target = root / rel_path
112
+ target.unlink()
113
+
114
+ for rel_path, content in snapshot.items():
115
+ target = root / rel_path
116
+ target.parent.mkdir(parents=True, exist_ok=True)
117
+ if isinstance(content, str):
118
+ target.write_text(content)
119
+ else:
120
+ target.write_bytes(content)
121
+
122
+ for dirpath in sorted(root.rglob("*"), reverse=True):
123
+ if dirpath.is_dir() and not any(dirpath.iterdir()):
124
+ dirpath.rmdir()
gr2/overlay/drivers.py ADDED
@@ -0,0 +1,113 @@
1
+ """Curated overlay merge drivers: deep, prepend, union."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import tomllib
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ import tomli_w
12
+ import yaml
13
+
14
+ from gr2.overlay.types import OverlayRef
15
+
16
+ CURATED_DRIVERS: dict[str, str] = {
17
+ "overlay-deep": "deep",
18
+ "overlay-prepend": "prepend",
19
+ "overlay-union": "union",
20
+ }
21
+
22
+
23
+ def install_driver_registry() -> None:
24
+ home = Path(os.environ["HOME"])
25
+ gitconfig = home / ".gitconfig"
26
+
27
+ existing = gitconfig.read_text() if gitconfig.exists() else ""
28
+
29
+ sections: list[str] = []
30
+ for driver_name in CURATED_DRIVERS:
31
+ header = f'[merge "{driver_name}"]'
32
+ if header not in existing:
33
+ sections.append(
34
+ f"{header}\n"
35
+ f"\tname = {driver_name}\n"
36
+ f"\tdriver = gr2-overlay-driver {driver_name} %O %A %B %P\n"
37
+ )
38
+
39
+ if sections:
40
+ with gitconfig.open("a") as f:
41
+ f.write("\n".join(sections) + "\n")
42
+
43
+
44
+ def invoke_driver(
45
+ driver_name: str,
46
+ ancestor: Path,
47
+ current: Path,
48
+ other: Path,
49
+ relative_path: str,
50
+ *,
51
+ source_overlay: OverlayRef,
52
+ trusted_overlay_sources: set[str],
53
+ ) -> None:
54
+ if driver_name not in CURATED_DRIVERS:
55
+ raise ValueError(f"Unknown overlay driver: {driver_name}")
56
+
57
+ if source_overlay.ref_path not in trusted_overlay_sources:
58
+ raise PermissionError(f"Overlay source {source_overlay.ref_path} is not in the allowlist")
59
+
60
+ handlers = {
61
+ "overlay-deep": _driver_deep,
62
+ "overlay-prepend": _driver_prepend,
63
+ "overlay-union": _driver_union,
64
+ }
65
+ handlers[driver_name](ancestor, current, other, relative_path)
66
+
67
+
68
+ def _driver_deep(ancestor: Path, current: Path, other: Path, relative_path: str) -> None:
69
+ suffix = Path(relative_path).suffix
70
+
71
+ if suffix == ".toml":
72
+ current_data = tomllib.loads(current.read_text())
73
+ other_data = tomllib.loads(other.read_text())
74
+ merged = _deep_merge(current_data, other_data)
75
+ current.write_bytes(tomli_w.dumps(merged).encode())
76
+ elif suffix in {".yml", ".yaml"}:
77
+ current_data = yaml.safe_load(current.read_text()) or {}
78
+ other_data = yaml.safe_load(other.read_text()) or {}
79
+ merged = _deep_merge(current_data, other_data)
80
+ current.write_text(yaml.dump(merged, default_flow_style=False))
81
+ elif suffix == ".json":
82
+ current_data = json.loads(current.read_text())
83
+ other_data = json.loads(other.read_text())
84
+ merged = _deep_merge(current_data, other_data)
85
+ current.write_text(json.dumps(merged, indent=2) + "\n")
86
+
87
+
88
+ def _deep_merge(base: dict[str, Any], overlay: dict[str, Any]) -> dict[str, Any]:
89
+ result = dict(base)
90
+ for key, value in overlay.items():
91
+ if key in result and isinstance(result[key], dict) and isinstance(value, dict):
92
+ result[key] = _deep_merge(result[key], value)
93
+ else:
94
+ result[key] = value
95
+ return result
96
+
97
+
98
+ def _driver_prepend(ancestor: Path, current: Path, other: Path, relative_path: str) -> None:
99
+ current.write_text(other.read_text() + current.read_text())
100
+
101
+
102
+ def _driver_union(ancestor: Path, current: Path, other: Path, relative_path: str) -> None:
103
+ current_lines = current.read_text().splitlines()
104
+ other_lines = other.read_text().splitlines()
105
+
106
+ seen = set(current_lines)
107
+ result = list(current_lines)
108
+ for line in other_lines:
109
+ if line not in seen:
110
+ result.append(line)
111
+ seen.add(line)
112
+
113
+ current.write_text("\n".join(result) + "\n" if result else "")
@@ -0,0 +1,155 @@
1
+ """Overlay introspection: stack, trace, why, impact, status queries."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ import tomllib
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from gr2.overlay.types import OverlayRef
11
+
12
+ GRIP_DIR = ".grip"
13
+
14
+
15
+ def overlay_stack(
16
+ workspace_root: Path,
17
+ overlay_store: Path,
18
+ json_output: bool,
19
+ ) -> str | dict[str, Any]:
20
+ stack_file = workspace_root / GRIP_DIR / "overlay-stack.toml"
21
+ data = _load_toml(stack_file)
22
+
23
+ active_refs = data.get("active", [])
24
+ available_refs = data.get("available", [])
25
+
26
+ active_entries = [_ref_entry(r) for r in active_refs]
27
+ available_entries = [_ref_entry(r) for r in available_refs]
28
+
29
+ if json_output:
30
+ return {"active": active_entries, "available": available_entries}
31
+
32
+ lines = ["Active overlays:"]
33
+ for entry in active_entries:
34
+ lines.append(f" {entry['author']}/{entry['name']} ({entry['ref']})")
35
+ lines.append("Available overlays:")
36
+ for entry in available_entries:
37
+ lines.append(f" {entry['author']}/{entry['name']} ({entry['ref']})")
38
+ return "\n".join(lines)
39
+
40
+
41
+ def overlay_trace(
42
+ workspace_root: Path,
43
+ overlay_store: Path,
44
+ file_path: str,
45
+ json_output: bool,
46
+ ) -> str | dict[str, Any]:
47
+ attr_file = workspace_root / GRIP_DIR / "overlay-attribution.toml"
48
+ data = _load_toml(attr_file)
49
+
50
+ file_data = data.get("files", {}).get(file_path, {})
51
+ regions = file_data.get("lines", [])
52
+
53
+ if json_output:
54
+ return {"file": file_path, "regions": regions}
55
+
56
+ lines = [f"Trace for {file_path}:"]
57
+ for region in regions:
58
+ lines.append(f" lines {region['start']}-{region['end']}: {region['ref']}")
59
+ return "\n".join(lines)
60
+
61
+
62
+ def overlay_why(
63
+ workspace_root: Path,
64
+ overlay_store: Path,
65
+ file_path: str,
66
+ json_output: bool,
67
+ ) -> str | dict[str, Any]:
68
+ why_file = workspace_root / GRIP_DIR / "overlay-why.toml"
69
+ data = _load_toml(why_file)
70
+
71
+ file_data = data.get("files", {}).get(file_path, {})
72
+ rule = file_data.get("rule", "")
73
+ reason = file_data.get("reason", "")
74
+ ref = file_data.get("ref", "")
75
+
76
+ if json_output:
77
+ return {"rule": rule, "reason": reason, "ref": ref}
78
+
79
+ return f"{file_path}: rule={rule}, reason={reason} (ref={ref})"
80
+
81
+
82
+ def overlay_impact(
83
+ overlay_store: Path,
84
+ overlay_ref: OverlayRef,
85
+ json_output: bool,
86
+ ) -> str | dict[str, Any]:
87
+ files = _read_overlay_file_list(overlay_store, overlay_ref)
88
+
89
+ if json_output:
90
+ return {"files": files}
91
+
92
+ lines = [f"Files touched by {overlay_ref.author}/{overlay_ref.name}:"]
93
+ for f in files:
94
+ lines.append(f" {f}")
95
+ return "\n".join(lines)
96
+
97
+
98
+ def overlay_status(
99
+ workspace_root: Path,
100
+ overlay_store: Path,
101
+ json_output: bool,
102
+ ) -> str | dict[str, Any]:
103
+ status_file = workspace_root / GRIP_DIR / "overlay-status.toml"
104
+ data = _load_toml(status_file)
105
+
106
+ active = data.get("active", [])
107
+ available = data.get("available", [])
108
+ applied = data.get("applied", [])
109
+
110
+ if json_output:
111
+ return {"active": active, "available": available, "applied": applied}
112
+
113
+ lines = [
114
+ "Active: " + ", ".join(active) if active else "Active: (none)",
115
+ "Available: " + ", ".join(available) if available else "Available: (none)",
116
+ "Applied: " + ", ".join(applied) if applied else "Applied: (none)",
117
+ ]
118
+ return "\n".join(lines)
119
+
120
+
121
+ def _ref_entry(ref_path: str) -> dict[str, str]:
122
+ parts = ref_path.replace("refs/overlays/", "").split("/", 1)
123
+ author = parts[0] if parts else ""
124
+ name = parts[1] if len(parts) > 1 else ""
125
+ return {"ref": ref_path, "author": author, "name": name}
126
+
127
+
128
+ def _load_toml(path: Path) -> dict[str, Any]:
129
+ if not path.exists():
130
+ return {}
131
+ text = path.read_text()
132
+ if not text.strip():
133
+ return {}
134
+ return tomllib.loads(text)
135
+
136
+
137
+ def _read_overlay_file_list(overlay_store: Path, overlay_ref: OverlayRef) -> list[str]:
138
+ tag_oid = _git_output(overlay_store, "rev-parse", overlay_ref.ref_path)
139
+ structured_tree_oid = _git_output(overlay_store, "rev-parse", f"{tag_oid}^{{tree}}")
140
+ wt_line = _git_output(overlay_store, "ls-tree", structured_tree_oid, "working_tree_tree")
141
+ working_tree_oid = wt_line.split()[2]
142
+ ls_output = _git_output(overlay_store, "ls-tree", "-r", "--name-only", working_tree_oid)
143
+ if not ls_output:
144
+ return []
145
+ return sorted(ls_output.splitlines())
146
+
147
+
148
+ def _git_output(git_dir: Path, *args: str) -> str:
149
+ result = subprocess.run(
150
+ ["git", f"--git-dir={git_dir}", *args],
151
+ check=True,
152
+ capture_output=True,
153
+ text=True,
154
+ )
155
+ return result.stdout.strip()
@@ -0,0 +1,115 @@
1
+ """AST-based language-aware merge drivers for overlay composition."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+ from pathlib import Path
7
+
8
+
9
+ class PythonCompositionConflict(Exception):
10
+ def __init__(self, symbol: str) -> None:
11
+ super().__init__(
12
+ f"Composition conflict: both base and overlay modify '{symbol}'"
13
+ )
14
+ self.error_code = "composition_conflict"
15
+ self.symbol = symbol
16
+
17
+
18
+ def merge_python_overlay(
19
+ *,
20
+ ancestor: Path,
21
+ current: Path,
22
+ other: Path,
23
+ relative_path: str,
24
+ ) -> None:
25
+ if not relative_path.endswith(".py"):
26
+ raise ValueError("Python driver only supports .py paths")
27
+
28
+ ancestor_tree = ast.parse(ancestor.read_text())
29
+ current_tree = ast.parse(current.read_text())
30
+ other_tree = ast.parse(other.read_text())
31
+
32
+ ancestor_imports, ancestor_defs = _split_nodes(ancestor_tree)
33
+ current_imports, current_defs = _split_nodes(current_tree)
34
+ other_imports, other_defs = _split_nodes(other_tree)
35
+
36
+ merged_imports = _union_imports(current_imports, other_imports)
37
+ merged_defs = _merge_definitions(ancestor_defs, current_defs, other_defs)
38
+
39
+ merged_module = ast.Module(body=merged_imports + merged_defs, type_ignores=[])
40
+ ast.fix_missing_locations(merged_module)
41
+ current.write_text(ast.unparse(merged_module) + "\n")
42
+
43
+
44
+ def _split_nodes(
45
+ tree: ast.Module,
46
+ ) -> tuple[list[ast.stmt], dict[str, ast.stmt]]:
47
+ imports: list[ast.stmt] = []
48
+ defs: dict[str, ast.stmt] = {}
49
+ for node in tree.body:
50
+ if isinstance(node, (ast.Import, ast.ImportFrom)):
51
+ imports.append(node)
52
+ else:
53
+ name = _node_name(node)
54
+ if name:
55
+ defs[name] = node
56
+ return imports, defs
57
+
58
+
59
+ def _node_name(node: ast.stmt) -> str | None:
60
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
61
+ return node.name
62
+ if isinstance(node, ast.Assign):
63
+ if len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
64
+ return node.targets[0].id
65
+ if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
66
+ return node.target.id
67
+ return None
68
+
69
+
70
+ def _union_imports(
71
+ current: list[ast.stmt], other: list[ast.stmt]
72
+ ) -> list[ast.stmt]:
73
+ seen: set[str] = set()
74
+ result: list[ast.stmt] = []
75
+ for node in current + other:
76
+ key = ast.dump(node)
77
+ if key not in seen:
78
+ seen.add(key)
79
+ result.append(node)
80
+ return result
81
+
82
+
83
+ def _merge_definitions(
84
+ ancestor: dict[str, ast.stmt],
85
+ current: dict[str, ast.stmt],
86
+ other: dict[str, ast.stmt],
87
+ ) -> list[ast.stmt]:
88
+ all_names = list(dict.fromkeys(list(current.keys()) + list(other.keys())))
89
+ result: list[ast.stmt] = []
90
+
91
+ for name in all_names:
92
+ a_node = ancestor.get(name)
93
+ c_node = current.get(name)
94
+ o_node = other.get(name)
95
+
96
+ a_dump = ast.dump(a_node) if a_node else None
97
+ c_dump = ast.dump(c_node) if c_node else None
98
+ o_dump = ast.dump(o_node) if o_node else None
99
+
100
+ current_changed = c_dump != a_dump
101
+ other_changed = o_dump != a_dump
102
+
103
+ if current_changed and other_changed and c_node and o_node:
104
+ if isinstance(o_node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
105
+ result.append(o_node)
106
+ else:
107
+ raise PythonCompositionConflict(name)
108
+ elif o_node and other_changed:
109
+ result.append(o_node)
110
+ elif c_node:
111
+ result.append(c_node)
112
+ elif o_node:
113
+ result.append(o_node)
114
+
115
+ return result