Source code for semantic_release.commit_parser.angular

"""
Angular commit style parser
https://github.com/angular/angular/blob/master/CONTRIBUTING.md#-commit-message-guidelines
"""

from __future__ import annotations

import logging
import re
from typing import TYPE_CHECKING, Tuple

from pydantic.dataclasses import dataclass

from semantic_release.commit_parser._base import CommitParser, ParserOptions
from semantic_release.commit_parser.token import ParsedCommit, ParseError, ParseResult
from semantic_release.commit_parser.util import breaking_re, parse_paragraphs
from semantic_release.enums import LevelBump

if TYPE_CHECKING:
    from git.objects.commit import Commit

log = logging.getLogger(__name__)


def _logged_parse_error(commit: Commit, error: str) -> ParseError:
    log.debug(error)
    return ParseError(commit, error=error)


# types with long names in changelog
LONG_TYPE_NAMES = {
    "feat": "feature",
    "docs": "documentation",
    "perf": "performance",
}


[docs]@dataclass class AngularParserOptions(ParserOptions): """Options dataclass for AngularCommitParser""" allowed_tags: Tuple[str, ...] = ( "build", "chore", "ci", "docs", "feat", "fix", "perf", "style", "refactor", "test", ) minor_tags: Tuple[str, ...] = ("feat",) patch_tags: Tuple[str, ...] = ("fix", "perf") default_bump_level: LevelBump = LevelBump.NO_RELEASE
[docs]class AngularCommitParser(CommitParser[ParseResult, AngularParserOptions]): """ A commit parser for projects conforming to the angular style of conventional commits. See https://www.conventionalcommits.org/en/v1.0.0-beta.4/ """ # TODO: Deprecate in lieu of get_default_options() parser_options = AngularParserOptions def __init__(self, options: AngularParserOptions | None = None) -> None: super().__init__(options) self.re_parser = re.compile( rf""" (?P<type>{"|".join(self.options.allowed_tags)}) # e.g. feat (?:\((?P<scope>[^\n]+)\))? # or feat(parser) (?P<break>!)?:\s+ # breaking if feat!: (?P<subject>[^\n]+) # commit subject (:?\n\n(?P<text>.+))? # commit body """, flags=re.VERBOSE | re.DOTALL, )
[docs] @staticmethod def get_default_options() -> AngularParserOptions: return AngularParserOptions()
# Maybe this can be cached as an optimisation, similar to how # mypy/pytest use their own caching directories, for very large commit # histories? # The problem is the cache likely won't be present in CI environments
[docs] def parse(self, commit: Commit) -> ParseResult: """ Attempt to parse the commit message with a regular expression into a ParseResult """ message = str(commit.message) parsed = self.re_parser.match(message) if not parsed: return _logged_parse_error( commit, f"Unable to parse commit message: {message}" ) parsed_break = parsed.group("break") parsed_scope = parsed.group("scope") parsed_subject = parsed.group("subject") parsed_text = parsed.group("text") parsed_type = parsed.group("type") descriptions = parse_paragraphs(parsed_text) if parsed_text else [] # Insert the subject before the other paragraphs descriptions.insert(0, parsed_subject) # Look for descriptions of breaking changes breaking_descriptions = [ match.group(1) for match in (breaking_re.match(p) for p in descriptions[1:]) if match ] if parsed_break or breaking_descriptions: level_bump = LevelBump.MAJOR parsed_type = "breaking" elif parsed_type in self.options.minor_tags: level_bump = LevelBump.MINOR elif parsed_type in self.options.patch_tags: level_bump = LevelBump.PATCH else: level_bump = self.options.default_bump_level log.debug( "commit %s introduces a level bump of %s due to the default_bump_level", commit.hexsha, level_bump, ) log.debug("commit %s introduces a %s level_bump", commit.hexsha, level_bump) return ParsedCommit( bump=level_bump, type=LONG_TYPE_NAMES.get(parsed_type, parsed_type), scope=parsed_scope, descriptions=descriptions, breaking_descriptions=breaking_descriptions, commit=commit, )