gitpro 0.3.1__tar.gz → 0.3.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 (50) hide show
  1. {gitpro-0.3.1 → gitpro-0.3.3}/PKG-INFO +10 -2
  2. {gitpro-0.3.1 → gitpro-0.3.3}/README.md +6 -0
  3. {gitpro-0.3.1 → gitpro-0.3.3}/pyproject.toml +3 -1
  4. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/__init__.py +1 -1
  5. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/config.py +1 -1
  6. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/discover.py +1 -1
  7. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/lint.py +3 -0
  8. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/resolve.py +65 -15
  9. {gitpro-0.3.1 → gitpro-0.3.3}/.gitignore +0 -0
  10. {gitpro-0.3.1 → gitpro-0.3.3}/LICENSE +0 -0
  11. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/__main__.py +0 -0
  12. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/agents.py +0 -0
  13. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/approval_policy.py +0 -0
  14. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/args.py +0 -0
  15. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/audit.py +0 -0
  16. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/change_contract.py +0 -0
  17. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/changes.py +0 -0
  18. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/cli.py +0 -0
  19. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/clone.py +0 -0
  20. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/completion.py +0 -0
  21. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/devcontainer.py +0 -0
  22. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/doctor.py +0 -0
  23. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/envcmd.py +0 -0
  24. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/example.toml +0 -0
  25. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/files.py +0 -0
  26. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/forge.py +0 -0
  27. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/guard.py +0 -0
  28. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/health.py +0 -0
  29. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/hot.py +0 -0
  30. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/jsonfile.py +0 -0
  31. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/jsonout.py +0 -0
  32. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/keys.py +0 -0
  33. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/lost.py +0 -0
  34. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/mcp.py +0 -0
  35. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/operation_contract.py +0 -0
  36. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/operation_io.py +0 -0
  37. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/operation_reconcile.py +0 -0
  38. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/operations.py +0 -0
  39. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/pin.py +0 -0
  40. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/plan.py +0 -0
  41. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/policy.py +0 -0
  42. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/prompt.py +0 -0
  43. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/render.py +0 -0
  44. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/scan.py +0 -0
  45. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/scanroots.py +0 -0
  46. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/setup.py +0 -0
  47. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/trailers.py +0 -0
  48. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/tui.py +0 -0
  49. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/undo.py +0 -0
  50. {gitpro-0.3.1 → gitpro-0.3.3}/src/gitpro/view.py +0 -0
@@ -1,10 +1,12 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: gitpro
3
- Version: 0.3.1
3
+ Version: 0.3.3
4
4
  Summary: Per-remote git identities (name, email, SSH key, commit signing) from one TOML file
5
- Project-URL: Homepage, https://github.com/Yantrakar/gitpro
5
+ Project-URL: Homepage, https://gitpro.dev
6
+ Project-URL: Source, https://github.com/Yantrakar/gitpro
6
7
  Project-URL: Documentation, https://github.com/Yantrakar/gitpro/blob/main/docs/guide.md
7
8
  Project-URL: Issues, https://github.com/Yantrakar/gitpro/issues
9
+ Project-URL: VS Code extension, https://marketplace.visualstudio.com/items?itemName=yantrakar.gitpro
8
10
  Author: Yantrakar, Sandeep Shelke
9
11
  License-Expression: MIT
10
12
  License-File: LICENSE
@@ -22,10 +24,16 @@ Provides-Extra: tui
22
24
  Requires-Dist: textual>=0.80; extra == 'tui'
23
25
  Description-Content-Type: text/markdown
24
26
 
27
+ <a href="https://gitpro.dev"><img src="https://gitpro.dev/icon.png" alt="gitpro" width="72" height="72"></a>
28
+
25
29
  # gitpro
26
30
 
31
+ <!-- mcp-name: io.github.Yantrakar/gitpro -->
32
+
27
33
  **The right Git identity, SSH key and signature for every repository.**
28
34
 
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
+
29
37
  Work on a company repository, commit with your work email. Switch to a personal
30
38
  project, use your personal identity. Gitpro chooses by **remote URL**, not by
31
39
  where you happened to clone the repository.
@@ -1,7 +1,13 @@
1
+ <a href="https://gitpro.dev"><img src="https://gitpro.dev/icon.png" alt="gitpro" width="72" height="72"></a>
2
+
1
3
  # gitpro
2
4
 
5
+ <!-- mcp-name: io.github.Yantrakar/gitpro -->
6
+
3
7
  **The right Git identity, SSH key and signature for every repository.**
4
8
 
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
+
5
11
  Work on a company repository, commit with your work email. Switch to a personal
6
12
  project, use your personal identity. Gitpro chooses by **remote URL**, not by
7
13
  where you happened to clone the repository.
