diff --git a/commitizen/bump.py b/commitizen/bump.py index 030c8f1e5..b9e4784ab 100644 --- a/commitizen/bump.py +++ b/commitizen/bump.py @@ -82,17 +82,27 @@ def update_version_in_files( for path, pattern in _resolve_files_and_regexes(version_files, current_version): current_version_found = False + inconsistent_lines: list[tuple[int, str]] = [] bumped_lines = [] with open(path, encoding=encoding) as version_file: - for line in version_file: - bumped_line = ( - line.replace(current_version, new_version) - if pattern.search(line) - else line - ) - - current_version_found = current_version_found or bumped_line != line + for lineno, line in enumerate(version_file, 1): + if pattern.search(line): + if current_version in line: + bumped_line = line.replace(current_version, new_version) + current_version_found = True + else: + # The version-files regex matched this line, but the + # current version isn't on it. If the line looks like + # it sets a version-shaped value, this is almost + # certainly an inconsistent source we'd otherwise miss + # silently (#595). + bumped_line = line + if check_consistency and _LIKELY_VERSION_VALUE_RE.search(line): + inconsistent_lines.append((lineno, line.rstrip("\r\n"))) + else: + bumped_line = line + bumped_lines.append(bumped_line) if check_consistency and not current_version_found: @@ -102,6 +112,17 @@ def update_version_in_files( "version_files are possibly inconsistent." ) + if check_consistency and inconsistent_lines: + details = "\n".join(f" line {n}: {text}" for n, text in inconsistent_lines) + raise CurrentVersionNotFoundError( + f"Found line(s) in {path} matching the version regex but " + f"holding a version other than {current_version}:\n" + f"{details}\n" + "This usually means another tool (e.g. poetry, pep621) is " + "tracking a different version. Either align them, narrow the " + "`version_files` regex, or drop `--check-consistency`." + ) + bumped_version_file_content = "".join(bumped_lines) # Write the file out again @@ -112,6 +133,12 @@ def update_version_in_files( return updated_files +# Lines that look like ``key = "1.2.3"`` / ``key: 1.2.3-rc.0`` etc. -- enough +# to catch the typical pyproject.toml ``[tool.poetry].version = "..."`` and +# ``[project].version = "..."`` cases handled by ``--check-consistency``. +_LIKELY_VERSION_VALUE_RE = re.compile(r"\d+\.\d+\.\d+(?:[\w.\-+]*)") + + def _resolve_files_and_regexes( patterns: Iterable[str], version: str ) -> Generator[tuple[str, re.Pattern], None, None]: diff --git a/tests/test_bump_update_version_in_files.py b/tests/test_bump_update_version_in_files.py index 80823a4e1..f25f21ce1 100644 --- a/tests/test_bump_update_version_in_files.py +++ b/tests/test_bump_update_version_in_files.py @@ -302,6 +302,61 @@ def test_update_version_in_files_with_check_consistency_true_failure( assert expected_msg in str(excinfo.value) +def test_update_version_in_files_check_consistency_detects_sibling_version(tmp_path): + """Regression test for #595: when a single file's version regex matches + multiple version-shaped values (typical pyproject.toml with + ``[tool.poetry].version`` and ``[tool.commitizen].version``), + ``--check-consistency`` should flag the lines whose version doesn't match + the current one instead of silently leaving the sibling out of date. + """ + pyproject = tmp_path / "pyproject.toml" + pyproject.write_text( + '[tool.poetry]\nversion = "2.5.7"\n\n[tool.commitizen]\nversion = "2.5.2"\n', + encoding="utf-8", + ) + + with pytest.raises(CurrentVersionNotFoundError) as excinfo: + bump.update_version_in_files( + current_version="2.5.2", + new_version="2.6.0", + version_files=[f"{pyproject}:version"], + check_consistency=True, + encoding="utf-8", + ) + + msg = str(excinfo.value) + assert "2.5.7" in msg + assert "2.5.2" in msg + # The original file must NOT have been rewritten when consistency fails. + # (We re-read it to confirm the bump didn't proceed.) + assert '"2.5.2"' in pyproject.read_text(encoding="utf-8") + + +def test_update_version_in_files_check_consistency_off_keeps_legacy_behaviour( + tmp_path, +): + """Without ``check_consistency``, the old behaviour is preserved: only + the lines that contain the current version are updated, and sibling + versions are left alone (no exception).""" + pyproject = tmp_path / "pyproject.toml" + pyproject.write_text( + '[tool.poetry]\nversion = "2.5.7"\n\n[tool.commitizen]\nversion = "2.5.2"\n', + encoding="utf-8", + ) + + bump.update_version_in_files( + current_version="2.5.2", + new_version="2.6.0", + version_files=[f"{pyproject}:version"], + check_consistency=False, + encoding="utf-8", + ) + + rewritten = pyproject.read_text(encoding="utf-8") + assert '[tool.poetry]\nversion = "2.5.7"' in rewritten + assert '[tool.commitizen]\nversion = "2.6.0"' in rewritten + + @pytest.mark.parametrize( ("encoding", "filename"), [