Repository: relekang/python-semantic-release
Pull request #488 (2022-08-13 12:18:22)

fix: search for release commit with commit_subject
Use release commit template settings in release commit search.

bernardcooke53: @d-ryzhikov this looks like a good catch! Since we don't want to identify the current version with a commit that doesn't match the specified `commit_subject`, is it worth adding an explicit test that a commit in the log which _doesn't_ match the `commit_subject` template but contains the current version returns `None`?
d-ryzhikov: @bernardcooke53 I've added a test for a case when there is a commit with a message containing current version but the subject doesn't match `commit_subject` setting. However, in this case `evaluate_version_bump` shouldn't return `None`. Instead it should not evaluate this commit as the release commit and continue the search.
bernardcooke53: @d-ryzhikov looks good! Yes you're right about the bump, in my head I was thinking of an out-of-scope commit not triggering a bump - the way you've done it is good too :slightly_smiling_face:
d-ryzhikov: I updated the release commit search logic in `evaluate_version_bump` and in `generate_changelog`. Now the release commit check is more strict: a commit message for a target version is constructed and only commits with exactly matching messages are considered to be target version commits.

Patch:
diff --git a/semantic_release/history/logs.py b/semantic_release/history/logs.py
index e4d2748..9559277 100644
--- a/semantic_release/history/logs.py
+++ b/semantic_release/history/logs.py
@@ -6,7 +6,7 @@ from typing import Optional
 from ..errors import UnknownCommitMessageStyleError
 from ..helpers import LoggedFunction
 from ..settings import config, current_commit_parser
-from ..vcs_helpers import get_commit_log
+from ..vcs_helpers import get_commit_log, get_formatted_commit
 
 logger = logging.getLogger(__name__)
 
