-
-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathapi.py
More file actions
274 lines (226 loc) · 9.16 KB
/
api.py
File metadata and controls
274 lines (226 loc) · 9.16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
"""Public Python API for commit-check.
This module exposes a lightweight, import-friendly interface that AI agents,
automation scripts, and tooling can call **without spawning a subprocess**.
All functions return plain dicts so results are easy to serialise, log, or
forward to an LLM.
Typical usage::
from commit_check.api import validate_message, validate_branch, validate_author
result = validate_message("feat: add streaming support")
if result["status"] == "fail":
for check in result["checks"]:
if check["status"] == "fail":
print(check["error"])
print(check["suggest"])
Return-value schema (all functions)::
{
"status": "pass" | "fail",
"checks": [
{
"check": "<rule name>",
"status": "pass" | "fail",
"value": "<actual value that was checked>",
"error": "<error description>",
"suggest": "<how to fix>",
},
...
]
}
"""
from __future__ import annotations
import copy
from typing import Any, Dict, Optional
from commit_check.config_merger import get_default_config
from commit_check.engine import (
CheckOutcome,
ValidationContext,
ValidationEngine,
)
from commit_check.rule_builder import RuleBuilder
# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------
def _build_result(outcomes: list[CheckOutcome]) -> Dict[str, Any]:
"""Convert a list of :class:`~commit_check.engine.CheckOutcome` into the
public return-value dict."""
overall = "fail" if any(o.status == "fail" for o in outcomes) else "pass"
return {
"status": overall,
"checks": [o.to_dict() for o in outcomes],
}
def _run_checks(
check_names: list[str],
context: ValidationContext,
config: Dict[str, Any],
) -> Dict[str, Any]:
"""Build rules, filter to *check_names*, run the engine, return result."""
rule_builder = RuleBuilder(config)
all_rules = rule_builder.build_all_rules()
filtered = [r for r in all_rules if r.check in check_names]
engine = ValidationEngine(filtered)
outcomes = engine.validate_all_detailed(context)
return _build_result(outcomes)
def _merge_config(user_config: Optional[Dict[str, Any]]) -> Dict[str, Any]:
"""Return the effective config: user overrides merged on top of defaults."""
base = get_default_config()
if user_config:
from commit_check.config_merger import deep_merge
# deep_copy the user config so that deep_merge cannot mutate the
# caller's dict (deep_merge operates in-place on `base`, and may
# assign nested objects from `override` directly into `base`).
deep_merge(base, copy.deepcopy(user_config))
return base
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
def validate_message(
message: str,
*,
config: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""Validate a commit message string.
:param message: The full commit message to validate (subject + optional body).
:param config: Optional configuration dict in the same shape as ``cchk.toml``.
If *None*, built-in defaults are used. You can pass a partial dict to
override only the keys you care about, e.g.
``{"commit": {"allow_commit_types": ["feat", "fix"]}}``.
:returns: A dict with ``"status"`` (``"pass"``/``"fail"``) and ``"checks"``
(list of per-rule outcomes).
Example::
>>> from commit_check.api import validate_message
>>> validate_message("feat: add streaming support")["status"]
'pass'
>>> validate_message("WIP bad message")["status"]
'fail'
"""
cfg = _merge_config(config)
context = ValidationContext(stdin_text=message.strip(), config=cfg)
check_names = [
"message",
"subject_imperative",
"subject_max_length",
"subject_min_length",
"subject_capitalized",
"require_signed_off_by",
"require_body",
"allow_merge_commits",
"allow_revert_commits",
"allow_empty_commits",
"allow_fixup_commits",
"allow_wip_commits",
]
return _run_checks(check_names, context, cfg)
def validate_branch(
branch: Optional[str] = None,
*,
config: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""Validate a branch name.
:param branch: Branch name to validate. If *None*, the current git branch
is used (via ``git branch --show-current``).
:param config: Optional configuration override dict.
:returns: A dict with ``"status"`` and ``"checks"``.
Example::
>>> from commit_check.api import validate_branch
>>> validate_branch("feature/add-json-output")["status"]
'pass'
>>> validate_branch("bad_branch_name")["status"]
'fail'
"""
cfg = _merge_config(config)
# Pass branch via stdin_text so BranchValidator picks it up without calling
# git. When branch is None the validator will fall back to git itself.
context = ValidationContext(
stdin_text=branch.strip() if branch else None,
config=cfg,
)
return _run_checks(["branch", "merge_base"], context, cfg)
def validate_author(
name: Optional[str] = None,
email: Optional[str] = None,
*,
config: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""Validate commit author name and/or email.
:param name: Author name to validate. If *None*, the value from
``git config user.name`` is used.
:param email: Author email to validate. If *None*, the value from
``git config user.email`` is used.
:param config: Optional configuration override dict.
:returns: A dict with ``"status"`` and ``"checks"``.
Example::
>>> from commit_check.api import validate_author
>>> validate_author(name="Ada Lovelace", email="ada@example.com")["status"]
'pass'
"""
cfg = _merge_config(config)
checks: list[str] = []
if name is not None:
checks.append("author_name")
if email is not None:
checks.append("author_email")
if not checks:
# Validate both from git config
checks = ["author_name", "author_email"]
# AuthorValidator reads from git config / git log when stdin_text is None.
# For an explicit single value we can only validate one at a time, so we
# run separate passes when both are supplied.
if name is not None and email is not None:
name_result = _run_checks(
["author_name"],
ValidationContext(stdin_text=name.strip(), config=cfg),
cfg,
)
email_result = _run_checks(
["author_email"],
ValidationContext(stdin_text=email.strip(), config=cfg),
cfg,
)
all_checks = name_result["checks"] + email_result["checks"]
overall = "fail" if any(c["status"] == "fail" for c in all_checks) else "pass"
return {"status": overall, "checks": all_checks}
stdin = None
if name is not None:
stdin = name.strip()
elif email is not None:
stdin = email.strip()
context = ValidationContext(stdin_text=stdin, config=cfg)
return _run_checks(checks, context, cfg)
def validate_all(
message: Optional[str] = None,
branch: Optional[str] = None,
author_name: Optional[str] = None,
author_email: Optional[str] = None,
*,
config: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""Run all requested validations and return a combined result.
This is the high-level entry point that mirrors the CLI ``commit-check -m -b``
invocation but returns structured data instead of printing to the terminal.
:param message: Commit message string to validate, or *None* to skip.
:param branch: Branch name to validate, or *None* to skip.
:param author_name: Author name to validate, or *None* to skip.
:param author_email: Author email to validate, or *None* to skip.
:param config: Optional configuration override dict.
:returns: A dict with ``"status"`` and ``"checks"`` combining all requested
validations.
Example::
>>> from commit_check.api import validate_all
>>> result = validate_all(
... message="feat: implement new feature",
... branch="feature/new-feature",
... )
>>> result["status"]
'pass'
"""
all_checks: list[Dict[str, Any]] = []
if message is not None:
msg_result = validate_message(message, config=config)
all_checks.extend(msg_result["checks"])
if branch is not None:
branch_result = validate_branch(branch, config=config)
all_checks.extend(branch_result["checks"])
if author_name is not None or author_email is not None:
author_result = validate_author(author_name, author_email, config=config)
all_checks.extend(author_result["checks"])
overall = "fail" if any(c["status"] == "fail" for c in all_checks) else "pass"
return {"status": overall, "checks": all_checks}