Skip to content

Commit 8eea9ea

Browse files
feat: Introduce tag_regex option with smart default
Closes #519 CLI flag name: --tag-regex Heavily inspired by #537, but extends it with a smart default value to exclude non-release tags. This was suggested in #519 (comment)
1 parent d22df2d commit 8eea9ea

File tree

11 files changed

+265
-29
lines changed

11 files changed

+265
-29
lines changed

Diff for: commitizen/changelog.py

-2
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,6 @@ def generate_tree_from_commits(
8080
"date": current_tag_date,
8181
"changes": changes,
8282
}
83-
# TODO: Check if tag matches the version pattern, otherwise skip it.
84-
# This in order to prevent tags that are not versions.
8583
current_tag_name = commit_tag.name
8684
current_tag_date = commit_tag.date
8785
changes = defaultdict(list)

Diff for: commitizen/cli.py

+7
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,13 @@
251251
"If not set, it will generate changelog from the start"
252252
),
253253
},
254+
{
255+
"name": "--tag-regex",
256+
"help": (
257+
"regex match for tags represented "
258+
"within the changelog. default: '.*'"
259+
),
260+
},
254261
],
255262
},
256263
{

Diff for: commitizen/commands/changelog.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os.path
2+
import re
23
from difflib import SequenceMatcher
34
from operator import itemgetter
45
from typing import Callable, Dict, List, Optional
@@ -15,7 +16,7 @@
1516
NotAllowed,
1617
)
1718
from commitizen.git import GitTag, smart_open
18-
from commitizen.tags import tag_from_version
19+
from commitizen.tags import make_tag_pattern, tag_from_version
1920

2021

2122
class Changelog:
@@ -51,6 +52,10 @@ def __init__(self, config: BaseConfig, args):
5152
self.tag_format: str = args.get("tag_format") or self.config.settings.get(
5253
"tag_format", DEFAULT_SETTINGS["tag_format"]
5354
)
55+
tag_regex = args.get("tag_regex") or self.config.settings.get("tag_regex")
56+
if not tag_regex:
57+
tag_regex = make_tag_pattern(self.tag_format)
58+
self.tag_pattern = re.compile(str(tag_regex), re.VERBOSE | re.IGNORECASE)
5459

