precommit-swear 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
precommit_swear/cli.py ADDED
@@ -0,0 +1,104 @@
1
+ import argparse
2
+ import stat
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ def make_hook_content(ignore: list[str] | None = None) -> str:
7
+ parts = ["#!/usr/bin/env sh", "# precommit-swear", "precommit-swear hook"]
8
+ if ignore:
9
+ for word in ignore:
10
+ parts[-1] += f" --ignore {word}"
11
+ return "\n".join(parts) + "\n"
12
+
13
+ SENTINEL = "# precommit-swear"
14
+
15
+
16
+ def install(repo: Path, force: bool, ignore: list[str] | None = None) -> None:
17
+ git_dir = repo / ".git"
18
+ if not git_dir.is_dir():
19
+ print(f"Error: {repo} is not a git repository.", file=sys.stderr)
20
+ sys.exit(1)
21
+
22
+ hooks_dir = git_dir / "hooks"
23
+ hooks_dir.mkdir(exist_ok=True)
24
+ hook_file = hooks_dir / "pre-commit"
25
+
26
+ if hook_file.exists() and not force:
27
+ print(
28
+ f"Error: {hook_file} already exists. Use --force to overwrite.",
29
+ file=sys.stderr,
30
+ )
31
+ sys.exit(1)
32
+
33
+ hook_file.write_text(make_hook_content(ignore), encoding="utf-8")
34
+ hook_file.chmod(hook_file.stat().st_mode | stat.S_IEXEC)
35
+ print(f"Installed pre-commit hook into {repo}")
36
+
37
+
38
+ def uninstall(repo: Path) -> None:
39
+ hook_file = repo / ".git" / "hooks" / "pre-commit"
40
+
41
+ if not hook_file.exists():
42
+ print("No pre-commit hook found.", file=sys.stderr)
43
+ sys.exit(1)
44
+
45
+ content = hook_file.read_text(encoding="utf-8")
46
+ if SENTINEL not in content:
47
+ print(
48
+ "Error: existing pre-commit hook was not installed by precommit-swear.",
49
+ file=sys.stderr,
50
+ )
51
+ sys.exit(1)
52
+
53
+ hook_file.unlink()
54
+ print(f"Uninstalled pre-commit hook from {repo}")
55
+
56
+
57
+ def main() -> None:
58
+ parser = argparse.ArgumentParser(prog="precommit-swear")
59
+ sub = parser.add_subparsers(dest="command")
60
+
61
+ install_parser = sub.add_parser(
62
+ "install", help="Install the pre-commit hook into a repo"
63
+ )
64
+ install_parser.add_argument(
65
+ "--repo", default=".", help="Path to the git repo (default: current directory)"
66
+ )
67
+ install_parser.add_argument(
68
+ "--force", action="store_true", help="Overwrite existing pre-commit hook"
69
+ )
70
+ install_parser.add_argument(
71
+ "--ignore",
72
+ action="append",
73
+ default=[],
74
+ help="Word to ignore (can be specified multiple times)",
75
+ )
76
+
77
+ uninstall_parser = sub.add_parser(
78
+ "uninstall", help="Remove the pre-commit hook from a repo"
79
+ )
80
+ uninstall_parser.add_argument(
81
+ "--repo", default=".", help="Path to the git repo (default: current directory)"
82
+ )
83
+
84
+ hook_parser = sub.add_parser("hook", help="Run the hook (called by git)")
85
+ hook_parser.add_argument(
86
+ "--ignore",
87
+ action="append",
88
+ default=[],
89
+ help="Word to ignore (can be specified multiple times)",
90
+ )
91
+
92
+ args = parser.parse_args()
93
+
94
+ if args.command == "install":
95
+ install(Path(args.repo).resolve(), args.force, args.ignore or None)
96
+ elif args.command == "uninstall":
97
+ uninstall(Path(args.repo).resolve())
98
+ elif args.command == "hook":
99
+ from precommit_swear.hook import main as hook_main
100
+
101
+ hook_main(ignore=set(args.ignore) if args.ignore else None)
102
+ else:
103
+ parser.print_help()
104
+ sys.exit(1)
@@ -0,0 +1,39 @@
1
+ import subprocess
2
+ import sys
3
+
4
+ from precommit_swear.scanner import scan_diff
5
+
6
+
7
+ def main(ignore: set[str] | None = None) -> None:
8
+ result = subprocess.run(
9
+ ["git", "diff", "--cached", "--diff-filter=ACMR"],
10
+ capture_output=True,
11
+ text=True,
12
+ )
13
+
14
+ if result.returncode != 0:
15
+ print("precommit-swear: failed to get staged diff", file=sys.stderr)
16
+ sys.exit(1)
17
+
18
+ if not result.stdout.strip():
19
+ sys.exit(0)
20
+
21
+ matches = scan_diff(result.stdout, ignore=ignore)
22
+ if not matches:
23
+ sys.exit(0)
24
+
25
+ print("=" * 40, file=sys.stderr)
26
+ print(" precommit-swear: bad words detected!", file=sys.stderr)
27
+ print("=" * 40, file=sys.stderr)
28
+ print(file=sys.stderr)
29
+
30
+ for filename, line_no, word, line_text in matches:
31
+ print(f" {filename}:{line_no} '{word}' -> {line_text.strip()}", file=sys.stderr)
32
+
33
+ print(file=sys.stderr)
34
+ print("Remove them and try again, or use 'git commit --no-verify' to skip.", file=sys.stderr)
35
+ sys.exit(1)
36
+
37
+
38
+ if __name__ == "__main__":
39
+ main()
@@ -0,0 +1,44 @@
1
+ import re
2
+ from importlib.resources import files
3
+
4
+
5
+ def load_bad_words() -> set[str]:
6
+ text = files("precommit_swear").joinpath("bad-words.txt").read_text(encoding="utf-8")
7
+ return {word.lower() for line in text.splitlines() if (word := line.strip())}
8
+
9
+
10
+ def scan_diff(
11
+ diff_text: str, ignore: set[str] | None = None
12
+ ) -> list[tuple[str, int, str, str]]:
13
+ """Scan a unified diff for bad words.
14
+
15
+ Returns a list of (filename, line_number, matched_word, line_text) tuples.
16
+ """
17
+ bad_words = load_bad_words()
18
+ if ignore:
19
+ bad_words -= {w.lower() for w in ignore}
20
+ matches = []
21
+ current_file = ""
22
+ current_line = 0
23
+
24
+ for line in diff_text.splitlines():
25
+ if line.startswith("+++ b/"):
26
+ current_file = line[6:]
27
+ continue
28
+
29
+ hunk = re.match(r"^@@ -\d+(?:,\d+)? \+(\d+)", line)
30
+ if hunk:
31
+ current_line = int(hunk.group(1))
32
+ continue
33
+
34
+ if line.startswith("+") and not line.startswith("+++"):
35
+ content = line[1:]
36
+ tokens = re.split(r"[^a-zA-Z]+", content)
37
+ for token in tokens:
38
+ if token and token.lower() in bad_words:
39
+ matches.append((current_file, current_line, token, content))
40
+ current_line += 1
41
+ elif not line.startswith("-"):
42
+ current_line += 1
43
+
44
+ return matches
@@ -0,0 +1,5 @@
1
+ Metadata-Version: 2.4
2
+ Name: precommit-swear
3
+ Version: 0.1.0
4
+ Summary: Git pre-commit hook that blocks commits containing bad words
5
+ Requires-Python: >=3.9
@@ -0,0 +1,9 @@
1
+ precommit_swear/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ precommit_swear/bad-words.txt,sha256=dkJScK8Opw4itvAUW6Zw1kvfQwTawBcyTCs9FOqblf4,11169
3
+ precommit_swear/cli.py,sha256=s7Q8GDZzjKyWSPJovDoV-tvPYA9O_YmMya495UOpwDk,3198
4
+ precommit_swear/hook.py,sha256=Rs8AuMbyefvMcUV6W--dYunpf5ThYS9DdI3s3Z7x5oM,1050
5
+ precommit_swear/scanner.py,sha256=-rght6zLdWCfJfmt00Fv-QLUSnaPEixq9Mr5CqJiA9w,1370
6
+ precommit_swear-0.1.0.dist-info/METADATA,sha256=vT9mF-vMSC0T4XhLLXCGCy0B2b4OH2NjsR_pIia5ZyU,152
7
+ precommit_swear-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
8
+ precommit_swear-0.1.0.dist-info/entry_points.txt,sha256=LJK61Frie0JMtxBR8A3EuDslis0myZnCjMbunKBxEhY,61
9
+ precommit_swear-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ precommit-swear = precommit_swear.cli:main