diff --git a/.github/workflows/bumpversion.yml b/.github/workflows/bumpversion.yml index ed0c8cffc..ba48d677e 100644 --- a/.github/workflows/bumpversion.yml +++ b/.github/workflows/bumpversion.yml @@ -12,7 +12,7 @@ jobs: name: "Bump version and create changelog with commitizen" steps: - name: Check out - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 token: "${{ secrets.PERSONAL_ACCESS_TOKEN }}" diff --git a/.github/workflows/docspublish.yml b/.github/workflows/docspublish.yml index 6418a20fa..7a2109266 100644 --- a/.github/workflows/docspublish.yml +++ b/.github/workflows/docspublish.yml @@ -10,7 +10,7 @@ jobs: update-cli-screenshots: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} fetch-depth: 0 @@ -43,7 +43,7 @@ jobs: runs-on: ubuntu-latest needs: update-cli-screenshots steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: token: "${{ secrets.PERSONAL_ACCESS_TOKEN }}" fetch-depth: 0 diff --git a/.github/workflows/homebrewpublish.yml b/.github/workflows/homebrewpublish.yml index f5d8004e7..c9a1b3508 100644 --- a/.github/workflows/homebrewpublish.yml +++ b/.github/workflows/homebrewpublish.yml @@ -12,7 +12,7 @@ jobs: if: ${{ github.event.workflow_run.conclusion == 'success' }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Python uses: actions/setup-python@v5 with: diff --git a/.github/workflows/label_pr.yml b/.github/workflows/label_pr.yml index 176d7cc79..a74a507bb 100644 --- a/.github/workflows/label_pr.yml +++ b/.github/workflows/label_pr.yml @@ -9,7 +9,7 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: sparse-checkout: | .github/labeler.yml diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index b50b02a68..00794babb 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -10,7 +10,7 @@ jobs: platform: [ubuntu-22.04, macos-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} diff --git a/.github/workflows/pythonpublish.yml b/.github/workflows/pythonpublish.yml index dc522bc0f..3976d309d 100644 --- a/.github/workflows/pythonpublish.yml +++ b/.github/workflows/pythonpublish.yml @@ -9,7 +9,7 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: token: "${{ secrets.PERSONAL_ACCESS_TOKEN }}" fetch-depth: 0 diff --git a/commitizen/cz/conventional_commits/conventional_commits.py b/commitizen/cz/conventional_commits/conventional_commits.py index 689342347..6de0baf1f 100644 --- a/commitizen/cz/conventional_commits/conventional_commits.py +++ b/commitizen/cz/conventional_commits/conventional_commits.py @@ -154,11 +154,21 @@ def message(self, answers: ConventionalCommitsAnswers) -> str: # type: ignore[o footer = answers["footer"] is_breaking_change = answers["is_breaking_change"] + # Check if breaking change exclamation in title is enabled + breaking_change_exclamation_in_title = self.config.settings.get( + "breaking_change_exclamation_in_title", False + ) + if scope: scope = f"({scope})" if body: body = f"\n\n{body}" if is_breaking_change: + if breaking_change_exclamation_in_title: + if scope: + scope = f"{scope}!" + else: + prefix = f"{prefix}!" footer = f"BREAKING CHANGE: {footer}" if footer: footer = f"\n\n{footer}" diff --git a/commitizen/defaults.py b/commitizen/defaults.py index 94d4d97b2..70d70c045 100644 --- a/commitizen/defaults.py +++ b/commitizen/defaults.py @@ -61,6 +61,7 @@ class Settings(TypedDict, total=False): version_scheme: str | None version_type: str | None version: str | None + breaking_change_exclamation_in_title: bool CONFIG_FILES: list[str] = [ @@ -108,6 +109,7 @@ class Settings(TypedDict, total=False): "always_signoff": False, "template": None, # default provided by plugin "extras": {}, + "breaking_change_exclamation_in_title": False, } MAJOR = "MAJOR" diff --git a/docs/config.md b/docs/config.md index 649881dae..c30dcbf7c 100644 --- a/docs/config.md +++ b/docs/config.md @@ -103,6 +103,15 @@ Default: `None` Create custom commit message, useful to skip CI. [Read more][bump_message] +### `breaking_change_exclamation_in_title` + +Type: `bool` + +Default: `false` + +When true, breaking changes will be indicated by an exclamation mark in the commit title (e.g., `feat!: breaking change`). +When false, breaking changes will be indicated by `BREAKING CHANGE:` in the footer. [Read more][breaking-change-exclamation] + ### `retry_after_failure` Type: `bool` diff --git a/docs/tutorials/writing_commits.md b/docs/tutorials/writing_commits.md index 7d9139929..a64480874 100644 --- a/docs/tutorials/writing_commits.md +++ b/docs/tutorials/writing_commits.md @@ -13,6 +13,10 @@ add to your commit body the following `BREAKING CHANGE`. Using these three keywords will allow the proper identification of the semantic version. Of course, there are other keywords, but I'll leave it to the reader to explore them. +Note: You can also indicate breaking changes by adding an exclamation mark in the commit title +(e.g., `feat!: breaking change`) by setting the `breaking_change_exclamation_in_title` +configuration option to `true`. [Read more][breaking-change-config] + ## Writing commits Now to the important part: when writing commits, it's important to think about: @@ -44,3 +48,4 @@ Emojis may be added as well (e.g., see [cz-emoji][cz_emoji]), which requires the [conventional_commits]: https://www.conventionalcommits.org [cz_emoji]: https://commitizen-tools.github.io/commitizen/third-party-commitizen/#cz-emoji [configuration]: ../config.md#encoding +[breaking-change-config]: ../config.md#breaking_change_exclamation_in_title diff --git a/tests/test_conf.py b/tests/test_conf.py index f89a0049f..202eadf1f 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -95,6 +95,7 @@ "always_signoff": False, "template": None, "extras": {}, + "breaking_change_exclamation_in_title": False, } _new_settings: dict[str, Any] = { @@ -126,6 +127,7 @@ "always_signoff": False, "template": None, "extras": {}, + "breaking_change_exclamation_in_title": False, } diff --git a/tests/test_cz_conventional_commits.py b/tests/test_cz_conventional_commits.py index c96e03670..1742b0f3b 100644 --- a/tests/test_cz_conventional_commits.py +++ b/tests/test_cz_conventional_commits.py @@ -105,6 +105,55 @@ def test_breaking_change_in_footer(config): ) +@pytest.mark.parametrize( + "scope,breaking_change_exclamation_in_title,expected_message", + [ + # Test with scope and breaking_change_exclamation_in_title enabled + ( + "users", + True, + "feat(users)!: email pattern corrected\n\ncomplete content\n\nBREAKING CHANGE: migrate by renaming user to users", + ), + # Test without scope and breaking_change_exclamation_in_title enabled + ( + "", + True, + "feat!: email pattern corrected\n\ncomplete content\n\nBREAKING CHANGE: migrate by renaming user to users", + ), + # Test with scope and breaking_change_exclamation_in_title disabled + ( + "users", + False, + "feat(users): email pattern corrected\n\ncomplete content\n\nBREAKING CHANGE: migrate by renaming user to users", + ), + # Test without scope and breaking_change_exclamation_in_title disabled + ( + "", + False, + "feat: email pattern corrected\n\ncomplete content\n\nBREAKING CHANGE: migrate by renaming user to users", + ), + ], +) +def test_breaking_change_message_formats( + config, scope, breaking_change_exclamation_in_title, expected_message +): + # Set the breaking_change_exclamation_in_title setting + config.settings["breaking_change_exclamation_in_title"] = ( + breaking_change_exclamation_in_title + ) + conventional_commits = ConventionalCommitsCz(config) + answers = { + "prefix": "feat", + "scope": scope, + "subject": "email pattern corrected", + "is_breaking_change": True, + "body": "complete content", + "footer": "migrate by renaming user to users", + } + message = conventional_commits.message(answers) + assert message == expected_message + + def test_example(config): """just testing a string is returned. not the content""" conventional_commits = ConventionalCommitsCz(config)