Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

feat: parse squashed commits individually #1112

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

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
7a47bb8
test(parser-angular): update unit tests for parser return value compa…
codejedi365 Dec 18, 2024
b5c5b38
test(parser-scipy): update unit tests for parser return value compati…
codejedi365 Dec 18, 2024
d991005
test(parser-emoji): update unit tests for parser return value compati…
codejedi365 Dec 18, 2024
04682c0
feat(version): parse squashed commits individually
codejedi365 Mar 8, 2024
4ae5be3
feat(changelog): parse squashed commits individually
codejedi365 Nov 27, 2023
51cbe95
refactor(helpers): centralize utility for applying multiple text subs…
codejedi365 Jan 20, 2025
4668086
feat(parser-angular): upgrade angular parser to parse squashed commit…
codejedi365 Oct 24, 2024
a3df10a
feat(parser-angular): apply PR/MR numbers to all parsed commits from …
codejedi365 Oct 24, 2024
7e6464a
feat(parser-emoji): add functionality to interpret scopes from gitmoj…
codejedi365 Jan 19, 2025
3c8a275
feat(parser-emoji): upgrade emoji parser to parse squashed commits in…
codejedi365 Dec 18, 2024
c5d364b
test(fixtures): adjust parser for squashed commit defs
codejedi365 Jan 23, 2025
05dcfa4
test(fixtures): change config of github flow repo to parse squash com…
codejedi365 Jan 23, 2025
82802b7
test(fixtures): add fixture to create gitlab formatted merge commit
codejedi365 Jan 23, 2025
6d66013
refactor(parser-scipy): standardize all category spelling applied to …
codejedi365 Jan 24, 2025
76aefc1
docs(commit-parsing): add description for squash commit evaluation op…
codejedi365 Jan 26, 2025
369b754
docs(configuration): update the `commit_parser_options` setting descr…
codejedi365 Jan 26, 2025
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
107 changes: 87 additions & 20 deletions 107 docs/commit_parsing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,13 @@ logic in relation to how PSR's core features:
message. If no issue numbers are found, the parser will return an empty tuple. *Feature
available in v9.15.0+.*

**Limitations:**
- **Squash Commit Evaluation**: This parser implements PSR's
:ref:`commit_parser-builtin-squash_commit_evaluation` to identify and extract each commit
message as a separate commit message within a single squashed commit. You can toggle this
feature on/off via the :ref:`config-commit_parser_options` setting. *Feature available in
v9.17.0+.*

- Squash commits are not currently supported. This means that the level bump for a squash
commit is only determined by the subject line of the squash commit. Our default changelog
template currently writes out the entire commit message body in the changelog in order to
provide the full detail of the changes. Track the implementation of this feature with
the issues `#733`_, `#1085`_, and `PR#1112`_.
**Limitations**:

- Commits with the ``revert`` type are not currently supported. Track the implementation
of this feature in the issue `#402`_.
Expand Down Expand Up @@ -179,6 +179,12 @@ how PSR's core features:
enabled by setting the configuration option ``commit_parser_options.parse_linked_issues``
to ``true``. *Feature available in v9.15.0+.*

- **Squash Commit Evaluation**: This parser implements PSR's
:ref:`commit_parser-builtin-squash_commit_evaluation` to identify and extract each commit
message as a separate commit message within a single squashed commit. You can toggle this
feature on/off via the :ref:`config-commit_parser_options` setting. *Feature available in
v9.17.0+.*

If no commit parser options are provided via the configuration, the parser will use PSR's
built-in :py:class:`defaults <semantic_release.commit_parser.emoji.EmojiParserOptions>`.

Expand Down Expand Up @@ -304,6 +310,72 @@ return an empty tuple.

----

.. _commit_parser-builtin-squash_commit_evaluation:

Common Squash Commit Evaluation
"""""""""""""""""""""""""""""""

*Introduced in v9.17.0*

All of the PSR built-in parsers implement common squash commit evaluation logic to identify
and extract individual commit messages from a single squashed commit. The parsers will
look for common squash commit delimiters and multiple matches of the commit message
format to identify each individual commit message that was squashed. The parsers will
return a list containing each commit message as a separate commit object. Squashed commits
will be evaluated individually for both the level bump and changelog generation. If no
squash commits are found, a list with the single commit object will be returned.

Currently, PSR has been tested against GitHub, BitBucket, and official ``git`` squash
merge commmit messages. GitLab does not have a default template for squash commit messages
but can be customized per project or server. If you are using GitLab, you will need to
ensure that the squash commit message format is similar to the example below.

**Example**:

*The following example will extract three separate commit messages from a single GitHub
formatted squash commit message of conventional commit style:*

.. code-block:: text

