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,441 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ import shutil
6
+ import subprocess
7
+ from dataclasses import asdict, dataclass, field
8
+ from enum import StrEnum
9
+ from pathlib import Path
10
+ from typing import Protocol
11
+ from urllib.parse import urlsplit
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class PRRef:
16
+ repo: str
17
+ number: int | None = None
18
+ url: str | None = None
19
+ head_branch: str | None = None
20
+ base_branch: str | None = None
21
+ title: str | None = None
22
+
23
+ def as_dict(self) -> dict[str, object]:
24
+ return asdict(self)
25
+
26
+
27
+ class MergeMethod(StrEnum):
28
+ MERGE = "merge"
29
+ SQUASH = "squash"
30
+ REBASE = "rebase"
31
+
32
+ @property
33
+ def gh_flag(self) -> str:
34
+ return f"--{self.value}"
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class MergeReceipt:
39
+ """Host-observed evidence for one completed merge operation.
40
+
41
+ `requested` is caller intent. `observed` and `commit_sha` come from the
42
+ hosting platform's structured PR record after the merge command returns.
43
+ Keeping both identities prevents a response for a different PR from being
44
+ accepted merely because the command exited successfully.
45
+
46
+ `commit_sha=None` has one meaning: the host explicitly returned no merge
47
+ commit identity. It never means parent verification succeeded.
48
+ """
49
+
50
+ requested: PRRef
51
+ observed: PRRef
52
+ commit_sha: str | None
53
+ requested_method: MergeMethod
54
+
55
+ def as_dict(self) -> dict[str, object]:
56
+ return {
57
+ "requested": self.requested.as_dict(),
58
+ "observed": self.observed.as_dict(),
59
+ "commit_sha": self.commit_sha,
60
+ "requested_method": self.requested_method.value,
61
+ }
62
+
63
+
64
+ @dataclass(frozen=True)
65
+ class PRCheck:
66
+ name: str
67
+ status: str
68
+ conclusion: str | None = None
69
+ details_url: str | None = None
70
+
71
+ def as_dict(self) -> dict[str, object]:
72
+ return asdict(self)
73
+
74
+
75
+ @dataclass(frozen=True)
76
+ class PRStatus:
77
+ ref: PRRef
78
+ state: str
79
+ mergeable: str | None = None
80
+ checks: list[PRCheck] = field(default_factory=list)
81
+
82
+ def as_dict(self) -> dict[str, object]:
83
+ return {
84
+ "ref": self.ref.as_dict(),
85
+ "state": self.state,
86
+ "mergeable": self.mergeable,
87
+ "checks": [item.as_dict() for item in self.checks],
88
+ }
89
+
90
+
91
+ @dataclass(frozen=True)
92
+ class CreatePRRequest:
93
+ repo: str
94
+ title: str
95
+ body: str
96
+ head_branch: str
97
+ base_branch: str
98
+ draft: bool = False
99
+
100
+
101
+ class PlatformAdapter(Protocol):
102
+ """Protocol for platform-backed PR orchestration.
103
+
104
+ gr2 owns the orchestration UX. Adapters hide the hosting platform backend.
105
+ """
106
+
107
+ name: str
108
+
109
+ def create_pr(self, request: CreatePRRequest) -> PRRef: ...
110
+
111
+ def merge_pr(
112
+ self,
113
+ repo: str,
114
+ number: int,
115
+ *,
116
+ method: MergeMethod,
117
+ ) -> MergeReceipt: ...
118
+
119
+ def pr_status(self, repo: str, number: int) -> PRStatus: ...
120
+
121
+ def list_prs(self, repo: str, *, head_branch: str | None = None) -> list[PRRef]: ...
122
+
123
+ def pr_checks(self, repo: str, number: int) -> list[PRCheck]: ...
124
+
125
+
126
+ class AdapterError(RuntimeError):
127
+ pass
128
+
129
+
130
+ class MergeEvidenceError(AdapterError):
131
+ """The host acknowledged a merge, but its immutable receipt is unavailable.
132
+
133
+ This is deliberately distinct from a failed merge command. Retrying after
134
+ this error could merge or report the same operation twice.
135
+ """
136
+
137
+ operation_acknowledged = True
138
+
139
+ def __init__(
140
+ self,
141
+ *,
142
+ requested: PRRef,
143
+ requested_method: MergeMethod,
144
+ reason: str,
145
+ ) -> None:
146
+ self.requested = requested
147
+ self.requested_method = requested_method
148
+ self.reason = reason
149
+ super().__init__(
150
+ f"merge command succeeded for {requested.repo}#{requested.number}, "
151
+ f"but its immutable receipt is unavailable: {reason}"
152
+ )
153
+
154
+
155
+ def _run_json(command: list[str], *, cwd: Path | None = None) -> object:
156
+ try:
157
+ proc = subprocess.run(
158
+ command,
159
+ cwd=cwd,
160
+ capture_output=True,
161
+ text=True,
162
+ check=False,
163
+ )
164
+ except OSError as exc:
165
+ raise AdapterError(
166
+ f"could not launch command for structured host evidence: {command[0]!r}: {exc}"
167
+ ) from exc
168
+ if proc.returncode != 0:
169
+ detail = proc.stderr.strip() or proc.stdout.strip()
170
+ raise AdapterError(detail or f"command failed: {' '.join(command)}")
171
+ try:
172
+ return json.loads(proc.stdout)
173
+ except json.JSONDecodeError as exc:
174
+ raise AdapterError(f"command did not return valid json: {' '.join(command)}") from exc
175
+
176
+
177
+ class GitHubAdapter:
178
+ name = "github"
179
+
180
+ def __init__(self, gh_binary: str = "gh") -> None:
181
+ if shutil.which(gh_binary) is None:
182
+ raise AdapterError(f"`{gh_binary}` not found in PATH")
183
+ self.gh_binary = gh_binary
184
+
185
+ def create_pr(self, request: CreatePRRequest) -> PRRef:
186
+ cmd = [
187
+ self.gh_binary,
188
+ "pr",
189
+ "create",
190
+ "--repo",
191
+ request.repo,
192
+ "--title",
193
+ request.title,
194
+ "--body",
195
+ request.body,
196
+ "--head",
197
+ request.head_branch,
198
+ "--base",
199
+ request.base_branch,
200
+ ]
201
+ if request.draft:
202
+ cmd.append("--draft")
203
+ proc = subprocess.run(cmd, capture_output=True, text=True, check=False)
204
+ if proc.returncode != 0:
205
+ raise AdapterError(proc.stderr.strip() or proc.stdout.strip() or "gh pr create failed")
206
+ url = proc.stdout.strip()
207
+ return PRRef(
208
+ repo=request.repo,
209
+ url=url or None,
210
+ head_branch=request.head_branch,
211
+ base_branch=request.base_branch,
212
+ title=request.title,
213
+ )
214
+
215
+ def merge_pr(
216
+ self,
217
+ repo: str,
218
+ number: int,
219
+ *,
220
+ method: MergeMethod,
221
+ ) -> MergeReceipt:
222
+ proc = subprocess.run(
223
+ [
224
+ self.gh_binary,
225
+ "pr",
226
+ "merge",
227
+ str(number),
228
+ "--repo",
229
+ repo,
230
+ method.gh_flag,
231
+ ],
232
+ capture_output=True,
233
+ text=True,
234
+ check=False,
235
+ )
236
+ if proc.returncode != 0:
237
+ raise AdapterError(proc.stderr.strip() or proc.stdout.strip() or "gh pr merge failed")
238
+
239
+ # `gh pr merge` is silent on successful non-interactive merges. Read
240
+ # the immutable PR record rather than reconstructing a receipt from
241
+ # the command inputs or asking what commit the base points at now.
242
+ requested = PRRef(repo=repo, number=number)
243
+ try:
244
+ payload = _run_json(
245
+ [
246
+ self.gh_binary,
247
+ "pr",
248
+ "view",
249
+ str(number),
250
+ "--repo",
251
+ repo,
252
+ "--json",
253
+ "number,url,state,mergeCommit",
254
+ ]
255
+ )
256
+ return _parse_merge_receipt(
257
+ payload,
258
+ requested=requested,
259
+ requested_method=method,
260
+ )
261
+ except AdapterError as exc:
262
+ raise MergeEvidenceError(
263
+ requested=requested,
264
+ requested_method=method,
265
+ reason=str(exc),
266
+ ) from exc
267
+
268
+ def pr_status(self, repo: str, number: int) -> PRStatus:
269
+ payload = _run_json(
270
+ [
271
+ self.gh_binary,
272
+ "pr",
273
+ "view",
274
+ str(number),
275
+ "--repo",
276
+ repo,
277
+ "--json",
278
+ "number,url,headRefName,baseRefName,title,state,mergeable,statusCheckRollup",
279
+ ]
280
+ )
281
+ assert isinstance(payload, dict)
282
+ checks = self._parse_checks(payload.get("statusCheckRollup") or [])
283
+ ref = PRRef(
284
+ repo=repo,
285
+ number=payload.get("number"),
286
+ url=payload.get("url"),
287
+ head_branch=payload.get("headRefName"),
288
+ base_branch=payload.get("baseRefName"),
289
+ title=payload.get("title"),
290
+ )
291
+ return PRStatus(
292
+ ref=ref,
293
+ state=str(payload.get("state", "UNKNOWN")),
294
+ mergeable=(
295
+ str(payload.get("mergeable"))
296
+ if payload.get("mergeable") is not None
297
+ else None
298
+ ),
299
+ checks=checks,
300
+ )
301
+
302
+ def list_prs(self, repo: str, *, head_branch: str | None = None) -> list[PRRef]:
303
+ payload = _run_json(
304
+ [
305
+ self.gh_binary,
306
+ "pr",
307
+ "list",
308
+ "--repo",
309
+ repo,
310
+ "--json",
311
+ "number,url,headRefName,baseRefName,title",
312
+ ]
313
+ )
314
+ assert isinstance(payload, list)
315
+ refs: list[PRRef] = []
316
+ for item in payload:
317
+ if not isinstance(item, dict):
318
+ continue
319
+ if head_branch and item.get("headRefName") != head_branch:
320
+ continue
321
+ refs.append(
322
+ PRRef(
323
+ repo=repo,
324
+ number=item.get("number"),
325
+ url=item.get("url"),
326
+ head_branch=item.get("headRefName"),
327
+ base_branch=item.get("baseRefName"),
328
+ title=item.get("title"),
329
+ )
330
+ )
331
+ return refs
332
+
333
+ def pr_checks(self, repo: str, number: int) -> list[PRCheck]:
334
+ return self.pr_status(repo, number).checks
335
+
336
+ @staticmethod
337
+ def _parse_checks(rows: list[object]) -> list[PRCheck]:
338
+ checks: list[PRCheck] = []
339
+ for row in rows:
340
+ if not isinstance(row, dict):
341
+ continue
342
+ checks.append(
343
+ PRCheck(
344
+ name=str(row.get("name", "unknown")),
345
+ status=str(row.get("status", "UNKNOWN")),
346
+ conclusion=(
347
+ str(row["conclusion"])
348
+ if row.get("conclusion") is not None
349
+ else None
350
+ ),
351
+ details_url=row.get("detailsUrl"),
352
+ )
353
+ )
354
+ return checks
355
+
356
+
357
+ def get_platform_adapter(name: str) -> PlatformAdapter:
358
+ normalized = name.strip().lower()
359
+ if normalized in {"github", "gh"}:
360
+ return GitHubAdapter()
361
+ raise AdapterError(f"unknown platform adapter: {name}")
362
+
363
+
364
+ _GIT_OBJECT_ID = re.compile(r"(?:[0-9a-f]{40}|[0-9a-f]{64})\Z")
365
+
366
+
367
+ def _parse_merge_receipt(
368
+ payload: object,
369
+ *,
370
+ requested: PRRef,
371
+ requested_method: MergeMethod,
372
+ ) -> MergeReceipt:
373
+ if not isinstance(payload, dict):
374
+ raise AdapterError("gh pr view did not return an object")
375
+ if payload.get("state") != "MERGED":
376
+ raise AdapterError(
377
+ "gh pr merge returned success but the structured PR record is not MERGED"
378
+ )
379
+
380
+ number = payload.get("number")
381
+ if isinstance(number, bool) or not isinstance(number, int):
382
+ raise AdapterError("merged PR record has no integer number")
383
+ url = payload.get("url")
384
+ if not isinstance(url, str) or not url:
385
+ raise AdapterError("merged PR record has no URL")
386
+
387
+ observed_repo, url_number = _repo_and_number_from_pr_url(url)
388
+ observed = PRRef(repo=observed_repo, number=number, url=url)
389
+ if number != requested.number or url_number != requested.number:
390
+ raise AdapterError(
391
+ f"merged PR record identifies #{number} at URL #{url_number}, "
392
+ f"expected #{requested.number}"
393
+ )
394
+ if observed.repo != requested.repo:
395
+ raise AdapterError(
396
+ f"merged PR record identifies repo {observed.repo!r}, "
397
+ f"expected {requested.repo!r}"
398
+ )
399
+
400
+ if "mergeCommit" not in payload:
401
+ raise AdapterError("merged PR record omitted mergeCommit")
402
+ merge_commit = payload["mergeCommit"]
403
+ commit_sha: str | None
404
+ if merge_commit is None:
405
+ commit_sha = None
406
+ elif isinstance(merge_commit, dict):
407
+ oid = merge_commit.get("oid")
408
+ if not isinstance(oid, str) or _GIT_OBJECT_ID.fullmatch(oid) is None:
409
+ raise AdapterError("merged PR record contains an invalid mergeCommit.oid")
410
+ commit_sha = oid
411
+ else:
412
+ raise AdapterError("merged PR record contains a malformed mergeCommit")
413
+
414
+ return MergeReceipt(
415
+ requested=requested,
416
+ observed=observed,
417
+ commit_sha=commit_sha,
418
+ requested_method=requested_method,
419
+ )
420
+
421
+
422
+ def _repo_and_number_from_pr_url(url: str) -> tuple[str, int]:
423
+ try:
424
+ parsed = urlsplit(url)
425
+ except ValueError as exc:
426
+ raise AdapterError(f"merged PR record contains an invalid PR URL: {url!r}") from exc
427
+ path = [part for part in parsed.path.split("/") if part]
428
+ if (
429
+ parsed.scheme != "https"
430
+ or parsed.netloc.lower() != "github.com"
431
+ or bool(parsed.query)
432
+ or bool(parsed.fragment)
433
+ or len(path) != 4
434
+ or path[2] != "pull"
435
+ ):
436
+ raise AdapterError(f"merged PR record contains an invalid PR URL: {url!r}")
437
+ try:
438
+ number = int(path[3])
439
+ except ValueError as exc:
440
+ raise AdapterError(f"merged PR record contains an invalid PR URL: {url!r}") from exc
441
+ return f"{path[0]}/{path[1]}", number