From c03d500a81a43c8b5eec17cdb2b8587fd15eaf0a Mon Sep 17 00:00:00 2001 From: nhcoleman0 Date: Mon, 5 Jan 2026 19:09:54 +0000 Subject: [PATCH 1/7] feat(config): add warning for multiple configuration files Detects when multiple configuration files exist (excluding pyproject.toml) and displays a warning message identifying which file is being used. Closes #1771 --- commitizen/config/__init__.py | 48 +++++++++++++++++- docs/config/configuration_file.md | 3 ++ tests/test_conf.py | 84 +++++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 1 deletion(-) diff --git a/commitizen/config/__init__.py b/commitizen/config/__init__.py index e30f9f789..cf5057221 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 @@ -34,7 +34,53 @@ def _resolve_config_paths(filepath: str | None = None) -> Generator[Path, None, yield out_path +def _check_and_warn_multiple_configs(filepath: str | None = None) -> None: + """Check if multiple config files exist and warn the user.""" + if filepath is not None: + # If user explicitly specified a config file, no need to warn + return + + git_project_root = git.find_git_project_root() + cfg_search_paths = [Path(".")] + if git_project_root: + cfg_search_paths.append(git_project_root) + + for path in cfg_search_paths: + # Find all existing config files (excluding pyproject.toml for clearer warning) + existing_files = [ + filename + for filename in defaults.CONFIG_FILES + if filename != "pyproject.toml" and (path / filename).exists() + ] + + # If more than one config file exists, warn the user + if len(existing_files) > 1: + # Find which one will be used (first non-empty one in the priority order) + used_config = None + for filename in defaults.CONFIG_FILES: + config_path = path / filename + if config_path.exists(): + try: + with open(config_path, "rb") as f: + data = f.read() + conf = create_config(data=data, path=config_path) + if not conf.is_empty_config: + used_config = filename + break + except Exception: + continue + + if used_config: + out.warn( + f"Multiple config files detected: {', '.join(existing_files)}. " + f"Using {used_config}." + ) + break + + def read_cfg(filepath: str | None = None) -> BaseConfig: + _check_and_warn_multiple_configs(filepath) + for filename in _resolve_config_paths(filepath): 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 846910255..6f8eff65c 100644 --- a/docs/config/configuration_file.md +++ b/docs/config/configuration_file.md @@ -23,6 +23,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", From 87bf26f291f710a0e0a22005c459836e3015d23f Mon Sep 17 00:00:00 2001 From: nhcoleman0 Date: Mon, 5 Jan 2026 20:26:48 +0000 Subject: [PATCH 2/7] refactor: simplify multiple config files warning Remove extra steps to check if config files are empty. Just warn about multiple files and show first file from the list. --- commitizen/config/__init__.py | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/commitizen/config/__init__.py b/commitizen/config/__init__.py index cf5057221..fac0d6a9e 100644 --- a/commitizen/config/__init__.py +++ b/commitizen/config/__init__.py @@ -55,26 +55,10 @@ def _check_and_warn_multiple_configs(filepath: str | None = None) -> None: # If more than one config file exists, warn the user if len(existing_files) > 1: - # Find which one will be used (first non-empty one in the priority order) - used_config = None - for filename in defaults.CONFIG_FILES: - config_path = path / filename - if config_path.exists(): - try: - with open(config_path, "rb") as f: - data = f.read() - conf = create_config(data=data, path=config_path) - if not conf.is_empty_config: - used_config = filename - break - except Exception: - continue - - if used_config: - out.warn( - f"Multiple config files detected: {', '.join(existing_files)}. " - f"Using {used_config}." - ) + out.warn( + f"Multiple config files detected: {', '.join(existing_files)}. " + f"Using {existing_files[0]}." + ) break From f825cd95b2d4f7616a220953712ebd3fd72a003e Mon Sep 17 00:00:00 2001 From: Nicholas Coleman Date: Tue, 6 Jan 2026 11:26:09 +0000 Subject: [PATCH 3/7] refactor(config): use walrus operator for git_project_root assignment Co-authored-by: Wei Lee --- commitizen/config/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/commitizen/config/__init__.py b/commitizen/config/__init__.py index fac0d6a9e..041edcca2 100644 --- a/commitizen/config/__init__.py +++ b/commitizen/config/__init__.py @@ -40,9 +40,8 @@ def _check_and_warn_multiple_configs(filepath: str | None = None) -> None: # If user explicitly specified a config file, no need to warn return - git_project_root = git.find_git_project_root() cfg_search_paths = [Path(".")] - if git_project_root: + if (git_project_root := git.find_git_project_root()): cfg_search_paths.append(git_project_root) for path in cfg_search_paths: From 77c2cdf33a7288f41d6ca17add4efc6b3c51bb2e Mon Sep 17 00:00:00 2001 From: Nicholas Coleman Date: Tue, 6 Jan 2026 11:26:37 +0000 Subject: [PATCH 4/7] fix(config): only check for multiple configs when filepath is None Co-authored-by: Tim Hsiung --- commitizen/config/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/commitizen/config/__init__.py b/commitizen/config/__init__.py index 041edcca2..d455f55ed 100644 --- a/commitizen/config/__init__.py +++ b/commitizen/config/__init__.py @@ -62,7 +62,8 @@ def _check_and_warn_multiple_configs(filepath: str | None = None) -> None: def read_cfg(filepath: str | None = None) -> BaseConfig: - _check_and_warn_multiple_configs(filepath) + if filepath is None: + _check_and_warn_multiple_configs() for filename in _resolve_config_paths(filepath): with open(filename, "rb") as f: From 6b8aef47c6ce338fcd3440dce26c45179d2f1aa7 Mon Sep 17 00:00:00 2001 From: Nicholas Coleman Date: Tue, 6 Jan 2026 11:27:13 +0000 Subject: [PATCH 5/7] fix(config): improve warning message clarity for multiple config files Co-authored-by: Tim Hsiung --- commitizen/config/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commitizen/config/__init__.py b/commitizen/config/__init__.py index d455f55ed..c6ddd1e24 100644 --- a/commitizen/config/__init__.py +++ b/commitizen/config/__init__.py @@ -56,7 +56,7 @@ def _check_and_warn_multiple_configs(filepath: str | None = None) -> None: if len(existing_files) > 1: out.warn( f"Multiple config files detected: {', '.join(existing_files)}. " - f"Using {existing_files[0]}." + f"Using config file: '{existing_files[0]}'." ) break From f2c8e4fd8327d1c897b18f743e5c0684c8fbd4bf Mon Sep 17 00:00:00 2001 From: nhcoleman0 Date: Tue, 6 Jan 2026 11:35:37 +0000 Subject: [PATCH 6/7] refactor(config): remove _check_and_warn_multiple_configs function; refactor read_cfg function --- commitizen/config/__init__.py | 38 ++++++++++------------------------- 1 file changed, 11 insertions(+), 27 deletions(-) diff --git a/commitizen/config/__init__.py b/commitizen/config/__init__.py index c6ddd1e24..1894974ce 100644 --- a/commitizen/config/__init__.py +++ b/commitizen/config/__init__.py @@ -34,38 +34,22 @@ def _resolve_config_paths(filepath: str | None = None) -> Generator[Path, None, yield out_path -def _check_and_warn_multiple_configs(filepath: str | None = None) -> None: - """Check if multiple config files exist and warn the user.""" - if filepath is not None: - # If user explicitly specified a config file, no need to warn - return - - cfg_search_paths = [Path(".")] - if (git_project_root := git.find_git_project_root()): - cfg_search_paths.append(git_project_root) +def read_cfg(filepath: str | None = None) -> BaseConfig: + config_candidates = list(_resolve_config_paths(filepath)) - for path in cfg_search_paths: - # Find all existing config files (excluding pyproject.toml for clearer warning) - existing_files = [ - filename - for filename in defaults.CONFIG_FILES - if filename != "pyproject.toml" and (path / filename).exists() + # Check for multiple config files and warn the user + if filepath is None: + config_candidates_exclude_pyproject = [ + path for path in config_candidates if path.name != "pyproject.toml" ] - - # If more than one config file exists, warn the user - if len(existing_files) > 1: + 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(existing_files)}. " - f"Using config file: '{existing_files[0]}'." + f"Multiple config files detected: {', '.join(filenames)}. " + f"Using config file: '{filenames[0]}'." ) - break - - -def read_cfg(filepath: str | None = None) -> BaseConfig: - if filepath is None: - _check_and_warn_multiple_configs() - for filename in _resolve_config_paths(filepath): + for filename in config_candidates: with open(filename, "rb") as f: data: bytes = f.read() From f0fc425dad7bd965aa8bf2b533cdb53c65aa5e69 Mon Sep 17 00:00:00 2001 From: nhcoleman0 Date: Tue, 6 Jan 2026 14:03:27 +0000 Subject: [PATCH 7/7] fix(config): streamline multiple config files warning logic --- commitizen/config/__init__.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/commitizen/config/__init__.py b/commitizen/config/__init__.py index 1894974ce..2fb84a123 100644 --- a/commitizen/config/__init__.py +++ b/commitizen/config/__init__.py @@ -38,16 +38,15 @@ def read_cfg(filepath: str | None = None) -> BaseConfig: config_candidates = list(_resolve_config_paths(filepath)) # Check for multiple config files and warn the user - if filepath is None: - 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]}'." - ) + 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: