Skip to content

Commit

Permalink
feat: Introduce tag_regex option with smart default
Browse files Browse the repository at this point in the history
Closes commitizen-tools#519

CLI flag name: --tag-regex

Heavily inspired by
commitizen-tools#537, but extends
it with a smart default value to exclude non-release tags. This was
suggested in
commitizen-tools#519 (comment)
  • Loading branch information
robertschweizer committed May 2, 2023
1 parent c780f4e commit e58e56f
Show file tree
Hide file tree
Showing 11 changed files with 276 additions and 28 deletions.
9 changes: 8 additions & 1 deletion commitizen/cli.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import argparse
import logging
import sys
from pathlib import Path
from functools import partial
from pathlib import Path
from types import TracebackType
from typing import List

Expand Down Expand Up @@ -274,6 +274,13 @@
"If not set, it will include prereleases in the changelog"
),
},
{
"name": "--tag-regex",
"help": (
"regex match for tags represented "
"within the changelog. default: '.*'"
),
},
],
},
{
Expand Down
10 changes: 8 additions & 2 deletions commitizen/commands/changelog.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os.path
import re
from difflib import SequenceMatcher
from operator import itemgetter
from typing import Callable, Dict, List, Optional
Expand All @@ -17,7 +18,7 @@
NotAllowed,
)
from commitizen.git import GitTag, smart_open
from commitizen.tags import tag_from_version
from commitizen.tags import make_tag_pattern, tag_from_version


class Changelog:
Expand Down Expand Up @@ -67,6 +68,11 @@ def __init__(self, config: BaseConfig, args):
version_type = self.config.settings.get("version_type")
self.version_type = version_type and version_types.VERSION_TYPES[version_type]

tag_regex = args.get("tag_regex") or self.config.settings.get("tag_regex")
if not tag_regex:
tag_regex = make_tag_pattern(self.tag_format)
self.tag_pattern = re.compile(str(tag_regex), re.VERBOSE | re.IGNORECASE)

