diff --git a/src/semantic_release/changelog/release_history.py b/src/semantic_release/changelog/release_history.py index 518229a1f..961ae074c 100644 --- a/src/semantic_release/changelog/release_history.py +++ b/src/semantic_release/changelog/release_history.py @@ -8,6 +8,7 @@ from git.objects.tag import TagObject from semantic_release.commit_parser import ParseError +from semantic_release.commit_parser.token import ParsedCommit from semantic_release.enums import LevelBump from semantic_release.version.algorithm import tags_and_versions @@ -136,6 +137,23 @@ def from_git_history( ) continue + if ( + isinstance(parse_result, ParsedCommit) + and not parse_result.include_in_changelog + ): + log.info( + str.join( + " ", + [ + "Excluding commit %s (%s) because parser determined", + "it should not included in the changelog", + ], + ), + commit.hexsha[:8], + commit_message.replace("\n", " ")[:20], + ) + continue + if the_version is None: log.info( "[Unreleased] adding '%s' commit(%s) to list", diff --git a/src/semantic_release/commit_parser/token.py b/src/semantic_release/commit_parser/token.py index 8eff9fbb0..db23a1fba 100644 --- a/src/semantic_release/commit_parser/token.py +++ b/src/semantic_release/commit_parser/token.py @@ -18,6 +18,7 @@ class ParsedMessageResult(NamedTuple): descriptions: tuple[str, ...] breaking_descriptions: tuple[str, ...] = () linked_merge_request: str = "" + include_in_changelog: bool = True class ParsedCommit(NamedTuple): @@ -28,6 +29,7 @@ class ParsedCommit(NamedTuple): breaking_descriptions: list[str] commit: Commit linked_merge_request: str = "" + include_in_changelog: bool = True @property def message(self) -> str: @@ -60,6 +62,7 @@ def from_parsed_message_result( breaking_descriptions=list(parsed_message_result.breaking_descriptions), commit=commit, linked_merge_request=parsed_message_result.linked_merge_request, + include_in_changelog=parsed_message_result.include_in_changelog, ) diff --git a/tests/e2e/cmd_changelog/test_changelog_custom_parser.py b/tests/e2e/cmd_changelog/test_changelog_custom_parser.py new file mode 100644 index 000000000..d59044b69 --- /dev/null +++ b/tests/e2e/cmd_changelog/test_changelog_custom_parser.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +import os +from typing import TYPE_CHECKING + +import pytest +from pytest_lazy_fixtures.lazy_fixture import lf as lazy_fixture + +from semantic_release.changelog.context import ChangelogMode +from semantic_release.cli.commands.main import main + +from tests.const import CHANGELOG_SUBCMD, MAIN_PROG_NAME +from tests.fixtures.repos import repo_w_no_tags_angular_commits +from tests.util import ( + CustomAngularParserWithIgnorePatterns, + assert_successful_exit_code, +) + +if TYPE_CHECKING: + from pathlib import Path + + from click.testing import CliRunner + + from tests.fixtures.example_project import UpdatePyprojectTomlFn, UseCustomParserFn + from tests.fixtures.git_repo import BuiltRepoResult, GetCommitDefFn + + +@pytest.mark.parametrize( + "repo_result", [lazy_fixture(repo_w_no_tags_angular_commits.__name__)] +) +def test_changelog_custom_parser_remove_from_changelog( + repo_result: BuiltRepoResult, + cli_runner: CliRunner, + update_pyproject_toml: UpdatePyprojectTomlFn, + use_custom_parser: UseCustomParserFn, + get_commit_def_of_angular_commit: GetCommitDefFn, + changelog_md_file: Path, + default_md_changelog_insertion_flag: str, +): + """ + Given when a changelog filtering custom parser is configured + When provided a commit message that matches the ignore syntax + Then the commit message is not included in the resulting changelog + """ + ignored_commit_def = get_commit_def_of_angular_commit( + "chore: do not include me in the changelog" + ) + + # Because we are in init mode, the insertion flag is not present in the changelog + # we must take it out manually because our repo generation fixture includes it automatically + with changelog_md_file.open(newline=os.linesep) as rfd: + # use os.linesep here because the insertion flag is os-specific + # but convert the content to universal newlines for comparison + expected_changelog_content = ( + rfd.read() + .replace(f"{default_md_changelog_insertion_flag}{os.linesep}", "") + .replace("\r", "") + ) + + # Set the project configurations + update_pyproject_toml( + "tool.semantic_release.changelog.mode", ChangelogMode.INIT.value + ) + use_custom_parser( + f"{CustomAngularParserWithIgnorePatterns.__module__}:{CustomAngularParserWithIgnorePatterns.__name__}" + ) + + # Setup: add the commit to be ignored + repo_result["repo"].git.commit(m=ignored_commit_def["msg"], a=True) + + # Act + cli_cmd = [MAIN_PROG_NAME, CHANGELOG_SUBCMD] + result = cli_runner.invoke(main, cli_cmd[1:]) + + # Take measurement after action + actual_content = changelog_md_file.read_text() + + # Evaluate + assert_successful_exit_code(result, cli_cmd) + + # Verify that the changelog content does not include our commit + assert ignored_commit_def["desc"] not in actual_content + + # Verify that the changelog content has not changed + assert expected_changelog_content == actual_content diff --git a/tests/util.py b/tests/util.py index b49942171..194d1f377 100644 --- a/tests/util.py +++ b/tests/util.py @@ -17,7 +17,13 @@ from semantic_release.changelog.context import ChangelogMode, make_changelog_context from semantic_release.changelog.release_history import ReleaseHistory from semantic_release.commit_parser._base import CommitParser, ParserOptions -from semantic_release.commit_parser.token import ParsedCommit, ParseResult +from semantic_release.commit_parser.angular import AngularCommitParser +from semantic_release.commit_parser.token import ( + ParsedCommit, + ParsedMessageResult, + ParseError, + ParseResult, +) from semantic_release.enums import LevelBump from tests.const import SUCCESS_EXIT_CODE @@ -38,7 +44,6 @@ from git import Commit from semantic_release.cli.config import RuntimeContext - from semantic_release.commit_parser.token import ParseError _R = TypeVar("_R") @@ -277,3 +282,21 @@ def parse(self, commit: Commit) -> ParsedCommit | ParseError: class IncompleteCustomParser(CommitParser): pass + + +class CustomAngularParserWithIgnorePatterns(AngularCommitParser): + def parse(self, commit: Commit) -> ParsedCommit | ParseError: + if not (parse_msg_result := super().parse_message(str(commit.message))): + return ParseError(commit, "Unable to parse commit") + + return ParsedCommit.from_parsed_message_result( + commit, + ParsedMessageResult( + **{ + **parse_msg_result._asdict(), + "include_in_changelog": bool( + not str(commit.message).startswith("chore") + ), + } + ), + )