Skip to content

Commit 3ec6ef3

Browse files
authored
Merge pull request #27 from benner/feat/configurable-max-subject-length
feat: make maximum subject length configurable
2 parents 9e71e8e + 3c2e1a8 commit 3ec6ef3

3 files changed

Lines changed: 137 additions & 8 deletions

File tree

README.md

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,15 @@ Available checks:
8080
* `signed-off` - `Signed-off-by:` trailer exists
8181
* `signature` - Verify GPG or SSH signature
8282

83+
### Subject length
84+
85+
The default maximum subject line length is 72 characters. Override with
86+
`--max-subject-length`:
87+
88+
```bash
89+
commit-guard --max-subject-length 100
90+
```
91+
8392
### Type validation
8493

8594
By default the standard conventional commit types are accepted. Use `--types`
@@ -113,25 +122,27 @@ commit-guard --scopes auth,api --require-scope
113122
### Configuration file
114123

115124
Place `.commit-guard.toml` in your project root (or any parent directory) to
116-
set defaults for `enable`, `disable`, `scopes`, `require-scope`, and `types`.
117-
commit-guard searches upward from the working directory and uses the first file
118-
found.
125+
set defaults for `enable`, `disable`, `scopes`, `require-scope`, `types`, and
126+
`max-subject-length`. commit-guard searches upward from the working directory
127+
and uses the first file found.
119128

120129
```toml
121130
# .commit-guard.toml
122131
disable = ["signature", "body"]
123132
scopes = ["auth", "api", "db"]
124133
require-scope = true
125134
types = ["feat", "fix", "chore", "wip"]
135+
max-subject-length = 100
126136
```
127137

128138
```toml
129139
# .commit-guard.toml
130140
enable = ["subject", "imperative"]
131141
```
132142

133-
CLI flags (`--enable`, `--disable`, `--scopes`, `--require-scope`, `--types`)
134-
take full precedence and ignore config file values when provided.
143+
CLI flags (`--enable`, `--disable`, `--scopes`, `--require-scope`, `--types`,
144+
`--max-subject-length`) take full precedence and ignore config file values when
145+
provided.
135146

136147
### Checking a range of commits
137148

src/git_commit_guard/__init__.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,12 @@ def _strip_comments(message):
120120
)
121121

122122

123-
def check_subject(
123+
def check_subject( # noqa: PLR0913 Too many arguments in function definition (6 > 5)
124124
line,
125125
result,
126126
allowed_scopes=frozenset(),
127127
allowed_types=TYPES,
128+
max_subject_length=MAX_SUBJECT_LEN,
128129
*,
129130
require_scope=False,
130131
):
@@ -147,8 +148,8 @@ def check_subject(
147148
result.error("description must not start with uppercase")
148149
if desc.endswith("."):
149150
result.error("description must not end with period")
150-
if len(line) > MAX_SUBJECT_LEN:
151-
result.error(f"subject too long: {len(line)} > {MAX_SUBJECT_LEN}")
151+
if len(line) > max_subject_length:
152+
result.error(f"subject too long: {len(line)} > {max_subject_length}")
152153
return desc
153154

154155

@@ -231,6 +232,7 @@ class Args:
231232
allowed_scopes: frozenset
232233
require_scope: bool
233234
allowed_types: frozenset
235+
max_subject_length: int
234236

235237

236238
def _resolve_enabled(args, config, parser):
@@ -249,6 +251,14 @@ def _resolve_enabled(args, config, parser):
249251
return enabled
250252

251253

254+
def _resolve_max_subject_length(args, config):
255+
if args.max_subject_length is not None:
256+
return args.max_subject_length
257+
if "max-subject-length" in config:
258+
return config["max-subject-length"]
259+
return MAX_SUBJECT_LEN
260+
261+
252262
def _resolve_types(args, config):
253263
if args.types:
254264
return frozenset(t.strip() for t in args.types.split(","))
@@ -313,11 +323,19 @@ def _parse_args():
313323
metavar="TYPE[,TYPE,...]",
314324
help="allowed commit types (replaces defaults when set)",
315325
)
326+
parser.add_argument(
327+
"--max-subject-length",
328+
type=int,
329+
default=None,
330+
metavar="N",
331+
help=f"maximum subject line length (default: {MAX_SUBJECT_LEN})",
332+
)
316333
args = parser.parse_args()
317334
config = _load_config()
318335
enabled = _resolve_enabled(args, config, parser)
319336
allowed_scopes, require_scope = _resolve_scopes(args, config)
320337
allowed_types = _resolve_types(args, config)
338+
max_subject_length = _resolve_max_subject_length(args, config)
321339

322340
if args.message_file:
323341
rev = None
@@ -339,6 +357,7 @@ def _parse_args():
339357
allowed_scopes=allowed_scopes,
340358
require_scope=require_scope,
341359
allowed_types=allowed_types,
360+
max_subject_length=max_subject_length,
342361
)
343362

344363

@@ -368,6 +387,7 @@ def main():
368387
result,
369388
args.allowed_scopes,
370389
args.allowed_types,
390+
args.max_subject_length,
371391
require_scope=args.require_scope,
372392
)
373393
if Check.IMPERATIVE in args.enabled:

