Skip to content
Open
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
17 changes: 15 additions & 2 deletions commitizen/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This for loop looks strange... I think we should remove the for loop

using_config_file = config_candidates[0]
...

but that can be another PR

iirc the underlying logic of BaseConfig is a bit confusing

Copy link
Author

@nicoleman0 nicoleman0 Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, so should the code just use config_candidates[0] directly with the first file found, whether it's empty or not?

If so, the behavior would change to:

  • When filepath is provided: Use that file, raise ConfigFileIsEmpty if empty (same as now)
  • When auto-discovering: Use the first file found, return empty BaseConfig() if it's empty (currently skips empty files)

This would be simpler and align the warning message with actual behavior. The only downside is if someone has an empty .cz.toml and a valid .cz.json, it would now ignore the .cz.json (whereas currently it would use it).

Shall I go ahead with this simplification in this PR, or would you prefer a separate one?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could just add a TODO to remind the future contributors to address this issue. The main goal for this PR is just to add a warning for multiple configuration.

with open(filename, "rb") as f:
data: bytes = f.read()

Expand Down
3 changes: 3 additions & 0 deletions docs/config/configuration_file.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
84 changes: 84 additions & 0 deletions tests/test_conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But technically, it's multiple configs. We probably should handle it better 🤔

"""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",
Expand Down