gitpro 0.3.3__tar.gz → 0.4.0__tar.gz
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-0.3.3 → gitpro-0.4.0}/PKG-INFO +38 -23
- {gitpro-0.3.3 → gitpro-0.4.0}/README.md +37 -22
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/__init__.py +1 -1
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/agents.py +48 -4
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/args.py +48 -0
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/audit.py +17 -6
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/cli.py +248 -26
- gitpro-0.4.0/src/gitpro/commitcheck.py +113 -0
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/completion.py +2 -2
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/config.py +3 -3
- gitpro-0.4.0/src/gitpro/deprecation.py +36 -0
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/discover.py +40 -9
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/guard.py +44 -9
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/hot.py +2 -1
- gitpro-0.4.0/src/gitpro/incident.py +133 -0
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/jsonfile.py +13 -4
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/jsonout.py +3 -0
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/keys.py +3 -2
- gitpro-0.4.0/src/gitpro/learn.py +116 -0
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/mcp.py +118 -119
- gitpro-0.4.0/src/gitpro/pin.py +166 -0
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/plan.py +11 -6
- gitpro-0.4.0/src/gitpro/report.py +804 -0
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/resolve.py +149 -19
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/scan.py +117 -9
- gitpro-0.4.0/src/gitpro/setup.py +668 -0
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/view.py +22 -3
- gitpro-0.3.3/src/gitpro/pin.py +0 -89
- gitpro-0.3.3/src/gitpro/setup.py +0 -260
- {gitpro-0.3.3 → gitpro-0.4.0}/.gitignore +0 -0
- {gitpro-0.3.3 → gitpro-0.4.0}/LICENSE +0 -0
- {gitpro-0.3.3 → gitpro-0.4.0}/pyproject.toml +0 -0
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/__main__.py +0 -0
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/approval_policy.py +0 -0
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/change_contract.py +0 -0
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/changes.py +0 -0
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/clone.py +0 -0
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/devcontainer.py +0 -0
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/doctor.py +0 -0
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/envcmd.py +0 -0
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/example.toml +0 -0
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/files.py +0 -0
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/forge.py +0 -0
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/health.py +0 -0
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/lint.py +0 -0
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/lost.py +0 -0
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/operation_contract.py +0 -0
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/operation_io.py +0 -0
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/operation_reconcile.py +0 -0
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/operations.py +0 -0
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/policy.py +0 -0
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/prompt.py +0 -0
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/render.py +0 -0
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/scanroots.py +0 -0
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/trailers.py +0 -0
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/tui.py +0 -0
- {gitpro-0.3.3 → gitpro-0.4.0}/src/gitpro/undo.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: gitpro
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Per-remote git identities (name, email, SSH key, commit signing) from one TOML file
|
|
5
5
|
Project-URL: Homepage, https://gitpro.dev
|
|
6
6
|
Project-URL: Source, https://github.com/Yantrakar/gitpro
|
|
@@ -48,26 +48,38 @@ Install from [PyPI](https://pypi.org/project/gitpro/) with pipx.
|
|
|
48
48
|
|
|
49
49
|
```sh
|
|
50
50
|
pipx install gitpro
|
|
51
|
-
gitpro setup
|
|
51
|
+
gitpro setup
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
54
|
+
Setup offers to look through your repos and proposes your identities. Accept,
|
|
55
|
+
edit or answer the questions yourself; **no TOML editing is needed.** One
|
|
56
|
+
preview shows everything, including the commit guard and, with Claude Code, its
|
|
57
|
+
hook and MCP server. One yes applies it all. Setup never changes your repos.
|
|
58
|
+
|
|
59
|
+
```mermaid
|
|
60
|
+
flowchart LR
|
|
61
|
+
S["gitpro setup"] --> HC{"Config <br/>exists?"}
|
|
62
|
+
HC -->|no| L["Read your repos, <br/>SSH config and <br/>editor recents, <br/>if you agree"]
|
|
63
|
+
L --> Q["Propose <br/>identities; you <br/>accept or edit"]
|
|
64
|
+
Q --> I
|
|
65
|
+
HC -->|yes| I["Inspect config, <br/>Git and SSH files, <br/>keys, guard, <br/>agent hook, MCP"]
|
|
66
|
+
I -->|all current| OK["Nothing <br/>to do"]
|
|
67
|
+
I -->|something to do| PV["One preview, <br/>each part with <br/>its undo"]
|
|
68
|
+
PV --> ASK{"Apply <br/>all?"}
|
|
69
|
+
ASK -->|no| NONE["Nothing <br/>written"]
|
|
70
|
+
ASK -->|yes| AP["Write it, print <br/>the public keys <br/>to register"]
|
|
71
|
+
AP --> NX["Next: <br/>gitpro scan"]
|
|
61
72
|
```
|
|
62
73
|
|
|
63
|
-
Then
|
|
74
|
+
Then:
|
|
64
75
|
|
|
65
76
|
```sh
|
|
66
|
-
gitpro why #
|
|
77
|
+
gitpro why # inside a repository: the expected identity and anything overriding it
|
|
78
|
+
gitpro scan # repos that need a fix, with the command for each
|
|
67
79
|
```
|
|
68
80
|
|
|
69
|
-
Already have a config?
|
|
70
|
-
|
|
81
|
+
Already have a config? `gitpro setup` brings this machine up to date with it.
|
|
82
|
+
`gitpro setup -n` only shows what is left to do. Prefer editing files? The
|
|
71
83
|
[user guide](docs/guide.md#configuration) covers the full TOML format.
|
|
72
84
|
|
|
73
85
|
## What you get
|
|
@@ -79,20 +91,17 @@ your existing settings or comments. Prefer editing files? The
|
|
|
79
91
|
|
|
80
92
|
Gitpro generates ordinary Git and SSH configuration; it needs no background
|
|
81
93
|
service. Changes can be previewed, existing hand-written files are backed up,
|
|
82
|
-
and key
|
|
94
|
+
and key uploads are never part of setup.
|
|
83
95
|
|
|
84
96
|
## Using VS Code?
|
|
85
97
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
repository-level overrides. [Install the extension](vscode/README.md).
|
|
98
|
+
The extension shows the current identity and helps find repository-level
|
|
99
|
+
overrides. It runs the same commands as the terminal. [Install the extension](vscode/README.md).
|
|
89
100
|
|
|
90
101
|
## Go further when you need to
|
|
91
102
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
and GitHub PRs. Persistent changes use exact proposals, human approval by default,
|
|
95
|
-
optional operator-scoped preauthorization, and recoverable apply. MCP execution is opt-in.
|
|
103
|
+
AI agents get the same answers through the MCP server and the Claude Code hook:
|
|
104
|
+
see [agent workflows](docs/agent-workflows.md).
|
|
96
105
|
|
|
97
106
|
| I want to… | Start here |
|
|
98
107
|
|---|---|
|
|
@@ -103,8 +112,14 @@ optional operator-scoped preauthorization, and recoverable apply. MCP execution
|
|
|
103
112
|
| Look up a command or undo changes | [Command reference](docs/guide.md#commands) · [Undo](docs/guide.md#undo-everything) |
|
|
104
113
|
| Troubleshoot or contribute | [Troubleshooting](docs/guide.md#troubleshooting) · [Development](docs/guide.md#development) |
|
|
105
114
|
|
|
106
|
-
|
|
107
|
-
|
|
115
|
+
## Problems and questions
|
|
116
|
+
|
|
117
|
+
Run `gitpro report` to draft an issue with the versions and a redacted
|
|
118
|
+
`gitpro why`; it shows you the draft and opens nothing until you say yes.
|
|
119
|
+
Or open an issue at [github.com/Yantrakar/gitpro/issues](https://github.com/Yantrakar/gitpro/issues).
|
|
120
|
+
Report a security problem privately with `gitpro report --security`, as the
|
|
121
|
+
[security policy](https://github.com/Yantrakar/gitpro/security/policy) describes,
|
|
122
|
+
not in a public issue.
|
|
108
123
|
|
|
109
124
|
## License
|
|
110
125
|
|
|
@@ -22,26 +22,38 @@ Install from [PyPI](https://pypi.org/project/gitpro/) with pipx.
|
|
|
22
22
|
|
|
23
23
|
```sh
|
|
24
24
|
pipx install gitpro
|
|
25
|
-
gitpro setup
|
|
25
|
+
gitpro setup
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
Setup offers to look through your repos and proposes your identities. Accept,
|
|
29
|
+
edit or answer the questions yourself; **no TOML editing is needed.** One
|
|
30
|
+
preview shows everything, including the commit guard and, with Claude Code, its
|
|
31
|
+
hook and MCP server. One yes applies it all. Setup never changes your repos.
|
|
32
|
+
|
|
33
|
+
```mermaid
|
|
34
|
+
flowchart LR
|
|
35
|
+
S["gitpro setup"] --> HC{"Config <br/>exists?"}
|
|
36
|
+
HC -->|no| L["Read your repos, <br/>SSH config and <br/>editor recents, <br/>if you agree"]
|
|
37
|
+
L --> Q["Propose <br/>identities; you <br/>accept or edit"]
|
|
38
|
+
Q --> I
|
|
39
|
+
HC -->|yes| I["Inspect config, <br/>Git and SSH files, <br/>keys, guard, <br/>agent hook, MCP"]
|
|
40
|
+
I -->|all current| OK["Nothing <br/>to do"]
|
|
41
|
+
I -->|something to do| PV["One preview, <br/>each part with <br/>its undo"]
|
|
42
|
+
PV --> ASK{"Apply <br/>all?"}
|
|
43
|
+
ASK -->|no| NONE["Nothing <br/>written"]
|
|
44
|
+
ASK -->|yes| AP["Write it, print <br/>the public keys <br/>to register"]
|
|
45
|
+
AP --> NX["Next: <br/>gitpro scan"]
|
|
35
46
|
```
|
|
36
47
|
|
|
37
|
-
Then
|
|
48
|
+
Then:
|
|
38
49
|
|
|
39
50
|
```sh
|
|
40
|
-
gitpro why #
|
|
51
|
+
gitpro why # inside a repository: the expected identity and anything overriding it
|
|
52
|
+
gitpro scan # repos that need a fix, with the command for each
|
|
41
53
|
```
|
|
42
54
|
|
|
43
|
-
Already have a config?
|
|
44
|
-
|
|
55
|
+
Already have a config? `gitpro setup` brings this machine up to date with it.
|
|
56
|
+
`gitpro setup -n` only shows what is left to do. Prefer editing files? The
|
|
45
57
|
[user guide](docs/guide.md#configuration) covers the full TOML format.
|
|
46
58
|
|
|
47
59
|
## What you get
|
|
@@ -53,20 +65,17 @@ your existing settings or comments. Prefer editing files? The
|
|
|
53
65
|
|
|
54
66
|
Gitpro generates ordinary Git and SSH configuration; it needs no background
|
|
55
67
|
service. Changes can be previewed, existing hand-written files are backed up,
|
|
56
|
-
and key
|
|
68
|
+
and key uploads are never part of setup.
|
|
57
69
|
|
|
58
70
|
## Using VS Code?
|
|
59
71
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
repository-level overrides. [Install the extension](vscode/README.md).
|
|
72
|
+
The extension shows the current identity and helps find repository-level
|
|
73
|
+
overrides. It runs the same commands as the terminal. [Install the extension](vscode/README.md).
|
|
63
74
|
|
|
64
75
|
## Go further when you need to
|
|
65
76
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
and GitHub PRs. Persistent changes use exact proposals, human approval by default,
|
|
69
|
-
optional operator-scoped preauthorization, and recoverable apply. MCP execution is opt-in.
|
|
77
|
+
AI agents get the same answers through the MCP server and the Claude Code hook:
|
|
78
|
+
see [agent workflows](docs/agent-workflows.md).
|
|
70
79
|
|
|
71
80
|
| I want to… | Start here |
|
|
72
81
|
|---|---|
|
|
@@ -77,8 +86,14 @@ optional operator-scoped preauthorization, and recoverable apply. MCP execution
|
|
|
77
86
|
| Look up a command or undo changes | [Command reference](docs/guide.md#commands) · [Undo](docs/guide.md#undo-everything) |
|
|
78
87
|
| Troubleshoot or contribute | [Troubleshooting](docs/guide.md#troubleshooting) · [Development](docs/guide.md#development) |
|
|
79
88
|
|
|
80
|
-
|
|
81
|
-
|
|
89
|
+
## Problems and questions
|
|
90
|
+
|
|
91
|
+
Run `gitpro report` to draft an issue with the versions and a redacted
|
|
92
|
+
`gitpro why`; it shows you the draft and opens nothing until you say yes.
|
|
93
|
+
Or open an issue at [github.com/Yantrakar/gitpro/issues](https://github.com/Yantrakar/gitpro/issues).
|
|
94
|
+
Report a security problem privately with `gitpro report --security`, as the
|
|
95
|
+
[security policy](https://github.com/Yantrakar/gitpro/security/policy) describes,
|
|
96
|
+
not in a public issue.
|
|
82
97
|
|
|
83
98
|
## License
|
|
84
99
|
|
|
@@ -34,7 +34,7 @@ MAX_SCAN = 1024 * 1024 # the regex scan reads at most this much
|
|
|
34
34
|
MAX_FOR = 20 # `for x in w1 .. wN` with more words: the body is judged once, x unknown
|
|
35
35
|
MAX_VALUE = 4096 # a longer expanded $VAR value counts as unknown
|
|
36
36
|
ALIAS_TIMEOUT = 3 # seconds for the `git config --get alias.X` lookup on the hook path
|
|
37
|
-
TRIGGERS = ("override", "pin_stale", "no_identity", "env_override")
|
|
37
|
+
TRIGGERS = ("ambiguous", "override", "pin_stale", "no_identity", "env_override")
|
|
38
38
|
SKIP_ENV = "GITPRO_SKIP"
|
|
39
39
|
BYPASS_HELP = (f"{SKIP_ENV}=1 in the environment the agent starts with turns the check off for that session; "
|
|
40
40
|
"[agents] hook = \"warn\" or \"off\" in the config changes it for good. The deny message never "
|
|
@@ -1113,6 +1113,9 @@ class Decision:
|
|
|
1113
1113
|
cause: str
|
|
1114
1114
|
fix: str
|
|
1115
1115
|
repo: str
|
|
1116
|
+
why: str = "" # the denial record's reason code (not `reason`: that is the text below)
|
|
1117
|
+
codes: Tuple[str, ...] = () # problem codes of the repo's report
|
|
1118
|
+
action: str = "" # the git subcommand or program
|
|
1116
1119
|
|
|
1117
1120
|
@property
|
|
1118
1121
|
def reason(self) -> str:
|
|
@@ -1312,10 +1315,15 @@ def _judge_one(c: Call, rep, cfg, own: set, pushed_to: Optional[str] = None) ->
|
|
|
1312
1315
|
top = str(rep.repo)
|
|
1313
1316
|
what = _what(c)
|
|
1314
1317
|
commits = c.prog == "git" and c.sub != "push"
|
|
1318
|
+
codes = tuple(p.code for p in rep.problems)
|
|
1319
|
+
action = c.sub if c.prog == "git" else c.prog
|
|
1320
|
+
why = ("no_identity" if exp.email is None else "committer" if c_bad else "author" if a_bad else "problem")
|
|
1315
1321
|
if exp.email is None:
|
|
1316
1322
|
from .resolve import redact_url
|
|
1317
1323
|
url = redact_url(next(iter(rep.effective.remotes.values()), None)) or top
|
|
1318
1324
|
headline = f"no identity matches this repo ({url}) and [default] has no email"
|
|
1325
|
+
elif any(p.code == "ambiguous" for p in triggers):
|
|
1326
|
+
headline = f"this {what} runs in a repo whose remotes belong to different identities"
|
|
1319
1327
|
elif c_bad or a_bad:
|
|
1320
1328
|
email, verb = (committer, "committed") if c_bad else (author, "authored")
|
|
1321
1329
|
email = email or "(no email)"
|
|
@@ -1329,9 +1337,10 @@ def _judge_one(c: Call, rep, cfg, own: set, pushed_to: Optional[str] = None) ->
|
|
|
1329
1337
|
if c_bad and c_src == "effective" and pushed_to and not triggers:
|
|
1330
1338
|
# the repo's other problems were computed for this remote's identity; they would mislead here
|
|
1331
1339
|
return Decision(headline, f"this repo's commits use {committer or '(no email)'} and {pushed_to} belongs "
|
|
1332
|
-
f"to {exp.id or '[default]'}", f"gitpro why {shlex.quote(top)}", top
|
|
1340
|
+
f"to {exp.id or '[default]'}", f"gitpro why {shlex.quote(top)}", top,
|
|
1341
|
+
"pushed_to", codes, action)
|
|
1333
1342
|
cause, fix = _cause(c, c_bad, c_src, committer, a_bad, a_src, author, rep, cfg, environ)
|
|
1334
|
-
return Decision(headline, cause, fix, top)
|
|
1343
|
+
return Decision(headline, cause, fix, top, why, codes, action)
|
|
1335
1344
|
|
|
1336
1345
|
|
|
1337
1346
|
def _command_text(c: Call, limit: int = 200) -> str:
|
|
@@ -1447,6 +1456,7 @@ def _check(args: argparse.Namespace, text: str) -> int:
|
|
|
1447
1456
|
sys.stdout.flush()
|
|
1448
1457
|
print(msg, file=sys.stderr)
|
|
1449
1458
|
return 0
|
|
1459
|
+
_record_denial(args.agent, d)
|
|
1450
1460
|
out, rc = adapter.deny(d)
|
|
1451
1461
|
if out:
|
|
1452
1462
|
sys.stdout.write(out + "\n")
|
|
@@ -1455,6 +1465,11 @@ def _check(args: argparse.Namespace, text: str) -> int:
|
|
|
1455
1465
|
return rc
|
|
1456
1466
|
|
|
1457
1467
|
|
|
1468
|
+
def _record_denial(agent: str, d: Decision) -> None:
|
|
1469
|
+
from . import incident # lazy: stdlib only, but most checks never deny
|
|
1470
|
+
incident.record_denial("agent-check", agent, d.action, d.why, d.codes)
|
|
1471
|
+
|
|
1472
|
+
|
|
1458
1473
|
def _fail_open(func: Callable[[], int]) -> int:
|
|
1459
1474
|
try:
|
|
1460
1475
|
return func()
|
|
@@ -1463,9 +1478,18 @@ def _fail_open(func: Callable[[], int]) -> int:
|
|
|
1463
1478
|
except _ArgError as e:
|
|
1464
1479
|
return _fail_open_msg(str(e))
|
|
1465
1480
|
except Exception as e: # fail open: never stop the agent
|
|
1481
|
+
_record_crash(e)
|
|
1466
1482
|
return _fail_open_msg(str(e) or type(e).__name__)
|
|
1467
1483
|
|
|
1468
1484
|
|
|
1485
|
+
def _record_crash(e: Exception) -> None:
|
|
1486
|
+
try:
|
|
1487
|
+
from . import incident
|
|
1488
|
+
incident.record_crash("agent-check", e)
|
|
1489
|
+
except Exception: # noqa: BLE001
|
|
1490
|
+
pass
|
|
1491
|
+
|
|
1492
|
+
|
|
1469
1493
|
def _fail_open_msg(msg: str) -> int:
|
|
1470
1494
|
try:
|
|
1471
1495
|
_warn(msg)
|
|
@@ -1790,11 +1814,31 @@ def _edit(args: argparse.Namespace, removing: bool) -> int:
|
|
|
1790
1814
|
view.step("added .claude/settings.local.json to .git/info/exclude so it is not committed")
|
|
1791
1815
|
view.ok(f"removed the gitpro hook from {shown}" if removing else
|
|
1792
1816
|
f"installed; {title} picks it up on its next session")
|
|
1793
|
-
restores = f"restores from {undo.tilde(res.backup)}" if res.backup else f"removes {shown}"
|
|
1817
|
+
restores = f"restores from {undo.tilde(res.backup)}" if res.backup else f"removes {shown} unless it changed since"
|
|
1794
1818
|
print(f"↩ undo: sh {undo.tilde(res.undo)} ({restores})")
|
|
1795
1819
|
return 0
|
|
1796
1820
|
|
|
1797
1821
|
|
|
1822
|
+
HOOK_AGENTS = ("claude", "cursor", "gemini")
|
|
1823
|
+
|
|
1824
|
+
|
|
1825
|
+
def plugin_enabled(home: Path) -> Optional[str]:
|
|
1826
|
+
"""The enabled gitpro plugin in ~/.claude/settings.json (it ships the hook and the MCP server), or None."""
|
|
1827
|
+
t = target("claude", "user", home, None)
|
|
1828
|
+
return _plugin(_read_lenient(t.path)) if t.path.is_file() else None
|
|
1829
|
+
|
|
1830
|
+
|
|
1831
|
+
def hook_change(agent: str, home: Path) -> Optional[str]:
|
|
1832
|
+
"""For gitpro setup: what `agent install --agent AGENT` would do at user scope ("create PATH" or
|
|
1833
|
+
"update PATH"), or None when the hook is in place. ConfigError for a file gitpro must not edit."""
|
|
1834
|
+
from . import undo
|
|
1835
|
+
t = target(agent, "user", home, gitpro_path())
|
|
1836
|
+
obj = _precheck(t.path, t, _snippet(t))
|
|
1837
|
+
if obj is not None and add_hook(copy.deepcopy(obj), t) == obj:
|
|
1838
|
+
return None
|
|
1839
|
+
return f"{'update' if obj is not None else 'create'} {undo.tilde(t.path)}"
|
|
1840
|
+
|
|
1841
|
+
|
|
1798
1842
|
def install(args: argparse.Namespace) -> int:
|
|
1799
1843
|
"""`gitpro agent install`."""
|
|
1800
1844
|
return _edit(args, removing=False)
|
|
@@ -11,6 +11,8 @@ so both routes accept the same arguments without importing cli.
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
13
|
import argparse
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
14
16
|
import shlex
|
|
15
17
|
import subprocess
|
|
16
18
|
import sys
|
|
@@ -41,7 +43,17 @@ def confirm() -> argparse.ArgumentParser:
|
|
|
41
43
|
return p
|
|
42
44
|
|
|
43
45
|
|
|
46
|
+
def dry_run(p: argparse.ArgumentParser) -> None:
|
|
47
|
+
"""-n/--dry-run for a command that already previews without -y (pin, scan, audit; spec 22 R4)."""
|
|
48
|
+
p.add_argument("-n", "--dry-run", action="store_true", help="show only, change nothing (as without -y)")
|
|
49
|
+
|
|
50
|
+
|
|
44
51
|
def preview_only(args: object) -> bool:
|
|
52
|
+
if getattr(args, "dry_run", False):
|
|
53
|
+
if getattr(args, "yes", False):
|
|
54
|
+
from .config import ConfigError
|
|
55
|
+
raise ConfigError("choose one: -n shows the change, -y makes it")
|
|
56
|
+
return True
|
|
45
57
|
return not (getattr(args, "yes", False) or getattr(args, "write", False))
|
|
46
58
|
|
|
47
59
|
|
|
@@ -64,6 +76,40 @@ def fail(msg: str, args: object) -> int:
|
|
|
64
76
|
return 2
|
|
65
77
|
|
|
66
78
|
|
|
79
|
+
_CMD_RE = re.compile(r"^[a-z][a-z0-9-]{0,30}$")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _command_path(args: object) -> str:
|
|
83
|
+
"""The command words of parsed args ("forge similar"), or "gitpro"."""
|
|
84
|
+
words = [getattr(args, "command", None)]
|
|
85
|
+
for key, val in sorted(vars(args).items()) if hasattr(args, "__dict__") else []:
|
|
86
|
+
if key != "command" and key.endswith(("_command", "_action")):
|
|
87
|
+
words.append(val)
|
|
88
|
+
return " ".join(w for w in words if isinstance(w, str) and _CMD_RE.match(w)) or "gitpro"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _internal_error(args: object, exc: Exception) -> int:
|
|
92
|
+
"""An unexpected exception: write the crash record, print two lines (the traceback only with
|
|
93
|
+
GITPRO_DEBUG=1), exit 2."""
|
|
94
|
+
from . import incident
|
|
95
|
+
command = _command_path(args)
|
|
96
|
+
incident.record_crash(command, exc)
|
|
97
|
+
name = type(exc).__qualname__
|
|
98
|
+
if not re.match(r"^[A-Za-z_][A-Za-z0-9_.]{0,80}$", name):
|
|
99
|
+
name = "Exception"
|
|
100
|
+
if os.environ.get("GITPRO_DEBUG") == "1":
|
|
101
|
+
import traceback
|
|
102
|
+
traceback.print_exception(type(exc), exc, exc.__traceback__)
|
|
103
|
+
if getattr(args, "json", False):
|
|
104
|
+
from . import jsonout
|
|
105
|
+
jsonout.error(getattr(args, "command", None) or "gitpro", f"internal error ({name})")
|
|
106
|
+
else:
|
|
107
|
+
print(f"✗ internal error ({name}): this is a gitpro bug", file=sys.stderr)
|
|
108
|
+
print("→ draft a report with: gitpro report --crash", file=sys.stderr)
|
|
109
|
+
return 2
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
|
|
67
113
|
def guarded(func: Callable[[argparse.Namespace], int], args: argparse.Namespace) -> int:
|
|
68
114
|
"""func(args) with gitpro's error handling: ConfigError, CalledProcessError, OSError and UnicodeError
|
|
69
115
|
become exit 2 through fail(); Ctrl-C / EOF exit 130."""
|
|
@@ -80,3 +126,5 @@ def guarded(func: Callable[[argparse.Namespace], int], args: argparse.Namespace)
|
|
|
80
126
|
except (KeyboardInterrupt, EOFError):
|
|
81
127
|
print(file=sys.stderr)
|
|
82
128
|
return 130
|
|
129
|
+
except Exception as e: # noqa: BLE001 - a bug in gitpro: record it, say so, exit 2
|
|
130
|
+
return _internal_error(args, e)
|
|
@@ -7,6 +7,7 @@ keeping every date; it never runs `rebase`, never touches pushed commits, the in
|
|
|
7
7
|
"""
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
import argparse
|
|
10
11
|
import os
|
|
11
12
|
import re
|
|
12
13
|
import shlex
|
|
@@ -18,7 +19,7 @@ from pathlib import Path
|
|
|
18
19
|
from typing import Dict, List, Optional, Sequence, Set, Tuple
|
|
19
20
|
|
|
20
21
|
from . import config, jsonout, resolve, trailers, undo
|
|
21
|
-
from .args import capability
|
|
22
|
+
from .args import capability, dry_run, preview_only
|
|
22
23
|
from .config import Config, ConfigError
|
|
23
24
|
|
|
24
25
|
PROG = "gitpro"
|
|
@@ -31,7 +32,7 @@ FORMAT = "%H%x00%an%x00%ae%x00%cn%x00%ce%x00%cI%x00%P%x00%s%x00%(trailers:only,u
|
|
|
31
32
|
# Options --range may contain besides revisions; anything else (--output=..., say) is refused.
|
|
32
33
|
RANGE_OPTIONS = ("--not", "--all", "--remotes", "--branches", "--tags", "--first-parent", "--no-merges",
|
|
33
34
|
"--merges", "--since", "--until", "--after", "--before", "--max-count")
|
|
34
|
-
_CAUSES = ("url_form", "override", "pin_stale", "global_override", "not_applied", "env_override")
|
|
35
|
+
_CAUSES = ("ambiguous", "url_form", "override", "pin_stale", "global_override", "not_applied", "env_override")
|
|
35
36
|
|
|
36
37
|
|
|
37
38
|
@dataclass
|
|
@@ -715,9 +716,11 @@ def _preview(data: dict, exp) -> List[str]:
|
|
|
715
716
|
|
|
716
717
|
def cmd_audit(args) -> int:
|
|
717
718
|
as_json = jsonout.wants_json(args)
|
|
719
|
+
args.rewrite = args.rewrite or getattr(args, "rewrite_flag", False)
|
|
718
720
|
if args.rewrite and getattr(args, "agents", False):
|
|
719
721
|
raise ConfigError("--rewrite needs the author audit; drop --agents")
|
|
720
|
-
|
|
722
|
+
show_only = bool(args.rewrite and args.dry_run and preview_only(args))
|
|
723
|
+
if args.rewrite and as_json and not args.yes and not show_only:
|
|
721
724
|
raise ConfigError("--json needs -y with --rewrite (there is no terminal to confirm on)")
|
|
722
725
|
home = Path(args.home).expanduser().resolve()
|
|
723
726
|
path, how = config.find(args.config, home)
|
|
@@ -746,6 +749,12 @@ def cmd_audit(args) -> int:
|
|
|
746
749
|
print()
|
|
747
750
|
print("\n".join(_preview(data, exp)))
|
|
748
751
|
n = data["rewrite"]["commits"]
|
|
752
|
+
if show_only:
|
|
753
|
+
if as_json:
|
|
754
|
+
jsonout.emit("audit", dict(_public(data), dry_run=True))
|
|
755
|
+
else:
|
|
756
|
+
print(f"Dry run: nothing written. Rewrite the {n} unpushed commit{'s' * (n != 1)} with -y.")
|
|
757
|
+
return 0 if data["ok"] else 1
|
|
749
758
|
if not _confirm(f"rewrite {n} unpushed commit{'s' * (n != 1)}?", args.yes):
|
|
750
759
|
print("✗ aborted, nothing written")
|
|
751
760
|
return 1
|
|
@@ -773,7 +782,7 @@ def cmd_audit(args) -> int:
|
|
|
773
782
|
def register(sub, common, confirm) -> None:
|
|
774
783
|
sp = sub.add_parser(
|
|
775
784
|
"audit", parents=[common, confirm],
|
|
776
|
-
help="find commits made as the wrong identity or with an AI agent; fix unpushed ones with --
|
|
785
|
+
help="find commits made as the wrong identity or with an AI agent; fix unpushed ones with --fix",
|
|
777
786
|
description="Audit the history of a repo: commits whose committer (or your own other email as author) is "
|
|
778
787
|
"not the repo's identity, commits made with an AI agent (Co-Authored-By trailers, bot "
|
|
779
788
|
"emails) and the [*.trailers] policy. Default: the unpushed commits (as of the last fetch) "
|
|
@@ -792,9 +801,11 @@ def register(sub, common, confirm) -> None:
|
|
|
792
801
|
w = sp.add_mutually_exclusive_group()
|
|
793
802
|
w.add_argument("--agents", action="store_true", help="only the agent / trailer audit")
|
|
794
803
|
w.add_argument("--authors", action="store_true", help="only the wrong-author audit")
|
|
795
|
-
sp.add_argument("--rewrite", action="store_true",
|
|
804
|
+
sp.add_argument("--fix", dest="rewrite", action="store_true",
|
|
796
805
|
help="fix the unpushed wrong-author commits (asks first; writes an undo script)")
|
|
806
|
+
sp.add_argument("--rewrite", dest="rewrite_flag", action="store_true", help=argparse.SUPPRESS) # 0.4: --fix
|
|
807
|
+
dry_run(sp)
|
|
797
808
|
sp.add_argument("--json", action="store_true", help="machine-readable output")
|
|
798
809
|
sp.add_argument("-v", "--verbose", action="store_true", help="list every commit, not the first ten")
|
|
799
810
|
sp.set_defaults(func=cmd_audit)
|
|
800
|
-
capability("audit", "audit.rewrite", "trailers")
|
|
811
|
+
capability("audit", "audit.rewrite", "audit.fix", "trailers")
|