Skip to content

Commit 828393f

Browse files
committed
fix(changelog): skip unreachable tags when picking previous release
`cz changelog <version>` resolves the previous-release tag by walking the creatordate-sorted tag list. When a parallel maintenance branch carries a tag whose creatordate sits between the current release and the legitimate previous release, that branch's tag was wrongly picked because it was reachable from some commit, just not from HEAD. `get_tags()` already supports a `reachable_only` flag that appends `git tag --merged`. Pass it from the changelog path so unreachable parallel-line tags are filtered out before the sort, mirroring the behaviour of `get_latest_tag_name()` which already uses `git describe --abbrev=0 --tags` (ancestry-based) for the same reason. Fixes #2083
1 parent 9432748 commit 828393f

2 files changed

Lines changed: 106 additions & 1 deletion

File tree

commitizen/commands/changelog.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,9 @@ def __call__(self) -> None:
209209
if not self.file_name:
210210
raise NotAllowed("filename is required.")
211211

212-
tags = self.tag_rules.get_version_tags(git.get_tags(), warn=True)
212+
tags = self.tag_rules.get_version_tags(
213+
git.get_tags(reachable_only=True), warn=True
214+
)
213215
changelog_meta = changelog.Metadata()
214216
if self.incremental:
215217
changelog_meta = self.changelog_format.get_metadata(self.file_name)

tests/commands/test_changelog_command.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1693,3 +1693,106 @@ def test_changelog_template_incremental_variable(
16931693
util.run_cli("changelog", "--file-name", target, "--incremental")
16941694
out = Path(target).read_text(encoding="utf-8")
16951695
file_regression.check(out, extension=".incremental.md")
1696+
1697+
1698+
@pytest.mark.usefixtures("tmp_commitizen_project")
1699+
@pytest.mark.freeze_time("2026-09-02")
1700+
def test_changelog_previous_release_skips_unreachable_tags(
1701+
config_path: Path,
1702+
changelog_path: Path,
1703+
monkeypatch: pytest.MonkeyPatch,
1704+
util: UtilFixture,
1705+
):
1706+
"""Regression test for #2083.
1707+
1708+
``cz changelog`` resolves the previous-release tag by walking the
1709+
creatordate-sorted tag list. A parallel maintenance branch's tag must
1710+
be skipped if it is not an ancestor of HEAD — otherwise the rendered
1711+
range pulls in commits from a branch that is not part of the current
1712+
release line.
1713+
"""
1714+
# Arrange — bare tag format, then a parallel v4 line and a main line.
1715+
with config_path.open("a", encoding="utf-8") as f:
1716+
f.write('tag_format = "$version"\n')
1717+
1718+
# The fixture resets GIT_COMMITTER_DATE on every ``tick``/
1719+
# ``patch_env``, so ``util.create_tag`` would clobber the date set
1720+
# here. Drive ``git tag -a`` directly via ``cmd.run`` to keep the
1721+
# creatordate stable, then restore the fixture-managed env so
1722+
# subsequent ``util.tick`` calls keep advancing frozen time as
1723+
# expected.
1724+
from commitizen import cmd as _cmd
1725+
1726+
def _tag_at(tag, iso_date):
1727+
"""Create an annotated tag pinned to ``iso_date`` regardless of the
1728+
fixture's frozen time, then restore the fixture env.
1729+
"""
1730+
monkeypatch.setenv("GIT_COMMITTER_DATE", iso_date)
1731+
result = _cmd.run(["git", "tag", "-a", tag, "-m", tag])
1732+
assert result.return_code == 0, result.err
1733+
util.patch_env()
1734+
1735+
# Main line: 5.3.1, then branch off to v4, then 5.4.0.
1736+
util.create_file_and_commit("feat: main feature a")
1737+
_tag_at("5.3.1", "2026-06-29T00:00:00")
1738+
1739+
util.create_branch("v4")
1740+
util.switch_branch("v4")
1741+
util.create_file_and_commit("feat: v4 feature a")
1742+
1743+
util.switch_branch("master")
1744+
util.create_file_and_commit("feat: main feature b")
1745+
_tag_at("5.4.0", "2026-09-02T00:00:00")
1746+
1747+
# v4 line ships 4.12.0 between 5.3.1 and 5.4.0 in time. 4.12.0's tag
1748+
# is NOT an ancestor of master — it sits on the v4 branch HEAD which
1749+
# is never merged back.
1750+
util.switch_branch("v4")
1751+
util.create_file_and_commit("feat: v4 feature b")
1752+
_tag_at("4.12.0", "2026-07-14T00:00:00")
1753+
util.switch_branch("master")
1754+
1755+
# Spy on git.get_tags so the test fails when the production call site
1756+
# drops ``reachable_only=True``. Without the spy, reverting the
1757+
# production fix would still pass because the test only checks
1758+
# render output.
1759+
from commitizen import git as cz_git
1760+
1761+
get_tags_calls: list[dict] = []
1762+
1763+
real_get_tags = cz_git.get_tags
1764+
1765+
def _tracked_get_tags(*args, **kwargs):
1766+
"""Wrap the real ``git.get_tags`` to record every call.
1767+
1768+
The wrap must call through the imported ``real_get_tags`` rather
1769+
than the module attribute, which the test itself patches and so
1770+
would recurse forever.
1771+
"""
1772+
get_tags_calls.append({"args": args, "kwargs": kwargs})
1773+
return real_get_tags(*args, **kwargs)
1774+
1775+
monkeypatch.setattr("commitizen.commands.changelog.git.get_tags", _tracked_get_tags)
1776+
1777+
# Act — single-version form goes through ``get_oldest_and_newest_rev``
1778+
# because ``rev_range`` carries the version string.
1779+
util.run_cli("changelog", "5.4.0", "--file-name", str(changelog_path))
1780+
out = changelog_path.read_text(encoding="utf-8")
1781+
1782+
# Assert — the production call site must ask git for reachable tags
1783+
# only. If a future change drops ``reachable_only=True`` from the
1784+
# call site the spy flags it.
1785+
assert get_tags_calls, "production call site never invoked git.get_tags"
1786+
assert all(
1787+
call["kwargs"].get("reachable_only") is True for call in get_tags_calls
1788+
), f"git.get_tags called without reachable_only=True: {get_tags_calls!r}"
1789+
1790+
# And the rendered output must contain the 5.4.0 entry whose body
1791+
# only carries main-line commits; the v4-line commits must not leak
1792+
# in via the wrong previous-release tag (which would otherwise
1793+
# include the 4.12.0 commit on the v4 branch once the date-sorted
1794+
# tag list resolves to 4.12.0 as the previous tag).
1795+
assert "## 5.4.0" in out
1796+
assert "main feature b" in out
1797+
assert "v4 feature a" not in out
1798+
assert "v4 feature b" not in out

0 commit comments

Comments
 (0)