5560
def _find_incremental_rev(self, latest_version: str, tags: List[GitTag]) -> str:
5661
"""Try to find the 'start_rev'.
@@ -124,7 +129,7 @@ def __call__(self):
124129
# Don't continue if no `file_name` specified.
125130
assert self.file_name
126131

127-
tags = git.get_tags()
132+
tags = git.get_tags(pattern=self.tag_pattern)
128133
if not tags:
129134
tags = []
130135

Diff for: commitizen/git.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import re
23
from enum import Enum
34
from os import linesep
45
from pathlib import Path
@@ -140,7 +141,7 @@ def get_filenames_in_commit(git_reference: str = ""):
140141
raise GitCommandError(c.err)
141142

142143

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

166-
return git_tags
167+
filtered_git_tags = [t for t in git_tags if pattern.fullmatch(t.name)]
168+
169+
return filtered_git_tags
167170

168171

169172
def tag_exist(tag: str) -> bool:

Diff for: commitizen/tags.py

+22-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import re
12
from string import Template
23
from typing import Union
34

4-
from packaging.version import Version
5+
from packaging.version import VERSION_PATTERN, Version
56

67

78
def tag_from_version(version: Union[Version, str], tag_format: str) -> str:
@@ -29,3 +30,23 @@ def tag_from_version(version: Union[Version, str], tag_format: str) -> str:
2930
return t.safe_substitute(
3031
version=version, major=major, minor=minor, patch=patch, prerelease=prerelease
3132
)
33+
34+
35+
def make_tag_pattern(tag_format: str) -> str:
36+
"""Make regex pattern to match all tags created by tag_format."""
37+
escaped_format = re.escape(tag_format)
38+
escaped_format = re.sub(
39+
r"\\\$(version|major|minor|patch|prerelease)", r"$\1", escaped_format
40+
)
41+
# pre-release part of VERSION_PATTERN
42+
pre_release_pattern = r"([-_\.]?(a|b|c|rc|alpha|beta|pre|preview)([-_\.]?[0-9]+)?)?"
43+
filter_regex = Template(escaped_format).safe_substitute(
44+
# VERSION_PATTERN allows the v prefix, but we'd rather have users configure it
45+
# explicitly.
46+
version=VERSION_PATTERN.lstrip("\n v?"),
47+
major="[0-9]+",
48+
minor="[0-9]+",
49+
patch="[0-9]+",
50+
prerelease=pre_release_pattern,
51+
)
52+
return filter_regex

Diff for: docs/changelog.md

+22
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,28 @@ cz changelog --start-rev="v0.2.0"
161161
changelog_start_rev = "v0.2.0"
162162
```
163163
164+
### `tag-regex`
165+
166+
This value can be set in the `toml` file with the key `tag_regex` under `tools.commitizen`.
167+
168+
`tag_regex` is the regex pattern that selects tags to include in the changelog.
169+
By default, the changelog will capture all git tags matching the `tag_format`, including pre-releases.
170+
171+
Example use-cases:
172+
173+
- Exclude pre-releases from the changelog
174+
- Include existing tags that do not follow `tag_format` in the changelog
175+
176+
```bash
177+
cz changelog --tag-regex="[0-9]*\\.[0-9]*\\.[0-9]"
178+
```
179+
180+
```toml
181+
[tools.commitizen]
182+
# ...
183+
tag_regex = "[0-9]*\\.[0-9]*\\.[0-9]"
184+
```
185+
164186
## Hooks
165187
166188
Supported hook methods:

Diff for: docs/config.md

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
| `version` | `str` | `None` | Current version. Example: "0.1.2" |
99
| `version_files` | `list` | `[ ]` | Files were the version will be updated. A pattern to match a line, can also be specified, separated by `:` [See more][version_files] |
1010
| `tag_format` | `str` | `$version` | Format for the git tag, useful for old projects, that use a convention like `"v1.2.1"`. [See more][tag_format] |
11+
| `tag_regex` | `str` | Based on `tag_format` | Tags must match this to be included in the changelog (e.g. `"([0-9.])*"` to exclude pre-releases). [See more][tag_regex] |
1112
| `update_changelog_on_bump` | `bool` | `false` | Create changelog when running `cz bump` |
1213
| `gpg_sign` | `bool` | `false` | Use gpg signed tags instead of lightweight tags. |
1314
| `annotated_tag` | `bool` | `false` | Use annotated tags instead of lightweight tags. [See difference][annotated-tags-vs-lightweight] |
@@ -114,6 +115,7 @@ commitizen:
114115
115116
[version_files]: bump.md#version_files
116117
[tag_format]: bump.md#tag_format
118+
[tag_regex]: changelog.md#tag_regex
117119
[bump_message]: bump.md#bump_message
118120
[major-version-zero]: bump.md#-major-version-zero
119121
[prerelease-offset]: bump.md#-prerelease_offset

Diff for: tests/commands/test_bump_command.py

+18
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,24 @@ def test_bump_with_changelog_config(mocker: MockFixture, changelog_path, config_
531531
assert "0.2.0" in out
532532

533533

534+
@pytest.mark.usefixtures("tmp_commitizen_project")
535+
def test_bump_with_changelog_excludes_custom_tags(mocker: MockFixture, changelog_path):
536+
create_file_and_commit("feat(user): new file")
537+
git.tag("custom-tag")
538+
create_file_and_commit("feat(user): Another new file")
539+
testargs = ["cz", "bump", "--yes", "--changelog"]
540+
mocker.patch.object(sys, "argv", testargs)
541+
cli.main()
542+
tag_exists = git.tag_exist("0.2.0")
543+
assert tag_exists is True
544+
545+
with open(changelog_path, "r") as f:
546+
out = f.read()
547+
assert out.startswith("#")
548+
assert "## 0.2.0" in out
549+
assert "custom-tag" not in out
550+
551+
534552
def test_prevent_prerelease_when_no_increment_detected(
535553
mocker: MockFixture, capsys, tmp_commitizen_project
536554
):

Diff for: tests/commands/test_changelog_command.py

+52
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import sys
22
from datetime import datetime
3+
from typing import List
4+
from unittest.mock import patch
35

46
import pytest
57
from pytest_mock import MockFixture
@@ -968,3 +970,53 @@ def test_empty_commit_list(mocker):
968970
mocker.patch.object(sys, "argv", testargs)
969971
with pytest.raises(NoCommitsFoundError):
970972
cli.main()
973+
974+
975+
@pytest.mark.parametrize(
976+
"config_file, expected_versions",
977+
[
978+
pytest.param("", ["Unreleased"], id="v-prefix-not-configured"),
979+
pytest.param(
980+
'tag_format = "v$version"',
981+
["v1.1.0", "v1.1.0-beta", "v1.0.0"],
982+
id="v-prefix-configured-as-tag-format",
983+
),
984+
pytest.param(
985+
'tag_format = "v$version"\n' + 'tag_regex = ".*"',
986+
["v1.1.0", "custom-tag", "v1.1.0-beta", "v1.0.0"],
987+
id="tag-regex-matches-all-tags",
988+
),
989+
pytest.param(
990+
'tag_format = "v$version"\n' + r'tag_regex = "v[0-9\\.]*"',
991+
["v1.1.0", "v1.0.0"],
992+
id="tag-regex-excludes-pre-releases",
993+
),
994+
],
995+
)
996+
def test_changelog_tag_regex(
997+
config_path, changelog_path, config_file: str, expected_versions: List[str]
998+
):
999+
with open(config_path, "a") as f:
1000+
f.write(config_file)
1001+
1002+
# Create 4 tags with one valid feature each
1003+
create_file_and_commit("feat: initial")
1004+
git.tag("v1.0.0")
1005+
create_file_and_commit("feat: add 1")
1006+
git.tag("v1.1.0-beta")
1007+
create_file_and_commit("feat: add 2")
1008+
git.tag("custom-tag")
1009+
create_file_and_commit("feat: add 3")
1010+
git.tag("v1.1.0")
1011+
1012+
# call CLI
1013+
with patch.object(sys, "argv", ["cz", "changelog"]):
1014+
cli.main()
1015+
1016+
# open CLI output
1017+
with open(changelog_path, "r") as f:
1018+
out = f.read()
1019+
1020+
headings = [line for line in out.splitlines() if line.startswith("## ")]
1021+
changelog_versions = [heading[3:].split()[0] for heading in headings]
1022+
assert changelog_versions == expected_versions

Diff for: tests/test_git.py

+57-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import inspect
22
import os
3+
import re
34
import shutil
45
from typing import List, Optional
56

67
import pytest
78
from pytest_mock import MockFixture
89

910
from commitizen import cmd, exceptions, git
11+
from commitizen.tags import make_tag_pattern
1012
from tests.utils import FakeCommand, create_file_and_commit
1113

1214

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

31-
git_tags = git.get_tags()
33+
git_tags = git.get_tags(pattern=re.compile(r"v[0-9\.]+"))
3234
latest_git_tag = git_tags[0]
3335
assert latest_git_tag.rev == "333"
3436
assert latest_git_tag.name == "v1.0.0"
@@ -37,7 +39,60 @@ def test_get_tags(mocker: MockFixture):
3739
mocker.patch(
3840
"commitizen.cmd.run", return_value=FakeCommand(out="", err="No tag available")
3941
)
40-
assert git.get_tags() == []
42+
assert git.get_tags(pattern=re.compile(r"v[0-9\.]+")) == []
43+
44+
45+
@pytest.mark.parametrize(
46+
"pattern, expected_tags",
47+
[
48+
pytest.param(
49+
make_tag_pattern(tag_format="$version"),
50+
[], # No versions with normal 1.2.3 pattern
51+
id="default-tag-format",
52+
),
53+
pytest.param(
54+
make_tag_pattern(tag_format="$major-$minor-$patch$prerelease"),
55+
["1-0-0", "1-0-0alpha2"],
56+
id="tag-format-with-hyphens",
57+
),
58+
pytest.param(
59+
r"[0-9]+\-[0-9]+\-[0-9]+",
60+
["1-0-0"],
61+
id="tag-regex-with-hyphens-that-excludes-alpha",
62+
),
63+
pytest.param(
64+
make_tag_pattern(tag_format="v$version"),
65+
["v0.5.0", "v0.0.1-pre"],
66+
id="tag-format-with-v-prefix",
67+
),
68+
pytest.param(
69+
make_tag_pattern(tag_format="custom-prefix-$version"),
70+
["custom-prefix-0.0.1"],
71+
id="tag-format-with-custom-prefix",
72+
),
73+
pytest.param(
74+
".*",
75+
["1-0-0", "1-0-0alpha2", "v0.5.0", "v0.0.1-pre", "custom-prefix-0.0.1"],
76+
id="custom-tag-regex-to-include-all-tags",
77+
),
78+
],
79+
)
80+
def test_get_tags_filtering(
81+
mocker: MockFixture, pattern: str, expected_tags: List[str]
82+
):
83+
tag_str = (
84+
"1-0-0---inner_delimiter---333---inner_delimiter---2020-01-20---inner_delimiter---\n"
85+
"1-0-0alpha2---inner_delimiter---333---inner_delimiter---2020-01-20---inner_delimiter---\n"
86+
"v0.5.0---inner_delimiter---222---inner_delimiter---2020-01-17---inner_delimiter---\n"
87+
"v0.0.1-pre---inner_delimiter---111---inner_delimiter---2020-01-17---inner_delimiter---\n"
88+
"custom-prefix-0.0.1---inner_delimiter---111---inner_delimiter---2020-01-17---inner_delimiter---\n"
89+
"custom-non-release-tag"
90+
)
91+
mocker.patch("commitizen.cmd.run", return_value=FakeCommand(out=tag_str))
92+
93+
git_tags = git.get_tags(pattern=re.compile(pattern, flags=re.VERBOSE))
94+
actual_name_list = [t.name for t in git_tags]
95+
assert actual_name_list == expected_tags
4196

4297

4398
def test_get_tag_names(mocker: MockFixture):

0 commit comments

Comments
 (0)