gitpro 0.4.1__tar.gz → 0.4.3__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.4.1 → gitpro-0.4.3}/PKG-INFO +6 -6
- {gitpro-0.4.1 → gitpro-0.4.3}/README.md +5 -5
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/VERSION +2 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/agents.py +2 -2
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/audit.py +6 -5
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/clone.py +1 -1
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/guard.py +17 -6
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/health.py +5 -5
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/keys.py +1 -1
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/lint.py +1 -1
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/mcp.py +3 -3
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/pin.py +1 -1
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/policy.py +3 -3
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/resolve.py +38 -27
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/tui.py +1 -1
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/view.py +1 -1
- {gitpro-0.4.1 → gitpro-0.4.3}/.gitignore +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/LICENSE +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/pyproject.toml +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/__init__.py +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/__main__.py +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/approval_policy.py +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/args.py +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/change_contract.py +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/changes.py +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/cli.py +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/commitcheck.py +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/completion.py +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/config.py +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/deprecation.py +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/devcontainer.py +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/discover.py +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/doctor.py +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/envcmd.py +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/example.toml +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/files.py +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/forge.py +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/hot.py +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/incident.py +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/jsonfile.py +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/jsonout.py +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/learn.py +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/lost.py +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/operation_contract.py +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/operation_io.py +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/operation_reconcile.py +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/operations.py +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/plan.py +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/prompt.py +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/render.py +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/report.py +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/scan.py +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/scanroots.py +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/setup.py +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/trailers.py +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/undo.py +0 -0
- {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/util.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: gitpro
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
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
|
|
@@ -34,6 +34,8 @@ Description-Content-Type: text/markdown
|
|
|
34
34
|
|
|
35
35
|
[gitpro.dev](https://gitpro.dev) · [PyPI](https://pypi.org/project/gitpro/) · [VS Code](https://marketplace.visualstudio.com/items?itemName=yantrakar.gitpro) · made by [Yantrakar](https://yantrakar.com)
|
|
36
36
|
|
|
37
|
+

|
|
38
|
+
|
|
37
39
|
Work on a company repository, commit with your work email. Switch to a personal
|
|
38
40
|
project, use your personal identity. Gitpro chooses by **remote URL**, not by
|
|
39
41
|
where you happened to clone the repository.
|
|
@@ -114,12 +116,10 @@ see [agent workflows](docs/agent-workflows.md).
|
|
|
114
116
|
|
|
115
117
|
## Problems and questions
|
|
116
118
|
|
|
117
|
-
Run `gitpro report` to draft an issue with the versions and a redacted
|
|
118
|
-
|
|
119
|
-
Or open an issue at [github.com/Yantrakar/gitpro/issues](https://github.com/Yantrakar/gitpro/issues).
|
|
119
|
+
Run `gitpro report` to draft an issue with the versions and a redacted `gitpro why`; it shows
|
|
120
|
+
you the draft and opens nothing until you say yes. Or open an issue at [github.com/Yantrakar/gitpro/issues](https://github.com/Yantrakar/gitpro/issues).
|
|
120
121
|
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.
|
|
122
|
+
[security policy](https://github.com/Yantrakar/gitpro/security/policy) describes, not in a public issue.
|
|
123
123
|
|
|
124
124
|
## License
|
|
125
125
|
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
[gitpro.dev](https://gitpro.dev) · [PyPI](https://pypi.org/project/gitpro/) · [VS Code](https://marketplace.visualstudio.com/items?itemName=yantrakar.gitpro) · made by [Yantrakar](https://yantrakar.com)
|
|
10
10
|
|
|
11
|
+

|
|
12
|
+
|
|
11
13
|
Work on a company repository, commit with your work email. Switch to a personal
|
|
12
14
|
project, use your personal identity. Gitpro chooses by **remote URL**, not by
|
|
13
15
|
where you happened to clone the repository.
|
|
@@ -88,12 +90,10 @@ see [agent workflows](docs/agent-workflows.md).
|
|
|
88
90
|
|
|
89
91
|
## Problems and questions
|
|
90
92
|
|
|
91
|
-
Run `gitpro report` to draft an issue with the versions and a redacted
|
|
92
|
-
|
|
93
|
-
Or open an issue at [github.com/Yantrakar/gitpro/issues](https://github.com/Yantrakar/gitpro/issues).
|
|
93
|
+
Run `gitpro report` to draft an issue with the versions and a redacted `gitpro why`; it shows
|
|
94
|
+
you the draft and opens nothing until you say yes. Or open an issue at [github.com/Yantrakar/gitpro/issues](https://github.com/Yantrakar/gitpro/issues).
|
|
94
95
|
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.
|
|
96
|
+
[security policy](https://github.com/Yantrakar/gitpro/security/policy) describes, not in a public issue.
|
|
97
97
|
|
|
98
98
|
## License
|
|
99
99
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
0.4.3 2026-10-21 release: gitpro.dev last, MCP Registry after PyPI only
|
|
2
|
+
0.4.2 2026-10-20 README demo GIF, Marketplace keywords, Open VSX in the release run
|
|
1
3
|
0.4.1 2026-10-18 secret masking, atomic writes, re-runnable release, version in one file
|
|
2
4
|
0.4.0 2026-10-16 setup replaces init and apply; guard refuses mixed-identity repos
|
|
3
5
|
0.3.4 report, support links and xdist CI
|
|
@@ -1756,7 +1756,7 @@ def _edit(args: argparse.Namespace, removing: bool) -> int:
|
|
|
1756
1756
|
shown = undo.tilde(t.path)
|
|
1757
1757
|
if say and agent == "claude" and not removing:
|
|
1758
1758
|
from . import view
|
|
1759
|
-
view.step("plugin users already have this hook (gitpro
|
|
1759
|
+
view.step("plugin users already have this hook (gitpro setup -n shows it)")
|
|
1760
1760
|
if after == before and (obj is not None or removing):
|
|
1761
1761
|
if args.json:
|
|
1762
1762
|
jsonout.emit(command, {"file": str(t.path), "changed": False, "backup": None, "undo": None})
|
|
@@ -1883,7 +1883,7 @@ def status_data(home: Path, explicit_config: Optional[Path] = None) -> dict:
|
|
|
1883
1883
|
plain, login = _command_v(False), _command_v(True)
|
|
1884
1884
|
if not plain and login:
|
|
1885
1885
|
warnings.append(f"gitpro is not on PATH for sh (found by login shell at {login}); the hook exits "
|
|
1886
|
-
f"silently without it → gitpro
|
|
1886
|
+
f"silently without it → gitpro setup --agent claude rewrites the entry with "
|
|
1887
1887
|
f"the absolute path")
|
|
1888
1888
|
return {"mode": mode, "agents": agents_, "guard_installed": guard_installed(),
|
|
1889
1889
|
"gitpro_path": {"sh": plain, "login": login}, "warnings": warnings}
|
|
@@ -580,7 +580,7 @@ def _rows(items: List[dict], verbose: bool, fmt) -> List[str]:
|
|
|
580
580
|
|
|
581
581
|
|
|
582
582
|
def _rewrite_cmd(data: dict, args) -> str:
|
|
583
|
-
argv = [PROG, "audit", "--
|
|
583
|
+
argv = [PROG, "audit", "--fix"]
|
|
584
584
|
if getattr(args, "config", None):
|
|
585
585
|
argv += ["-c", str(args.config)]
|
|
586
586
|
rw = data["rewrite"]
|
|
@@ -704,12 +704,13 @@ def _preview(data: dict, exp) -> List[str]:
|
|
|
704
704
|
|
|
705
705
|
def cmd_audit(args) -> int:
|
|
706
706
|
as_json = jsonout.wants_json(args)
|
|
707
|
+
flag = "--rewrite" if getattr(args, "rewrite_flag", False) else "--fix" # errors name the flag given
|
|
707
708
|
args.rewrite = args.rewrite or getattr(args, "rewrite_flag", False)
|
|
708
709
|
if args.rewrite and getattr(args, "agents", False):
|
|
709
|
-
raise ConfigError("
|
|
710
|
+
raise ConfigError(f"{flag} needs the author audit; drop --agents")
|
|
710
711
|
show_only = bool(args.rewrite and args.dry_run and preview_only(args))
|
|
711
712
|
if args.rewrite and as_json and not args.yes and not show_only:
|
|
712
|
-
raise ConfigError("--json needs -y with
|
|
713
|
+
raise ConfigError(f"--json needs -y with {flag} (there is no terminal to confirm on)")
|
|
713
714
|
home = Path(args.home).expanduser().resolve()
|
|
714
715
|
path, how = config.find(args.config, home)
|
|
715
716
|
cfg = config.load_all(path, how)
|
|
@@ -775,11 +776,11 @@ def register(sub, common, confirm) -> None:
|
|
|
775
776
|
"not the repo's identity, commits made with an AI agent (Co-Authored-By trailers, bot "
|
|
776
777
|
"emails) and the [*.trailers] policy. Default: the unpushed commits (as of the last fetch) "
|
|
777
778
|
"plus the newest pushed ones.",
|
|
778
|
-
epilog="--
|
|
779
|
+
epilog="--fix fixes only the unpushed offending commits, keeps every date and never runs rebase; "
|
|
779
780
|
"pushed commits are only reported. \"Pushed\" means reachable from a remote-tracking ref "
|
|
780
781
|
"(refs/remotes/*) as of the last fetch: commits published only through a tag, `git push <url>` "
|
|
781
782
|
"or Gerrit's refs/for/* count as unpushed, so fetch first. With commit.gpgsign set the new "
|
|
782
|
-
"commits are signed; notes are copied to them. With the guard installed (gitpro
|
|
783
|
+
"commits are signed; notes are copied to them. With the guard installed (gitpro setup) the commit-msg "
|
|
783
784
|
"hook applies the trailer policy at commit time; skip it once in your own shell with "
|
|
784
785
|
"GITPRO_SKIP=1 git commit ...")
|
|
785
786
|
sp.add_argument("dir", nargs="?", default=".", help="repo directory (default: .)")
|
|
@@ -402,7 +402,7 @@ def _problem_line(prob: resolve.Problem, p: ClonePlan, top: Path, home: Path) ->
|
|
|
402
402
|
if prob.code == "no_identity":
|
|
403
403
|
where = f"{p.host}/{p.owner}" if p.host and p.owner else resolve.redact_url(p.clone_url)
|
|
404
404
|
return (f"no identity for {where}: git will refuse to commit here → "
|
|
405
|
-
f"gitpro
|
|
405
|
+
f"gitpro setup --add {_sh(_tilde(top, home))}")
|
|
406
406
|
fix = resolve._shown_fix(prob)
|
|
407
407
|
return prob.message + (f" → {fix}" if fix else "")
|
|
408
408
|
|
|
@@ -348,7 +348,7 @@ def identity_push(ctx: Context) -> int:
|
|
|
348
348
|
n = len(bad)
|
|
349
349
|
lines = [f"{h} {e} {s}" for e, h, s in bad[:5]] + ([f"… and {n - 5} more"] if n > 5 else [])
|
|
350
350
|
if _audit_available():
|
|
351
|
-
lines.append(f"fix: {PROG} audit --
|
|
351
|
+
lines.append(f"fix: {PROG} audit --fix {_tilde(str(ctx.repo))}")
|
|
352
352
|
else:
|
|
353
353
|
lines.append("fix: amend the listed commits with the right user.email (git rebase -i <base>, "
|
|
354
354
|
"then git commit --amend --no-edit on each)")
|
|
@@ -500,6 +500,17 @@ def _has_block(path: Path) -> bool:
|
|
|
500
500
|
return False
|
|
501
501
|
|
|
502
502
|
|
|
503
|
+
def _setup_hint(then: bool = False) -> str:
|
|
504
|
+
"""How to install the global guard: setup does it unless the config says [guard] hooks = false."""
|
|
505
|
+
try:
|
|
506
|
+
off = _load_cfg(_home()).guard.hooks is False
|
|
507
|
+
except Exception: # noqa: BLE001 - no config or a broken one: setup reports that itself
|
|
508
|
+
off = False
|
|
509
|
+
if off:
|
|
510
|
+
return f"{'then ' if then else ''}set [guard] hooks = true in your config and run {PROG} setup"
|
|
511
|
+
return f"{'then run ' if then else ''}{PROG} setup"
|
|
512
|
+
|
|
513
|
+
|
|
503
514
|
def cmd_status(args) -> int:
|
|
504
515
|
from . import jsonout
|
|
505
516
|
from .render import GUARD_HOOKS, HOOK_NAMES, NOT_FORWARDED
|
|
@@ -528,9 +539,9 @@ def cmd_status(args) -> int:
|
|
|
528
539
|
"stubs call 'git lfs <hook>' unless the repo hook already does")
|
|
529
540
|
elif st.get("effective_hooks_path"):
|
|
530
541
|
other = st["effective_hooks_path"]
|
|
531
|
-
print(f"guard: core.hooksPath = {other} (not gitpro) → {chain_advice(other)}, then
|
|
542
|
+
print(f"guard: core.hooksPath = {other} (not gitpro) → {chain_advice(other)}, {_setup_hint(then=True)}")
|
|
532
543
|
else:
|
|
533
|
-
print(f"guard: not installed → {
|
|
544
|
+
print(f"guard: not installed → {_setup_hint()}")
|
|
534
545
|
if st.get("error"):
|
|
535
546
|
print(f"⚠ guard status: {st['error']}", file=sys.stderr)
|
|
536
547
|
if st["system_hooks_path"]:
|
|
@@ -662,7 +673,7 @@ def chain_missing(cfg, env) -> Optional[str]:
|
|
|
662
673
|
if os.path.lexists(p):
|
|
663
674
|
return None
|
|
664
675
|
return (f"[guard] chain_to {cfg.guard.chain_to} does not exist; the hooks run without it → create it, "
|
|
665
|
-
f"or remove chain_to from the config and run {PROG}
|
|
676
|
+
f"or remove chain_to from the config and run {PROG} setup")
|
|
666
677
|
|
|
667
678
|
|
|
668
679
|
def check_chain_to(cfg, env, missing_ok: bool = False) -> Optional[Path]:
|
|
@@ -863,7 +874,7 @@ def cmd_uninstall(args) -> int:
|
|
|
863
874
|
view.ok("guard removed; git runs each repo's own hooks again")
|
|
864
875
|
print(f"↩ undo: sh {undo.tilde(script)}")
|
|
865
876
|
if hooks_on:
|
|
866
|
-
view.warn(f"[guard] hooks = true in your config: the next '{PROG}
|
|
877
|
+
view.warn(f"[guard] hooks = true in your config: the next '{PROG} setup' installs them again; set it to false")
|
|
867
878
|
elif hooks_on is None:
|
|
868
879
|
view.step(f"'{PROG} setup' installs them again; to keep them off, set [guard] hooks = false in your config")
|
|
869
880
|
return 0
|
|
@@ -901,7 +912,7 @@ def _refuse_shared_hooks_dir(top: Path, hooks: Path) -> None:
|
|
|
901
912
|
where = origin[5:] if origin.startswith("file:") else origin
|
|
902
913
|
raise ConfigError(f"{tilde(hooks)} is core.hooksPath from {scope or 'unknown'} config"
|
|
903
914
|
f"{' (' + tilde(Path(where)) + ')' if where else ''}; other repos share it, so gitpro "
|
|
904
|
-
f"does not edit it.
|
|
915
|
+
f"does not edit it. Set [guard] chain_to = \"{hooks}\" in your config and run {PROG} setup, or add "
|
|
905
916
|
f"this block to {tilde(hooks / 'pre-commit')} yourself:\n{_block_help('pre-commit')}")
|
|
906
917
|
|
|
907
918
|
|
|
@@ -117,14 +117,14 @@ def classify(ktype: str, bits: int) -> Tuple[str, Optional[dict]]:
|
|
|
117
117
|
if t == "RSA":
|
|
118
118
|
if bits < 2048:
|
|
119
119
|
return "rsa", _check("error", f"RSA {bits} is too weak; generate a new key",
|
|
120
|
-
"gitpro
|
|
120
|
+
"gitpro setup after changing ssh_key", "weak")
|
|
121
121
|
if bits < 3072:
|
|
122
122
|
return "rsa", _check("warning", f"RSA {bits} is below the recommended 3072 bits; ed25519 is better",
|
|
123
|
-
"gitpro
|
|
123
|
+
"gitpro setup after changing ssh_key", "rsa-2048")
|
|
124
124
|
return "rsa", None
|
|
125
125
|
if t == "DSA":
|
|
126
126
|
return "dsa", _check("error", "DSA is too weak (current OpenSSH refuses it); generate a new key",
|
|
127
|
-
"gitpro
|
|
127
|
+
"gitpro setup after changing ssh_key", "weak")
|
|
128
128
|
if t == "ECDSA":
|
|
129
129
|
name = f"ecdsa-{bits}"
|
|
130
130
|
return name, _check("warning", f"{name}: ed25519 is preferred", tag="ecdsa")
|
|
@@ -473,7 +473,7 @@ def _key_record(k, home: Path, uses: List[Use], agent: Optional[set], shared: Li
|
|
|
473
473
|
"fixes": [], "checks": []}
|
|
474
474
|
checks: List[dict] = rec["checks"]
|
|
475
475
|
if not private.exists():
|
|
476
|
-
checks.append(_check("error", "missing", "gitpro
|
|
476
|
+
checks.append(_check("error", "missing", "gitpro setup", "missing"))
|
|
477
477
|
elif not pub.exists():
|
|
478
478
|
ps = _tilde(pub, home)
|
|
479
479
|
checks.append(_check("error", f"{ps} missing", f"ssh-keygen -y -f {_sh(shown)} > {_sh(ps)}", "missing"))
|
|
@@ -1000,7 +1000,7 @@ def upload(cfg, ids: Optional[List[str]] = None, *, title: Optional[str] = None,
|
|
|
1000
1000
|
tpub, where = _tilde(pub, home), f"{t.acc.host} ({t.acc.login})"
|
|
1001
1001
|
mat = material(pub)
|
|
1002
1002
|
if mat is None:
|
|
1003
|
-
warn(f"✗ {tpub} is missing or not a public key → gitpro
|
|
1003
|
+
warn(f"✗ {tpub} is missing or not a public key → gitpro setup")
|
|
1004
1004
|
failed += 1
|
|
1005
1005
|
continue
|
|
1006
1006
|
unsafe = pub_unsafe(pub)
|
|
@@ -57,4 +57,4 @@ def report(keys: list[Key], expand: Expand) -> None:
|
|
|
57
57
|
role = "auth + signing" if k.sign else "auth"
|
|
58
58
|
print(f" {k.path} ({role}) -> {'; '.join(k.uses)}")
|
|
59
59
|
pub = pub_path(expand(k.path))
|
|
60
|
-
print(f" {pub.read_text().strip()}" if pub.exists() else " (missing -- run: gitpro
|
|
60
|
+
print(f" {pub.read_text().strip()}" if pub.exists() else " (missing -- run: gitpro setup)")
|
|
@@ -47,7 +47,7 @@ def warnings(d: Default, ids: list[Identity], env: Env) -> list[str]:
|
|
|
47
47
|
for k in keys.inventory(d, ids):
|
|
48
48
|
p = env.expand(k.path)
|
|
49
49
|
if not p.exists():
|
|
50
|
-
out.append(f"{k.path} missing -- run: gitpro
|
|
50
|
+
out.append(f"{k.path} missing -- run: gitpro setup")
|
|
51
51
|
elif p.stat().st_mode & 0o077:
|
|
52
52
|
out.append(f"{k.path} is accessible by others -- chmod 600 {p}")
|
|
53
53
|
return out
|
|
@@ -936,8 +936,8 @@ def install_change(agent: str, home: Path) -> Optional[str]:
|
|
|
936
936
|
if _ours(current, entry):
|
|
937
937
|
return None
|
|
938
938
|
if current is not None:
|
|
939
|
-
raise ConfigError(f"{_foreign(path, current)}; left alone (
|
|
940
|
-
"
|
|
939
|
+
raise ConfigError(f"{_foreign(path, current)}; left alone (remove that entry, then run "
|
|
940
|
+
f"gitpro setup --agent {agent})")
|
|
941
941
|
if agent == "claude" and shutil.which("claude"):
|
|
942
942
|
return f"run {shlex.join(CLAUDE_ADD)}"
|
|
943
943
|
return f"{'update' if obj is not None else 'create'} {_tilde_path(path)}"
|
|
@@ -993,7 +993,7 @@ def doctor_section(ctx) -> dict:
|
|
|
993
993
|
def register(sub, common, confirm) -> None:
|
|
994
994
|
from .args import capability
|
|
995
995
|
p = sub.add_parser("mcp", parents=[common],
|
|
996
|
-
help="run the read-only MCP server for AI assistants (stdio);
|
|
996
|
+
help="run the read-only MCP server for AI assistants (stdio); gitpro setup --agent NAME registers it")
|
|
997
997
|
p.add_argument("--log", type=Path, metavar="FILE", help="append every request and response to FILE (0600)")
|
|
998
998
|
p.add_argument("--allow-execution", action="store_true",
|
|
999
999
|
help="expose operation and approved-change execution; the host must authorize each write")
|
|
@@ -43,7 +43,7 @@ def _resolved(repo: str, key: str, pin_path: Path, base: Path) -> str:
|
|
|
43
43
|
origin, value, src = found_origin, found_value, found_src
|
|
44
44
|
check(value, f"git resolves no {key} for this repo; nothing to pin")
|
|
45
45
|
check(src is not None and src.is_file() and render.is_generated(src.read_text()),
|
|
46
|
-
f"{key} comes from {origin}, not from gitpro; run 'gitpro
|
|
46
|
+
f"{key} comes from {origin}, not from gitpro; run 'gitpro setup', "
|
|
47
47
|
"and run pin on the host, not inside the container")
|
|
48
48
|
return value
|
|
49
49
|
|
|
@@ -1099,7 +1099,7 @@ def identity_checks(cfg, person: _Person, pol: Policy, rules: Sequence[Rule], ke
|
|
|
1099
1099
|
label = "required" if required else "formats " + ", ".join(fmts)
|
|
1100
1100
|
if required and not person.sign:
|
|
1101
1101
|
out.append(_check("signing", label, "error", f"{person.label} has sign = false",
|
|
1102
|
-
f"set sign = true, run {PROG}
|
|
1102
|
+
f"set sign = true, run {PROG} setup"))
|
|
1103
1103
|
elif person.sign and fmts and "ssh" not in fmts:
|
|
1104
1104
|
out.append(_check("signing", label, "error", f"{PROG} signs with ssh but the policy allows {', '.join(fmts)}",
|
|
1105
1105
|
"sign with an allowed format outside gitpro"))
|
|
@@ -1114,7 +1114,7 @@ def identity_checks(cfg, person: _Person, pol: Policy, rules: Sequence[Rule], ke
|
|
|
1114
1114
|
shown = _tilde(pub)
|
|
1115
1115
|
reg = (keys or {}).get(pub) if online else None
|
|
1116
1116
|
if not os.path.isfile(pub):
|
|
1117
|
-
out.append(_check("signing key", shown, "error", f"{shown} not found", f"{PROG}
|
|
1117
|
+
out.append(_check("signing key", shown, "error", f"{shown} not found", f"{PROG} setup"))
|
|
1118
1118
|
elif reg is True:
|
|
1119
1119
|
out.append(_check("signing key", shown, "ok", "registered as a signing key"))
|
|
1120
1120
|
elif reg is False:
|
|
@@ -1264,7 +1264,7 @@ def check_report(report) -> List[resolve.Problem]:
|
|
|
1264
1264
|
f"use an address in {', '.join(doms)}" if doms else None))
|
|
1265
1265
|
if any(r.require_signing for r in rules) and not report.expected.sign:
|
|
1266
1266
|
out.append(resolve.Problem("policy", f"policy {pol.name} requires signed commits but identity {ident.id} has "
|
|
1267
|
-
f"sign = false", f"set sign = true, run {PROG}
|
|
1267
|
+
f"sign = false", f"set sign = true, run {PROG} setup"))
|
|
1268
1268
|
return out
|
|
1269
1269
|
|
|
1270
1270
|
|
|
@@ -703,6 +703,15 @@ def _p(code: str, message: str, argv: Optional[Sequence[str]], fix: Optional[str
|
|
|
703
703
|
tuple(argv) if argv else None)
|
|
704
704
|
|
|
705
705
|
|
|
706
|
+
# The fix when only gitpro's generated files settle it. setup needs a terminal (spec 22 §5.7), so it has no
|
|
707
|
+
# argv a tool could run (spec 22 §10.5).
|
|
708
|
+
SETUP = "gitpro setup"
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
def _p_or_setup(code: str, message: str, argv: Optional[Sequence[str]]) -> Problem:
|
|
712
|
+
return _p(code, message, argv, None if argv else SETUP)
|
|
713
|
+
|
|
714
|
+
|
|
706
715
|
_DEFAULT_PORT = {"ssh": 22, "https": 443, "http": 80}
|
|
707
716
|
_CODECOMMIT = re.compile(r"^git-codecommit\.[a-z0-9-]+\.amazonaws\.com$", re.I)
|
|
708
717
|
_Fix = Tuple[Optional[str], str, Identity, str, Optional[Tuple[str, str]]]
|
|
@@ -751,7 +760,7 @@ def _url_fixes(url: str, cfg: Config, label: str = "the remote") -> List[_Fix]:
|
|
|
751
760
|
if port and port[1] != _DEFAULT_PORT[port[0]]:
|
|
752
761
|
if port[0] == "ssh":
|
|
753
762
|
tail = f"it looks like {i.id}, whose patterns have no port {port[1]}"
|
|
754
|
-
fix = f"set ssh_port = {port[1]} on identity {i.id} in the gitpro config, then run: gitpro
|
|
763
|
+
fix = f"set ssh_port = {port[1]} on identity {i.id} in the gitpro config, then run: gitpro setup"
|
|
755
764
|
if i.ssh_port:
|
|
756
765
|
fix += f" (it is {i.ssh_port} now; an identity has one ssh_port)"
|
|
757
766
|
else:
|
|
@@ -761,7 +770,7 @@ def _url_fixes(url: str, cfg: Config, label: str = "the remote") -> List[_Fix]:
|
|
|
761
770
|
if reason.endswith("user git@ in the URL") and is_codecommit(i.host):
|
|
762
771
|
return [(None, reason, i, pat, (
|
|
763
772
|
f"it looks like {i.id}, but CodeCommit's SSH user is the SSH key ID, not git",
|
|
764
|
-
f'set ssh_user = "<your SSH key ID>" on identity {i.id} in the gitpro config, then run: gitpro
|
|
773
|
+
f'set ssh_user = "<your SSH key ID>" on identity {i.id} in the gitpro config, then run: gitpro setup'))]
|
|
765
774
|
return [(fixed, reason, i, pat, None)]
|
|
766
775
|
from .render import url_patterns
|
|
767
776
|
for i in cfg.identities:
|
|
@@ -835,8 +844,7 @@ def compare(repo: Path, cfg: Config, *, remote: Optional[str] = None,
|
|
|
835
844
|
|
|
836
845
|
if exp.email is None:
|
|
837
846
|
out.append(_p("no_identity", f"no identity for {redact_url(first_url) or top}",
|
|
838
|
-
|
|
839
|
-
f"add an identity: gitpro scan --suggest --repo {shlex.quote(str(top))}"))
|
|
847
|
+
None, f"add an identity: gitpro setup --add {_word(str(top))}"))
|
|
840
848
|
|
|
841
849
|
if exp.match.identity is None:
|
|
842
850
|
for name, url in candidates:
|
|
@@ -872,23 +880,23 @@ def compare(repo: Path, cfg: Config, *, remote: Optional[str] = None,
|
|
|
872
880
|
if beats:
|
|
873
881
|
k, s = beats[0]
|
|
874
882
|
out.append(_p("global_override", f"user.{k} comes from {_shown_origin(s, top)} ({s.scope}) and beats "
|
|
875
|
-
f"{who}; the generated include must come last",
|
|
883
|
+
f"{who}; the generated include must come last", None, SETUP))
|
|
876
884
|
|
|
877
885
|
home = Path.home() if home is None else Path(home)
|
|
878
886
|
old = [k for k, s, want in (("name", eff.name, exp.name), ("email", eff.email, exp.email))
|
|
879
887
|
if s.scope == "generated" and want is not None and s.value != want]
|
|
880
888
|
if exp.file and not os.path.exists(exp.file):
|
|
881
|
-
out.append(_p("not_applied", f"{_tilde(exp.file)} does not exist; the config has not been applied",
|
|
882
|
-
|
|
889
|
+
out.append(_p("not_applied", f"{_tilde(exp.file)} does not exist; the config has not been applied", None,
|
|
890
|
+
SETUP))
|
|
883
891
|
elif not _home_has_block(home):
|
|
884
|
-
out.append(_p("not_applied", "~/.gitconfig has no gitpro block; the config has not been applied",
|
|
885
|
-
|
|
892
|
+
out.append(_p("not_applied", "~/.gitconfig has no gitpro block; the config has not been applied", None,
|
|
893
|
+
SETUP))
|
|
886
894
|
elif old:
|
|
887
895
|
out.append(_p("not_applied", f"the generated files give user.{old[0]} "
|
|
888
896
|
f"{getattr(eff, old[0]).value}; they are older than the config",
|
|
889
|
-
|
|
897
|
+
None, SETUP))
|
|
890
898
|
|
|
891
|
-
def fix_for(s: Setting, key: str) -> List[str]:
|
|
899
|
+
def fix_for(s: Setting, key: str) -> Optional[List[str]]:
|
|
892
900
|
if s.scope in ("repo", "worktree"):
|
|
893
901
|
return ["git", "-C", str(top), "config"] + (["--worktree"] if s.scope == "worktree" else []) \
|
|
894
902
|
+ ["--unset", key]
|
|
@@ -896,7 +904,7 @@ def compare(repo: Path, cfg: Config, *, remote: Optional[str] = None,
|
|
|
896
904
|
return ["git", "config", "--file", s.origin[5:], "--unset", key]
|
|
897
905
|
if repin and s.scope in ("generated", "pin", None):
|
|
898
906
|
return repin # the other identity's include decides it; a pin by remote overrides it
|
|
899
|
-
return
|
|
907
|
+
return None # setup rewrites the generated files (fix text SETUP)
|
|
900
908
|
|
|
901
909
|
def own(s: Setting) -> str:
|
|
902
910
|
# a value from the user's own config, outside gitpro's generated files
|
|
@@ -911,18 +919,19 @@ def compare(repo: Path, cfg: Config, *, remote: Optional[str] = None,
|
|
|
911
919
|
if not _same_path(got, exp.ssh_key):
|
|
912
920
|
msg = (f"core.sshcommand uses {_tilde(got)}{own(eff.sshcommand)}; {who} uses {_tilde(exp.ssh_key)}"
|
|
913
921
|
if got else f"core.sshcommand is not set; {who} uses {_tilde(exp.ssh_key)}")
|
|
914
|
-
out.append(
|
|
922
|
+
out.append(_p_or_setup("key_mismatch", msg, fix_for(eff.sshcommand, "core.sshCommand")))
|
|
915
923
|
|
|
916
924
|
sign_now = _bool(eff.gpgsign.value)
|
|
917
925
|
if sign_now != exp.sign:
|
|
918
|
-
out.append(
|
|
919
|
-
|
|
920
|
-
|
|
926
|
+
out.append(_p_or_setup("sign_mismatch", f"commit.gpgsign is {'true' if sign_now else 'false'}"
|
|
927
|
+
f"{own(eff.gpgsign)}; {who} "
|
|
928
|
+
f"{'signs' if exp.sign else 'does not sign'} commits",
|
|
929
|
+
fix_for(eff.gpgsign, "commit.gpgsign")))
|
|
921
930
|
elif exp.sign and exp.signingkey and not _same_path(eff.signingkey.value, exp.signingkey):
|
|
922
|
-
out.append(
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
931
|
+
out.append(_p_or_setup("sign_mismatch", f"user.signingkey is {_tilde(eff.signingkey.value) or '(unset)'}"
|
|
932
|
+
f"{own(eff.signingkey)}; {who} "
|
|
933
|
+
f"signs with {_tilde(exp.signingkey)}",
|
|
934
|
+
fix_for(eff.signingkey, "user.signingkey")))
|
|
926
935
|
|
|
927
936
|
envp = _env_problem(exp, eff, env)
|
|
928
937
|
if envp:
|
|
@@ -970,19 +979,21 @@ def _shown_origin(s: Setting, top: Path) -> str:
|
|
|
970
979
|
return _tilde(path)
|
|
971
980
|
|
|
972
981
|
|
|
982
|
+
def _word(a: str) -> str:
|
|
983
|
+
"""a shell-quoted, with the home folder shown as ~."""
|
|
984
|
+
home = str(Path.home())
|
|
985
|
+
if a.startswith(home + "/"):
|
|
986
|
+
return "~/" + shlex.quote(a[len(home) + 1:])
|
|
987
|
+
return shlex.quote(a)
|
|
988
|
+
|
|
989
|
+
|
|
973
990
|
def _shown_fix(p: Problem) -> str:
|
|
974
991
|
if not p.fix_argv:
|
|
975
992
|
return p.fix or ""
|
|
976
|
-
home = str(Path.home())
|
|
977
|
-
|
|
978
|
-
def word(a: str) -> str:
|
|
979
|
-
if a.startswith(home + "/"):
|
|
980
|
-
return "~/" + shlex.quote(a[len(home) + 1:])
|
|
981
|
-
return shlex.quote(a)
|
|
982
993
|
# keep a leading explanation ("add an identity: ") that the argv itself does not carry
|
|
983
994
|
cut = (p.fix or "").find(p.fix_argv[0])
|
|
984
995
|
prefix = p.fix[:cut] if cut > 0 else ""
|
|
985
|
-
return prefix + " ".join(
|
|
996
|
+
return prefix + " ".join(_word(a) for a in p.fix_argv)
|
|
986
997
|
|
|
987
998
|
|
|
988
999
|
def render_text(rep: Report, cfg: Config) -> str:
|
|
@@ -46,7 +46,7 @@ class Suggestion(ModalScreen[None]):
|
|
|
46
46
|
|
|
47
47
|
def compose(self) -> ComposeResult:
|
|
48
48
|
with Vertical(classes="dialog"):
|
|
49
|
-
yield Static("To keep this identity, paste the block into your config and run 'gitpro
|
|
49
|
+
yield Static("To keep this identity, paste the block into your config and run 'gitpro setup'. "
|
|
50
50
|
"The repos it matches then keep it after the fix.\n")
|
|
51
51
|
yield Static(Syntax(self.toml, "toml", theme="ansi_dark", background_color="default"))
|
|
52
52
|
yield Footer()
|
|
@@ -131,7 +131,7 @@ def scan(rep: dict, show: Callable[[str], str], *, verbose: bool, suggest: bool,
|
|
|
131
131
|
out.print(f"\n# {show(path)} ({r['owner']}): no identity can match it; after fixing it uses {who(r['after'])}",
|
|
132
132
|
markup=False, soft_wrap=True)
|
|
133
133
|
elif rep["suggestions"] and not fix:
|
|
134
|
-
out.print(Text(f"\nto keep some of these identities instead: {prog}
|
|
134
|
+
out.print(Text(f"\nto keep some of these identities instead: {prog} setup --add", style="dim"), soft_wrap=True)
|
|
135
135
|
|
|
136
136
|
if not fix:
|
|
137
137
|
settings = sum(len(r["fixable"]) for r in over)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|