From 9d857b205a8fd108345acdcf480f437b38902d54 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Nerijus=20Bend=C5=BEi=C5=ABnas?=
Date: Fri, 10 Jul 2026 04:44:42 +0300
Subject: [PATCH 1/2] refactor: extract per-commit reporting from main
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
main carried the jsonl/text dispatch twice — once in the range loop, once
in the single-commit path.
Signed-off-by: Nerijus Bendžiūnas
---
src/git_commit_guard/__init__.py | 21 +++++++++++----------
tests/test_git_commit_guard.py | 30 ++++++++++++++++++++++++++++++
2 files changed, 41 insertions(+), 10 deletions(-)
diff --git a/src/git_commit_guard/__init__.py b/src/git_commit_guard/__init__.py
index 3966ae8..51e2cac 100644
--- a/src/git_commit_guard/__init__.py
+++ b/src/git_commit_guard/__init__.py
@@ -892,6 +892,14 @@ def _run_checks(args, rev, message, result):
check_signature(rev, result)
+def _report_commit(args, result, sha, subject):
+ if args.output == OutputFormat.JSONL:
+ return _report_jsonl(result, sha, subject)
+ if args.rev_range:
+ print(f"{sha[:7]} {subject}")
+ return _report_text(result)
+
+
def main():
args = _parse_args()
@@ -911,13 +919,8 @@ def main():
subject = message.split("\n")[0]
result = Result()
_run_checks(args, rev, message, result)
- if args.output == OutputFormat.JSONL:
- if _report_jsonl(result, rev, subject) != 0:
- failed = True
- else:
- print(f"{rev[:7]} {subject}")
- if _report_text(result) != 0:
- failed = True
+ if _report_commit(args, result, rev, subject) != 0:
+ failed = True
if out_file:
_write_jsonl_record(result, rev, subject, out_file)
return 1 if failed else 0
@@ -927,6 +930,4 @@ def main():
_run_checks(args, args.rev, args.message, result)
if out_file:
_write_jsonl_record(result, args.rev, subject, out_file)
- if args.output == OutputFormat.JSONL:
- return _report_jsonl(result, args.rev, subject)
- return _report_text(result)
+ return _report_commit(args, result, args.rev, subject)
diff --git a/tests/test_git_commit_guard.py b/tests/test_git_commit_guard.py
index 4435964..89ea1d9 100644
--- a/tests/test_git_commit_guard.py
+++ b/tests/test_git_commit_guard.py
@@ -12,6 +12,7 @@
GIT_TIMEOUT,
MAX_SUBJECT_LEN,
TYPES,
+ OutputFormat,
Result,
_download_if_missing,
_ensure_nltk_data,
@@ -29,6 +30,7 @@
_parse_checks,
_parse_config_checks,
_parse_noreply_username,
+ _report_commit,
_report_jsonl,
_report_text,
_resolve_max_subject_length,
@@ -1545,6 +1547,34 @@ def test_output_is_single_line(self, capsys):
assert out.count("\n") == 1
+class TestReportCommit:
+ def test_range_text_prints_sha_header(self, capsys):
+ args = Namespace(output=OutputFormat.TEXT, rev_range="a..b")
+ ret = _report_commit(args, Result(), "abc1234567890", "fix: add thing")
+ assert ret == 0
+ out = capsys.readouterr().out
+ assert out.startswith("abc1234 fix: add thing")
+ assert "all checks passed" in out
+
+ def test_single_commit_text_has_no_header(self, capsys):
+ args = Namespace(output=OutputFormat.TEXT, rev_range=None)
+ ret = _report_commit(args, Result(), None, "fix: add thing")
+ assert ret == 0
+ out = capsys.readouterr().out
+ assert "fix: add thing" not in out
+ assert "all checks passed" in out
+
+ def test_jsonl_dispatch(self, capsys):
+ args = Namespace(output=OutputFormat.JSONL, rev_range="a..b")
+ r = Result()
+ r.error("missing body", check="body")
+ ret = _report_commit(args, r, "abc1234567890", "fix: add thing")
+ assert ret == 1
+ data = json.loads(capsys.readouterr().out)
+ assert data["sha"] == "abc1234567890"
+ assert data["ok"] is False
+
+
_VALID_MSG = "fix: add thing\n\nbody text\n\nSigned-off-by: A User "
From 262ca3d867d30be78b187435be0b4808684db089 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Nerijus=20Bend=C5=BEi=C5=ABnas?=
Date: Fri, 10 Jul 2026 04:56:06 +0300
Subject: [PATCH 2/2] feat: add --quiet to print only offending commits
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Long ranges bury failures under per-commit "all checks passed" noise;
suppression keys on warn level and above so --output-file still records
every commit.
Signed-off-by: Nerijus Bendžiūnas
---
README.md | 16 +++
docs/index.html | 9 ++
src/git_commit_guard/__init__.py | 29 ++++-
tests/test_git_commit_guard.py | 190 ++++++++++++++++++++++++++++++-
4 files changed, 235 insertions(+), 9 deletions(-)
diff --git a/README.md b/README.md
index 816a6c4..f05e6e5 100644
--- a/README.md
+++ b/README.md
@@ -331,6 +331,22 @@ misconfigured range specs in CI. Use `--allow-empty` to exit 0 instead:
commit-guard --range origin/main..HEAD --allow-empty
```
+### Quiet mode
+
+Use `--quiet` (or `-q`) to suppress output for commits that have nothing to
+report — only commits with errors or warnings are printed. On a long range
+this leaves exactly the offending commits in the output; a fully compliant
+range prints nothing and exits 0:
+
+```bash
+commit-guard --range origin/main..HEAD --quiet
+```
+
+Quiet mode applies to single-commit and range mode, in both text and
+`--output jsonl` formats. Exit codes are unchanged, so it composes with CI
+gating. `--output-file` is not affected — the file always receives the
+complete record stream.
+
### Machine-readable output
Use `--output jsonl` to emit one JSON line per commit to stdout instead of the
diff --git a/docs/index.html b/docs/index.html
index 4df4546..4153732 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -551,6 +551,15 @@ Output #
readable logs and structured results for downstream steps:
commit-guard --range origin/main..HEAD --output-file results.jsonl
+
+ Use --quiet (or -q) to suppress output for
+ commits that have nothing to report — only commits with errors or
+ warnings are printed, in both text and JSONL formats. A fully
+ compliant range prints nothing and exits 0. Exit codes are unchanged,
+ and --output-file always receives the complete record
+ stream:
+
+ commit-guard --range origin/main..HEAD --quiet
diff --git a/src/git_commit_guard/__init__.py b/src/git_commit_guard/__init__.py
index 51e2cac..8199f65 100644
--- a/src/git_commit_guard/__init__.py
+++ b/src/git_commit_guard/__init__.py
@@ -146,6 +146,10 @@ def info(self, msg, check=None):
def ok(self):
return not any(lvl == Level.ERROR for _, lvl, _ in self.errors)
+ @property
+ def noteworthy(self):
+ return any(lvl in (Level.ERROR, Level.WARN) for _, lvl, _ in self.errors)
+
def _ensure_nltk_data():
_download_if_missing("taggers/averaged_perceptron_tagger_eng")
@@ -565,6 +569,7 @@ class Args:
subject_pattern: re.Pattern | None
output: OutputFormat
output_file: Path | None
+ quiet: bool
def _resolve_enabled(args, config, parser):
@@ -662,7 +667,7 @@ def _parse_checks(parser, value):
parser.error(str(e))
-def _parse_args(): # noqa: PLR0915 Too many statements (59 > 50)
+def _parse_args(): # noqa: PLR0915 Too many statements (60 > 50)
checks_list = ",".join(sorted(Check))
parser = ArgumentParser(description="conventional commit checker")
parser.add_argument("rev", nargs="?", default=None)
@@ -761,6 +766,13 @@ def _parse_args(): # noqa: PLR0915 Too many statements (59 > 50)
metavar="FILE",
help="write JSONL results to FILE (text still goes to stdout)",
)
+ parser.add_argument(
+ "-q",
+ "--quiet",
+ action="store_true",
+ default=False,
+ help="suppress output for commits without errors or warnings",
+ )
args = parser.parse_args()
config = _load_config()
enabled = _resolve_enabled(args, config, parser)
@@ -823,6 +835,7 @@ def _parse_args(): # noqa: PLR0915 Too many statements (59 > 50)
subject_pattern=subject_pattern,
output=OutputFormat(args.output),
output_file=args.output_file,
+ quiet=args.quiet,
)
@@ -838,7 +851,9 @@ def _jsonl_record(result, sha, subject):
}
-def _report_jsonl(result, sha, subject):
+def _report_jsonl(result, sha, subject, *, quiet=False):
+ if quiet and not result.noteworthy:
+ return 0
print(json.dumps(_jsonl_record(result, sha, subject)))
return 0 if result.ok else 1
@@ -847,7 +862,9 @@ def _write_jsonl_record(result, sha, subject, file):
file.write(json.dumps(_jsonl_record(result, sha, subject)) + "\n")
-def _report_text(result):
+def _report_text(result, *, quiet=False):
+ if quiet and not result.noteworthy:
+ return 0
color = sys.stdout.isatty()
for check, level, msg in result.errors:
prefix = f"[{check}] " if check else ""
@@ -894,10 +911,10 @@ def _run_checks(args, rev, message, result):
def _report_commit(args, result, sha, subject):
if args.output == OutputFormat.JSONL:
- return _report_jsonl(result, sha, subject)
- if args.rev_range:
+ return _report_jsonl(result, sha, subject, quiet=args.quiet)
+ if args.rev_range and (not args.quiet or result.noteworthy):
print(f"{sha[:7]} {subject}")
- return _report_text(result)
+ return _report_text(result, quiet=args.quiet)
def main():
diff --git a/tests/test_git_commit_guard.py b/tests/test_git_commit_guard.py
index 89ea1d9..bec990a 100644
--- a/tests/test_git_commit_guard.py
+++ b/tests/test_git_commit_guard.py
@@ -1495,6 +1495,33 @@ def test_ok_no_ansi_when_not_tty(self, capsys):
_report_text(r)
assert "\033[" not in capsys.readouterr().out
+ def test_quiet_suppresses_clean_result(self, capsys):
+ r = Result()
+ ret = _report_text(r, quiet=True)
+ assert ret == 0
+ assert capsys.readouterr().out == ""
+
+ def test_quiet_suppresses_info_only_result(self, capsys):
+ r = Result()
+ r.info("signature type: SSH", check="signature")
+ ret = _report_text(r, quiet=True)
+ assert ret == 0
+ assert capsys.readouterr().out == ""
+
+ def test_quiet_shows_warning(self, capsys):
+ r = Result()
+ r.warn("heads up")
+ ret = _report_text(r, quiet=True)
+ assert ret == 0
+ assert "heads up" in capsys.readouterr().out
+
+ def test_quiet_shows_error(self, capsys):
+ r = Result()
+ r.error("something broke")
+ ret = _report_text(r, quiet=True)
+ assert ret == 1
+ assert "something broke" in capsys.readouterr().out
+
class TestReportJsonl:
def test_ok_commit(self, capsys):
@@ -1524,6 +1551,19 @@ def test_failed_commit(self, capsys):
"message": "missing body",
}
+ def test_quiet_suppresses_ok_commit(self, capsys):
+ r = Result()
+ ret = _report_jsonl(r, "abc1234567890", "fix: add thing", quiet=True)
+ assert ret == 0
+ assert capsys.readouterr().out == ""
+
+ def test_quiet_shows_failed_commit(self, capsys):
+ r = Result()
+ r.error("missing body", check="body")
+ ret = _report_jsonl(r, "abc1234567890", "fix: add thing", quiet=True)
+ assert ret == 1
+ assert json.loads(capsys.readouterr().out)["ok"] is False
+
def test_null_sha(self, capsys):
r = Result()
ret = _report_jsonl(r, None, "fix: add thing")
@@ -1549,7 +1589,7 @@ def test_output_is_single_line(self, capsys):
class TestReportCommit:
def test_range_text_prints_sha_header(self, capsys):
- args = Namespace(output=OutputFormat.TEXT, rev_range="a..b")
+ args = Namespace(output=OutputFormat.TEXT, rev_range="a..b", quiet=False)
ret = _report_commit(args, Result(), "abc1234567890", "fix: add thing")
assert ret == 0
out = capsys.readouterr().out
@@ -1557,7 +1597,7 @@ def test_range_text_prints_sha_header(self, capsys):
assert "all checks passed" in out
def test_single_commit_text_has_no_header(self, capsys):
- args = Namespace(output=OutputFormat.TEXT, rev_range=None)
+ args = Namespace(output=OutputFormat.TEXT, rev_range=None, quiet=False)
ret = _report_commit(args, Result(), None, "fix: add thing")
assert ret == 0
out = capsys.readouterr().out
@@ -1565,7 +1605,7 @@ def test_single_commit_text_has_no_header(self, capsys):
assert "all checks passed" in out
def test_jsonl_dispatch(self, capsys):
- args = Namespace(output=OutputFormat.JSONL, rev_range="a..b")
+ args = Namespace(output=OutputFormat.JSONL, rev_range="a..b", quiet=False)
r = Result()
r.error("missing body", check="body")
ret = _report_commit(args, r, "abc1234567890", "fix: add thing")
@@ -1574,6 +1614,12 @@ def test_jsonl_dispatch(self, capsys):
assert data["sha"] == "abc1234567890"
assert data["ok"] is False
+ def test_quiet_range_suppresses_header_for_clean_commit(self, capsys):
+ args = Namespace(output=OutputFormat.TEXT, rev_range="a..b", quiet=True)
+ ret = _report_commit(args, Result(), "abc1234567890", "fix: add thing")
+ assert ret == 0
+ assert capsys.readouterr().out == ""
+
_VALID_MSG = "fix: add thing\n\nbody text\n\nSigned-off-by: A User "
@@ -2482,3 +2528,141 @@ def test_failed_commit_written_to_file(self, tmp_path):
assert main() == 1
data = json.loads(out_file.read_text())
assert data["ok"] is False
+
+
+class TestQuiet:
+ def test_range_text_shows_only_failing_commits(self, capsys):
+ messages = {"aaa1234": _VALID_MSG, "bbb5678": "bad message"}
+ with (
+ patch(
+ "sys.argv",
+ [
+ "cg",
+ "--range",
+ "HEAD~2..HEAD",
+ "--disable",
+ "signature,body,signed-off,imperative",
+ "--quiet",
+ ],
+ ),
+ patch(
+ "git_commit_guard._get_range_revs",
+ return_value=["aaa1234", "bbb5678"],
+ ),
+ patch(
+ "git_commit_guard._get_message",
+ side_effect=lambda rev: messages[rev],
+ ),
+ ):
+ assert main() == 1
+ out = capsys.readouterr().out
+ assert "bbb5678" in out
+ assert "aaa1234" not in out
+
+ def test_range_text_all_pass_prints_nothing(self, capsys):
+ with (
+ patch(
+ "sys.argv",
+ ["cg", "--range", "HEAD~2..HEAD", "--disable", "signature", "-q"],
+ ),
+ patch(
+ "git_commit_guard._get_range_revs",
+ return_value=["aaa1234", "bbb5678"],
+ ),
+ patch("git_commit_guard._get_message", return_value=_VALID_MSG),
+ ):
+ assert main() == 0
+ assert capsys.readouterr().out == ""
+
+ def test_single_commit_pass_prints_nothing(self, capsys, tmp_path):
+ msg_file = tmp_path / "msg"
+ msg_file.write_text(_VALID_MSG)
+ with patch(
+ "sys.argv",
+ [
+ "cg",
+ "--message-file",
+ str(msg_file),
+ "--disable",
+ "signature,imperative",
+ "--quiet",
+ ],
+ ):
+ assert main() == 0
+ assert capsys.readouterr().out == ""
+
+ def test_single_commit_failure_still_printed(self, capsys, tmp_path):
+ msg_file = tmp_path / "msg"
+ msg_file.write_text("fix: add thing")
+ with patch(
+ "sys.argv",
+ [
+ "cg",
+ "--message-file",
+ str(msg_file),
+ "--disable",
+ "signature,imperative",
+ "--quiet",
+ ],
+ ):
+ assert main() == 1
+ assert "body" in capsys.readouterr().out
+
+ def test_range_jsonl_emits_only_failing_records(self, capsys):
+ messages = {"aaa1234": _VALID_MSG, "bbb5678": "bad message"}
+ with (
+ patch(
+ "sys.argv",
+ [
+ "cg",
+ "--range",
+ "HEAD~2..HEAD",
+ "--disable",
+ "signature,body,signed-off,imperative",
+ "--output",
+ "jsonl",
+ "--quiet",
+ ],
+ ),
+ patch(
+ "git_commit_guard._get_range_revs",
+ return_value=["aaa1234", "bbb5678"],
+ ),
+ patch(
+ "git_commit_guard._get_message",
+ side_effect=lambda rev: messages[rev],
+ ),
+ ):
+ assert main() == 1
+ lines = capsys.readouterr().out.strip().splitlines()
+ assert len(lines) == 1
+ data = json.loads(lines[0])
+ assert data["sha"] == "bbb5678"
+ assert data["ok"] is False
+
+ def test_output_file_still_receives_all_records(self, capsys, tmp_path):
+ out_file = tmp_path / "results.jsonl"
+ with (
+ patch(
+ "sys.argv",
+ [
+ "cg",
+ "--range",
+ "HEAD~2..HEAD",
+ "--disable",
+ "signature",
+ "--quiet",
+ "--output-file",
+ str(out_file),
+ ],
+ ),
+ patch(
+ "git_commit_guard._get_range_revs",
+ return_value=["aaa1234", "bbb5678"],
+ ),
+ patch("git_commit_guard._get_message", return_value=_VALID_MSG),
+ ):
+ assert main() == 0
+ assert capsys.readouterr().out == ""
+ lines = out_file.read_text().strip().splitlines()
+ assert len(lines) == 2