diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 2a7cc5672b..9ff73d544d 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -56,7 +56,7 @@ repos:
- tomli
- repo: https://github.com/commitizen-tools/commitizen
- rev: v4.12.1 # automatically updated by Commitizen
+ rev: v4.13.1 # automatically updated by Commitizen
hooks:
- id: commitizen
- id: commitizen-branch
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6c7b7598c8..15d690d15a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,30 @@
+## v4.13.1 (2026-02-03)
+
+### Refactor
+
+- **config**: replace is_empty_config with contains_commitizen_section, improve multi config resolution algorithm (#1842)
+
+## v4.13.0 (2026-02-01)
+
+### Feat
+
+- **bump**: add --version-files-only and deprecate --files-only (#1802)
+- **version**: add --tag tag to version command (#1819)
+- **cli**: add description when choosing a commit rule (#1825)
+- **tags**: enable version schemes with less than 3 components (#1705)
+
+### Fix
+
+- **config**: include pyproject.toml in multi config file warning (#1803)
+- add pytest ruff rule PT and fix missing deprecation warning (#1826)
+- **message_length_limit**: align the behavior of message_length_limit (#1813)
+- **cli**: capitalize the first characters of help texts and fix minor grammar errors
+
+### Refactor
+
+- replace hard-coded string "cz_conventional_commits" with DEFAULT_SETTINGS (#1830)
+- **bump**: fix unbounded variable type issue
+
## v4.12.1 (2026-01-22)
### Fix
diff --git a/commitizen/__version__.py b/commitizen/__version__.py
index 33111828cc..ab9d6f875c 100644
--- a/commitizen/__version__.py
+++ b/commitizen/__version__.py
@@ -1 +1 @@
-__version__ = "4.12.1"
+__version__ = "4.13.1"
diff --git a/commitizen/config/__init__.py b/commitizen/config/__init__.py
index ae980af9c0..85414496f7 100644
--- a/commitizen/config/__init__.py
+++ b/commitizen/config/__init__.py
@@ -13,23 +13,17 @@ def _resolve_config_candidates() -> list[BaseConfig]:
git_project_root = git.find_git_project_root()
cfg_search_paths = [Path(".")]
- if git_project_root and not cfg_search_paths[0].samefile(git_project_root):
+ if git_project_root and cfg_search_paths[0].resolve() != git_project_root.resolve():
cfg_search_paths.append(git_project_root)
- # The following algorithm is ugly, but we need to ensure that the order of the candidates are preserved before v5.
- # Also, the number of possible config files is limited, so the complexity is not a problem.
candidates: list[BaseConfig] = []
for dir in cfg_search_paths:
for filename in defaults.CONFIG_FILES:
- out_path = dir / Path(filename)
- if (
- out_path.exists()
- and not any(
- out_path.samefile(candidate.path) for candidate in candidates
- )
- and not (conf := _create_config_from_path(out_path)).is_empty_config
- ):
- candidates.append(conf)
+ out_path = dir / filename
+ if out_path.is_file():
+ conf = _create_config_from_path(out_path)
+ if conf.contains_commitizen_section():
+ candidates.append(conf)
return candidates
@@ -43,10 +37,10 @@ def _create_config_from_path(path: Path) -> BaseConfig:
def read_cfg(filepath: str | None = None) -> BaseConfig:
if filepath is not None:
conf_path = Path(filepath)
- if not conf_path.exists():
+ if not conf_path.is_file():
raise ConfigFileNotFound()
conf = _create_config_from_path(conf_path)
- if conf.is_empty_config:
+ if not conf.contains_commitizen_section():
raise ConfigFileIsEmpty()
return conf
diff --git a/commitizen/config/base_config.py b/commitizen/config/base_config.py
index 98270915d8..f100cf9953 100644
--- a/commitizen/config/base_config.py
+++ b/commitizen/config/base_config.py
@@ -17,10 +17,16 @@
class BaseConfig:
def __init__(self) -> None:
- self.is_empty_config = False
self._settings: Settings = DEFAULT_SETTINGS.copy()
self._path: Path | None = None
+ def contains_commitizen_section(self) -> bool:
+ """Check if the config file contains a commitizen section.
+
+ The implementation is different for each config file type.
+ """
+ raise NotImplementedError()
+
@property
def settings(self) -> Settings:
return self._settings
diff --git a/commitizen/config/json_config.py b/commitizen/config/json_config.py
index 860ca8ed5a..3951c52854 100644
--- a/commitizen/config/json_config.py
+++ b/commitizen/config/json_config.py
@@ -25,6 +25,11 @@ def __init__(self, *, data: bytes | str, path: Path) -> None:
self.path = path
self._parse_setting(data)
+ def contains_commitizen_section(self) -> bool:
+ with self.path.open("rb") as json_file:
+ config_doc = json.load(json_file)
+ return config_doc.get("commitizen") is not None
+
def init_empty_config_content(self) -> None:
with smart_open(
self.path, "a", encoding=self._settings["encoding"]
@@ -32,7 +37,7 @@ def init_empty_config_content(self) -> None:
json.dump({"commitizen": {}}, json_file)
def set_key(self, key: str, value: object) -> Self:
- with open(self.path, "rb") as f:
+ with self.path.open("rb") as f:
config_doc = json.load(f)
config_doc["commitizen"][key] = value
@@ -59,4 +64,4 @@ def _parse_setting(self, data: bytes | str) -> None:
try:
self.settings.update(doc["commitizen"])
except KeyError:
- self.is_empty_config = True
+ pass
diff --git a/commitizen/config/toml_config.py b/commitizen/config/toml_config.py
index b10cf9bd3e..ed0bf73efb 100644
--- a/commitizen/config/toml_config.py
+++ b/commitizen/config/toml_config.py
@@ -26,6 +26,11 @@ def __init__(self, *, data: bytes | str, path: Path) -> None:
self.path = path
self._parse_setting(data)
+ def contains_commitizen_section(self) -> bool:
+ with self.path.open("rb") as f:
+ config_doc = parse(f.read())
+ return config_doc.get("tool", {}).get("commitizen") is not None
+
def init_empty_config_content(self) -> None:
config_doc = TOMLDocument()
if os.path.isfile(self.path):
@@ -67,4 +72,4 @@ def _parse_setting(self, data: bytes | str) -> None:
try:
self.settings.update(doc["tool"]["commitizen"]) # type: ignore[index,typeddict-item] # TODO: fix this
except exceptions.NonExistentKey:
- self.is_empty_config = True
+ pass
diff --git a/commitizen/config/yaml_config.py b/commitizen/config/yaml_config.py
index 58722d0f60..dd1c2c6308 100644
--- a/commitizen/config/yaml_config.py
+++ b/commitizen/config/yaml_config.py
@@ -32,6 +32,11 @@ def init_empty_config_content(self) -> None:
) as json_file:
yaml.dump({"commitizen": {}}, json_file, explicit_start=True)
+ def contains_commitizen_section(self) -> bool:
+ with self.path.open("rb") as yaml_file:
+ config_doc = yaml.load(yaml_file, Loader=yaml.FullLoader)
+ return config_doc.get("commitizen") is not None
+
def _parse_setting(self, data: bytes | str) -> None:
"""We expect to have a section in cz.yaml looking like
@@ -40,8 +45,6 @@ def _parse_setting(self, data: bytes | str) -> None:
name: cz_conventional_commits
```
"""
- import yaml.scanner
-
try:
doc = yaml.safe_load(data)
except yaml.YAMLError as e:
@@ -50,7 +53,7 @@ def _parse_setting(self, data: bytes | str) -> None:
try:
self.settings.update(doc["commitizen"])
except (KeyError, TypeError):
- self.is_empty_config = True
+ pass
def set_key(self, key: str, value: object) -> Self:
with open(self.path, "rb") as yaml_file:
diff --git a/docs/images/cli_help/cz___help.svg b/docs/images/cli_help/cz___help.svg
index ae9a135743..64b79fc0bb 100644
--- a/docs/images/cli_help/cz___help.svg
+++ b/docs/images/cli_help/cz___help.svg
@@ -19,136 +19,136 @@
font-weight: 700;
}
- .terminal-1688774822-matrix {
+ .terminal-199999382-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
- .terminal-1688774822-title {
+ .terminal-199999382-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
- .terminal-1688774822-r1 { fill: #c5c8c6 }
-.terminal-1688774822-r2 { fill: #c5c8c6;font-weight: bold }
-.terminal-1688774822-r3 { fill: #d0b344 }
-.terminal-1688774822-r4 { fill: #1984e9;text-decoration: underline; }
-.terminal-1688774822-r5 { fill: #68a0b3;font-weight: bold }
+ .terminal-199999382-r1 { fill: #c5c8c6 }
+.terminal-199999382-r2 { fill: #c5c8c6;font-weight: bold }
+.terminal-199999382-r3 { fill: #d0b344 }
+.terminal-199999382-r4 { fill: #1984e9;text-decoration: underline; }
+.terminal-199999382-r5 { fill: #68a0b3;font-weight: bold }
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -160,46 +160,46 @@
-
+
-
- $ cz --help
-usage: cz [-h][--config CONFIG][--debug][-n NAME][-nr NO_RAISE]
-{init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version}
-...
-
-Commitizen is a powerful release management tool that helps teams maintain
-consistent and meaningful commit messages while automating version management.
-For more information, please visit https://commitizen-tools.github.io/commitizen
-
-options:
- -h, --help show this help message and exit
- --config CONFIG the path of configuration file
- --debug use debug mode
- -n NAME, --name NAME use the given commitizen (default:
- cz_conventional_commits)
- -nr NO_RAISE, --no-raise NO_RAISE
- comma separated error codes that won't raise error,
- e.g: cz -nr 1,2,3 bump. See codes at
-https://commitizen-
- tools.github.io/commitizen/exit_codes/
-
-commands:
-{init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version}
- init init commitizen configuration
- commit (c) create new commit
- ls show available commitizens
- example show commit example
- info show information about the cz
- schema show commit schema
- bump bump semantic version based on the git log
- changelog (ch) generate changelog (note that it will overwrite
- existing file)
- check validates that a commit message matches the commitizen
- schema
- version get the version of the installed commitizen or the
- current project (default: installed commitizen)
-
+
+ $ cz --help
+usage: cz [-h][--config CONFIG][--debug][-n NAME][-nr NO_RAISE]
+{init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version}
+...
+
+Commitizen is a powerful release management tool that helps teams maintain
+consistent and meaningful commit messages while automating version management.
+For more information, please visit https://commitizen-tools.github.io/commitizen
+
+options:
+ -h, --help show this help message and exit
+ --config CONFIG The path to the configuration file.
+ --debug Use debug mode.
+ -n NAME, --name NAME Use the given commitizen (default:
+ cz_conventional_commits).
+ -nr NO_RAISE, --no-raise NO_RAISE
+ Comma-separated error codes that won't raise error,
+ e.g., cz -nr 1,2,3 bump. See codes at
+https://commitizen-
+ tools.github.io/commitizen/exit_codes/
+
+commands:
+{init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version}
+ init Initialize commitizen configuration.
+ commit (c) Create new commit.
+ ls Show available Commitizens.
+ example Show commit example.
+ info Show information about the cz.
+ schema Show commit schema.
+ bump Bump semantic version based on the git log.
+ changelog (ch) Generate changelog (note that it will overwrite
+ existing files).
+ check Validate that a commit message matches the commitizen
+ schema.
+ version Get the version of the installed commitizen or the
+ current project (default: installed commitizen).
+
diff --git a/docs/images/cli_help/cz_bump___help.svg b/docs/images/cli_help/cz_bump___help.svg
index dff62998d2..ec6b4816f1 100644
--- a/docs/images/cli_help/cz_bump___help.svg
+++ b/docs/images/cli_help/cz_bump___help.svg
@@ -1,4 +1,4 @@
-