diff --git a/commit_check/imperatives.py b/commit_check/imperatives.py index 4444785c..00b0e905 100644 --- a/commit_check/imperatives.py +++ b/commit_check/imperatives.py @@ -171,6 +171,7 @@ "read", "record", "redesign", + "refactor", "refer", "refresh", "register", diff --git a/commit_check/main.py b/commit_check/main.py index 02d8cf2e..cad9cca3 100644 --- a/commit_check/main.py +++ b/commit_check/main.py @@ -47,6 +47,13 @@ def _get_parser() -> argparse.ArgumentParser: help="path to config file (cchk.toml or commit-check.toml). If not specified, searches for config in: cchk.toml, commit-check.toml, .github/cchk.toml, .github/commit-check.toml", ) + parser.add_argument( + "commit_msg_file", + nargs="?", + default=None, + help="path to commit message file (positional argument for pre-commit compatibility)", + ) + # Main check type arguments check_group = parser.add_argument_group( "check types", "Specify which validation checks to run" @@ -55,9 +62,8 @@ def _get_parser() -> argparse.ArgumentParser: check_group.add_argument( "-m", "--message", - nargs="?", - const="", - help="validate commit message. Optionally specify file path, otherwise reads from stdin if available", + action="store_true", + help="validate commit message (file path can be provided as positional argument for pre-commit compatibility)", ) check_group.add_argument( @@ -310,11 +316,17 @@ def main() -> int: rule_builder = RuleBuilder(config_data) all_rules = rule_builder.build_all_rules() + # Handle positional commit_msg_file argument for pre-commit compatibility + # Store the file path separately from the boolean flag + commit_msg_file_path = None + if args.commit_msg_file: + commit_msg_file_path = args.commit_msg_file + # If a file was provided positionally, always enable message checking + args.message = True + # Filter rules based on CLI arguments requested_checks = [] - if ( - args.message is not None - ): # Check for None explicitly since empty string is valid + if args.message: # args.message is now a boolean flag # Add commit message related checks requested_checks.extend( [ @@ -354,18 +366,16 @@ def main() -> int: stdin_content = None commit_file_path = None - if ( - args.message is not None - ): # Check explicitly for None since empty string is valid - if args.message == "": - # Only set stdin_content if there's actual piped input + if args.message: # args.message is a boolean flag + # Check if we have a file path from positional argument + if commit_msg_file_path: + commit_file_path = commit_msg_file_path + else: + # No file path provided, try reading from stdin stdin_content = stdin_reader.read_piped_input() if not stdin_content: # No stdin and no file - let validators get data from git themselves stdin_content = None - else: - # Message is a file path - commit_file_path = args.message elif not any([args.branch, args.author_name, args.author_email]): # If no specific validation type is requested, don't read stdin pass diff --git a/tests/main_test.py b/tests/main_test.py index 71da25ca..d67715a7 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -474,3 +474,90 @@ def test_env_overrides_default(self, mocker, monkeypatch): sys.argv = ["commit-check", "--message"] result = main() assert result == 1 # Env var wins, should fail + + +class TestPositionalArgumentFeature: + """Test positional commit_msg_file argument for pre-commit compatibility.""" + + @pytest.mark.benchmark + def test_positional_arg_without_message_flag(self): + """Test using just the positional argument without --message flag.""" + with tempfile.NamedTemporaryFile(mode="w", delete=False) as f: + f.write("feat: add positional argument support") + f.flush() + + try: + # Use positional argument only (no --message flag) + sys.argv = ["commit-check", f.name] + result = main() + assert result == 0 # Should pass validation + finally: + os.unlink(f.name) + + @pytest.mark.benchmark + def test_positional_arg_with_message_flag(self): + """Test using positional argument with --message flag.""" + with tempfile.NamedTemporaryFile(mode="w", delete=False) as f: + f.write("fix: resolve bug in validation") + f.flush() + + try: + # Use both positional argument and --message flag + sys.argv = ["commit-check", "--message", f.name] + result = main() + assert result == 0 # Should pass validation + finally: + os.unlink(f.name) + + @pytest.mark.benchmark + def test_positional_arg_with_branch_flag(self, mocker): + """Test positional argument with other check flags (edge case).""" + # Mock git command to return a valid branch name + mocker.patch( + "subprocess.run", + return_value=type( + "MockResult", (), {"stdout": "feature/test-branch", "returncode": 0} + )(), + ) + + with tempfile.NamedTemporaryFile(mode="w", delete=False) as f: + f.write("chore: update documentation") + f.flush() + + try: + # Use positional argument with --branch flag + sys.argv = ["commit-check", "--branch", f.name] + result = main() + # Should validate both commit message and branch name + assert result == 0 # Should pass both validations + finally: + os.unlink(f.name) + + @pytest.mark.benchmark + def test_positional_arg_invalid_commit(self): + """Test that positional argument correctly rejects invalid commits.""" + with tempfile.NamedTemporaryFile(mode="w", delete=False) as f: + f.write("invalid commit message without type") + f.flush() + + try: + # Use positional argument with invalid message + sys.argv = ["commit-check", f.name] + result = main() + assert result == 1 # Should fail validation + finally: + os.unlink(f.name) + + @pytest.mark.benchmark + def test_positional_arg_nonexistent_file(self, mocker): + """Test that positional argument with non-existent file falls back to git.""" + # Mock git to return a valid commit message + mocker.patch( + "commit_check.engine.get_commit_info", + return_value="feat: add fallback commit from git", + ) + + sys.argv = ["commit-check", "/nonexistent/commit_msg.txt"] + result = main() + # Should fall back to git and pass + assert result == 0