gitpro 0.3.1__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.
- gitpro/__init__.py +4 -0
- gitpro/__main__.py +5 -0
- gitpro/agents.py +1928 -0
- gitpro/approval_policy.py +151 -0
- gitpro/args.py +82 -0
- gitpro/audit.py +800 -0
- gitpro/change_contract.py +117 -0
- gitpro/changes.py +853 -0
- gitpro/cli.py +499 -0
- gitpro/clone.py +505 -0
- gitpro/completion.py +407 -0
- gitpro/config.py +517 -0
- gitpro/devcontainer.py +442 -0
- gitpro/discover.py +1579 -0
- gitpro/doctor.py +572 -0
- gitpro/envcmd.py +370 -0
- gitpro/example.toml +102 -0
- gitpro/files.py +139 -0
- gitpro/forge.py +1244 -0
- gitpro/guard.py +1121 -0
- gitpro/health.py +1241 -0
- gitpro/hot.py +38 -0
- gitpro/jsonfile.py +195 -0
- gitpro/jsonout.py +26 -0
- gitpro/keys.py +59 -0
- gitpro/lint.py +50 -0
- gitpro/lost.py +874 -0
- gitpro/mcp.py +1055 -0
- gitpro/operation_contract.py +178 -0
- gitpro/operation_io.py +256 -0
- gitpro/operation_reconcile.py +191 -0
- gitpro/operations.py +841 -0
- gitpro/pin.py +89 -0
- gitpro/plan.py +199 -0
- gitpro/policy.py +1808 -0
- gitpro/prompt.py +304 -0
- gitpro/render.py +474 -0
- gitpro/resolve.py +898 -0
- gitpro/scan.py +358 -0
- gitpro/scanroots.py +146 -0
- gitpro/setup.py +260 -0
- gitpro/trailers.py +218 -0
- gitpro/tui.py +178 -0
- gitpro/undo.py +63 -0
- gitpro/view.py +155 -0
- gitpro-0.3.1.dist-info/METADATA +103 -0
- gitpro-0.3.1.dist-info/RECORD +50 -0
- gitpro-0.3.1.dist-info/WHEEL +4 -0
- gitpro-0.3.1.dist-info/entry_points.txt +2 -0
- gitpro-0.3.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Versioned, bounded persistent-change requests. Approval is deliberately not a request field."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from . import config
|
|
10
|
+
from .approval_policy import AUTHORITY_SCHEMA
|
|
11
|
+
from .operation_contract import MAX_REQUEST, OperationError
|
|
12
|
+
|
|
13
|
+
KINDS = {
|
|
14
|
+
"configure": ("config_path", "toml"),
|
|
15
|
+
"regenerate": ("config_path",),
|
|
16
|
+
"repo-identity": ("config_path", "repo"),
|
|
17
|
+
"undo": ("change_id",),
|
|
18
|
+
}
|
|
19
|
+
REQUIRED = {"configure": ("toml",), "regenerate": (), "repo-identity": ("repo",), "undo": ("change_id",)}
|
|
20
|
+
STRING = {"type": "string", "minLength": 1, "maxLength": MAX_REQUEST}
|
|
21
|
+
REQUEST_SCHEMA = {
|
|
22
|
+
"type": "object", "additionalProperties": False, "required": ["version", "kind"],
|
|
23
|
+
"properties": {
|
|
24
|
+
"version": {"type": "integer", "enum": [1]}, "kind": {"type": "string", "enum": list(KINDS)},
|
|
25
|
+
"config_path": {**STRING, "description": "Absolute gitpro TOML path inside your home directory."},
|
|
26
|
+
"repo": {**STRING, "description": "Absolute worktree root; repo-identity only."},
|
|
27
|
+
"toml": {**STRING, "description": "Complete proposed configuration; values must be explicitly supplied."},
|
|
28
|
+
"change_id": {**STRING, "pattern": "^[0-9a-f]{64}$"},
|
|
29
|
+
},
|
|
30
|
+
"oneOf": [
|
|
31
|
+
{"properties": {"kind": {"const": kind}}, "required": list(REQUIRED[kind]),
|
|
32
|
+
"not": {"anyOf": [{"required": [k]} for k in
|
|
33
|
+
sorted(set(sum(KINDS.values(), ())) - set(fields))]}}
|
|
34
|
+
for kind, fields in KINDS.items()
|
|
35
|
+
],
|
|
36
|
+
}
|
|
37
|
+
RESULT_SCHEMA = {
|
|
38
|
+
"type": "object", "additionalProperties": False,
|
|
39
|
+
"required": ["version", "command", "ok", "decision", "problems"],
|
|
40
|
+
"properties": {
|
|
41
|
+
"version": {"const": 1}, "command": {"enum": ["change propose", "change approve", "change apply",
|
|
42
|
+
"change status", "change recover"]},
|
|
43
|
+
"ok": {"type": "boolean"}, "decision": {"enum": ["ready", "blocked", "needs_input",
|
|
44
|
+
"needs_approval", "indeterminate"]},
|
|
45
|
+
"problems": {"type": "array", "items": {
|
|
46
|
+
"type": "object", "required": ["code", "message", "decision"]}},
|
|
47
|
+
"plan_id": {"type": "string", "pattern": "^[0-9a-f]{64}$"},
|
|
48
|
+
"status": {"enum": ["proposed", "no_changes", "approved", "applying", "applied", "rolled_back",
|
|
49
|
+
"indeterminate", "rejected", "stale"]},
|
|
50
|
+
"request": REQUEST_SCHEMA,
|
|
51
|
+
"changes": {"type": "array", "items": {
|
|
52
|
+
"type": "object", "required": ["path", "action", "before_sha256", "after_sha256", "diff"],
|
|
53
|
+
"properties": {"path": STRING, "action": {"enum": ["create", "update", "chmod", "remove"]},
|
|
54
|
+
"before_sha256": {"type": ["string", "null"]},
|
|
55
|
+
"after_sha256": {"type": ["string", "null"]},
|
|
56
|
+
"diff": {"type": "string"}, "before_mode": {"type": ["integer", "null"]},
|
|
57
|
+
"after_mode": {"type": ["integer", "null"]}}}},
|
|
58
|
+
"replayed": {"type": "boolean"}, "backup_store": {"type": "string"},
|
|
59
|
+
"files": {"type": "array", "items": {"type": "string"}},
|
|
60
|
+
"approval": {"type": "string"}, "verification": {"type": "string"},
|
|
61
|
+
"authority": AUTHORITY_SCHEMA,
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def plan_id(value: object) -> str:
|
|
67
|
+
if not isinstance(value, str) or re.fullmatch(r"[0-9a-f]{64}", value) is None:
|
|
68
|
+
raise OperationError("invalid_plan", "Supply the complete 64-character plan ID.", "needs_input")
|
|
69
|
+
return value
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def parse(raw: object) -> dict:
|
|
73
|
+
if not isinstance(raw, dict) or type(raw.get("version")) is not int or raw["version"] != 1:
|
|
74
|
+
raise OperationError("invalid_request", "Supply a version-1 change request.", "needs_input")
|
|
75
|
+
try:
|
|
76
|
+
size = len(json.dumps(raw, ensure_ascii=False).encode("utf-8"))
|
|
77
|
+
except (TypeError, ValueError, RecursionError):
|
|
78
|
+
raise OperationError("invalid_request", "Change requests must contain JSON values.", "needs_input") from None
|
|
79
|
+
if size > MAX_REQUEST:
|
|
80
|
+
raise OperationError("invalid_request", "Change requests are limited to 128 KiB.", "needs_input")
|
|
81
|
+
kind = raw.get("kind")
|
|
82
|
+
if not isinstance(kind, str) or kind not in KINDS:
|
|
83
|
+
raise OperationError("invalid_request", "Use configure, regenerate, repo-identity or undo.", "needs_input")
|
|
84
|
+
if set(raw) - {"version", "kind", *KINDS[kind]}:
|
|
85
|
+
raise OperationError("invalid_request", "Unknown or inapplicable change-request field.", "needs_input")
|
|
86
|
+
for key in REQUIRED[kind]:
|
|
87
|
+
if key not in raw:
|
|
88
|
+
raise OperationError("missing_input", f"Supply {key}; gitpro will not invent its value.", "needs_input")
|
|
89
|
+
out = dict(raw)
|
|
90
|
+
for key, value in raw.items():
|
|
91
|
+
if key in ("version", "kind"):
|
|
92
|
+
continue
|
|
93
|
+
if not isinstance(value, str) or not value.strip() or "\0" in value or len(value.encode()) > MAX_REQUEST:
|
|
94
|
+
raise OperationError("invalid_request", f"Invalid {key}.", "needs_input")
|
|
95
|
+
if kind == "undo":
|
|
96
|
+
out["change_id"] = plan_id(raw["change_id"])
|
|
97
|
+
return out
|
|
98
|
+
home = Path.home().resolve()
|
|
99
|
+
source = Path(raw.get("config_path") or os.environ.get(config.ENV_VAR) or home / config.USER_CONFIG)
|
|
100
|
+
if not source.is_absolute() or source.suffix != ".toml" or home not in source.parents:
|
|
101
|
+
raise OperationError("invalid_config_path", "Use an absolute .toml configuration path inside your home.",
|
|
102
|
+
"needs_input")
|
|
103
|
+
if ".." in source.parts or any(ord(c) < 32 or ord(c) == 127 for c in str(source)):
|
|
104
|
+
raise OperationError("invalid_config_path", "Use a normalized configuration path without control characters.",
|
|
105
|
+
"needs_input")
|
|
106
|
+
out["config_path"] = str(source)
|
|
107
|
+
if kind == "repo-identity":
|
|
108
|
+
repo = Path(raw["repo"])
|
|
109
|
+
if not repo.is_absolute() or any(ord(c) < 32 or ord(c) == 127 for c in str(repo)):
|
|
110
|
+
raise OperationError("invalid_repo", "Supply an absolute worktree root.", "needs_input")
|
|
111
|
+
out["repo"] = str(repo.resolve())
|
|
112
|
+
return out
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def error(command: str, exc: OperationError) -> dict:
|
|
116
|
+
return {"version": 1, "command": "change " + command, "ok": False, "decision": exc.decision,
|
|
117
|
+
"problems": [exc.problem()]}
|