@@ -26,9 +26,11 @@ classifiers = [
26
26
  tui = ["textual>=0.80"] # interactive scan --fix
27
27
 
28
28
  [project.urls]
29
- Homepage = "https://github.com/Yantrakar/gitpro"
29
+ Homepage = "https://gitpro.dev"
30
+ Source = "https://github.com/Yantrakar/gitpro"
30
31
  Documentation = "https://github.com/Yantrakar/gitpro/blob/main/docs/guide.md"
31
32
  Issues = "https://github.com/Yantrakar/gitpro/issues"
33
+ "VS Code extension" = "https://marketplace.visualstudio.com/items?itemName=yantrakar.gitpro"
32
34
 
33
35
  [project.scripts]
34
36
  gitpro = "gitpro.hot:main"
@@ -1,4 +1,4 @@
1
1
  """Per-remote git identities generated from one TOML file (see cli.py)."""
2
2
  from __future__ import annotations
3
3
 
4
- __version__ = "0.3.1"
4
+ __version__ = "0.3.3"
@@ -24,7 +24,7 @@ _LABEL = r"[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?"
24
24
  HOST_RE = re.compile(rf"^{_LABEL}(?:\.{_LABEL})*$")
25
25
  ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
26
26
  USER_RE = ID_RE
27
- OWNER_RE = re.compile(r"^[A-Za-z0-9*][A-Za-z0-9._*-]*(?:/[A-Za-z0-9*][A-Za-z0-9._*-]*)*$")
27
+ OWNER_RE = re.compile(r"^~?[A-Za-z0-9*][A-Za-z0-9._*-]*(?:/[A-Za-z0-9*][A-Za-z0-9._*-]*)*$") # ~: SourceHut users
28
28
  PATH_RE = re.compile(r"^(?:~/|/)[A-Za-z0-9._/@+-]+$")
29
29
  EMAIL_RE = re.compile(r'^[^@\s<>"]+@[^@\s<>"]+$')
30
30
  CONTROL_RE = re.compile(r"[\x00-\x08\x0a-\x1f\x7f]")
@@ -1228,7 +1228,7 @@ def render_toml(d: dict) -> str:
1228
1228
  notes.append(f"{where}: already in config" + (f" (identity {o['config_id']})" if o.get("config_id") else ""))
1229
1229
  continue
1230
1230
  if not OWNER_RE.match(o["owner"]):
1231
- notes.append(f"{where}: owners can hold only A-Z a-z 0-9 . _ - /; add it by hand if it is yours")
1231
+ notes.append(f"{where}: owners can hold only A-Z a-z 0-9 . _ - / and a leading ~; add it by hand if it is yours")
1232
1232
  continue
1233
1233
  rule = o.get("rule") or {}
1234
1234
  email = _top(o.get("emails"), _usable_email)[0]
@@ -7,11 +7,14 @@ import subprocess
7
7
  from . import keys, render
8
8
  from .config import Default, Identity
9
9
  from .plan import Env
10
+ from .resolve import is_codecommit
10
11
 
11
12
 
12
13
  def warnings(d: Default, ids: list[Identity], env: Env) -> list[str]:
13
14
  out = []
14
15
  for i in ids:
16
+ if is_codecommit(i.host) and i.ssh_user == "git" and (i.ssh_key or i.ssh_alias):
17
+ out.append(f"identity {i.id!r}: CodeCommit's SSH user is the SSH key ID, not git; set ssh_user to it")
15
18
  if i.ssh_alias and i.rewrite:
16
19
  out += [f"identity {i.id!r}: owner {o!r} is a glob; insteadOf needs a literal prefix, so "
17
20
  f"its URLs aren't rewritten to {i.ssh_alias}" for o in i.owners if "*" in o]
@@ -607,28 +607,75 @@ def _p(code: str, message: str, argv: Optional[Sequence[str]], fix: Optional[str
607
607
  tuple(argv) if argv else None)
608
608
 
609
609
 
610
- def _url_fixes(url: str, cfg: Config) -> List[Tuple[str, str, Identity, str]]:
611
- """[(fixed url, reason, identity, pattern)] for the three repairs, in order: userinfo removed,
612
- missing user added, port removed. First repair that makes some identity match wins."""
613
- tries: List[Tuple[str, str]] = []
610
+ _DEFAULT_PORT = {"ssh": 22, "https": 443, "http": 80}
611
+ _CODECOMMIT = re.compile(r"^git-codecommit\.[a-z0-9-]+\.amazonaws\.com$", re.I)
612
+ _Fix = Tuple[Optional[str], str, Identity, str, Optional[Tuple[str, str]]]
613
+
614
+
615
+ def is_codecommit(host: str) -> bool:
616
+ return bool(_CODECOMMIT.match(host))
617
+
618
+
619
+ def _repairs(url: str, users: List[str]) -> List[Tuple[str, str, Optional[Tuple[str, int]]]]:
620
+ tries: List[Tuple[str, str, Optional[Tuple[str, int]]]] = []
614
621
  m = re.match(r"^(https?://)[^@/]+@(.*)$", url)
615
622
  if m:
616
- tries.append((m[1] + m[2], "no user in the URL"))
617
- users = list(dict.fromkeys(i.ssh_user for i in cfg.identities)) or ["git"]
623
+ tries.append((m[1] + m[2], "no user in the URL", None))
618
624
  m = re.match(r"^ssh://([^@/]+)(/.*)$", url)
619
625
  if m:
620
- tries += [(f"ssh://{u}@{m[1]}{m[2]}", f"user {u}@ in the URL") for u in users]
626
+ tries += [(f"ssh://{u}@{m[1]}{m[2]}", f"user {u}@ in the URL", None) for u in users]
621
627
  elif "://" not in url:
622
628
  m = _SCP_RE.match(url)
623
629
  if m and m[1] is None and "/" not in url.split(":", 1)[0]:
624
- tries += [(f"{u}@{url}", f"user {u}@ in the URL") for u in users]
625
- m = re.match(r"^((?:ssh|https?)://[^/]*?):\d+(/.*)$", url)
630
+ tries += [(f"{u}@{url}", f"user {u}@ in the URL", None) for u in users]
631
+ m = re.match(r"^((ssh|https?)://[^/]*?):(\d+)(/.*)$", url)
626
632
  if m:
627
- tries.append((m[1] + m[2], "no port in the URL"))
628
- for fixed, reason in tries:
633
+ tries.append((m[1] + m[4], "no port in the URL", (m[2], int(m[3]))))
634
+ return tries
635
+
636
+
637
+ def _url_fixes(url: str, cfg: Config, label: str = "the remote") -> List[_Fix]:
638
+ """[(fixed url, reason, identity, pattern, note)] for the repairs, in order: git+ssh:// or ssh+git://
639
+ written as ssh://, then (on that url) userinfo removed, missing user added, port removed, then
640
+ letter case changed to an identity's. First repair that makes some identity match wins. When the
641
+ fixed url would point at the wrong place (a non-default port removed, user git on CodeCommit),
642
+ fixed is None and note is (message tail, fix text) instead."""
643
+ users = list(dict.fromkeys(i.ssh_user for i in cfg.identities)) or ["git"]
644
+ tries: List[Tuple[str, str, Optional[Tuple[str, int]]]] = []
645
+ m = re.match(r"^(git\+ssh|ssh\+git)://(.*)$", url)
646
+ if m:
647
+ pre = f"ssh:// instead of {m[1]}://"
648
+ tries.append((f"ssh://{m[2]}", pre, None))
649
+ tries += [(f, f"{pre}, {r}", p) for f, r, p in _repairs(f"ssh://{m[2]}", users)]
650
+ tries += _repairs(url, users)
651
+ for fixed, reason, port in tries:
629
652
  i, pat = match_url(fixed, cfg)
630
- if i is not None:
631
- return [(fixed, reason, i, pat)]
653
+ if i is None:
654
+ continue
655
+ if port and port[1] != _DEFAULT_PORT[port[0]]:
656
+ if port[0] == "ssh":
657
+ tail = f"it looks like {i.id}, whose patterns have no port {port[1]}"
658
+ fix = f"set ssh_port = {port[1]} on identity {i.id} in the gitpro config, then run: gitpro apply"
659
+ if i.ssh_port:
660
+ fix += f" (it is {i.ssh_port} now; an identity has one ssh_port)"
661
+ else:
662
+ tail = f"it looks like {i.id}, but gitpro's https patterns never carry a port"
663
+ fix = f"switch {label} to an ssh URL for {i.id}"
664
+ return [(None, reason, i, pat, (tail, fix))]
665
+ if reason.endswith("user git@ in the URL") and is_codecommit(i.host):
666
+ return [(None, reason, i, pat, (
667
+ f"it looks like {i.id}, but CodeCommit's SSH user is the SSH key ID, not git",
668
+ f'set ssh_user = "<your SSH key ID>" on identity {i.id} in the gitpro config, then run: gitpro apply'))]
669
+ return [(fixed, reason, i, pat, None)]
670
+ from .render import url_patterns
671
+ for i in cfg.identities:
672
+ for pat in url_patterns(i):
673
+ prefix = pat.split("*", 1)[0]
674
+ if url.lower().startswith(prefix.lower()) and not url.startswith(prefix):
675
+ fixed = prefix + url[len(prefix):]
676
+ j, jpat = match_url(fixed, cfg)
677
+ if j is not None:
678
+ return [(fixed, "letter case differs; git matches case-sensitively", j, jpat, None)]
632
679
  return []
633
680
 
634
681
 
@@ -686,9 +733,12 @@ def compare(repo: Path, cfg: Config, *, remote: Optional[str] = None,
686
733
 
687
734
  if exp.match.identity is None:
688
735
  for name, url in candidates:
689
- fixes = _url_fixes(url, cfg)
736
+ fixes = _url_fixes(url, cfg, name or "the remote")
690
737
  if fixes:
691
- fixed, reason, i, pat = fixes[0]
738
+ fixed, reason, i, pat, note = fixes[0]
739
+ if note:
740
+ out.append(_p("url_form", f"{name or 'the remote'} is {redact_url(url)}; {note[0]}", None, note[1]))
741
+ break
692
742
  argv = ["git", "-C", str(top), "remote", "set-url", name, fixed] \
693
743
  if name and remote_url is None else None
694
744
  need = pat.replace("**", "…")
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