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
3 changes: 2 additions & 1 deletion src/semantic_release/cli/commands/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
from semantic_release.version.algorithm import (
next_version,
tags_and_versions,
tags_and_versions_in_history,
)
from semantic_release.version.translator import VersionTranslator

Expand Down Expand Up @@ -96,7 +97,7 @@ def version_from_forced_level(
repo_dir: Path, forced_level_bump: LevelBump, translator: VersionTranslator
) -> Version:
with Repo(str(repo_dir)) as git_repo:
ts_and_vs = tags_and_versions(git_repo.tags, translator)
ts_and_vs = tags_and_versions_in_history(git_repo, translator)

# If we have no tags, return the default version
if not ts_and_vs:
Expand Down
50 changes: 32 additions & 18 deletions src/semantic_release/version/algorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,34 @@ def tags_and_versions(
return sorted(ts_and_vs, reverse=True, key=lambda v: v[1])


def tags_and_versions_in_history(
repo: Repo, translator: VersionTranslator
) -> list[tuple[Tag, Version]]:
"""Return parsed versions whose tags are reachable from the active branch."""
parsed_tags_and_versions = tags_and_versions(repo.tags, translator)

# A noop version command reports that it would unshallow the repository but
# deliberately leaves the history incomplete. Preserve the existing forced-bump
# behavior until the full graph is available.
if repo.git.rev_parse("--is-shallow-repository") == "true":
return parsed_tags_and_versions

commit_hash_set = {
commit.hexsha
for commit in _traverse_graph_for_commits(head_commit=repo.active_branch.commit)
}

historic_tags_and_versions: list[tuple[Tag, Version]] = []
for tag, version in parsed_tags_and_versions:
# Tags pointing to tags are resolved automatically, but tags pointing to
# blobs or trees cannot be part of a branch's commit history.
with suppress(ValueError):
if tag.commit.hexsha in commit_hash_set:
historic_tags_and_versions.append((tag, version))

return historic_tags_and_versions


def _traverse_graph_for_commits(
head_commit: Commit,
latest_release_tag_str: str = "",
Expand Down Expand Up @@ -266,24 +294,10 @@ def next_version(
"Translator was unable to parse the embedded default version"
)

# Step 1. All tags, sorted descending by semver ordering rules
all_git_tags_as_versions = tags_and_versions(repo.tags, translator)

# Retrieve all commit hashes (regardless of merges) in the current branch's history from repo origin
commit_hash_set = {
commit.hexsha
for commit in _traverse_graph_for_commits(head_commit=repo.active_branch.commit)
}

# Filter all releases that are not found in the current branch's history
historic_versions: list[Version] = []
for tag, version in all_git_tags_as_versions:
# TODO: move this to tags_and_versions() function?
# Ignore the error that is raised when tag points to a Blob or Tree object rather
# than a commit object (tags that point to tags that then point to commits are resolved automatically)
with suppress(ValueError):
if tag.commit.hexsha in commit_hash_set:
historic_versions.append(version)
# Step 1. All versions in the current branch's history, sorted descending by semver
historic_versions = [
version for _, version in tags_and_versions_in_history(repo, translator)
]

# Step 2. Get the latest final release version in the history of the current branch
# or fallback to the default 0.0.0 starting version value if none are found
Expand Down
33 changes: 33 additions & 0 deletions tests/e2e/cmd_version/test_version_print.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,39 @@ def test_version_print_next_version(
assert post_mocker.call_count == 0


@pytest.mark.parametrize(
"repo_result",
[lazy_fixture(repo_w_trunk_only_conventional_commits.__name__)],
)
def test_version_print_forced_bump_ignores_tags_outside_branch_history(
repo_result: BuiltRepoResult,
file_in_repo: str,
run_cli: RunCliFn,
):
"""
Given a higher version tag exists on a divergent branch,
When forcing a patch bump on the release branch,
Then the next version is based on the latest tag in the release branch history.
"""
repo = repo_result["repo"]
release_branch = repo.active_branch

divergent_branch = repo.create_head("divergent-release").checkout()
add_text_to_file(repo, file_in_repo)
repo.git.commit(m="feat: divergent release", a=True)
repo.create_tag("v1.0.0")
release_branch.checkout()

cli_cmd = [MAIN_PROG_NAME, VERSION_SUBCMD, "--print", "--patch"]
result = run_cli(cli_cmd[1:])

assert_successful_exit_code(result, cli_cmd)
assert result.stdout == "0.1.2\n"
assert divergent_branch.commit.hexsha not in {
commit.hexsha for commit in repo.iter_commits(release_branch)
}


@pytest.mark.parametrize(
"repo_result, commits, force_args, next_release_version",
[
Expand Down
Loading