Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,15 @@ Available checks:
* `signed-off` - `Signed-off-by:` trailer exists
* `signature` - Verify GPG or SSH signature

### Subject length

The default maximum subject line length is 72 characters. Override with
`--max-subject-length`:

```bash
commit-guard --max-subject-length 100
```

### Type validation

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

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

```toml
# .commit-guard.toml
disable = ["signature", "body"]
scopes = ["auth", "api", "db"]
require-scope = true
types = ["feat", "fix", "chore", "wip"]
max-subject-length = 100
```

```toml
# .commit-guard.toml
enable = ["subject", "imperative"]
```

CLI flags (`--enable`, `--disable`, `--scopes`, `--require-scope`, `--types`)
take full precedence and ignore config file values when provided.
CLI flags (`--enable`, `--disable`, `--scopes`, `--require-scope`, `--types`,
`--max-subject-length`) take full precedence and ignore config file values when
provided.

### Checking a range of commits

Expand Down
26 changes: 23 additions & 3 deletions src/git_commit_guard/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,12 @@ def _strip_comments(message):
)


def check_subject(
def check_subject( # noqa: PLR0913 Too many arguments in function definition (6 > 5)
line,
result,
allowed_scopes=frozenset(),
allowed_types=TYPES,
max_subject_length=MAX_SUBJECT_LEN,
*,
require_scope=False,
):
Expand All @@ -147,8 +148,8 @@ def check_subject(
result.error("description must not start with uppercase")
if desc.endswith("."):
result.error("description must not end with period")
if len(line) > MAX_SUBJECT_LEN:
result.error(f"subject too long: {len(line)} > {MAX_SUBJECT_LEN}")
if len(line) > max_subject_length:
result.error(f"subject too long: {len(line)} > {max_subject_length}")
return desc


Expand Down Expand Up @@ -231,6 +232,7 @@ class Args:
allowed_scopes: frozenset
require_scope: bool
allowed_types: frozenset
max_subject_length: int


def _resolve_enabled(args, config, parser):
Expand All @@ -249,6 +251,14 @@ def _resolve_enabled(args, config, parser):
return enabled


def _resolve_max_subject_length(args, config):
if args.max_subject_length is not None:
return args.max_subject_length
if "max-subject-length" in config:
return config["max-subject-length"]
return MAX_SUBJECT_LEN


def _resolve_types(args, config):
if args.types:
return frozenset(t.strip() for t in args.types.split(","))
Expand Down Expand Up @@ -313,11 +323,19 @@ def _parse_args():
metavar="TYPE[,TYPE,...]",
help="allowed commit types (replaces defaults when set)",
)
parser.add_argument(
"--max-subject-length",
type=int,
default=None,
metavar="N",
help=f"maximum subject line length (default: {MAX_SUBJECT_LEN})",
)
args = parser.parse_args()
config = _load_config()
enabled = _resolve_enabled(args, config, parser)
allowed_scopes, require_scope = _resolve_scopes(args, config)
allowed_types = _resolve_types(args, config)
max_subject_length = _resolve_max_subject_length(args, config)

if args.message_file:
rev = None
Expand All @@ -339,6 +357,7 @@ def _parse_args():
allowed_scopes=allowed_scopes,
require_scope=require_scope,
allowed_types=allowed_types,
max_subject_length=max_subject_length,
)


Expand Down Expand Up @@ -368,6 +387,7 @@ def main():
result,
args.allowed_scopes,
args.allowed_types,
args.max_subject_length,
require_scope=args.require_scope,
)
if Check.IMPERATIVE in args.enabled:
Expand Down
98 changes: 98 additions & 0 deletions tests/test_git_commit_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import pytest

from git_commit_guard import (
MAX_SUBJECT_LEN,
TYPES,
Result,
_download_if_missing,
Expand All @@ -14,6 +15,7 @@
_parse_checks,
_parse_config_checks,
_report,
_resolve_max_subject_length,
_resolve_types,
_strip_comments,
check_body,
Expand Down Expand Up @@ -148,6 +150,16 @@ def test_empty_allowlist_accepts_any_scope(self):
check_subject("fix(anything): add token", r, allowed_scopes=frozenset())
assert r.ok

def test_custom_max_length_enforced(self):
r = Result()
check_subject("fix: add thing", r, max_subject_length=10)
assert not r.ok

def test_custom_max_length_passes(self):
r = Result()
check_subject("fix: ok", r, max_subject_length=10)
assert r.ok

def test_custom_type_passes(self):
r = Result()
check_subject("wip: add thing", r, allowed_types=frozenset(["wip"]))
Expand Down Expand Up @@ -420,6 +432,28 @@ def test_invalid_check_name_exits(self):
_parse_config_checks({"disable": ["bogus"]}, "disable")


class TestResolveMaxSubjectLength:
def test_defaults_when_no_config_or_flag(self):
result = _resolve_max_subject_length(Namespace(max_subject_length=None), {})
assert result == MAX_SUBJECT_LEN

def test_cli_flag_overrides_default(self):
result = _resolve_max_subject_length(Namespace(max_subject_length=50), {})
assert result == 50 # noqa: PLR2004 Magic value used in comparison, consider replacing 50 with a constant variable

def test_config_overrides_default(self):
result = _resolve_max_subject_length(
Namespace(max_subject_length=None), {"max-subject-length": 60}
)
assert result == 60 # noqa: PLR2004 Magic value used in comparison, consider replacing 60 with a constant variable

def test_cli_overrides_config(self):
result = _resolve_max_subject_length(
Namespace(max_subject_length=50), {"max-subject-length": 60}
)
assert result == 50 # noqa: PLR2004 Magic value used in comparison, consider replacing 50 with a constant variable


class TestResolveTypes:
def test_defaults_when_no_config_or_flag(self):
assert _resolve_types(Namespace(types=None), {}) == TYPES
Expand Down Expand Up @@ -713,6 +747,70 @@ def test_types_from_config(self, tmp_path):
):
assert main() == 0

def test_max_subject_length_flag_passes(self, tmp_path):
f = tmp_path / "msg"
f.write_text("fix: ok\n\nbody\n\nSigned-off-by: A User <a@b.com>")
argv = [
"cg",
"--message-file",
str(f),
"--disable",
"signature",
"--max-subject-length",
"10",
]
with patch("sys.argv", argv):
assert main() == 0

def test_max_subject_length_flag_fails(self, tmp_path):
f = tmp_path / "msg"
f.write_text(_VALID_MSG)
argv = [
"cg",
"--message-file",
str(f),
"--disable",
"signature",
"--max-subject-length",
"5",
]
with patch("sys.argv", argv):
assert main() == 1

def test_max_subject_length_from_config(self, tmp_path):
f = tmp_path / "msg"
f.write_text(_VALID_MSG)
argv = ["cg", "--message-file", str(f), "--disable", "signature"]
with (
patch("sys.argv", argv),
patch(
"git_commit_guard._load_config",
return_value={"max-subject-length": 5},
),
):
assert main() == 1

def test_max_subject_length_cli_overrides_config(self, tmp_path):
f = tmp_path / "msg"
f.write_text(_VALID_MSG)
argv = [
"cg",
"--message-file",
str(f),
"--disable",
"signature",
"--max-subject-length",
"100",
]
with (
patch("sys.argv", argv),
patch(
"git_commit_guard._load_config",
return_value={"max-subject-length": 5},
),
):
assert main() == 0

def test_types_cli_overrides_config(self, tmp_path):
f = tmp_path / "msg"
f.write_text("wip: add thing\n\nbody\n\nSigned-off-by: A User <a@b.com>")
Expand Down
Loading