diff --git a/commitizen/config/__init__.py b/commitizen/config/__init__.py index e30f9f789..2fb84a123 100644 --- a/commitizen/config/__init__.py +++ b/commitizen/config/__init__.py @@ -3,7 +3,7 @@ from pathlib import Path from typing import TYPE_CHECKING -from commitizen import defaults, git +from commitizen import defaults, git, out from commitizen.config.factory import create_config from commitizen.exceptions import ConfigFileIsEmpty, ConfigFileNotFound @@ -35,7 +35,20 @@ def _resolve_config_paths(filepath: str | None = None) -> Generator[Path, None, def read_cfg(filepath: str | None = None) -> BaseConfig: - for filename in _resolve_config_paths(filepath): + config_candidates = list(_resolve_config_paths(filepath)) + + # Check for multiple config files and warn the user + config_candidates_exclude_pyproject = [ + path for path in config_candidates if path.name != "pyproject.toml" + ] + if len(config_candidates_exclude_pyproject) > 1: + filenames = [path.name for path in config_candidates_exclude_pyproject] + out.warn( + f"Multiple config files detected: {', '.join(filenames)}. " + f"Using config file: '{filenames[0]}'." + ) + + for filename in config_candidates: with open(filename, "rb") as f: data: bytes = f.read() diff --git a/docs/config/configuration_file.md b/docs/config/configuration_file.md index 71944cb71..8e4c1f214 100644 --- a/docs/config/configuration_file.md +++ b/docs/config/configuration_file.md @@ -31,6 +31,9 @@ The first valid configuration file found will be used. If no configuration file !!! tip For Python projects, it's recommended to add your Commitizen configuration to `pyproject.toml` to keep all project configuration in one place. +!!! warning "Multiple Configuration Files" + If Commitizen detects more than one configuration file in your project directory (excluding `pyproject.toml`), it will display a warning message and identify which file is being used. To avoid confusion, ensure you have only one Commitizen configuration file in your project. + ## Supported Formats Commitizen supports three configuration file formats: diff --git a/tests/test_conf.py b/tests/test_conf.py index 0df0d1864..1ef30d515 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -239,6 +239,90 @@ def test_load_empty_pyproject_toml_from_config_argument(_, tmpdir): with pytest.raises(ConfigFileIsEmpty): config.read_cfg(filepath="./not_in_root/pyproject.toml") + def test_warn_multiple_config_files(_, tmpdir, capsys): + """Test that a warning is issued when multiple config files exist.""" + with tmpdir.as_cwd(): + # Create multiple config files + tmpdir.join(".cz.toml").write(PYPROJECT) + tmpdir.join(".cz.json").write(JSON_STR) + + # Read config + cfg = config.read_cfg() + + # Check that the warning was issued + captured = capsys.readouterr() + assert "Multiple config files detected" in captured.err + assert ".cz.toml" in captured.err + assert ".cz.json" in captured.err + assert "Using" in captured.err + + # Verify the correct config is loaded (first in priority order) + assert cfg.settings == _settings + + def test_warn_multiple_config_files_with_pyproject(_, tmpdir, capsys): + """Test warning excludes pyproject.toml from the warning message.""" + with tmpdir.as_cwd(): + # Create multiple config files including pyproject.toml + tmpdir.join("pyproject.toml").write(PYPROJECT) + tmpdir.join(".cz.json").write(JSON_STR) + + # Read config - should use pyproject.toml (first in priority) + cfg = config.read_cfg() + + # No warning should be issued as only one non-pyproject config exists + captured = capsys.readouterr() + assert "Multiple config files detected" not in captured.err + + # Verify the correct config is loaded + assert cfg.settings == _settings + + def test_warn_multiple_config_files_uses_correct_one(_, tmpdir, capsys): + """Test that the correct config file is used when multiple exist.""" + with tmpdir.as_cwd(): + # Create .cz.json with different settings + json_different = """ + { + "commitizen": { + "name": "cz_conventional_commits", + "version": "2.0.0" + } + } + """ + tmpdir.join(".cz.json").write(json_different) + tmpdir.join(".cz.toml").write(PYPROJECT) + + # Read config - should use pyproject.toml (first in defaults.CONFIG_FILES) + # But since pyproject.toml doesn't exist, .cz.toml is second in priority + cfg = config.read_cfg() + + # Check that warning mentions both files + captured = capsys.readouterr() + assert "Multiple config files detected" in captured.err + assert ".cz.toml" in captured.err + assert ".cz.json" in captured.err + + # Verify .cz.toml was used (second in priority after pyproject.toml) + assert cfg.settings["name"] == "cz_jira" # from PYPROJECT + assert cfg.settings["version"] == "1.0.0" + + def test_no_warn_with_explicit_config_path(_, tmpdir, capsys): + """Test that no warning is issued when user explicitly specifies config.""" + with tmpdir.as_cwd(): + # Create multiple config files + tmpdir.join(".cz.toml").write(PYPROJECT) + tmpdir.join(".cz.json").write(JSON_STR) + + # Read config with explicit path + cfg = config.read_cfg(filepath=".cz.json") + + # No warning should be issued + captured = capsys.readouterr() + assert "Multiple config files detected" not in captured.err + + # Verify the explicitly specified config is loaded (compare to expected JSON config) + json_cfg_expected = JsonConfig(data=JSON_STR, path=Path(".cz.json")) + assert cfg.settings == json_cfg_expected.settings + @pytest.mark.parametrize( "config_file, exception_string",