python-semantic-release 10.3.2__py3-none-any.whl → 10.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_semantic_release-10.3.2.dist-info → python_semantic_release-10.4.0.dist-info}/METADATA +1 -1
- {python_semantic_release-10.3.2.dist-info → python_semantic_release-10.4.0.dist-info}/RECORD +13 -9
- semantic_release/cli/config.py +4 -2
- semantic_release/commit_parser/__init__.py +23 -0
- semantic_release/commit_parser/conventional/__init__.py +17 -0
- semantic_release/commit_parser/conventional/options.py +72 -0
- semantic_release/commit_parser/conventional/options_monorepo.py +90 -0
- semantic_release/commit_parser/{conventional.py → conventional/parser.py} +85 -127
- semantic_release/commit_parser/conventional/parser_monorepo.py +467 -0
- {python_semantic_release-10.3.2.dist-info → python_semantic_release-10.4.0.dist-info}/WHEEL +0 -0
- {python_semantic_release-10.3.2.dist-info → python_semantic_release-10.4.0.dist-info}/entry_points.txt +0 -0
- {python_semantic_release-10.3.2.dist-info → python_semantic_release-10.4.0.dist-info}/licenses/LICENSE +0 -0
- {python_semantic_release-10.3.2.dist-info → python_semantic_release-10.4.0.dist-info}/top_level.txt +0 -0
{python_semantic_release-10.3.2.dist-info → python_semantic_release-10.4.0.dist-info}/RECORD
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
python_semantic_release-10.
|
|
1
|
+
python_semantic_release-10.4.0.dist-info/licenses/LICENSE,sha256=NE85nszX252sdQdu0xgS9qwfYES0k8qS6gW3uO4jRGE,1083
|
|
2
2
|
semantic_release/__init__.py,sha256=tRJWhrn_dUt0QycXD2DoJSfEP5uwmxngH7jvbG2i-hA,1317
|
|
3
3
|
semantic_release/__main__.py,sha256=pksxr6g1vkKq98Q1lShsxG8tk55IMiSMHzAHKyFU5x0,1704
|
|
4
4
|
semantic_release/const.py,sha256=wInJR7vcOgT1ysm5VuJQ6lD_ZGYnCwRVKz7Uz3htQc4,861
|
|
@@ -15,7 +15,7 @@ semantic_release/changelog/template.py,sha256=eqOjtVfBbksgTK4CAZOajfkaAcKueJ-FhF
|
|
|
15
15
|
semantic_release/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
16
|
semantic_release/cli/changelog_writer.py,sha256=2jP5b0LK61Y2tb22GQSFFmwHwXd2WjbopgsMAmsQsfY,9284
|
|
17
17
|
semantic_release/cli/cli_context.py,sha256=Nop71LdVCJOeSUHgTXunMyK3xAu_QKQC2cRp1QBVkX0,4134
|
|
18
|
-
semantic_release/cli/config.py,sha256=
|
|
18
|
+
semantic_release/cli/config.py,sha256=dCQ1HqzBc_8RG_oTQtBMuCnM7zOEj9E1bDblxobg8os,33569
|
|
19
19
|
semantic_release/cli/const.py,sha256=h7XE2D0D__TAZSrUUtVszwvzpkHTMOiQCf97XQNbEvA,163
|
|
20
20
|
semantic_release/cli/github_actions_output.py,sha256=yC0nsMvEFGACjDwB8DdmGKwNGI8aIIhDxRHrmcS7tzA,5410
|
|
21
21
|
semantic_release/cli/masking_filter.py,sha256=GsTyaoZbUVJLXVMqeXhCttXK84UnBQ8cNDSHxd52sOc,3218
|
|
@@ -26,15 +26,19 @@ semantic_release/cli/commands/generate_config.py,sha256=2xZOu3NpyhBp0pWr7d8ugKl_
|
|
|
26
26
|
semantic_release/cli/commands/main.py,sha256=u1zhkkvKCZ2TtUqjzvdFTe5UZsvfws_pjqqo6CY0bBo,4351
|
|
27
27
|
semantic_release/cli/commands/publish.py,sha256=CE_LJTxFnc337MfpsfdJopi7QCwwE13GqGNQ-dNgWis,2871
|
|
28
28
|
semantic_release/cli/commands/version.py,sha256=CwFwoBP896Y9dc_p5bAyjZFbuD78tCS-q5IueQsOVSU,26634
|
|
29
|
-
semantic_release/commit_parser/__init__.py,sha256=
|
|
29
|
+
semantic_release/commit_parser/__init__.py,sha256=uCC2YI-EDPUAa6TwWYJA8TS7baWE2bXrLEGcrhJKofI,1323
|
|
30
30
|
semantic_release/commit_parser/_base.py,sha256=DLsHnbXG-39JkUbcnsBCSV2GmV35w1rasyoMhK8G0UE,3058
|
|
31
31
|
semantic_release/commit_parser/angular.py,sha256=MY_fo9F4EZ-ac8wYzBR0uD94O5Li2D-8zEMR01wss4c,18534
|
|
32
|
-
semantic_release/commit_parser/conventional.py,sha256=Iyy8txkshK8HikizFVa7pnJHBSWDhOhF45zR866QxQM,18672
|
|
33
32
|
semantic_release/commit_parser/emoji.py,sha256=0VecUMMcj43UaKDZ2GaFFUG26zOJAvXsEobTHaXNkk0,17749
|
|
34
33
|
semantic_release/commit_parser/scipy.py,sha256=gA9TfmrxGVvtv6kki6N_9abJx-_WlpBsSX2TBh0YgPw,19463
|
|
35
34
|
semantic_release/commit_parser/tag.py,sha256=bVO2XghM0G_eW2rG9Xc2q5TPsjtxr-xcHK5RpE1u_HM,3537
|
|
36
35
|
semantic_release/commit_parser/token.py,sha256=1_q8mJ4SRu7kNfa-Nxr8fEyuvCfjPgiPEitqSP1KR5g,7904
|
|
37
36
|
semantic_release/commit_parser/util.py,sha256=hcLjc16o7l6y_5Hfl8IxmF4ijaJD62JdjdB2DJWAe-g,3959
|
|
37
|
+
semantic_release/commit_parser/conventional/__init__.py,sha256=JHPL3S6trM4Le9MXnXc6iPYxxQnE4j0LG83_BWIomOw,602
|
|
38
|
+
semantic_release/commit_parser/conventional/options.py,sha256=AnRgTRMF-3X5QonTqq6bqJEbmGgQBzppyRZ79hct28g,2598
|
|
39
|
+
semantic_release/commit_parser/conventional/options_monorepo.py,sha256=7OCpuaYhw01bfoffTGfPgEWzRRyVsdsq_n2eo8Dt8-8,3210
|
|
40
|
+
semantic_release/commit_parser/conventional/parser.py,sha256=Pex26cvMqAKx9NPH3KROYg7k76R0Nsbs4aHor5SbMZg,16671
|
|
41
|
+
semantic_release/commit_parser/conventional/parser_monorepo.py,sha256=nVAc_gK4tJ5UM_rd9KbXgBWeBgrI_q2-vav3_4748r8,21189
|
|
38
42
|
semantic_release/data/templates/conventional/md/.release_notes.md.j2,sha256=DlMVAJMGqE27TwJ-2kviYaFhd3uWqXiU6Ikl15Ukne8,2512
|
|
39
43
|
semantic_release/data/templates/conventional/md/CHANGELOG.md.j2,sha256=FZmrQ-qOIoSoJmAa_NFaRelfmqUpypU2xlDeScdGOf4,729
|
|
40
44
|
semantic_release/data/templates/conventional/md/.components/changelog_header.md.j2,sha256=qNxTuSr59CV_yyimVU_RYp5azCnK0l6nJ03Zf0u5Ugg,166
|
|
@@ -73,8 +77,8 @@ semantic_release/version/declarations/enum.py,sha256=3n5Py9DoFkmItIdsmtQrJgmAhep
|
|
|
73
77
|
semantic_release/version/declarations/i_version_replacer.py,sha256=oP6BxJuxwI44roI6448tomShv1sMoy9ry8TlhhIQtfc,2416
|
|
74
78
|
semantic_release/version/declarations/pattern.py,sha256=MpUmsHYGAVAuFSKSb29FLcWeUCEHG_TRyhMO-2DWAAs,8308
|
|
75
79
|
semantic_release/version/declarations/toml.py,sha256=2K4DtX5Qq1iHT8cG8mISPTMmp50w6Av0KmLAKZPYqq8,4931
|
|
76
|
-
python_semantic_release-10.
|
|
77
|
-
python_semantic_release-10.
|
|
78
|
-
python_semantic_release-10.
|
|
79
|
-
python_semantic_release-10.
|
|
80
|
-
python_semantic_release-10.
|
|
80
|
+
python_semantic_release-10.4.0.dist-info/METADATA,sha256=f9vrM8xJB80Ene7btnU3Yjj7lMI5XfLGaK8Nw8h25bU,3927
|
|
81
|
+
python_semantic_release-10.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
82
|
+
python_semantic_release-10.4.0.dist-info/entry_points.txt,sha256=kzkCyDJsMOwgpFwEWKE9wxN1tXaUP6g6GIO4xtc0QuE,162
|
|
83
|
+
python_semantic_release-10.4.0.dist-info/top_level.txt,sha256=qYA24nyg3eP-ti5UW7Vuj2aXVmM0wqVHx4mREdRZNAA,17
|
|
84
|
+
python_semantic_release-10.4.0.dist-info/RECORD,,
|
semantic_release/cli/config.py
CHANGED
|
@@ -39,6 +39,7 @@ from semantic_release.cli.masking_filter import MaskingFilter
|
|
|
39
39
|
from semantic_release.commit_parser import (
|
|
40
40
|
AngularCommitParser,
|
|
41
41
|
CommitParser,
|
|
42
|
+
ConventionalCommitMonorepoParser,
|
|
42
43
|
ConventionalCommitParser,
|
|
43
44
|
EmojiCommitParser,
|
|
44
45
|
ParseResult,
|
|
@@ -71,9 +72,10 @@ class HvcsClient(str, Enum):
|
|
|
71
72
|
GITEA = "gitea"
|
|
72
73
|
|
|
73
74
|
|
|
74
|
-
_known_commit_parsers:
|
|
75
|
-
"conventional": ConventionalCommitParser,
|
|
75
|
+
_known_commit_parsers: dict[str, type[CommitParser[Any, Any]]] = {
|
|
76
76
|
"angular": AngularCommitParser,
|
|
77
|
+
"conventional": ConventionalCommitParser,
|
|
78
|
+
"conventional-monorepo": ConventionalCommitMonorepoParser,
|
|
77
79
|
"emoji": EmojiCommitParser,
|
|
78
80
|
"scipy": ScipyCommitParser,
|
|
79
81
|
"tag": TagCommitParser,
|
|
@@ -7,6 +7,8 @@ from semantic_release.commit_parser.angular import (
|
|
|
7
7
|
AngularParserOptions,
|
|
8
8
|
)
|
|
9
9
|
from semantic_release.commit_parser.conventional import (
|
|
10
|
+
ConventionalCommitMonorepoParser,
|
|
11
|
+
ConventionalCommitMonorepoParserOptions,
|
|
10
12
|
ConventionalCommitParser,
|
|
11
13
|
ConventionalCommitParserOptions,
|
|
12
14
|
)
|
|
@@ -28,3 +30,24 @@ from semantic_release.commit_parser.token import (
|
|
|
28
30
|
ParseResult,
|
|
29
31
|
ParseResultType,
|
|
30
32
|
)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"CommitParser",
|
|
36
|
+
"ParserOptions",
|
|
37
|
+
"AngularCommitParser",
|
|
38
|
+
"AngularParserOptions",
|
|
39
|
+
"ConventionalCommitParser",
|
|
40
|
+
"ConventionalCommitParserOptions",
|
|
41
|
+
"ConventionalCommitMonorepoParser",
|
|
42
|
+
"ConventionalCommitMonorepoParserOptions",
|
|
43
|
+
"EmojiCommitParser",
|
|
44
|
+
"EmojiParserOptions",
|
|
45
|
+
"ScipyCommitParser",
|
|
46
|
+
"ScipyParserOptions",
|
|
47
|
+
"TagCommitParser",
|
|
48
|
+
"TagParserOptions",
|
|
49
|
+
"ParsedCommit",
|
|
50
|
+
"ParseError",
|
|
51
|
+
"ParseResult",
|
|
52
|
+
"ParseResultType",
|
|
53
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from semantic_release.commit_parser.conventional.options import (
|
|
2
|
+
ConventionalCommitParserOptions,
|
|
3
|
+
)
|
|
4
|
+
from semantic_release.commit_parser.conventional.options_monorepo import (
|
|
5
|
+
ConventionalCommitMonorepoParserOptions,
|
|
6
|
+
)
|
|
7
|
+
from semantic_release.commit_parser.conventional.parser import ConventionalCommitParser
|
|
8
|
+
from semantic_release.commit_parser.conventional.parser_monorepo import (
|
|
9
|
+
ConventionalCommitMonorepoParser,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"ConventionalCommitParser",
|
|
14
|
+
"ConventionalCommitParserOptions",
|
|
15
|
+
"ConventionalCommitMonorepoParser",
|
|
16
|
+
"ConventionalCommitMonorepoParserOptions",
|
|
17
|
+
]
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from itertools import zip_longest
|
|
4
|
+
from typing import Tuple
|
|
5
|
+
|
|
6
|
+
from pydantic.dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from semantic_release.commit_parser._base import ParserOptions
|
|
9
|
+
from semantic_release.enums import LevelBump
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class ConventionalCommitParserOptions(ParserOptions):
|
|
14
|
+
"""Options dataclass for the ConventionalCommitParser."""
|
|
15
|
+
|
|
16
|
+
minor_tags: Tuple[str, ...] = ("feat",)
|
|
17
|
+
"""Commit-type prefixes that should result in a minor release bump."""
|
|
18
|
+
|
|
19
|
+
patch_tags: Tuple[str, ...] = ("fix", "perf")
|
|
20
|
+
"""Commit-type prefixes that should result in a patch release bump."""
|
|
21
|
+
|
|
22
|
+
other_allowed_tags: Tuple[str, ...] = (
|
|
23
|
+
"build",
|
|
24
|
+
"chore",
|
|
25
|
+
"ci",
|
|
26
|
+
"docs",
|
|
27
|
+
"style",
|
|
28
|
+
"refactor",
|
|
29
|
+
"test",
|
|
30
|
+
)
|
|
31
|
+
"""Commit-type prefixes that are allowed but do not result in a version bump."""
|
|
32
|
+
|
|
33
|
+
allowed_tags: Tuple[str, ...] = (
|
|
34
|
+
*minor_tags,
|
|
35
|
+
*patch_tags,
|
|
36
|
+
*other_allowed_tags,
|
|
37
|
+
)
|
|
38
|
+
"""
|
|
39
|
+
All commit-type prefixes that are allowed.
|
|
40
|
+
|
|
41
|
+
These are used to identify a valid commit message. If a commit message does not start with
|
|
42
|
+
one of these prefixes, it will not be considered a valid commit message.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
default_bump_level: LevelBump = LevelBump.NO_RELEASE
|
|
46
|
+
"""The minimum bump level to apply to valid commit message."""
|
|
47
|
+
|
|
48
|
+
parse_squash_commits: bool = True
|
|
49
|
+
"""Toggle flag for whether or not to parse squash commits"""
|
|
50
|
+
|
|
51
|
+
ignore_merge_commits: bool = True
|
|
52
|
+
"""Toggle flag for whether or not to ignore merge commits"""
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def tag_to_level(self) -> dict[str, LevelBump]:
|
|
56
|
+
"""A mapping of commit tags to the level bump they should result in."""
|
|
57
|
+
return self._tag_to_level
|
|
58
|
+
|
|
59
|
+
def __post_init__(self) -> None:
|
|
60
|
+
self._tag_to_level: dict[str, LevelBump] = {
|
|
61
|
+
str(tag): level
|
|
62
|
+
for tag, level in [
|
|
63
|
+
# we have to do a type ignore as zip_longest provides a type that is not specific enough
|
|
64
|
+
# for our expected output. Due to the empty second array, we know the first is always longest
|
|
65
|
+
# and that means no values in the first entry of the tuples will ever be a LevelBump. We
|
|
66
|
+
# apply a str() to make mypy happy although it will never happen.
|
|
67
|
+
*zip_longest(self.allowed_tags, (), fillvalue=self.default_bump_level),
|
|
68
|
+
*zip_longest(self.patch_tags, (), fillvalue=LevelBump.PATCH),
|
|
69
|
+
*zip_longest(self.minor_tags, (), fillvalue=LevelBump.MINOR),
|
|
70
|
+
]
|
|
71
|
+
if "|" not in str(tag)
|
|
72
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from re import compile as regexp, error as RegExpError # noqa: N812
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Iterable, Tuple
|
|
6
|
+
|
|
7
|
+
from pydantic import Field, field_validator
|
|
8
|
+
from pydantic.dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
# typing_extensions is for Python 3.8, 3.9, 3.10 compatibility
|
|
11
|
+
from typing_extensions import Annotated
|
|
12
|
+
|
|
13
|
+
from semantic_release.commit_parser.conventional.options import (
|
|
14
|
+
ConventionalCommitParserOptions,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class ConventionalCommitMonorepoParserOptions(ConventionalCommitParserOptions):
|
|
23
|
+
# TODO: add example into the docstring
|
|
24
|
+
"""Options dataclass for ConventionalCommitMonorepoParser."""
|
|
25
|
+
|
|
26
|
+
path_filters: Annotated[Tuple[str, ...], Field(validate_default=True)] = (".",)
|
|
27
|
+
"""
|
|
28
|
+
A set of relative paths to filter commits by. Only commits with file changes that
|
|
29
|
+
match these file paths or its subdirectories will be considered valid commits.
|
|
30
|
+
|
|
31
|
+
Syntax is similar to .gitignore with file path globs and inverse file match globs
|
|
32
|
+
via `!` prefix. Paths should be relative to the current working directory.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
scope_prefix: str = ""
|
|
36
|
+
"""
|
|
37
|
+
A prefix that will be striped from the scope when parsing commit messages.
|
|
38
|
+
|
|
39
|
+
If set, it will cause unscoped commits to be ignored. Use this in tandem with
|
|
40
|
+
the `path_filters` option to filter commits by directory and scope. This will
|
|
41
|
+
be fed into a regular expression so you must escape any special characters that
|
|
42
|
+
are meaningful in regular expressions (e.g. `.`, `*`, `?`, `+`, etc.) if you want
|
|
43
|
+
to match them literally.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
@field_validator("path_filters", mode="before")
|
|
48
|
+
def convert_strs_to_paths(cls, value: Any) -> tuple[Path, ...]:
|
|
49
|
+
values = value if isinstance(value, Iterable) else [value]
|
|
50
|
+
results: list[Path] = []
|
|
51
|
+
|
|
52
|
+
for val in values:
|
|
53
|
+
if isinstance(val, (str, Path)):
|
|
54
|
+
results.append(Path(val))
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
raise TypeError(f"Invalid type: {type(val)}, expected str or Path.")
|
|
58
|
+
|
|
59
|
+
return tuple(results)
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
@field_validator("path_filters", mode="after")
|
|
63
|
+
def resolve_path(cls, dir_paths: tuple[Path, ...]) -> tuple[Path, ...]:
|
|
64
|
+
return tuple(
|
|
65
|
+
(
|
|
66
|
+
Path(f"!{Path(str_path[1:]).expanduser().absolute().resolve()}")
|
|
67
|
+
# maintains the negation prefix if it exists
|
|
68
|
+
if (str_path := str(path)).startswith("!")
|
|
69
|
+
# otherwise, resolve the path normally
|
|
70
|
+
else path.expanduser().absolute().resolve()
|
|
71
|
+
)
|
|
72
|
+
for path in dir_paths
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
@field_validator("scope_prefix", mode="after")
|
|
77
|
+
def validate_scope_prefix(cls, scope_prefix: str) -> str:
|
|
78
|
+
if not scope_prefix:
|
|
79
|
+
return ""
|
|
80
|
+
|
|
81
|
+
# Allow the special case of a plain wildcard although it's not a valid regex
|
|
82
|
+
if scope_prefix == "*":
|
|
83
|
+
return ".*"
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
regexp(scope_prefix)
|
|
87
|
+
except RegExpError as err:
|
|
88
|
+
raise ValueError(f"Invalid regex {scope_prefix!r}") from err
|
|
89
|
+
|
|
90
|
+
return scope_prefix
|
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import re
|
|
4
3
|
from functools import reduce
|
|
5
|
-
from
|
|
6
|
-
from re import
|
|
4
|
+
from logging import getLogger
|
|
5
|
+
from re import (
|
|
6
|
+
DOTALL,
|
|
7
|
+
IGNORECASE,
|
|
8
|
+
MULTILINE,
|
|
9
|
+
Match as RegexMatch,
|
|
10
|
+
Pattern,
|
|
11
|
+
compile as regexp,
|
|
12
|
+
error as RegexError, # noqa: N812
|
|
13
|
+
)
|
|
7
14
|
from textwrap import dedent
|
|
8
|
-
from typing import TYPE_CHECKING,
|
|
15
|
+
from typing import TYPE_CHECKING, ClassVar
|
|
9
16
|
|
|
10
17
|
from git.objects.commit import Commit
|
|
11
|
-
from pydantic.dataclasses import dataclass
|
|
12
18
|
|
|
13
|
-
from semantic_release.commit_parser._base import CommitParser
|
|
19
|
+
from semantic_release.commit_parser._base import CommitParser
|
|
20
|
+
from semantic_release.commit_parser.conventional.options import (
|
|
21
|
+
ConventionalCommitParserOptions,
|
|
22
|
+
)
|
|
14
23
|
from semantic_release.commit_parser.token import (
|
|
15
24
|
ParsedCommit,
|
|
16
25
|
ParsedMessageResult,
|
|
@@ -25,16 +34,10 @@ from semantic_release.commit_parser.util import (
|
|
|
25
34
|
)
|
|
26
35
|
from semantic_release.enums import LevelBump
|
|
27
36
|
from semantic_release.errors import InvalidParserOptions
|
|
28
|
-
from semantic_release.globals import logger
|
|
29
37
|
from semantic_release.helpers import sort_numerically, text_reducer
|
|
30
38
|
|
|
31
|
-
if TYPE_CHECKING:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def _logged_parse_error(commit: Commit, error: str) -> ParseError:
|
|
36
|
-
logger.debug(error)
|
|
37
|
-
return ParseError(commit, error=error)
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
pass
|
|
38
41
|
|
|
39
42
|
|
|
40
43
|
# TODO: Remove from here, allow for user customization instead via options
|
|
@@ -53,69 +56,6 @@ LONG_TYPE_NAMES = {
|
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
|
|
56
|
-
@dataclass
|
|
57
|
-
class ConventionalCommitParserOptions(ParserOptions):
|
|
58
|
-
"""Options dataclass for the ConventionalCommitParser."""
|
|
59
|
-
|
|
60
|
-
minor_tags: Tuple[str, ...] = ("feat",)
|
|
61
|
-
"""Commit-type prefixes that should result in a minor release bump."""
|
|
62
|
-
|
|
63
|
-
patch_tags: Tuple[str, ...] = ("fix", "perf")
|
|
64
|
-
"""Commit-type prefixes that should result in a patch release bump."""
|
|
65
|
-
|
|
66
|
-
other_allowed_tags: Tuple[str, ...] = (
|
|
67
|
-
"build",
|
|
68
|
-
"chore",
|
|
69
|
-
"ci",
|
|
70
|
-
"docs",
|
|
71
|
-
"style",
|
|
72
|
-
"refactor",
|
|
73
|
-
"test",
|
|
74
|
-
)
|
|
75
|
-
"""Commit-type prefixes that are allowed but do not result in a version bump."""
|
|
76
|
-
|
|
77
|
-
allowed_tags: Tuple[str, ...] = (
|
|
78
|
-
*minor_tags,
|
|
79
|
-
*patch_tags,
|
|
80
|
-
*other_allowed_tags,
|
|
81
|
-
)
|
|
82
|
-
"""
|
|
83
|
-
All commit-type prefixes that are allowed.
|
|
84
|
-
|
|
85
|
-
These are used to identify a valid commit message. If a commit message does not start with
|
|
86
|
-
one of these prefixes, it will not be considered a valid commit message.
|
|
87
|
-
"""
|
|
88
|
-
|
|
89
|
-
default_bump_level: LevelBump = LevelBump.NO_RELEASE
|
|
90
|
-
"""The minimum bump level to apply to valid commit message."""
|
|
91
|
-
|
|
92
|
-
parse_squash_commits: bool = True
|
|
93
|
-
"""Toggle flag for whether or not to parse squash commits"""
|
|
94
|
-
|
|
95
|
-
ignore_merge_commits: bool = True
|
|
96
|
-
"""Toggle flag for whether or not to ignore merge commits"""
|
|
97
|
-
|
|
98
|
-
@property
|
|
99
|
-
def tag_to_level(self) -> dict[str, LevelBump]:
|
|
100
|
-
"""A mapping of commit tags to the level bump they should result in."""
|
|
101
|
-
return self._tag_to_level
|
|
102
|
-
|
|
103
|
-
def __post_init__(self) -> None:
|
|
104
|
-
self._tag_to_level: dict[str, LevelBump] = {
|
|
105
|
-
str(tag): level
|
|
106
|
-
for tag, level in [
|
|
107
|
-
# we have to do a type ignore as zip_longest provides a type that is not specific enough
|
|
108
|
-
# for our expected output. Due to the empty second array, we know the first is always longest
|
|
109
|
-
# and that means no values in the first entry of the tuples will ever be a LevelBump. We
|
|
110
|
-
# apply a str() to make mypy happy although it will never happen.
|
|
111
|
-
*zip_longest(self.allowed_tags, (), fillvalue=self.default_bump_level),
|
|
112
|
-
*zip_longest(self.patch_tags, (), fillvalue=LevelBump.PATCH),
|
|
113
|
-
*zip_longest(self.minor_tags, (), fillvalue=LevelBump.MINOR),
|
|
114
|
-
]
|
|
115
|
-
if "|" not in str(tag)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
119
59
|
class ConventionalCommitParser(
|
|
120
60
|
CommitParser[ParseResult, ConventionalCommitParserOptions]
|
|
121
61
|
):
|
|
@@ -128,14 +68,57 @@ class ConventionalCommitParser(
|
|
|
128
68
|
# TODO: Deprecate in lieu of get_default_options()
|
|
129
69
|
parser_options = ConventionalCommitParserOptions
|
|
130
70
|
|
|
71
|
+
# GitHub & Gitea use (#123), GitLab uses (!123), and BitBucket uses (pull request #123)
|
|
72
|
+
mr_selector = regexp(r"[\t ]+\((?:pull request )?(?P<mr_number>[#!]\d+)\)[\t ]*$")
|
|
73
|
+
|
|
74
|
+
issue_selector = regexp(
|
|
75
|
+
str.join(
|
|
76
|
+
"",
|
|
77
|
+
[
|
|
78
|
+
r"^(?:clos(?:e|es|ed|ing)|fix(?:es|ed|ing)?|resolv(?:e|es|ed|ing)|implement(?:s|ed|ing)?):",
|
|
79
|
+
r"[\t ]+(?P<issue_predicate>.+)[\t ]*$",
|
|
80
|
+
],
|
|
81
|
+
),
|
|
82
|
+
flags=MULTILINE | IGNORECASE,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
notice_selector = regexp(r"^NOTICE: (?P<notice>.+)$")
|
|
86
|
+
|
|
87
|
+
common_commit_msg_filters: ClassVar[dict[str, tuple[Pattern[str], str]]] = {
|
|
88
|
+
"typo-extra-spaces": (regexp(r"(\S) +(\S)"), r"\1 \2"),
|
|
89
|
+
"git-header-commit": (
|
|
90
|
+
regexp(r"^[\t ]*commit [0-9a-f]+$\n?", flags=MULTILINE),
|
|
91
|
+
"",
|
|
92
|
+
),
|
|
93
|
+
"git-header-author": (
|
|
94
|
+
regexp(r"^[\t ]*Author: .+$\n?", flags=MULTILINE),
|
|
95
|
+
"",
|
|
96
|
+
),
|
|
97
|
+
"git-header-date": (
|
|
98
|
+
regexp(r"^[\t ]*Date: .+$\n?", flags=MULTILINE),
|
|
99
|
+
"",
|
|
100
|
+
),
|
|
101
|
+
"git-squash-heading": (
|
|
102
|
+
regexp(
|
|
103
|
+
r"^[\t ]*Squashed commit of the following:.*$\n?",
|
|
104
|
+
flags=MULTILINE,
|
|
105
|
+
),
|
|
106
|
+
"",
|
|
107
|
+
),
|
|
108
|
+
}
|
|
109
|
+
|
|
131
110
|
def __init__(self, options: ConventionalCommitParserOptions | None = None) -> None:
|
|
132
111
|
super().__init__(options)
|
|
133
112
|
|
|
113
|
+
self._logger = getLogger(
|
|
114
|
+
str.join(".", [self.__module__, self.__class__.__name__])
|
|
115
|
+
)
|
|
116
|
+
|
|
134
117
|
try:
|
|
135
118
|
commit_type_pattern = regexp(
|
|
136
119
|
r"(?P<type>%s)" % str.join("|", self.options.allowed_tags)
|
|
137
120
|
)
|
|
138
|
-
except
|
|
121
|
+
except RegexError as err:
|
|
139
122
|
raise InvalidParserOptions(
|
|
140
123
|
str.join(
|
|
141
124
|
"\n",
|
|
@@ -167,45 +150,11 @@ class ConventionalCommitParser(
|
|
|
167
150
|
r"(?:\n\n(?P<text>.+))?", # commit body
|
|
168
151
|
],
|
|
169
152
|
),
|
|
170
|
-
flags=
|
|
153
|
+
flags=DOTALL,
|
|
171
154
|
)
|
|
172
155
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
r"[\t ]+\((?:pull request )?(?P<mr_number>[#!]\d+)\)[\t ]*$"
|
|
176
|
-
)
|
|
177
|
-
self.issue_selector = regexp(
|
|
178
|
-
str.join(
|
|
179
|
-
"",
|
|
180
|
-
[
|
|
181
|
-
r"^(?:clos(?:e|es|ed|ing)|fix(?:es|ed|ing)?|resolv(?:e|es|ed|ing)|implement(?:s|ed|ing)?):",
|
|
182
|
-
r"[\t ]+(?P<issue_predicate>.+)[\t ]*$",
|
|
183
|
-
],
|
|
184
|
-
),
|
|
185
|
-
flags=re.MULTILINE | re.IGNORECASE,
|
|
186
|
-
)
|
|
187
|
-
self.notice_selector = regexp(r"^NOTICE: (?P<notice>.+)$")
|
|
188
|
-
self.filters = {
|
|
189
|
-
"typo-extra-spaces": (regexp(r"(\S) +(\S)"), r"\1 \2"),
|
|
190
|
-
"git-header-commit": (
|
|
191
|
-
regexp(r"^[\t ]*commit [0-9a-f]+$\n?", flags=re.MULTILINE),
|
|
192
|
-
"",
|
|
193
|
-
),
|
|
194
|
-
"git-header-author": (
|
|
195
|
-
regexp(r"^[\t ]*Author: .+$\n?", flags=re.MULTILINE),
|
|
196
|
-
"",
|
|
197
|
-
),
|
|
198
|
-
"git-header-date": (
|
|
199
|
-
regexp(r"^[\t ]*Date: .+$\n?", flags=re.MULTILINE),
|
|
200
|
-
"",
|
|
201
|
-
),
|
|
202
|
-
"git-squash-heading": (
|
|
203
|
-
regexp(
|
|
204
|
-
r"^[\t ]*Squashed commit of the following:.*$\n?",
|
|
205
|
-
flags=re.MULTILINE,
|
|
206
|
-
),
|
|
207
|
-
"",
|
|
208
|
-
),
|
|
156
|
+
self.filters: dict[str, tuple[Pattern[str], str]] = {
|
|
157
|
+
**self.common_commit_msg_filters,
|
|
209
158
|
"git-squash-commit-prefix": (
|
|
210
159
|
regexp(
|
|
211
160
|
str.join(
|
|
@@ -215,17 +164,20 @@ class ConventionalCommitParser(
|
|
|
215
164
|
commit_type_pattern.pattern + r"\b", # prior to commit type
|
|
216
165
|
],
|
|
217
166
|
),
|
|
218
|
-
flags=
|
|
167
|
+
flags=MULTILINE,
|
|
219
168
|
),
|
|
220
169
|
# move commit type to the start of the line
|
|
221
170
|
r"\1",
|
|
222
171
|
),
|
|
223
172
|
}
|
|
224
173
|
|
|
225
|
-
|
|
226
|
-
def get_default_options() -> ConventionalCommitParserOptions:
|
|
174
|
+
def get_default_options(self) -> ConventionalCommitParserOptions:
|
|
227
175
|
return ConventionalCommitParserOptions()
|
|
228
176
|
|
|
177
|
+
def log_parse_error(self, commit: Commit, error: str) -> ParseError:
|
|
178
|
+
self._logger.debug(error)
|
|
179
|
+
return ParseError(commit, error=error)
|
|
180
|
+
|
|
229
181
|
def commit_body_components_separator(
|
|
230
182
|
self, accumulator: dict[str, list[str]], text: str
|
|
231
183
|
) -> dict[str, list[str]]:
|
|
@@ -267,14 +219,20 @@ class ConventionalCommitParser(
|
|
|
267
219
|
return accumulator
|
|
268
220
|
|
|
269
221
|
def parse_message(self, message: str) -> ParsedMessageResult | None:
|
|
270
|
-
|
|
271
|
-
|
|
222
|
+
return (
|
|
223
|
+
self.create_parsed_message_result(match)
|
|
224
|
+
if (match := self.commit_msg_pattern.match(message))
|
|
225
|
+
else None
|
|
226
|
+
)
|
|
272
227
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
228
|
+
def create_parsed_message_result(
|
|
229
|
+
self, match: RegexMatch[str]
|
|
230
|
+
) -> ParsedMessageResult:
|
|
231
|
+
parsed_break = match.group("break")
|
|
232
|
+
parsed_scope = match.group("scope") or ""
|
|
233
|
+
parsed_subject = match.group("subject")
|
|
234
|
+
parsed_text = match.group("text")
|
|
235
|
+
parsed_type = match.group("type")
|
|
278
236
|
|
|
279
237
|
linked_merge_request = ""
|
|
280
238
|
if mr_match := self.mr_selector.search(parsed_subject):
|
|
@@ -322,7 +280,7 @@ class ConventionalCommitParser(
|
|
|
322
280
|
|
|
323
281
|
def parse_commit(self, commit: Commit) -> ParseResult:
|
|
324
282
|
if not (parsed_msg_result := self.parse_message(force_str(commit.message))):
|
|
325
|
-
return
|
|
283
|
+
return self.log_parse_error(
|
|
326
284
|
commit,
|
|
327
285
|
f"Unable to parse commit message: {commit.message!r}",
|
|
328
286
|
)
|
|
@@ -342,7 +300,7 @@ class ConventionalCommitParser(
|
|
|
342
300
|
will be returned as a list of a single ParseResult.
|
|
343
301
|
"""
|
|
344
302
|
if self.options.ignore_merge_commits and self.is_merge_commit(commit):
|
|
345
|
-
return
|
|
303
|
+
return self.log_parse_error(
|
|
346
304
|
commit, "Ignoring merge commit: %s" % commit.hexsha[:8]
|
|
347
305
|
)
|
|
348
306
|
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from fnmatch import fnmatch
|
|
5
|
+
from logging import getLogger
|
|
6
|
+
from pathlib import Path, PurePath, PurePosixPath, PureWindowsPath
|
|
7
|
+
from re import DOTALL, compile as regexp, error as RegexError # noqa: N812
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from semantic_release.commit_parser._base import CommitParser
|
|
11
|
+
from semantic_release.commit_parser.conventional.options import (
|
|
12
|
+
ConventionalCommitParserOptions,
|
|
13
|
+
)
|
|
14
|
+
from semantic_release.commit_parser.conventional.options_monorepo import (
|
|
15
|
+
ConventionalCommitMonorepoParserOptions,
|
|
16
|
+
)
|
|
17
|
+
from semantic_release.commit_parser.conventional.parser import ConventionalCommitParser
|
|
18
|
+
from semantic_release.commit_parser.token import (
|
|
19
|
+
ParsedCommit,
|
|
20
|
+
ParsedMessageResult,
|
|
21
|
+
ParseError,
|
|
22
|
+
ParseResult,
|
|
23
|
+
)
|
|
24
|
+
from semantic_release.commit_parser.util import force_str
|
|
25
|
+
from semantic_release.errors import InvalidParserOptions
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
28
|
+
from git.objects.commit import Commit
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ConventionalCommitMonorepoParser(
|
|
32
|
+
CommitParser[ParseResult, ConventionalCommitMonorepoParserOptions]
|
|
33
|
+
):
|
|
34
|
+
# TODO: Remove for v11 compatibility, get_default_options() will be called instead
|
|
35
|
+
parser_options = ConventionalCommitMonorepoParserOptions
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self, options: ConventionalCommitMonorepoParserOptions | None = None
|
|
39
|
+
) -> None:
|
|
40
|
+
super().__init__(options)
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
commit_scope_pattern = regexp(
|
|
44
|
+
r"\(" + self.options.scope_prefix + r"(?P<scope>[^\n]+)?\)",
|
|
45
|
+
)
|
|
46
|
+
except RegexError as err:
|
|
47
|
+
raise InvalidParserOptions(
|
|
48
|
+
str.join(
|
|
49
|
+
"\n",
|
|
50
|
+
[
|
|
51
|
+
f"Invalid options for {self.__class__.__name__}",
|
|
52
|
+
"Unable to create regular expression from configured scope_prefix.",
|
|
53
|
+
"Please check the configured scope_prefix and remove or escape any regular expression characters.",
|
|
54
|
+
],
|
|
55
|
+
)
|
|
56
|
+
) from err
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
commit_type_pattern = regexp(
|
|
60
|
+
r"(?P<type>%s)" % str.join("|", self.options.allowed_tags)
|
|
61
|
+
)
|
|
62
|
+
except RegexError as err:
|
|
63
|
+
raise InvalidParserOptions(
|
|
64
|
+
str.join(
|
|
65
|
+
"\n",
|
|
66
|
+
[
|
|
67
|
+
f"Invalid options for {self.__class__.__name__}",
|
|
68
|
+
"Unable to create regular expression from configured commit-types.",
|
|
69
|
+
"Please check the configured commit-types and remove or escape any regular expression characters.",
|
|
70
|
+
],
|
|
71
|
+
)
|
|
72
|
+
) from err
|
|
73
|
+
|
|
74
|
+
# This regular expression includes scope prefix into the pattern and forces a scope to be present
|
|
75
|
+
# PSR will match the full scope but we don't include it in the scope match,
|
|
76
|
+
# which implicitly strips it from being included in the returned scope.
|
|
77
|
+
self._strict_scope_pattern = regexp(
|
|
78
|
+
str.join(
|
|
79
|
+
"",
|
|
80
|
+
[
|
|
81
|
+
r"^" + commit_type_pattern.pattern,
|
|
82
|
+
commit_scope_pattern.pattern,
|
|
83
|
+
r"(?P<break>!)?:\s+",
|
|
84
|
+
r"(?P<subject>[^\n]+)",
|
|
85
|
+
r"(?:\n\n(?P<text>.+))?", # commit body
|
|
86
|
+
],
|
|
87
|
+
),
|
|
88
|
+
flags=DOTALL,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
self._optional_scope_pattern = regexp(
|
|
92
|
+
str.join(
|
|
93
|
+
"",
|
|
94
|
+
[
|
|
95
|
+
r"^" + commit_type_pattern.pattern,
|
|
96
|
+
r"(?:\((?P<scope>[^\n]+)\))?",
|
|
97
|
+
r"(?P<break>!)?:\s+",
|
|
98
|
+
r"(?P<subject>[^\n]+)",
|
|
99
|
+
r"(?:\n\n(?P<text>.+))?", # commit body
|
|
100
|
+
],
|
|
101
|
+
),
|
|
102
|
+
flags=DOTALL,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
file_select_filters, file_ignore_filters = self._process_path_filter_options(
|
|
106
|
+
self.options.path_filters
|
|
107
|
+
)
|
|
108
|
+
self._file_selection_filters: list[str] = file_select_filters
|
|
109
|
+
self._file_ignore_filters: list[str] = file_ignore_filters
|
|
110
|
+
|
|
111
|
+
self._logger = getLogger(
|
|
112
|
+
str.join(".", [self.__module__, self.__class__.__name__])
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
self._base_parser = ConventionalCommitParser(
|
|
116
|
+
options=ConventionalCommitParserOptions(
|
|
117
|
+
**{
|
|
118
|
+
k: getattr(self.options, k)
|
|
119
|
+
for k in ConventionalCommitParserOptions().__dataclass_fields__
|
|
120
|
+
}
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def get_default_options(self) -> ConventionalCommitMonorepoParserOptions:
|
|
125
|
+
return ConventionalCommitMonorepoParserOptions()
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
def _process_path_filter_options( # noqa: C901
|
|
129
|
+
path_filters: tuple[str, ...],
|
|
130
|
+
) -> tuple[list[str], list[str]]:
|
|
131
|
+
file_ignore_filters: list[str] = []
|
|
132
|
+
file_selection_filters: list[str] = []
|
|
133
|
+
unique_selection_filters: set[str] = set()
|
|
134
|
+
unique_ignore_filters: set[str] = set()
|
|
135
|
+
|
|
136
|
+
for str_path in path_filters:
|
|
137
|
+
str_filter = str_path[1:] if str_path.startswith("!") else str_path
|
|
138
|
+
filter_list = (
|
|
139
|
+
file_ignore_filters
|
|
140
|
+
if str_path.startswith("!")
|
|
141
|
+
else file_selection_filters
|
|
142
|
+
)
|
|
143
|
+
unique_cache = (
|
|
144
|
+
unique_ignore_filters
|
|
145
|
+
if str_path.startswith("!")
|
|
146
|
+
else unique_selection_filters
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Since fnmatch is not too flexible, we will expand the path filters to include the name and any subdirectories
|
|
150
|
+
# as this is how gitignore is interpreted. Possible scenarios:
|
|
151
|
+
# | Input | Path Normalization | Filter List |
|
|
152
|
+
# | ---------- | ------------------ | ------------------------- |
|
|
153
|
+
# | / | / | /** | done
|
|
154
|
+
# | /./ | / | /** | done
|
|
155
|
+
# | /** | /** | /** | done
|
|
156
|
+
# | /./** | /** | /** | done
|
|
157
|
+
# | /* | /* | /* | done
|
|
158
|
+
# | . | . | ./** | done
|
|
159
|
+
# | ./ | . | ./** | done
|
|
160
|
+
# | ././ | . | ./** | done
|
|
161
|
+
# | ./** | ./** | ./** | done
|
|
162
|
+
# | ./* | ./* | ./* | done
|
|
163
|
+
# | .. | .. | ../** | done
|
|
164
|
+
# | ../ | .. | ../** | done
|
|
165
|
+
# | ../** | ../** | ../** | done
|
|
166
|
+
# | ../* | ../* | ../* | done
|
|
167
|
+
# | ../.. | ../.. | ../../** | done
|
|
168
|
+
# | ../../ | ../../ | ../../** | done
|
|
169
|
+
# | ../../docs | ../../docs | ../../docs, ../../docs/** | done
|
|
170
|
+
# | src | src | src, src/** | done
|
|
171
|
+
# | src/ | src | src/** | done
|
|
172
|
+
# | src/* | src/* | src/* | done
|
|
173
|
+
# | src/** | src/** | src/** | done
|
|
174
|
+
# | /src | /src | /src, /src/** | done
|
|
175
|
+
# | /src/ | /src | /src/** | done
|
|
176
|
+
# | /src/** | /src/** | /src/** | done
|
|
177
|
+
# | /src/* | /src/* | /src/* | done
|
|
178
|
+
# | ../d/f.txt | ../d/f.txt | ../d/f.txt, ../d/f.txt/** | done
|
|
179
|
+
# This expansion will occur regardless of the negation prefix
|
|
180
|
+
|
|
181
|
+
os_path: PurePath | PurePosixPath | PureWindowsPath = PurePath(str_filter)
|
|
182
|
+
|
|
183
|
+
if r"\\" in str_filter:
|
|
184
|
+
# Windows paths were given so we convert them to posix paths
|
|
185
|
+
os_path = PureWindowsPath(str_filter)
|
|
186
|
+
os_path = (
|
|
187
|
+
PureWindowsPath(
|
|
188
|
+
os_path.root, *os_path.parts[1:]
|
|
189
|
+
) # drop any drive letter
|
|
190
|
+
if os_path.is_absolute()
|
|
191
|
+
else os_path
|
|
192
|
+
)
|
|
193
|
+
os_path = PurePosixPath(os_path.as_posix())
|
|
194
|
+
|
|
195
|
+
path_normalized = str(os_path)
|
|
196
|
+
if path_normalized == str(
|
|
197
|
+
Path(".").absolute().root
|
|
198
|
+
) or path_normalized == str(Path("/**")):
|
|
199
|
+
path_normalized = "/**"
|
|
200
|
+
|
|
201
|
+
elif path_normalized == str(Path("/*")):
|
|
202
|
+
pass
|
|
203
|
+
|
|
204
|
+
elif path_normalized == str(Path(".")) or path_normalized == str(
|
|
205
|
+
Path("./**")
|
|
206
|
+
):
|
|
207
|
+
path_normalized = "./**"
|
|
208
|
+
|
|
209
|
+
elif path_normalized == str(Path("./*")):
|
|
210
|
+
path_normalized = "./*"
|
|
211
|
+
|
|
212
|
+
elif path_normalized == str(Path("..")) or path_normalized == str(
|
|
213
|
+
Path("../**")
|
|
214
|
+
):
|
|
215
|
+
path_normalized = "../**"
|
|
216
|
+
|
|
217
|
+
elif path_normalized == str(Path("../*")):
|
|
218
|
+
path_normalized = "../*"
|
|
219
|
+
|
|
220
|
+
elif path_normalized.endswith(("..", "../**")):
|
|
221
|
+
path_normalized = f"{path_normalized.rstrip('*')}/**"
|
|
222
|
+
|
|
223
|
+
elif str_filter.endswith(os.sep):
|
|
224
|
+
# If the path ends with a separator, it is a directory, so we add the directory and all subdirectories
|
|
225
|
+
path_normalized = f"{path_normalized}/**"
|
|
226
|
+
|
|
227
|
+
elif not path_normalized.endswith("*"):
|
|
228
|
+
all_subdirs = f"{path_normalized}/**"
|
|
229
|
+
if all_subdirs not in unique_cache:
|
|
230
|
+
unique_cache.add(all_subdirs)
|
|
231
|
+
filter_list.append(all_subdirs)
|
|
232
|
+
# And fall through to add the path as is
|
|
233
|
+
|
|
234
|
+
# END IF
|
|
235
|
+
|
|
236
|
+
# Add the normalized path to the filter list if it is not already present
|
|
237
|
+
if path_normalized not in unique_cache:
|
|
238
|
+
unique_cache.add(path_normalized)
|
|
239
|
+
filter_list.append(path_normalized)
|
|
240
|
+
|
|
241
|
+
return file_selection_filters, file_ignore_filters
|
|
242
|
+
|
|
243
|
+
def logged_parse_error(self, commit: Commit, error: str) -> ParseError:
|
|
244
|
+
self._logger.debug(error)
|
|
245
|
+
return ParseError(commit, error=error)
|
|
246
|
+
|
|
247
|
+
def parse(self, commit: Commit) -> ParseResult | list[ParseResult]:
|
|
248
|
+
if self.options.ignore_merge_commits and self._base_parser.is_merge_commit(
|
|
249
|
+
commit
|
|
250
|
+
):
|
|
251
|
+
return self._base_parser.log_parse_error(
|
|
252
|
+
commit, "Ignoring merge commit: %s" % commit.hexsha[:8]
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
separate_commits: list[Commit] = (
|
|
256
|
+
self._base_parser.unsquash_commit(commit)
|
|
257
|
+
if self.options.parse_squash_commits
|
|
258
|
+
else [commit]
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Parse each commit individually if there were more than one
|
|
262
|
+
parsed_commits: list[ParseResult] = list(
|
|
263
|
+
map(self.parse_commit, separate_commits)
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
def add_linked_merge_request(
|
|
267
|
+
parsed_result: ParseResult, mr_number: str
|
|
268
|
+
) -> ParseResult:
|
|
269
|
+
return (
|
|
270
|
+
parsed_result
|
|
271
|
+
if not isinstance(parsed_result, ParsedCommit)
|
|
272
|
+
else ParsedCommit(
|
|
273
|
+
**{
|
|
274
|
+
**parsed_result._asdict(),
|
|
275
|
+
"linked_merge_request": mr_number,
|
|
276
|
+
}
|
|
277
|
+
)
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# TODO: improve this for other VCS systems other than GitHub & BitBucket
|
|
281
|
+
# Github works as the first commit in a squash merge commit has the PR number
|
|
282
|
+
# appended to the first line of the commit message
|
|
283
|
+
lead_commit = next(iter(parsed_commits))
|
|
284
|
+
|
|
285
|
+
if isinstance(lead_commit, ParsedCommit) and lead_commit.linked_merge_request:
|
|
286
|
+
# If the first commit has linked merge requests, assume all commits
|
|
287
|
+
# are part of the same PR and add the linked merge requests to all
|
|
288
|
+
# parsed commits
|
|
289
|
+
parsed_commits = [
|
|
290
|
+
lead_commit,
|
|
291
|
+
*map(
|
|
292
|
+
lambda parsed_result, mr=lead_commit.linked_merge_request: ( # type: ignore[misc]
|
|
293
|
+
add_linked_merge_request(parsed_result, mr)
|
|
294
|
+
),
|
|
295
|
+
parsed_commits[1:],
|
|
296
|
+
),
|
|
297
|
+
]
|
|
298
|
+
|
|
299
|
+
elif isinstance(lead_commit, ParseError) and (
|
|
300
|
+
mr_match := self._base_parser.mr_selector.search(
|
|
301
|
+
force_str(lead_commit.message)
|
|
302
|
+
)
|
|
303
|
+
):
|
|
304
|
+
# Handle BitBucket Squash Merge Commits (see #1085), which have non angular commit
|
|
305
|
+
# format but include the PR number in the commit subject that we want to extract
|
|
306
|
+
linked_merge_request = mr_match.group("mr_number")
|
|
307
|
+
|
|
308
|
+
# apply the linked MR to all commits
|
|
309
|
+
parsed_commits = [
|
|
310
|
+
add_linked_merge_request(parsed_result, linked_merge_request)
|
|
311
|
+
for parsed_result in parsed_commits
|
|
312
|
+
]
|
|
313
|
+
|
|
314
|
+
return parsed_commits
|
|
315
|
+
|
|
316
|
+
def parse_message(
|
|
317
|
+
self, message: str, strict_scope: bool = False
|
|
318
|
+
) -> ParsedMessageResult | None:
|
|
319
|
+
if (
|
|
320
|
+
not (parsed_match := self._strict_scope_pattern.match(message))
|
|
321
|
+
and strict_scope
|
|
322
|
+
):
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
if not parsed_match and not (
|
|
326
|
+
parsed_match := self._optional_scope_pattern.match(message)
|
|
327
|
+
):
|
|
328
|
+
return None
|
|
329
|
+
|
|
330
|
+
return self._base_parser.create_parsed_message_result(parsed_match)
|
|
331
|
+
|
|
332
|
+
def parse_commit(self, commit: Commit) -> ParseResult:
|
|
333
|
+
"""Attempt to parse the commit message with a regular expression into a ParseResult."""
|
|
334
|
+
# Multiple scenarios to consider when parsing a commit message [Truth table]:
|
|
335
|
+
# =======================================================================================================
|
|
336
|
+
# | || INPUTS || |
|
|
337
|
+
# | # ||------------------------+----------------+--------------|| Result |
|
|
338
|
+
# | || Example Commit Message | Relevant Files | Scope Prefix || |
|
|
339
|
+
# |----||------------------------+----------------+--------------||-------------------------------------|
|
|
340
|
+
# | 1 || type(prefix-cli): msg | yes | "prefix-" || ParsedCommit |
|
|
341
|
+
# | 2 || type(prefix-cli): msg | yes | "" || ParsedCommit |
|
|
342
|
+
# | 3 || type(prefix-cli): msg | no | "prefix-" || ParsedCommit |
|
|
343
|
+
# | 4 || type(prefix-cli): msg | no | "" || ParseError[No files] |
|
|
344
|
+
# | 5 || type(scope-cli): msg | yes | "prefix-" || ParsedCommit |
|
|
345
|
+
# | 6 || type(scope-cli): msg | yes | "" || ParsedCommit |
|
|
346
|
+
# | 7 || type(scope-cli): msg | no | "prefix-" || ParseError[No files & wrong scope] |
|
|
347
|
+
# | 8 || type(scope-cli): msg | no | "" || ParseError[No files] |
|
|
348
|
+
# | 9 || type(cli): msg | yes | "prefix-" || ParsedCommit |
|
|
349
|
+
# | 10 || type(cli): msg | yes | "" || ParsedCommit |
|
|
350
|
+
# | 11 || type(cli): msg | no | "prefix-" || ParseError[No files & wrong scope] |
|
|
351
|
+
# | 12 || type(cli): msg | no | "" || ParseError[No files] |
|
|
352
|
+
# | 13 || type: msg | yes | "prefix-" || ParsedCommit |
|
|
353
|
+
# | 14 || type: msg | yes | "" || ParsedCommit |
|
|
354
|
+
# | 15 || type: msg | no | "prefix-" || ParseError[No files & wrong scope] |
|
|
355
|
+
# | 16 || type: msg | no | "" || ParseError[No files] |
|
|
356
|
+
# | 17 || non-conventional msg | yes | "prefix-" || ParseError[Invalid Syntax] |
|
|
357
|
+
# | 18 || non-conventional msg | yes | "" || ParseError[Invalid Syntax] |
|
|
358
|
+
# | 19 || non-conventional msg | no | "prefix-" || ParseError[Invalid Syntax] |
|
|
359
|
+
# | 20 || non-conventional msg | no | "" || ParseError[Invalid Syntax] |
|
|
360
|
+
# =======================================================================================================
|
|
361
|
+
|
|
362
|
+
# Initial Logic Flow:
|
|
363
|
+
# [1] When there are no relevant files and a scope prefix is defined, we enforce a strict scope
|
|
364
|
+
# [2] When there are no relevant files and no scope prefix is defined, we parse scoped or unscoped commits
|
|
365
|
+
# [3] When there are relevant files, we parse scoped or unscoped commits regardless of any defined prefix
|
|
366
|
+
has_relevant_changed_files = self._has_relevant_changed_files(commit)
|
|
367
|
+
strict_scope = bool(
|
|
368
|
+
not has_relevant_changed_files and self.options.scope_prefix
|
|
369
|
+
)
|
|
370
|
+
pmsg_result = self.parse_message(
|
|
371
|
+
message=force_str(commit.message),
|
|
372
|
+
strict_scope=strict_scope,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
if pmsg_result and (has_relevant_changed_files or strict_scope):
|
|
376
|
+
self._logger.debug(
|
|
377
|
+
"commit %s introduces a %s level_bump",
|
|
378
|
+
commit.hexsha[:8],
|
|
379
|
+
pmsg_result.bump,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
return ParsedCommit.from_parsed_message_result(commit, pmsg_result)
|
|
383
|
+
|
|
384
|
+
if pmsg_result and not has_relevant_changed_files:
|
|
385
|
+
return self.logged_parse_error(
|
|
386
|
+
commit,
|
|
387
|
+
f"Commit {commit.hexsha[:7]} has no changed files matching the path filter(s)",
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
if strict_scope and self.parse_message(str(commit.message), strict_scope=False):
|
|
391
|
+
return self.logged_parse_error(
|
|
392
|
+
commit,
|
|
393
|
+
str.join(
|
|
394
|
+
" and ",
|
|
395
|
+
[
|
|
396
|
+
f"Commit {commit.hexsha[:7]} has no changed files matching the path filter(s)",
|
|
397
|
+
f"the scope does not match scope prefix '{self.options.scope_prefix}'",
|
|
398
|
+
],
|
|
399
|
+
),
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
return self.logged_parse_error(
|
|
403
|
+
commit,
|
|
404
|
+
f"Format Mismatch! Unable to parse commit message: {commit.message!r}",
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
def unsquash_commit_message(self, message: str) -> list[str]:
|
|
408
|
+
return self._base_parser.unsquash_commit_message(message)
|
|
409
|
+
|
|
410
|
+
def _has_relevant_changed_files(self, commit: Commit) -> bool:
|
|
411
|
+
# Extract git root from commit
|
|
412
|
+
git_root = (
|
|
413
|
+
Path(commit.repo.working_tree_dir or commit.repo.working_dir)
|
|
414
|
+
.absolute()
|
|
415
|
+
.resolve()
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
cwd = Path.cwd().absolute().resolve()
|
|
419
|
+
|
|
420
|
+
rel_cwd = cwd.relative_to(git_root) if git_root in cwd.parents else Path(".")
|
|
421
|
+
|
|
422
|
+
sandboxed_selection_filters: list[str] = [
|
|
423
|
+
str(file_filter)
|
|
424
|
+
for file_filter in (
|
|
425
|
+
(
|
|
426
|
+
git_root / select_filter.rstrip("/")
|
|
427
|
+
if Path(select_filter).is_absolute()
|
|
428
|
+
else git_root / rel_cwd / select_filter
|
|
429
|
+
)
|
|
430
|
+
for select_filter in self._file_selection_filters
|
|
431
|
+
)
|
|
432
|
+
if git_root in file_filter.parents
|
|
433
|
+
]
|
|
434
|
+
|
|
435
|
+
sandboxed_ignore_filters: list[str] = [
|
|
436
|
+
str(file_filter)
|
|
437
|
+
for file_filter in (
|
|
438
|
+
(
|
|
439
|
+
git_root / ignore_filter.rstrip("/")
|
|
440
|
+
if Path(ignore_filter).is_absolute()
|
|
441
|
+
else git_root / rel_cwd / ignore_filter
|
|
442
|
+
)
|
|
443
|
+
for ignore_filter in self._file_ignore_filters
|
|
444
|
+
)
|
|
445
|
+
if git_root in file_filter.parents
|
|
446
|
+
]
|
|
447
|
+
|
|
448
|
+
# Check if the changed files of the commit that match the path filters
|
|
449
|
+
for full_path in iter(
|
|
450
|
+
str(git_root / rel_git_path) for rel_git_path in commit.stats.files
|
|
451
|
+
):
|
|
452
|
+
# Check if the filepath matches any of the file selection filters
|
|
453
|
+
if not any(
|
|
454
|
+
fnmatch(full_path, select_filter)
|
|
455
|
+
for select_filter in sandboxed_selection_filters
|
|
456
|
+
):
|
|
457
|
+
continue
|
|
458
|
+
|
|
459
|
+
# Pass filter matches, so now evaluate if it is supposed to be ignored
|
|
460
|
+
if not any(
|
|
461
|
+
fnmatch(full_path, ignore_filter)
|
|
462
|
+
for ignore_filter in sandboxed_ignore_filters
|
|
463
|
+
):
|
|
464
|
+
# No ignore filter matched, so it must be a relevant file
|
|
465
|
+
return True
|
|
466
|
+
|
|
467
|
+
return False
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_semantic_release-10.3.2.dist-info → python_semantic_release-10.4.0.dist-info}/top_level.txt
RENAMED
|
File without changes
|