@@ -36,8 +36,9 @@ def evaluate_version_bump(current_version: str, force: str = None) -> Optional[s
     changes = []
     commit_count = 0
 
+    current_version_commit = get_formatted_commit(current_version)
     for _hash, commit_message in get_commit_log(current_version):
-        if commit_message.startswith(current_version):
+        if commit_message == current_version_commit:
             # Stop once we reach the current version
             # (we are looping in the order of newest -> oldest)
             logger.debug(
@@ -67,7 +68,7 @@ def evaluate_version_bump(current_version: str, force: str = None) -> Optional[s
 
     if config.get("patch_without_tag") and commit_count > 0 and bump is None:
         bump = "patch"
-        logger.debug(f"Changing bump level to patch based on config patch_without_tag")
+        logger.debug("Changing bump level to patch based on config patch_without_tag")
 
     if (
         not config.get("major_on_zero")
@@ -81,7 +82,7 @@ def evaluate_version_bump(current_version: str, force: str = None) -> Optional[s
 
 
 @LoggedFunction(logger)
-def generate_changelog(from_version: str, to_version: str = None) -> dict:
+def generate_changelog(from_version: Optional[str], to_version: Optional[str] = None) -> dict:
     """
     Parse a changelog dictionary for the given version.
 
@@ -98,11 +99,13 @@ def generate_changelog(from_version: str, to_version: str = None) -> dict:
         rev = from_version
 
     found_the_release = to_version is None
+    to_version_commit = to_version and get_formatted_commit(to_version)
+    from_version_commit = from_version and get_formatted_commit(from_version)
     for _hash, commit_message in get_commit_log(rev):
         if not found_the_release:
             # Skip until we find the last commit in this release
             # (we are looping in the order of newest -> oldest)
-            if to_version and to_version not in commit_message:
+            if to_version_commit and commit_message != to_version_commit:
                 continue
             else:
                 logger.debug(
@@ -110,7 +113,7 @@ def generate_changelog(from_version: str, to_version: str = None) -> dict:
                 )
                 found_the_release = True
 
-        if from_version is not None and from_version in commit_message:
+        if from_version_commit and commit_message == from_version_commit:
             # We reached the previous release
             logger.debug(f"{from_version} reached, ending changelog generation")
             break
diff --git a/semantic_release/vcs_helpers.py b/semantic_release/vcs_helpers.py
index 5057021..8063e73 100644
--- a/semantic_release/vcs_helpers.py
+++ b/semantic_release/vcs_helpers.py
@@ -37,6 +37,19 @@ def get_formatted_tag(version):
     return tag_format.format(version=version)
 
 
+def get_formatted_commit(version: str) -> str:
+    commit_subject = config.get("commit_subject")
+    message = commit_subject.format(version=version)
+
+    # Add an extended message if one is configured
+    commit_message = config.get("commit_message")
+    if commit_message:
+        message += "\n\n"
+        message += commit_message.format(version=version)
+
+    return message
+
+
 def get_commit_log(from_rev=None):
     """Yield all commit messages from last to first."""
     rev = None
@@ -126,15 +139,7 @@ def commit_new_version(version: str):
     """
     from .history import load_version_declarations
 
-    commit_subject = config.get("commit_subject")
-    message = commit_subject.format(version=version)
-
-    # Add an extended message if one is configured
-    commit_message = config.get("commit_message")
-    if commit_message:
-        message += "\n\n"
-        message += commit_message.format(version=version)
-
+    commit_message = get_formatted_commit(version)
     commit_author = config.get(
         "commit_author",
         "semantic-release <semantic-release>",
@@ -144,7 +149,7 @@ def commit_new_version(version: str):
         git_path: PurePath = PurePath(os.getcwd(), declaration.path).relative_to(repo().working_dir)  # type: ignore
         repo().git.add(str(git_path))
 
-    return repo().git.commit(m=message, author=commit_author)
+    return repo().git.commit(m=commit_message, author=commit_author)
 
 
 @LoggedFunction(logger)


Test patch:
diff --git a/tests/history/__init__.py b/tests/history/__init__.py
index 43e02d9..3162436 100644
--- a/tests/history/__init__.py
+++ b/tests/history/__init__.py
@@ -37,5 +37,13 @@ UNKNOWN_STYLE = ("7", "random commits are the worst")
 ALL_KINDS_OF_COMMIT_MESSAGES = [MINOR, MAJOR, MINOR, PATCH]
 MINOR_AND_PATCH_COMMIT_MESSAGES = [MINOR, PATCH]
 PATCH_COMMIT_MESSAGES = [PATCH, PATCH]
-MAJOR_LAST_RELEASE_MINOR_AFTER = [MINOR, ("22", "1.1.0"), MAJOR]
-MAJOR_MENTIONING_LAST_VERSION = [MAJOR_MENTIONING_1_0_0, ("22", "1.0.0"), MAJOR]
+MAJOR_LAST_RELEASE_MINOR_AFTER = [
+    MINOR,
+    ("22", "1.1.0\n\nAutomatically generated by python-semantic-release"),
+    MAJOR
+]
+MAJOR_MENTIONING_LAST_VERSION = [
+    MAJOR_MENTIONING_1_0_0,
+    ("22", "1.0.0\n\nAutomatically generated by python-semantic-release"),
+    MAJOR
+]
diff --git a/tests/history/test_version_bump.py b/tests/history/test_version_bump.py
index 737643d..2f9f654 100644
--- a/tests/history/test_version_bump.py
+++ b/tests/history/test_version_bump.py
@@ -81,11 +81,7 @@ def test_should_return_none_without_commits():
     """
     Make sure that we do not release if there are no commits since last release.
     """
-    with mock.patch("semantic_release.history.config.get", lambda *x: True):
-        assert evaluate_version_bump("1.1.0") is None
-
-    with mock.patch("semantic_release.history.config.get", lambda *x: False):
-        assert evaluate_version_bump("1.1.0") is None
+    assert evaluate_version_bump("1.1.0") is None
 
 
 @mock.patch(
@@ -94,3 +90,29 @@ def test_should_return_none_without_commits():
 @mock.patch("semantic_release.history.logs.get_commit_log", lambda *a, **kw: [MAJOR])
 def test_should_minor_without_major_on_zero():
     assert evaluate_version_bump("0.1.0") == "minor"
+
+@mock.patch(
+    "semantic_release.history.config.get", wrapped_config_get(
+        commit_subject="chore(release): {version}",
+        commit_message="Custom message",
+    )
+)
+@mock.patch(
+    "semantic_release.history.logs.get_commit_log",
+    lambda *a, **kw: [MINOR, ("777", "chore(release): 1.0.0\n\nCustom message"), MAJOR]
+)
+def test_should_search_for_release_with_commit_subject_pattern():
+    assert evaluate_version_bump("1.0.0") == "minor"
+
+@mock.patch(
+    "semantic_release.history.config.get", wrapped_config_get(
+        commit_subject="chore(release): {version}",
+        commit_message="Custom message",
+    )
+)
+@mock.patch(
+    "semantic_release.history.logs.get_commit_log",
+    lambda *a, **kw: [MINOR, ("777", "1.0.0"), MAJOR]
+)
+def test_should_not_search_for_release_without_commit_subject_pattern():
+    assert evaluate_version_bump("1.0.0") == "major"