commit-shield 0.2.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.
commit_guard/config.py ADDED
@@ -0,0 +1,340 @@
1
+ """
2
+ Configuration loading for commit-guard.
3
+
4
+ Settings are resolved in order of increasing precedence:
5
+
6
+ 1. built-in defaults
7
+ 2. ``[tool.commit-guard]`` in ``pyproject.toml``
8
+ 3. ``.commit-guard.toml`` (whole file, or a ``[commit-guard]`` table)
9
+ 4. explicit command-line flags
10
+
11
+ Discovery walks upwards from the starting directory to the repository root, so
12
+ running ``git commit`` in a subdirectory picks up the project configuration.
13
+ Unknown keys are reported as errors rather than ignored, so that a typo such as
14
+ ``max_header_length`` does not silently leave the default in place.
15
+ """
16
+
17
+ import os
18
+ from typing import Any, Dict, List, Optional, Sequence, Set
19
+
20
+ from commit_guard._toml import TomlError, loads
21
+
22
+ __all__ = ["Config", "ConfigError", "load_config", "find_config_files"]
23
+
24
+ CONFIG_FILENAME = ".commit-guard.toml"
25
+ PYPROJECT_FILENAME = "pyproject.toml"
26
+ TOOL_TABLE = "commit-guard"
27
+
28
+
29
+ class ConfigError(Exception):
30
+ """Raised when a configuration file is malformed or has invalid values."""
31
+
32
+
33
+ # Conventional Commit types recognised unless configuration overrides them.
34
+ DEFAULT_TYPES = (
35
+ "feat",
36
+ "fix",
37
+ "docs",
38
+ "style",
39
+ "refactor",
40
+ "perf",
41
+ "test",
42
+ "build",
43
+ "ci",
44
+ "chore",
45
+ "revert",
46
+ )
47
+
48
+ # Filename patterns that should never be committed. These are regexes matched
49
+ # against the forward-slash-normalised path.
50
+ DEFAULT_SENSITIVE_PATTERNS = (
51
+ r"(^|/)\.env($|\..*)",
52
+ r"(^|/).*\.(pem|pkcs12|pfx|p12|jks|keystore)$",
53
+ r"(^|/)id_(rsa|dsa|ed25519|ecdsa)($|\..*)",
54
+ r"(^|/).*(secret|password|credential)s?.*\.(json|ya?ml|txt|ini|cfg)$",
55
+ r"(^|/)\.(npmrc|pypirc|netrc)$",
56
+ r"(^|/)\.aws/credentials$",
57
+ r"(^|/).*\.key$",
58
+ )
59
+
60
+ # Paths excluded from sensitive-name matching by default. Sample and template
61
+ # files are published deliberately, and ``*.key`` has enough innocent uses that
62
+ # shipping these keeps the first run from being all false positives.
63
+ DEFAULT_ALLOWLIST = (
64
+ "*.example",
65
+ "*.sample",
66
+ "*.template",
67
+ "*.dist",
68
+ ".env.example",
69
+ ".env.sample",
70
+ ".env.template",
71
+ )
72
+
73
+ # Commit subjects starting with any of these are generated by git itself or by
74
+ # an interactive rebase, and are not the author's to format.
75
+ DEFAULT_SKIP_PREFIXES = (
76
+ "merge ",
77
+ "revert ",
78
+ "fixup!",
79
+ "squash!",
80
+ "amend!",
81
+ )
82
+
83
+ _BOOL_KEYS = (
84
+ "require_scope",
85
+ "allow_trailing_period",
86
+ "skip_merge_commits",
87
+ )
88
+ _INT_KEYS = (
89
+ "max_header_len",
90
+ "max_body_line_len",
91
+ "subject_min_len",
92
+ )
93
+ _FLOAT_KEYS = ("max_size_mb",)
94
+ _LIST_KEYS = (
95
+ "types",
96
+ "scopes",
97
+ "sensitive_patterns",
98
+ "extra_sensitive_patterns",
99
+ "allowlist",
100
+ "skip_prefixes",
101
+ )
102
+
103
+ KNOWN_KEYS = frozenset(_BOOL_KEYS + _INT_KEYS + _FLOAT_KEYS + _LIST_KEYS)
104
+
105
+
106
+ class Config:
107
+ """Resolved commit-guard settings."""
108
+
109
+ __slots__ = (
110
+ "types",
111
+ "scopes",
112
+ "require_scope",
113
+ "max_header_len",
114
+ "max_body_line_len",
115
+ "subject_min_len",
116
+ "allow_trailing_period",
117
+ "max_size_mb",
118
+ "sensitive_patterns",
119
+ "allowlist",
120
+ "skip_merge_commits",
121
+ "skip_prefixes",
122
+ "sources",
123
+ )
124
+
125
+ def __init__(
126
+ self,
127
+ types: Sequence[str] = DEFAULT_TYPES,
128
+ scopes: Sequence[str] = (),
129
+ require_scope: bool = False,
130
+ max_header_len: int = 72,
131
+ max_body_line_len: int = 0,
132
+ subject_min_len: int = 3,
133
+ allow_trailing_period: bool = False,
134
+ max_size_mb: float = 10.0,
135
+ sensitive_patterns: Sequence[str] = DEFAULT_SENSITIVE_PATTERNS,
136
+ allowlist: Sequence[str] = DEFAULT_ALLOWLIST,
137
+ skip_merge_commits: bool = True,
138
+ skip_prefixes: Sequence[str] = DEFAULT_SKIP_PREFIXES,
139
+ sources: Sequence[str] = (),
140
+ ) -> None:
141
+ self.types: Set[str] = set(types)
142
+ self.scopes: Set[str] = set(scopes)
143
+ self.require_scope = require_scope
144
+ self.max_header_len = max_header_len
145
+ # 0 disables the body line-length check.
146
+ self.max_body_line_len = max_body_line_len
147
+ self.subject_min_len = subject_min_len
148
+ self.allow_trailing_period = allow_trailing_period
149
+ self.max_size_mb = max_size_mb
150
+ self.sensitive_patterns: List[str] = list(sensitive_patterns)
151
+ self.allowlist: List[str] = list(allowlist)
152
+ self.skip_merge_commits = skip_merge_commits
153
+ self.skip_prefixes: List[str] = [p.lower() for p in skip_prefixes]
154
+ self.sources: List[str] = list(sources)
155
+
156
+ def __repr__(self) -> str:
157
+ return "Config(types={0}, max_header_len={1}, sources={2})".format(
158
+ sorted(self.types), self.max_header_len, self.sources
159
+ )
160
+
161
+
162
+ def find_config_files(start_dir: Optional[str] = None) -> List[str]:
163
+ """
164
+ Locate configuration files, nearest-last so later files win.
165
+
166
+ Walks upward from ``start_dir`` to the filesystem root, stopping after the
167
+ directory that contains ``.git``. Returns absolute paths.
168
+ """
169
+ base = os.path.abspath(start_dir or os.getcwd())
170
+ chain: List[str] = []
171
+
172
+ current = base
173
+ while True:
174
+ chain.append(current)
175
+ if os.path.exists(os.path.join(current, ".git")):
176
+ break
177
+ parent = os.path.dirname(current)
178
+ if parent == current:
179
+ break
180
+ current = parent
181
+
182
+ found: List[str] = []
183
+ # Reversed so the outermost directory is processed first and the nearest
184
+ # configuration file ends up last in the list.
185
+ for directory in reversed(chain):
186
+ for name in (PYPROJECT_FILENAME, CONFIG_FILENAME):
187
+ candidate = os.path.join(directory, name)
188
+ if os.path.isfile(candidate):
189
+ found.append(candidate)
190
+ return found
191
+
192
+
193
+ def _extract_table(data: Dict[str, Any], path: str) -> Optional[Dict[str, Any]]:
194
+ """
195
+ Pull the commit-guard settings table out of one parsed file.
196
+
197
+ ``pyproject.toml`` must use ``[tool.commit-guard]``. A dedicated
198
+ ``.commit-guard.toml`` may either use a ``[commit-guard]`` table or place
199
+ settings at the top level.
200
+ """
201
+ if os.path.basename(path) == PYPROJECT_FILENAME:
202
+ tool = data.get("tool")
203
+ if not isinstance(tool, dict):
204
+ return None
205
+ section = tool.get(TOOL_TABLE)
206
+ if section is None:
207
+ return None
208
+ if not isinstance(section, dict):
209
+ raise ConfigError(
210
+ "{0}: [tool.{1}] must be a table".format(path, TOOL_TABLE)
211
+ )
212
+ return section
213
+
214
+ section = data.get(TOOL_TABLE)
215
+ if isinstance(section, dict):
216
+ return section
217
+ # Top-level settings; drop nested tables so a stray [tool] block in a
218
+ # standalone config file does not look like a setting.
219
+ return {k: v for k, v in data.items() if not isinstance(v, dict)}
220
+
221
+
222
+ def _coerce(path: str, key: str, value: Any) -> Any:
223
+ """Validate one setting's type, raising ConfigError with the file name."""
224
+ where = "{0}: '{1}'".format(path, key)
225
+
226
+ if key in _BOOL_KEYS:
227
+ if not isinstance(value, bool):
228
+ raise ConfigError("{0} must be true or false".format(where))
229
+ return value
230
+
231
+ if key in _INT_KEYS:
232
+ # bool is a subclass of int; reject it explicitly.
233
+ if isinstance(value, bool) or not isinstance(value, int):
234
+ raise ConfigError("{0} must be an integer".format(where))
235
+ if value < 0:
236
+ raise ConfigError("{0} must not be negative".format(where))
237
+ return value
238
+
239
+ if key in _FLOAT_KEYS:
240
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
241
+ raise ConfigError("{0} must be a number".format(where))
242
+ if value <= 0:
243
+ raise ConfigError("{0} must be greater than zero".format(where))
244
+ return float(value)
245
+
246
+ if key in _LIST_KEYS:
247
+ if not isinstance(value, list) or not all(
248
+ isinstance(item, str) for item in value
249
+ ):
250
+ raise ConfigError("{0} must be a list of strings".format(where))
251
+ return list(value)
252
+
253
+ raise ConfigError(
254
+ "{0} is not a recognised setting. Valid keys: {1}".format(
255
+ where, ", ".join(sorted(KNOWN_KEYS))
256
+ )
257
+ )
258
+
259
+
260
+ def _apply(config: Config, path: str, section: Dict[str, Any]) -> None:
261
+ """Merge one settings table onto an existing Config in place."""
262
+ values: Dict[str, Any] = {}
263
+ for key, raw in section.items():
264
+ values[key] = _coerce(path, key, raw)
265
+
266
+ if "types" in values:
267
+ if not values["types"]:
268
+ raise ConfigError("{0}: 'types' must not be empty".format(path))
269
+ config.types = set(values["types"])
270
+ if "scopes" in values:
271
+ config.scopes = set(values["scopes"])
272
+ if "sensitive_patterns" in values:
273
+ config.sensitive_patterns = list(values["sensitive_patterns"])
274
+ # extra_sensitive_patterns adds to whatever is in effect, so a project can
275
+ # extend the defaults without restating them.
276
+ if "extra_sensitive_patterns" in values:
277
+ config.sensitive_patterns = config.sensitive_patterns + list(
278
+ values["extra_sensitive_patterns"]
279
+ )
280
+ if "allowlist" in values:
281
+ config.allowlist = list(values["allowlist"])
282
+ if "skip_prefixes" in values:
283
+ config.skip_prefixes = [p.lower() for p in values["skip_prefixes"]]
284
+
285
+ for key in _BOOL_KEYS + _INT_KEYS + _FLOAT_KEYS:
286
+ if key in values:
287
+ setattr(config, key, values[key])
288
+
289
+ if config.require_scope and not config.scopes:
290
+ raise ConfigError(
291
+ "{0}: 'require_scope' is set but 'scopes' is empty".format(path)
292
+ )
293
+
294
+ config.sources.append(path)
295
+
296
+
297
+ def load_config(
298
+ start_dir: Optional[str] = None,
299
+ config_path: Optional[str] = None,
300
+ use_files: bool = True,
301
+ ) -> Config:
302
+ """
303
+ Build a Config from discovered files.
304
+
305
+ ``config_path`` names a single file explicitly and disables discovery; a
306
+ missing file is an error in that case, since the user asked for it by name.
307
+ ``use_files=False`` returns the built-in defaults, for ``--no-config``.
308
+ """
309
+ config = Config()
310
+ if not use_files:
311
+ return config
312
+
313
+ if config_path:
314
+ if not os.path.isfile(config_path):
315
+ raise ConfigError("configuration file not found: {0}".format(config_path))
316
+ paths = [os.path.abspath(config_path)]
317
+ else:
318
+ paths = find_config_files(start_dir)
319
+
320
+ for path in paths:
321
+ try:
322
+ with open(path, encoding="utf-8") as handle:
323
+ text = handle.read()
324
+ except OSError as exc:
325
+ raise ConfigError("cannot read {0}: {1}".format(path, exc)) from exc
326
+
327
+ try:
328
+ data = loads(text)
329
+ except TomlError as exc:
330
+ # A broken pyproject.toml is somebody else's problem unless the user
331
+ # pointed us at it; skipping keeps commit-guard usable meanwhile.
332
+ if os.path.basename(path) == PYPROJECT_FILENAME and not config_path:
333
+ continue
334
+ raise ConfigError("cannot parse {0}: {1}".format(path, exc)) from exc
335
+
336
+ section = _extract_table(data, path)
337
+ if section:
338
+ _apply(config, path, section)
339
+
340
+ return config
@@ -0,0 +1,106 @@
1
+ """
2
+ Git interaction helpers.
3
+
4
+ Every function here either returns a trustworthy answer or raises
5
+ :class:`GitError`. Callers must not treat a git failure as "nothing to check":
6
+ v0.1.0 did, which made the file checks silently pass whenever git was missing,
7
+ the repository was unusable, or the command changed behaviour.
8
+ """
9
+
10
+ import os
11
+ import subprocess
12
+ from typing import List, Optional
13
+
14
+ __all__ = [
15
+ "GitError",
16
+ "run_git",
17
+ "is_git_repo",
18
+ "git_dir",
19
+ "repo_root",
20
+ "staged_files",
21
+ "staged_blob_size",
22
+ ]
23
+
24
+
25
+ class GitError(Exception):
26
+ """Raised when a git command cannot be run or fails."""
27
+
28
+
29
+ def run_git(args: List[str], cwd: Optional[str] = None) -> str:
30
+ """Run a git command and return stdout, raising GitError on any failure."""
31
+ try:
32
+ completed = subprocess.run(
33
+ ["git"] + args,
34
+ cwd=cwd,
35
+ capture_output=True,
36
+ check=False,
37
+ )
38
+ except FileNotFoundError as exc:
39
+ raise GitError("git executable not found on PATH") from exc
40
+ except OSError as exc:
41
+ raise GitError("cannot run git: {0}".format(exc)) from exc
42
+
43
+ if completed.returncode != 0:
44
+ detail = completed.stderr.decode("utf-8", "replace").strip()
45
+ raise GitError(
46
+ "git {0} failed (exit {1}){2}".format(
47
+ " ".join(args),
48
+ completed.returncode,
49
+ ": " + detail if detail else "",
50
+ )
51
+ )
52
+ return completed.stdout.decode("utf-8", "replace")
53
+
54
+
55
+ def is_git_repo(cwd: Optional[str] = None) -> bool:
56
+ """Report whether cwd is inside a git working tree."""
57
+ try:
58
+ return run_git(["rev-parse", "--is-inside-work-tree"], cwd).strip() == "true"
59
+ except GitError:
60
+ return False
61
+
62
+
63
+ def git_dir(cwd: Optional[str] = None) -> str:
64
+ """Absolute path to the repository's .git directory."""
65
+ path = run_git(["rev-parse", "--absolute-git-dir"], cwd).strip()
66
+ if not path:
67
+ raise GitError("could not determine the .git directory")
68
+ return path
69
+
70
+
71
+ def repo_root(cwd: Optional[str] = None) -> str:
72
+ """Absolute path to the top level of the working tree."""
73
+ path = run_git(["rev-parse", "--show-toplevel"], cwd).strip()
74
+ if not path:
75
+ raise GitError("could not determine the repository root")
76
+ return os.path.normpath(path)
77
+
78
+
79
+ def staged_files(cwd: Optional[str] = None) -> List[str]:
80
+ """
81
+ Paths staged for commit, added/copied/modified only.
82
+
83
+ Uses -z so that paths containing spaces, quotes or non-ASCII characters
84
+ survive intact; the default output quotes and escapes such names.
85
+ """
86
+ out = run_git(["diff", "--cached", "--name-only", "--diff-filter=ACM", "-z"], cwd)
87
+ return [entry for entry in out.split("\0") if entry]
88
+
89
+
90
+ def staged_blob_size(path: str, cwd: Optional[str] = None) -> Optional[int]:
91
+ """
92
+ Size in bytes of the staged blob for ``path``.
93
+
94
+ This is the content git would actually commit, which is not necessarily
95
+ what is on disk: the working-tree file may have been modified or deleted
96
+ after ``git add``. Returns None when the path has no staged blob.
97
+ """
98
+ spec = ":{0}".format(path.replace("\\", "/"))
99
+ try:
100
+ out = run_git(["cat-file", "-s", spec], cwd)
101
+ except GitError:
102
+ return None
103
+ try:
104
+ return int(out.strip())
105
+ except ValueError:
106
+ return None
@@ -0,0 +1,179 @@
1
+ Metadata-Version: 2.5
2
+ Name: commit-shield
3
+ Version: 0.2.0
4
+ Summary: Fast, zero-dependency Git commit message and staged file linter.
5
+ Project-URL: Homepage, https://github.com/lui01212/commit-guard
6
+ Project-URL: Repository, https://github.com/lui01212/commit-guard.git
7
+ Project-URL: Issues, https://github.com/lui01212/commit-guard/issues
8
+ Author-email: lui01212 <luynbv@glotechvn.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: commit,conventional-commits,developer-tools,git,linter,pre-commit,security
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.8
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Software Development :: Quality Assurance
25
+ Classifier: Topic :: Software Development :: Version Control :: Git
26
+ Requires-Python: >=3.8
27
+ Provides-Extra: dev
28
+ Requires-Dist: build; extra == 'dev'
29
+ Requires-Dist: mypy==1.11.2; extra == 'dev'
30
+ Requires-Dist: ruff==0.6.9; extra == 'dev'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # commit-shield 🛡️
34
+
35
+ [![PyPI version](https://img.shields.io/pypi/v/commit-shield.svg)](https://pypi.org/project/commit-shield/)
36
+ [![Python versions](https://img.shields.io/pypi/pyversions/commit-shield.svg)](https://pypi.org/project/commit-shield/)
37
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
38
+ [![Tests](https://github.com/lui01212/commit-guard/actions/workflows/ci.yml/badge.svg)](https://github.com/lui01212/commit-guard/actions)
39
+ [![good first issues](https://img.shields.io/github/issues/lui01212/commit-guard/good%20first%20issue?label=good%20first%20issues&color=7057ff)](https://github.com/lui01212/commit-guard/issues?q=is%3Aissue+state%3Aopen+label%3A%22good+first+issue%22)
40
+ [![Hacktoberfest](https://img.shields.io/badge/Hacktoberfest-2024-ff7a59?logo=hacktoberfest)](https://hacktoberfest.com/)
41
+
42
+ **Fast, zero-dependency Git commit message and staged file linter.**
43
+
44
+ Enforces [Conventional Commits](https://www.conventionalcommits.org/) standards and guards against accidental commits of secrets (`.env`, `.pem`, `id_rsa`) or oversized files (> 10MB).
45
+
46
+ ---
47
+
48
+ ## ⚡ Why commit-shield?
49
+
50
+ - **Zero dependencies:** Written in pure standard Python. Instant install, lightweight, no massive node_modules or heavy binary dependencies.
51
+ - **Fast:** Runs in milliseconds during `git commit`.
52
+ - **Pre-commit ready:** Seamless drop-in integration with the popular `pre-commit` framework.
53
+ - **Dual protection:** Validates both commit message formatting AND safeguards against accidentally committed credentials/blobs.
54
+
55
+ ---
56
+
57
+ ## 📦 Installation
58
+
59
+ ```bash
60
+ pip install commit-shield
61
+ ```
62
+
63
+ Or install from source:
64
+ ```bash
65
+ git clone https://github.com/lui01212/commit-guard.git
66
+ cd commit-guard
67
+ pip install -e .
68
+ ```
69
+
70
+ ---
71
+
72
+ ## 🚀 Usage
73
+
74
+ ### 1. Check Commit Messages
75
+
76
+ Validate commit message strings directly:
77
+
78
+ ```bash
79
+ # Valid commit message -> exit code 0
80
+ commit-guard check-msg -m "feat(auth): add google oauth2 login provider"
81
+
82
+ # Invalid commit message -> exit code 1 with actionable errors
83
+ commit-guard check-msg -m "fixed stuff"
84
+ ```
85
+
86
+ Output:
87
+ ```text
88
+ [commit-guard] Commit message validation failed:
89
+ - Header does not follow Conventional Commits format: '<type>(<scope>): <description>'.
90
+ Received: 'fixed stuff'
91
+ Allowed types: build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test
92
+ ```
93
+
94
+ ### 2. Check Staged Files (Secrets & Large Blobs)
95
+
96
+ ```bash
97
+ # Checks all currently staged files in git
98
+ commit-guard check-files --strict
99
+ ```
100
+
101
+ ### 3. One-Click Git Hook Setup (No pre-commit framework needed)
102
+
103
+ Install the hook directly into your local `.git/hooks/commit-msg`:
104
+
105
+ ```bash
106
+ commit-guard install
107
+ ```
108
+
109
+ ---
110
+
111
+ ## 🔧 Integration with `pre-commit`
112
+
113
+ Add this to your `.pre-commit-config.yaml`:
114
+
115
+ ```yaml
116
+ repos:
117
+ - repo: https://github.com/lui01212/commit-guard
118
+ rev: v0.2.0
119
+ hooks:
120
+ - id: commit-guard-msg
121
+ - id: commit-guard-files
122
+ ```
123
+
124
+ ---
125
+
126
+ ## ⚙️ Configuration
127
+
128
+ `commit-guard` works zero-config out of the box, but can be fully customized via `.commit-guard.toml` or `[tool.commit-guard]` in `pyproject.toml`:
129
+
130
+ ```toml
131
+ # .commit-guard.toml
132
+ max_header_len = 72
133
+ require_scope = false
134
+ skip_merge_commits = true
135
+ max_size_mb = 10.0
136
+
137
+ # Add extra sensitive patterns to protect
138
+ extra_sensitive_patterns = ["*.secret", "*_token.json"]
139
+
140
+ # Allowlist false-positives
141
+ allowlist = ["*.example", "*.sample", "*.template"]
142
+ ```
143
+
144
+ See [.commit-guard.toml.example](.commit-guard.toml.example) for all available options.
145
+
146
+ ---
147
+
148
+ ## 📋 Allowed Commit Types
149
+
150
+ | Type | Purpose |
151
+ | :--- | :--- |
152
+ | `feat` | A new feature |
153
+ | `fix` | A bug fix |
154
+ | `docs` | Documentation only changes |
155
+ | `style` | Formatting, missing semi-colons, white-space changes |
156
+ | `refactor` | Code restructuring without fixing bugs or adding features |
157
+ | `perf` | Performance improvement |
158
+ | `test` | Adding missing tests or correcting existing tests |
159
+ | `build` | Changes that affect the build system or dependencies |
160
+ | `ci` | Changes to CI configuration files and scripts |
161
+ | `chore` | Maintenance tasks, tooling updates |
162
+ | `revert` | Reverting a previous commit |
163
+
164
+ ---
165
+
166
+ ## 🤝 Contributing
167
+
168
+ Contributions are warmly welcomed! We have plenty of beginner-friendly tasks:
169
+ - Adding custom pattern checks.
170
+ - Expanding sensitive file extension detections.
171
+ - Adding localized error messages (Vietnamese, Spanish, etc.).
172
+
173
+ Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to get started.
174
+
175
+ ---
176
+
177
+ ## 📄 License
178
+
179
+ [MIT License](LICENSE) © 2026 lui01212
@@ -0,0 +1,11 @@
1
+ commit_guard/__init__.py,sha256=TZJxOCvu8aTIKoQClPZU-fen_hVfyKLoayOmMay3-8E,110
2
+ commit_guard/_toml.py,sha256=cbGmGwfJayHIrJu-CDzDMdacpS_Ys-QwpNouW8ZKKhw,7847
3
+ commit_guard/checkers.py,sha256=TOoXmNLGlm3bO6rjtC83pzUdEp7ib3uh2W9BEtLMLWM,9126
4
+ commit_guard/cli.py,sha256=cT9gzRfWsB4NfeZ1Cy7W6XD8ftZ-YgQf462Jmp71EbA,13061
5
+ commit_guard/config.py,sha256=aylDj6hTB58g4QJNLfxDNBYEGBY2iGgU86zJMpPsF2s,11102
6
+ commit_guard/gitutil.py,sha256=7db5sJ7llHk6mJLrcSQ9xetDJhhHvIopRDzMY5OMj8w,3351
7
+ commit_shield-0.2.0.dist-info/METADATA,sha256=jwp9ni46H7j9lct9Vk1smdJmhx7sgQjzmti-HamX06M,5891
8
+ commit_shield-0.2.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
9
+ commit_shield-0.2.0.dist-info/entry_points.txt,sha256=miu01XDEo2TnpI6yISlO1teLXGnR3nMrfBpkoiqqNkM,93
10
+ commit_shield-0.2.0.dist-info/licenses/LICENSE,sha256=RWxza2uvDjCv6Rh2BuAIA6Y_trYT1WF7EZ5fUFYldWc,1065
11
+ commit_shield-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ commit-guard = commit_guard.cli:main
3
+ commit-shield = commit_guard.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 lui01212
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.