feat(config): add new config option (#123)

* refactor(config): change the implementation of config loading

* docs(configuration): defined new config option for the project

When parsed with the default angular parser with squash commits toggled on, the version
bump will be determined by the highest level bump of the three commits (in this case, a
minor bump because of the feature commit) and the release notes would look similar to
the following:

.. code-block:: markdown

## Features

- **config**: add new config option (#123)

## Documentation

- **configuration**: defined new config option for the project (#123)

## Refactoring

- **config**: change the implementation of config loading (#123)

Merge request numbers and commit hash values will be the same across all extracted
commits. Additionally, any :ref:`config-changelog-exclude_commit_patterns` will be
applied individually to each extracted commit so if you are have an exclusion match
for ignoring ``refactor`` commits, the second commit in the example above would be
excluded from the changelog.

.. important::
When squash commit evaluation is enabled, if you squashed a higher level bump commit
into the body of a lower level bump commit, the higher level bump commit will be
evaluated as the level bump for the entire squashed commit. This includes breaking
change descriptions.

----

.. _commit_parser-builtin-customization:

Customization
Expand Down Expand Up @@ -429,28 +501,23 @@ available.
.. _catching exceptions in Python is slower: https://docs.python.org/3/faq/design.html#how-fast-are-exceptions
.. _namedtuple: https://docs.python.org/3/library/typing.html#typing.NamedTuple

.. _commit-parsing-parser-options:
.. _commit_parser-parser-options:

Parser Options
""""""""""""""

To provide options to the commit parser which is configured in the :ref:`configuration file
<configuration>`, Python Semantic Release includes a
:py:class:`ParserOptions <semantic_release.commit_parser._base.ParserOptions>`
class. Each parser built into Python Semantic Release has a corresponding "options" class, which
subclasses :py:class:`ParserOptions <semantic_release.commit_parser._base.ParserOptions>`.

The configuration in :ref:`commit_parser_options <config-commit_parser_options>` is passed to the
"options" class which is specified by the configured :ref:`commit_parser <config-commit_parser>` -
more information on how this is specified is below.
When writing your own parser, you should accompany the parser with an "options" class
which accepts the appropriate keyword arguments. This class' ``__init__`` method should
store the values that are needed for parsing appropriately. Python Semantic Release will
pass any configuration options from the configuration file's
:ref:`commit_parser_options <config-commit_parser_options>`, into your custom parser options
class. To ensure that the configuration options are passed correctly, the options class
should inherit from the
:py:class:`ParserOptions <semantic_release.commit_parser._base.ParserOptions>` class.

The "options" class is used to validate the options which are configured in the repository,
and to provide default values for these options where appropriate.

If you are writing your own parser, you should accompany it with an "options" class
which accepts the appropriate keyword arguments. This class' ``__init__`` method should
store the values that are needed for parsing appropriately.

.. _commit-parsing-commit-parsers:

Commit Parsers
Expand Down
62 changes: 5 additions & 57 deletions 62 docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -811,66 +811,14 @@ For more information see :ref:`commit-parsing`.

**Type:** ``dict[str, Any]``

These options are passed directly to the ``parser_options`` method of
:ref:`the commit parser <config-commit_parser>`, without validation
or transformation.
This set of options are passed directly to the commit parser class specified in
:ref:`the commit parser <config-commit_parser>` configuration option.

For more information, see :ref:`commit-parsing-parser-options`.

The default value for this setting depends on what you specify as
:ref:`commit_parser <config-commit_parser>`. The table below outlines
the expections from ``commit_parser`` value to default options value.

================== == =================================
``commit_parser`` Default ``commit_parser_options``
================== == =================================
``"angular"`` -> .. code-block:: toml

[semantic_release.commit_parser_options]
allowed_types = [
"build", "chore", "ci", "docs", "feat", "fix",
"perf", "style", "refactor", "test"
]
minor_types = ["feat"]
patch_types = ["fix", "perf"]

``"emoji"`` -> .. code-block:: toml

[semantic_release.commit_parser_options]
major_tags = [":boom:"]
minor_tags = [
":sparkles:", ":children_crossing:", ":lipstick:",
":iphone:", ":egg:", ":chart_with_upwards_trend:"
]
patch_tags = [
":ambulance:", ":lock:", ":bug:", ":zap:", ":goal_net:",
":alien:", ":wheelchair:", ":speech_balloon:", ":mag:",
":apple:", ":penguin:", ":checkered_flag:", ":robot:",
":green_apple:"
]

``"scipy"`` -> .. code-block:: toml

[semantic_release.commit_parser_options]
allowed_tags = [
"API", "DEP", "ENH", "REV", "BUG", "MAINT", "BENCH",
"BLD", "DEV", "DOC", "STY", "TST", "REL", "FEAT", "TEST",
]
major_tags = ["API",]
minor_tags = ["DEP", "DEV", "ENH", "REV", "FEAT"]
patch_tags = ["BLD", "BUG", "MAINT"]

``"tag"`` -> .. code-block:: toml

[semantic_release.commit_parser_options]
minor_tag = ":sparkles:"
patch_tag = ":nut_and_bolt:"

``"module:class"`` -> ``**module:class.parser_options()``
================== == =================================
For more information (to include defaults), see
:ref:`commit_parser-builtin-customization`.

**Default:** ``ParserOptions { ... }``, where ``...`` depends on
:ref:`config-commit_parser` as indicated above.
:ref:`commit_parser <config-commit_parser>`.

----

Expand Down
122 changes: 66 additions & 56 deletions 122 src/semantic_release/changelog/release_history.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,75 +102,85 @@ def from_git_history(

released.setdefault(the_version, release)

# mypy will be happy if we make this an explicit string
commit_message = str(commit.message)

log.info(
"parsing commit [%s] %s",
commit.hexsha[:8],
commit_message.replace("\n", " ")[:54],
)
parse_result = commit_parser.parse(commit)
commit_type = (
"unknown" if isinstance(parse_result, ParseError) else parse_result.type
)

has_exclusion_match = any(
pattern.match(commit_message) for pattern in exclude_commit_patterns
)

commit_level_bump = (
LevelBump.NO_RELEASE
if isinstance(parse_result, ParseError)
else parse_result.bump
str(commit.message).replace("\n", " ")[:54],
)
# returns a ParseResult or list of ParseResult objects,
# it is usually one, but we split a commit if a squashed merge is detected
parse_results = commit_parser.parse(commit)
if not isinstance(parse_results, list):
parse_results = [parse_results]

is_squash_commit = bool(len(parse_results) > 1)

# iterate through parsed commits to add to changelog definition
for parsed_result in parse_results:
commit_message = str(parsed_result.commit.message)
commit_type = (
"unknown"
if isinstance(parsed_result, ParseError)
else parsed_result.type
)
log.debug("commit has type '%s'", commit_type)

# Skip excluded commits except for any commit causing a version bump
# Reasoning: if a commit causes a version bump, and no other commits
# are included, then the changelog will be empty. Even if ther was other
# commits included, the true reason for a version bump would be missing.
if has_exclusion_match and commit_level_bump == LevelBump.NO_RELEASE:
log.info(
"Excluding commit[%s] %s",
parse_result.short_hash,
commit_message.split("\n", maxsplit=1)[0][:40],
has_exclusion_match = any(
pattern.match(commit_message) for pattern in exclude_commit_patterns
)
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",
],
),
parse_result.short_hash,
commit_message.replace("\n", " ")[:20],
commit_level_bump = (
LevelBump.NO_RELEASE
if isinstance(parsed_result, ParseError)
else parsed_result.bump
)
continue

if the_version is None:
# Skip excluded commits except for any commit causing a version bump
# Reasoning: if a commit causes a version bump, and no other commits
# are included, then the changelog will be empty. Even if ther was other
# commits included, the true reason for a version bump would be missing.
if has_exclusion_match and commit_level_bump == LevelBump.NO_RELEASE:
log.info(
"Excluding %s commit[%s] %s",
"piece of squashed" if is_squash_commit else "",
parsed_result.short_hash,
commit_message.split("\n", maxsplit=1)[0][:20],
)
continue

if (
isinstance(parsed_result, ParsedCommit)
and not parsed_result.include_in_changelog
):
log.info(
str.join(
" ",
[
"Excluding commit[%s] because parser determined",
"it should not included in the changelog",
],
),
parsed_result.short_hash,
)
continue

if the_version is None:
log.info(
"[Unreleased] adding commit[%s] to unreleased '%s'",
parsed_result.short_hash,
commit_type,
)
unreleased[commit_type].append(parsed_result)
continue

log.info(
"[Unreleased] adding commit[%s] to unreleased '%s'",
parse_result.short_hash,
"[%s] adding commit[%s] to release '%s'",
the_version,
parsed_result.short_hash,
commit_type,
)
unreleased[commit_type].append(parse_result)
continue

log.info(
"[%s] adding commit[%s] to release '%s'",
the_version,
parse_result.short_hash,
commit_type,
)

released[the_version]["elements"][commit_type].append(parse_result)
released[the_version]["elements"][commit_type].append(parsed_result)

return cls(unreleased=unreleased, released=released)

Expand Down
2 changes: 1 addition & 1 deletion 2 src/semantic_release/commit_parser/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,4 @@ def get_default_options(self) -> _OPTS:
return self.parser_options() # type: ignore[return-value]

@abstractmethod
def parse(self, commit: Commit) -> _TT: ...
def parse(self, commit: Commit) -> _TT | list[_TT]: ...
Loading
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.