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.
Files changed (57) hide show
  1. {gitpro-0.4.1 → gitpro-0.4.3}/PKG-INFO +6 -6
  2. {gitpro-0.4.1 → gitpro-0.4.3}/README.md +5 -5
  3. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/VERSION +2 -0
  4. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/agents.py +2 -2
  5. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/audit.py +6 -5
  6. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/clone.py +1 -1
  7. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/guard.py +17 -6
  8. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/health.py +5 -5
  9. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/keys.py +1 -1
  10. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/lint.py +1 -1
  11. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/mcp.py +3 -3
  12. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/pin.py +1 -1
  13. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/policy.py +3 -3
  14. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/resolve.py +38 -27
  15. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/tui.py +1 -1
  16. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/view.py +1 -1
  17. {gitpro-0.4.1 → gitpro-0.4.3}/.gitignore +0 -0
  18. {gitpro-0.4.1 → gitpro-0.4.3}/LICENSE +0 -0
  19. {gitpro-0.4.1 → gitpro-0.4.3}/pyproject.toml +0 -0
  20. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/__init__.py +0 -0
  21. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/__main__.py +0 -0
  22. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/approval_policy.py +0 -0
  23. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/args.py +0 -0
  24. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/change_contract.py +0 -0
  25. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/changes.py +0 -0
  26. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/cli.py +0 -0
  27. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/commitcheck.py +0 -0
  28. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/completion.py +0 -0
  29. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/config.py +0 -0
  30. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/deprecation.py +0 -0
  31. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/devcontainer.py +0 -0
  32. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/discover.py +0 -0
  33. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/doctor.py +0 -0
  34. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/envcmd.py +0 -0
  35. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/example.toml +0 -0
  36. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/files.py +0 -0
  37. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/forge.py +0 -0
  38. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/hot.py +0 -0
  39. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/incident.py +0 -0
  40. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/jsonfile.py +0 -0
  41. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/jsonout.py +0 -0
  42. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/learn.py +0 -0
  43. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/lost.py +0 -0
  44. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/operation_contract.py +0 -0
  45. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/operation_io.py +0 -0
  46. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/operation_reconcile.py +0 -0
  47. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/operations.py +0 -0
  48. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/plan.py +0 -0
  49. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/prompt.py +0 -0
  50. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/render.py +0 -0
  51. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/report.py +0 -0
  52. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/scan.py +0 -0
  53. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/scanroots.py +0 -0
  54. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/setup.py +0 -0
  55. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/trailers.py +0 -0
  56. {gitpro-0.4.1 → gitpro-0.4.3}/src/gitpro/undo.py +0 -0
  57. {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.1
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
+ ![gitpro refuses a commit in a work repo that would use the personal email, removes the repo's own email setting, and the commit then uses the work email, while a personal repo keeps the personal one](https://gitpro.dev/demo.gif)
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
- `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).
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
+ ![gitpro refuses a commit in a work repo that would use the personal email, removes the repo's own email setting, and the commit then uses the work email, while a personal repo keeps the personal one](https://gitpro.dev/demo.gif)
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
- `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).
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 agent status shows it)")
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 agent install --agent claude rewrites the entry with "
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", "--rewrite"]
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("--rewrite needs the author audit; drop --agents")
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 --rewrite (there is no terminal to confirm on)")
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="--rewrite fixes only the unpushed offending commits, keeps every date and never runs rebase; "
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 guard install) the commit-msg "
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 scan --suggest --repo {_sh(_tilde(top, home))}")
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 --rewrite {_tilde(str(ctx.repo))}")
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 run {PROG} guard install")
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 → {PROG} guard install")
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} apply")
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} apply' installs them again; set it to false")
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. Use {PROG} guard install with [guard] chain_to = \"{hooks}\", or add "
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 keys --generate after changing ssh_key", "weak")
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 keys --generate after changing ssh_key", "rsa-2048")
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 keys --generate after changing ssh_key", "weak")
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 keys --generate", "missing"))
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 keys --generate")
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 keys --generate)")
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 keys --generate")
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 (gitpro mcp install --agent {agent} --force "
940
- "replaces it)")
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); install registers it")
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 apply', "
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} apply"))
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} keys --generate"))
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} apply"))
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 apply"
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 apply'))]
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
- ["gitpro", "scan", "--suggest", "--repo", str(top)],
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", ["gitpro", "apply"]))
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
- ["gitpro", "apply"]))
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
- ["gitpro", "apply"]))
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
- ["gitpro", "apply"]))
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 ["gitpro", "apply"]
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(_p("key_mismatch", msg, fix_for(eff.sshcommand, "core.sshCommand")))
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(_p("sign_mismatch", f"commit.gpgsign is {'true' if sign_now else 'false'}{own(eff.gpgsign)}; {who} "
919
- f"{'signs' if exp.sign else 'does not sign'} commits",
920
- fix_for(eff.gpgsign, "commit.gpgsign")))
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(_p("sign_mismatch", f"user.signingkey is {_tilde(eff.signingkey.value) or '(unset)'}"
923
- f"{own(eff.signingkey)}; {who} "
924
- f"signs with {_tilde(exp.signingkey)}",
925
- fix_for(eff.signingkey, "user.signingkey")))
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(word(a) for a in p.fix_argv)
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 apply'. "
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} scan --suggest", style="dim"), soft_wrap=True)
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