Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(algorithm): correct bfs to not abort on previously visited node #822

Merged
Merged
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
72 changes: 45 additions & 27 deletions semantic_release/version/algorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,48 +71,66 @@ def _bfs_for_latest_version_in_history(
`merge_base`'s parents' history. If no commits in the history correspond
to a released version, return None
"""

tag_sha_2_version_lookup = {
tag.commit.hexsha: version for tag, version in full_release_tags_and_versions
}
# Step 3. Latest full release version within the history of the current branch
# Breadth-first search the merge-base and its parent commits for one which matches
# the tag of the latest full release tag in history
def bfs(visited: set[Commit], q: Queue[Commit]) -> Version | None:
if q.empty():
log.debug("queue is empty, returning none")
return None
def bfs(start_commit: Commit | TagObject | Blob | Tree) -> Version | None:
# Derived from Geeks for Geeks
# https://www.geeksforgeeks.org/python-program-for-breadth-first-search-or-bfs-for-a-graph/?ref=lbp

node = q.get()
if node in visited:
log.debug("commit %s already visited, returning none", node.hexsha)
return None
# Create a queue for BFS
q: Queue[Commit | TagObject | Blob | Tree] = Queue()

for tag, version in full_release_tags_and_versions:
log.debug(
"checking if tag %r (%s) matches commit %s",
tag.name,
tag.commit.hexsha,
node.hexsha,
)
if tag.commit == node:
# Create a set to store visited graph nodes (commit objects in this case)
visited: set[Commit | TagObject | Blob | Tree] = set()

# Add the source node in the queue & mark as visited to start the search
q.put(start_commit)
visited.add(start_commit)

# Initialize the result to None
result = None

# Traverse the git history until it finds a version tag if one exists
while not q.empty():
node = q.get()
visited.add(node)

log.debug("checking if commit %s matches any tags", node.hexsha)
version = tag_sha_2_version_lookup.get(node.hexsha, None)

if version is not None:
log.info(
"found latest version in branch history: %r (%s)",
str(version),
node.hexsha[:7],
)
return version
result = version
break

log.debug("commit %s doesn't match any tags", node.hexsha)

visited.add(node)
for parent in node.parents:
log.debug("queuing parent commit %s", parent.hexsha)
q.put(parent)
# Add all parent commits to the queue if they haven't been visited
for parent in node.parents:
if parent in visited:
log.debug("parent commit %s already visited", node.hexsha)
continue

log.debug("queuing parent commit %s", parent.hexsha)
q.put(parent)

return bfs(visited, q)
return result

q: Queue[Commit] = Queue()
q.put(merge_base)
latest_version = bfs(set(), q)
# Run a Breadth First Search to find the latest version in the history
latest_version = bfs(merge_base)
if latest_version is not None:
log.info("the latest version in this branch's history is %s", latest_version)
else:
log.info("no version tags found in this branch's history")

log.info("the latest version in this branch's history is %s", latest_version)
return latest_version


Expand Down
54 changes: 52 additions & 2 deletions tests/unit/semantic_release/version/test_algorithm.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,62 @@
import pytest
from git import Repo
from git import Commit, Repo, TagReference

from semantic_release.enums import LevelBump
from semantic_release.version.algorithm import _increment_version, tags_and_versions
from semantic_release.version.algorithm import (
_bfs_for_latest_version_in_history,
_increment_version,
tags_and_versions,
)
from semantic_release.version.translator import VersionTranslator
from semantic_release.version.version import Version


def test_bfs_for_latest_version_in_history():
# Setup fake git graph
"""
* merge commit 6 (start)
|\
| * commit 5
| * commit 4
|/
* commit 3
* commit 2
* commit 1
* v1.0.0
"""
repo = Repo()
expected_version = Version.parse("1.0.0")
v1_commit = Commit(repo, binsha=b"0" * 20)
class TagReferenceOverride(TagReference):
commit = v1_commit # type: ignore - mocking the commit property

v1_tag = TagReferenceOverride(repo, "refs/tags/v1.0.0", check_path=False)

trunk = Commit(repo, binsha=b"3" * 20, parents=[
Commit(repo, binsha=b"2" * 20, parents=[
Commit(repo, binsha=b"1" * 20, parents=[v1_commit]),
]),
])
start_commit = Commit(
repo,
binsha=b"6" * 20,
parents=[
trunk,
Commit(repo, binsha=b"5" * 20, parents=[
Commit(repo, binsha=b"4" * 20, parents=[trunk]),
]),
]
)

# Execute
actual = _bfs_for_latest_version_in_history(start_commit, [
(v1_tag, expected_version),
])

# Verify
assert expected_version == actual


@pytest.mark.parametrize(
"tags, sorted_tags",
[
Expand Down