tests/test_git_commit_guard.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import pytest
66

77
from git_commit_guard import (
8+
MAX_SUBJECT_LEN,
89
TYPES,
910
Result,
1011
_download_if_missing,
@@ -14,6 +15,7 @@
1415
_parse_checks,
1516
_parse_config_checks,
1617
_report,
18+
_resolve_max_subject_length,
1719
_resolve_types,
1820
_strip_comments,
1921
check_body,
@@ -148,6 +150,16 @@ def test_empty_allowlist_accepts_any_scope(self):
148150
check_subject("fix(anything): add token", r, allowed_scopes=frozenset())
149151
assert r.ok
150152

153+
def test_custom_max_length_enforced(self):
154+
r = Result()
155+
check_subject("fix: add thing", r, max_subject_length=10)
156+
assert not r.ok
157+
158+
def test_custom_max_length_passes(self):
159+
r = Result()
160+
check_subject("fix: ok", r, max_subject_length=10)
161+
assert r.ok
162+
151163
def test_custom_type_passes(self):
152164
r = Result()
153165
check_subject("wip: add thing", r, allowed_types=frozenset(["wip"]))
@@ -420,6 +432,28 @@ def test_invalid_check_name_exits(self):
420432
_parse_config_checks({"disable": ["bogus"]}, "disable")
421433

422434

435+
class TestResolveMaxSubjectLength:
436+
def test_defaults_when_no_config_or_flag(self):
437+
result = _resolve_max_subject_length(Namespace(max_subject_length=None), {})
438+
assert result == MAX_SUBJECT_LEN
439+
440+
def test_cli_flag_overrides_default(self):
441+
result = _resolve_max_subject_length(Namespace(max_subject_length=50), {})
442+
assert result == 50 # noqa: PLR2004 Magic value used in comparison, consider replacing 50 with a constant variable
443+
444+
def test_config_overrides_default(self):
445+
result = _resolve_max_subject_length(
446+
Namespace(max_subject_length=None), {"max-subject-length": 60}
447+
)
448+
assert result == 60 # noqa: PLR2004 Magic value used in comparison, consider replacing 60 with a constant variable
449+
450+
def test_cli_overrides_config(self):
451+
result = _resolve_max_subject_length(
452+
Namespace(max_subject_length=50), {"max-subject-length": 60}
453+
)
454+
assert result == 50 # noqa: PLR2004 Magic value used in comparison, consider replacing 50 with a constant variable
455+
456+
423457
class TestResolveTypes:
424458
def test_defaults_when_no_config_or_flag(self):
425459
assert _resolve_types(Namespace(types=None), {}) == TYPES
@@ -713,6 +747,70 @@ def test_types_from_config(self, tmp_path):
713747
):
714748
assert main() == 0
715749

750+
def test_max_subject_length_flag_passes(self, tmp_path):
751+
f = tmp_path / "msg"
752+
f.write_text("fix: ok\n\nbody\n\nSigned-off-by: A User <a@b.com>")
753+
argv = [
754+
"cg",
755+
"--message-file",
756+
str(f),
757+
"--disable",
758+
"signature",
759+
"--max-subject-length",
760+
"10",
761+
]
762+
with patch("sys.argv", argv):
763+
assert main() == 0
764+
765+
def test_max_subject_length_flag_fails(self, tmp_path):
766+
f = tmp_path / "msg"
767+
f.write_text(_VALID_MSG)
768+
argv = [
769+
"cg",
770+
"--message-file",
771+
str(f),
772+
"--disable",
773+
"signature",
774+
"--max-subject-length",
775+
"5",
776+
]
777+
with patch("sys.argv", argv):
778+
assert main() == 1
779+
780+
def test_max_subject_length_from_config(self, tmp_path):
781+
f = tmp_path / "msg"
782+
f.write_text(_VALID_MSG)
783+
argv = ["cg", "--message-file", str(f), "--disable", "signature"]
784+
with (
785+
patch("sys.argv", argv),
786+
patch(
787+
"git_commit_guard._load_config",
788+
return_value={"max-subject-length": 5},
789+
),
790+
):
791+
assert main() == 1
792+
793+
def test_max_subject_length_cli_overrides_config(self, tmp_path):
794+
f = tmp_path / "msg"
795+
f.write_text(_VALID_MSG)
796+
argv = [
797+
"cg",
798+
"--message-file",
799+
str(f),
800+
"--disable",
801+
"signature",
802+
"--max-subject-length",
803+
"100",
804+
]
805+
with (
806+
patch("sys.argv", argv),
807+
patch(
808+
"git_commit_guard._load_config",
809+
return_value={"max-subject-length": 5},
810+
),
811+
):
812+
assert main() == 0
813+
716814
def test_types_cli_overrides_config(self, tmp_path):
717815
f = tmp_path / "msg"
718816
f.write_text("wip: add thing\n\nbody\n\nSigned-off-by: A User <a@b.com>")

0 commit comments

Comments
 (0)