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
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,19 @@ commit-guard --range v1.0..v2.0
commit-guard --range origin/main..HEAD --enable subject,imperative
```

Merge commits are excluded by default. Use `--include-merges` to check them:

```bash
commit-guard --range origin/main..HEAD --include-merges
```

An empty range (no commits) exits non-zero by default — this catches
misconfigured range specs in CI. Use `--allow-empty` to exit 0 instead:

```bash
commit-guard --range origin/main..HEAD --allow-empty
```

### pre-commit

Add to your `.pre-commit-config.yaml`:
Expand Down
33 changes: 29 additions & 4 deletions src/git_commit_guard/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,10 +224,14 @@ def _get_message(rev):
sys.exit(f"git error: {stderr}")


def _get_range_revs(rev_range):
def _get_range_revs(rev_range, *, include_merges=False):
cmd = ["git", "log", "--format=%H"]
if not include_merges:
cmd.append("--no-merges")
cmd.append(rev_range)
try:
output = subprocess.check_output( # noqa: S603
["git", "log", "--format=%H", rev_range], # noqa: S607
cmd,
text=True,
stderr=subprocess.PIPE,
timeout=GIT_TIMEOUT,
Expand All @@ -247,6 +251,8 @@ class Args:
allowed_types: frozenset
max_subject_length: int
rev_range: str | None
allow_empty: bool
include_merges: bool


def _resolve_enabled(args, config, parser):
Expand Down Expand Up @@ -350,13 +356,30 @@ def _parse_args():
metavar="REF..REF",
help="check all commits in the given revision range",
)
parser.add_argument(
"--allow-empty",
action="store_true",
default=False,
help="exit 0 when --range yields no commits (default: exit 1)",
)
parser.add_argument(
"--include-merges",
action="store_true",
default=False,
help="include merge commits when checking a range (default: excluded)",
)
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.allow_empty and not args.rev_range:
parser.error("--allow-empty requires --range")
if args.include_merges and not args.rev_range:
parser.error("--include-merges requires --range")

if args.rev_range:
if args.rev is not None or args.message_file:
parser.error("--range cannot be combined with rev or --message-file")
Expand Down Expand Up @@ -384,6 +407,8 @@ def _parse_args():
allowed_types=allowed_types,
max_subject_length=max_subject_length,
rev_range=args.rev_range,
allow_empty=args.allow_empty,
include_merges=args.include_merges,
)


Expand Down Expand Up @@ -430,10 +455,10 @@ def main():
_ensure_nltk_data()

if args.rev_range:
revs = _get_range_revs(args.rev_range)
revs = _get_range_revs(args.rev_range, include_merges=args.include_merges)
if not revs:
sys.stderr.write("no commits in range\n")
return 0
return 0 if args.allow_empty else 1
failed = False
for rev in revs:
message = _strip_comments(_get_message(rev))
Expand Down
56 changes: 55 additions & 1 deletion tests/test_git_commit_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -894,14 +894,58 @@ def test_range_all_fail_returns_one(self):
):
assert main() == 1

def test_range_empty_returns_zero(self, capsys):
def test_range_empty_returns_one(self, capsys):
with (
patch("sys.argv", ["cg", "--range", "origin/main..HEAD"]),
patch("git_commit_guard._get_range_revs", return_value=[]),
):
assert main() == 1
assert "no commits in range" in capsys.readouterr().err

def test_range_empty_with_allow_empty_returns_zero(self, capsys):
with (
patch("sys.argv", ["cg", "--range", "origin/main..HEAD", "--allow-empty"]),
patch("git_commit_guard._get_range_revs", return_value=[]),
):
assert main() == 0
assert "no commits in range" in capsys.readouterr().err

def test_allow_empty_without_range_exits(self):
with (
patch("sys.argv", ["cg", "--allow-empty"]),
pytest.raises(SystemExit),
):
main()

def test_include_merges_without_range_exits(self):
with (
patch("sys.argv", ["cg", "--include-merges"]),
pytest.raises(SystemExit),
):
main()

def test_range_include_merges_flag(self):
with (
patch(
"sys.argv",
[
"cg",
"--range",
"origin/main..HEAD",
"--include-merges",
"--disable",
"signature",
],
),
patch(
"git_commit_guard._get_range_revs",
return_value=["abc1234"],
) as mock,
patch("git_commit_guard._get_message", return_value=_VALID_MSG),
):
main()
mock.assert_called_once_with("origin/main..HEAD", include_merges=True)

def test_range_conflicts_with_rev(self):
with (
patch("sys.argv", ["cg", "abc123", "--range", "origin/main..HEAD"]),
Expand Down Expand Up @@ -930,6 +974,16 @@ def test_returns_shas(self):
):
assert _get_range_revs("origin/main..HEAD") == ["abc1234", "def5678"]

def test_excludes_merges_by_default(self):
with patch("git_commit_guard.subprocess.check_output", return_value="") as mock:
_get_range_revs("origin/main..HEAD")
assert "--no-merges" in mock.call_args[0][0]

def test_includes_merges_when_requested(self):
with patch("git_commit_guard.subprocess.check_output", return_value="") as mock:
_get_range_revs("origin/main..HEAD", include_merges=True)
assert "--no-merges" not in mock.call_args[0][0]

def test_empty_range_returns_empty_list(self):
with patch("git_commit_guard.subprocess.check_output", return_value=""):
assert _get_range_revs("origin/main..HEAD") == []
Expand Down
Loading