def _find_incremental_rev(self, latest_version: str, tags: List[GitTag]) -> str:
"""Try to find the 'start_rev'.
Expand Down Expand Up @@ -140,7 +146,7 @@ def __call__(self):
# Don't continue if no `file_name` specified.
assert self.file_name

tags = git.get_tags()
tags = git.get_tags(pattern=self.tag_pattern)
if not tags:
tags = []

Expand Down
7 changes: 5 additions & 2 deletions commitizen/git.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import re
from enum import Enum
from os import linesep
from pathlib import Path
Expand Down Expand Up @@ -140,7 +141,7 @@ def get_filenames_in_commit(git_reference: str = ""):
raise GitCommandError(c.err)


def get_tags(dateformat: str = "%Y-%m-%d") -> List[GitTag]:
def get_tags(dateformat: str = "%Y-%m-%d", *, pattern: re.Pattern) -> List[GitTag]:
inner_delimiter = "---inner_delimiter---"
formatter = (
f'"%(refname:lstrip=2){inner_delimiter}'
Expand All @@ -163,7 +164,9 @@ def get_tags(dateformat: str = "%Y-%m-%d") -> List[GitTag]:
for line in c.out.split("\n")[:-1]
]

return git_tags
filtered_git_tags = [t for t in git_tags if pattern.fullmatch(t.name)]

return filtered_git_tags


def tag_exist(tag: str) -> bool:
Expand Down
23 changes: 22 additions & 1 deletion commitizen/tags.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import re
import sys
from string import Template
from typing import Any, Optional, Type, Union

from packaging.version import Version
from packaging.version import VERSION_PATTERN, Version

if sys.version_info >= (3, 8):
from commitizen.version_types import VersionProtocol
Expand Down Expand Up @@ -42,3 +43,23 @@ def tag_from_version(
return t.safe_substitute(
version=version, major=major, minor=minor, patch=patch, prerelease=prerelease
)


def make_tag_pattern(tag_format: str) -> str:
"""Make regex pattern to match all tags created by tag_format."""
escaped_format = re.escape(tag_format)
escaped_format = re.sub(
r"\\\$(version|major|minor|patch|prerelease)", r"$\1", escaped_format
)
# pre-release part of VERSION_PATTERN
pre_release_pattern = r"([-_\.]?(a|b|c|rc|alpha|beta|pre|preview)([-_\.]?[0-9]+)?)?"
filter_regex = Template(escaped_format).safe_substitute(
# VERSION_PATTERN allows the v prefix, but we'd rather have users configure it
# explicitly.
version=VERSION_PATTERN.lstrip("\n v?"),
major="[0-9]+",
minor="[0-9]+",
patch="[0-9]+",
prerelease=pre_release_pattern,
)
return filter_regex
22 changes: 22 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,28 @@ cz changelog --merge-prerelease
changelog_merge_prerelease = true
```
### `tag-regex`
This value can be set in the `toml` file with the key `tag_regex` under `tools.commitizen`.
`tag_regex` is the regex pattern that selects tags to include in the changelog.
By default, the changelog will capture all git tags matching the `tag_format`, including pre-releases.
Example use-cases:
- Exclude pre-releases from the changelog
- Include existing tags that do not follow `tag_format` in the changelog
```bash
cz changelog --tag-regex="[0-9]*\\.[0-9]*\\.[0-9]"
```
```toml
[tools.commitizen]
# ...
tag_regex = "[0-9]*\\.[0-9]*\\.[0-9]"
```
## Hooks
Supported hook methods:
Expand Down
9 changes: 9 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ Default: `$version`

Format for the git tag, useful for old projects, that use a convention like `"v1.2.1"`. [Read more][tag_format]

### `tag_regex`

Type: `str`

Default: Based on `tag_format`

Tags must match this to be included in the changelog (e.g. `"([0-9.])*"` to exclude pre-releases). [Read more][tag_regex]

### `update_changelog_on_bump`

Type: `bool`
Expand Down Expand Up @@ -339,6 +347,7 @@ setup(

[version_files]: bump.md#version_files
[tag_format]: bump.md#tag_format
[tag_regex]: changelog.md#tag_regex
[bump_message]: bump.md#bump_message
[major-version-zero]: bump.md#-major-version-zero
[prerelease-offset]: bump.md#-prerelease_offset
Expand Down
2 changes: 2 additions & 0 deletions poetry.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[virtualenvs]
in-project = true
18 changes: 18 additions & 0 deletions tests/commands/test_bump_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,24 @@ def test_bump_with_changelog_config(mocker: MockFixture, changelog_path, config_
assert "0.2.0" in out


@pytest.mark.usefixtures("tmp_commitizen_project")
def test_bump_with_changelog_excludes_custom_tags(mocker: MockFixture, changelog_path):
create_file_and_commit("feat(user): new file")
git.tag("custom-tag")
create_file_and_commit("feat(user): Another new file")
testargs = ["cz", "bump", "--yes", "--changelog"]
mocker.patch.object(sys, "argv", testargs)
cli.main()
tag_exists = git.tag_exist("0.2.0")
assert tag_exists is True

with open(changelog_path, "r") as f:
out = f.read()
assert out.startswith("#")
assert "## 0.2.0" in out
assert "custom-tag" not in out


@pytest.mark.usefixtures("tmp_commitizen_project")
def test_prevent_prerelease_when_no_increment_detected(mocker: MockFixture, capsys):
create_file_and_commit("feat: new file")
Expand Down
52 changes: 52 additions & 0 deletions tests/commands/test_changelog_command.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import itertools
import sys
from datetime import datetime
from typing import List
from unittest.mock import patch

import pytest
from pytest_mock import MockFixture
Expand Down Expand Up @@ -1271,3 +1273,53 @@ def test_changelog_prerelease_rev_with_use_version_type_semver(
out, _ = capsys.readouterr()

file_regression.check(out, extension=".second-prerelease.md")


@pytest.mark.parametrize(
"config_file, expected_versions",
[
pytest.param("", ["Unreleased"], id="v-prefix-not-configured"),
pytest.param(
'tag_format = "v$version"',
["v1.1.0", "v1.1.0-beta", "v1.0.0"],
id="v-prefix-configured-as-tag-format",
),
pytest.param(
'tag_format = "v$version"\n' + 'tag_regex = ".*"',
["v1.1.0", "custom-tag", "v1.1.0-beta", "v1.0.0"],
id="tag-regex-matches-all-tags",
),
pytest.param(
'tag_format = "v$version"\n' + r'tag_regex = "v[0-9\\.]*"',
["v1.1.0", "v1.0.0"],
id="tag-regex-excludes-pre-releases",
),
],
)
def test_changelog_tag_regex(
config_path, changelog_path, config_file: str, expected_versions: List[str]
):
with open(config_path, "a") as f:
f.write(config_file)

# Create 4 tags with one valid feature each
create_file_and_commit("feat: initial")
git.tag("v1.0.0")
create_file_and_commit("feat: add 1")
git.tag("v1.1.0-beta")
create_file_and_commit("feat: add 2")
git.tag("custom-tag")
create_file_and_commit("feat: add 3")
git.tag("v1.1.0")

# call CLI
with patch.object(sys, "argv", ["cz", "changelog"]):
cli.main()

# open CLI output
with open(changelog_path, "r") as f:
out = f.read()

headings = [line for line in out.splitlines() if line.startswith("## ")]
changelog_versions = [heading[3:].split()[0] for heading in headings]
assert changelog_versions == expected_versions
59 changes: 57 additions & 2 deletions tests/test_git.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import inspect
import os
import re
import shutil
from typing import List, Optional

import pytest
from pytest_mock import MockFixture

from commitizen import cmd, exceptions, git
from commitizen.tags import make_tag_pattern
from tests.utils import FakeCommand, create_file_and_commit


Expand All @@ -28,7 +30,7 @@ def test_get_tags(mocker: MockFixture):
)
mocker.patch("commitizen.cmd.run", return_value=FakeCommand(out=tag_str))

git_tags = git.get_tags()
git_tags = git.get_tags(pattern=re.compile(r"v[0-9\.]+"))
latest_git_tag = git_tags[0]
assert latest_git_tag.rev == "333"
assert latest_git_tag.name == "v1.0.0"
Expand All @@ -37,7 +39,60 @@ def test_get_tags(mocker: MockFixture):
mocker.patch(
"commitizen.cmd.run", return_value=FakeCommand(out="", err="No tag available")
)
assert git.get_tags() == []
assert git.get_tags(pattern=re.compile(r"v[0-9\.]+")) == []


@pytest.mark.parametrize(
"pattern, expected_tags",
[
pytest.param(
make_tag_pattern(tag_format="$version"),
[], # No versions with normal 1.2.3 pattern
id="default-tag-format",
),
pytest.param(
make_tag_pattern(tag_format="$major-$minor-$patch$prerelease"),
["1-0-0", "1-0-0alpha2"],
id="tag-format-with-hyphens",
),
pytest.param(
r"[0-9]+\-[0-9]+\-[0-9]+",
["1-0-0"],
id="tag-regex-with-hyphens-that-excludes-alpha",
),
pytest.param(
make_tag_pattern(tag_format="v$version"),
["v0.5.0", "v0.0.1-pre"],
id="tag-format-with-v-prefix",
),
pytest.param(
make_tag_pattern(tag_format="custom-prefix-$version"),
["custom-prefix-0.0.1"],
id="tag-format-with-custom-prefix",
),
pytest.param(
".*",
["1-0-0", "1-0-0alpha2", "v0.5.0", "v0.0.1-pre", "custom-prefix-0.0.1"],
id="custom-tag-regex-to-include-all-tags",
),
],
)
def test_get_tags_filtering(
mocker: MockFixture, pattern: str, expected_tags: List[str]
):
tag_str = (
"1-0-0---inner_delimiter---333---inner_delimiter---2020-01-20---inner_delimiter---\n"
"1-0-0alpha2---inner_delimiter---333---inner_delimiter---2020-01-20---inner_delimiter---\n"
"v0.5.0---inner_delimiter---222---inner_delimiter---2020-01-17---inner_delimiter---\n"
"v0.0.1-pre---inner_delimiter---111---inner_delimiter---2020-01-17---inner_delimiter---\n"
"custom-prefix-0.0.1---inner_delimiter---111---inner_delimiter---2020-01-17---inner_delimiter---\n"
"custom-non-release-tag"
)
mocker.patch("commitizen.cmd.run", return_value=FakeCommand(out=tag_str))

git_tags = git.get_tags(pattern=re.compile(pattern, flags=re.VERBOSE))
actual_name_list = [t.name for t in git_tags]
assert actual_name_list == expected_tags


def test_get_tag_names(mocker: MockFixture):
Expand Down
Loading

0 comments on commit e58e56f

Please sign in to comment.