Skip to content

Commit e58e56f

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 c780f4e commit e58e56f

File tree

11 files changed

+276
-28
lines changed

11 files changed

+276
-28
lines changed

commitizen/cli.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import argparse
22
import logging
33
import sys
4-
from pathlib import Path
54
from functools import partial
5+
from pathlib import Path
66
from types import TracebackType
77
from typing import List
88

@@ -274,6 +274,13 @@
274274
"If not set, it will include prereleases in the changelog"
275275
),
276276
},
277+
{
278+
"name": "--tag-regex",
279+
"help": (
280+
"regex match for tags represented "
281+
"within the changelog. default: '.*'"
282+
),
283+
},
277284
],
278285
},
279286
{

commitizen/commands/changelog.py

Lines changed: 8 additions & 2 deletions
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
@@ -17,7 +18,7 @@
1718
NotAllowed,
1819
)
1920
from commitizen.git import GitTag, smart_open
20-
from commitizen.tags import tag_from_version
21+
from commitizen.tags import make_tag_pattern, tag_from_version
2122

2223

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

71+
tag_regex = args.get("tag_regex") or self.config.settings.get("tag_regex")
72+
if not tag_regex:
73+
tag_regex = make_tag_pattern(self.tag_format)
74+
self.tag_pattern = re.compile(str(tag_regex), re.VERBOSE | re.IGNORECASE)
75+
7076
def _find_incremental_rev(self, latest_version: str, tags: List[GitTag]) -> str:
7177
"""Try to find the 'start_rev'.
7278
@@ -140,7 +146,7 @@ def __call__(self):
140146
# Don't continue if no `file_name` specified.
141147
assert self.file_name
142148

143-
tags = git.get_tags()
149+
tags = git.get_tags(pattern=self.tag_pattern)
144150
if not tags:
145151
tags = []
146152

commitizen/git.py

Lines changed: 5 additions & 2 deletions
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:

commitizen/tags.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import re
12
import sys
23
from string import Template
34
from typing import Any, Optional, Type, Union
45

5-
from packaging.version import Version
6+
from packaging.version import VERSION_PATTERN, Version
67

78
if sys.version_info >= (3, 8):
89
from commitizen.version_types import VersionProtocol
@@ -42,3 +43,23 @@ def tag_from_version(
4243
return t.safe_substitute(
4344
version=version, major=major, minor=minor, patch=patch, prerelease=prerelease
4445
)
46+
47+
48+
def make_tag_pattern(tag_format: str) -> str:
49+
"""Make regex pattern to match all tags created by tag_format."""
50+
escaped_format = re.escape(tag_format)
51+
escaped_format = re.sub(
52+
r"\\\$(version|major|minor|patch|prerelease)", r"$\1", escaped_format
53+
)
54+
# pre-release part of VERSION_PATTERN
55+
pre_release_pattern = r"([-_\.]?(a|b|c|rc|alpha|beta|pre|preview)([-_\.]?[0-9]+)?)?"
56+
filter_regex = Template(escaped_format).safe_substitute(
57+
# VERSION_PATTERN allows the v prefix, but we'd rather have users configure it
58+
# explicitly.
59+
version=VERSION_PATTERN.lstrip("\n v?"),
60+
major="[0-9]+",
61+
minor="[0-9]+",
62+
patch="[0-9]+",
63+
prerelease=pre_release_pattern,
64+
)
65+
return filter_regex

docs/changelog.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,28 @@ cz changelog --merge-prerelease
186186
changelog_merge_prerelease = true
187187
```
188188
189+
### `tag-regex`
190+
191+
This value can be set in the `toml` file with the key `tag_regex` under `tools.commitizen`.
192+
193+
`tag_regex` is the regex pattern that selects tags to include in the changelog.
194+
By default, the changelog will capture all git tags matching the `tag_format`, including pre-releases.
195+
196+
Example use-cases:
197+
198+
- Exclude pre-releases from the changelog
199+
- Include existing tags that do not follow `tag_format` in the changelog
200+
201+
```bash
202+
cz changelog --tag-regex="[0-9]*\\.[0-9]*\\.[0-9]"
203+
```
204+
205+
```toml
206+
[tools.commitizen]
207+
# ...
208+
tag_regex = "[0-9]*\\.[0-9]*\\.[0-9]"
209+
```
210+
189211
## Hooks
190212
191213
Supported hook methods:

docs/config.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ Default: `$version`
4242

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

45+
### `tag_regex`
46+
47+
Type: `str`
48+
49+
Default: Based on `tag_format`
50+
51+
Tags must match this to be included in the changelog (e.g. `"([0-9.])*"` to exclude pre-releases). [Read more][tag_regex]
52+
4553
### `update_changelog_on_bump`
4654

4755
Type: `bool`
@@ -339,6 +347,7 @@ setup(
339347

340348
[version_files]: bump.md#version_files
341349
[tag_format]: bump.md#tag_format
350+
[tag_regex]: changelog.md#tag_regex
342351
[bump_message]: bump.md#bump_message
343352
[major-version-zero]: bump.md#-major-version-zero
344353
[prerelease-offset]: bump.md#-prerelease_offset

poetry.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[virtualenvs]
2+
in-project = true

tests/commands/test_bump_command.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,24 @@ def test_bump_with_changelog_config(mocker: MockFixture, changelog_path, config_
533533
assert "0.2.0" in out
534534

535535

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

tests/commands/test_changelog_command.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import itertools
22
import sys
33
from datetime import datetime
4+
from typing import List
5+
from unittest.mock import patch
46

57
import pytest
68
from pytest_mock import MockFixture
@@ -1271,3 +1273,53 @@ def test_changelog_prerelease_rev_with_use_version_type_semver(
12711273
out, _ = capsys.readouterr()
12721274

12731275
file_regression.check(out, extension=".second-prerelease.md")
1276+
1277+
1278+
@pytest.mark.parametrize(
1279+
"config_file, expected_versions",
1280+
[
1281+
pytest.param("", ["Unreleased"], id="v-prefix-not-configured"),
1282+
pytest.param(
1283+
'tag_format = "v$version"',
1284+
["v1.1.0", "v1.1.0-beta", "v1.0.0"],
1285+
id="v-prefix-configured-as-tag-format",
1286+
),
1287+
pytest.param(
1288+
'tag_format = "v$version"\n' + 'tag_regex = ".*"',
1289+
["v1.1.0", "custom-tag", "v1.1.0-beta", "v1.0.0"],
1290+
id="tag-regex-matches-all-tags",
1291+
),
1292+
pytest.param(
1293+
'tag_format = "v$version"\n' + r'tag_regex = "v[0-9\\.]*"',
1294+
["v1.1.0", "v1.0.0"],
1295+
id="tag-regex-excludes-pre-releases",
1296+
),
1297+
],
1298+
)
1299+
def test_changelog_tag_regex(
1300+
config_path, changelog_path, config_file: str, expected_versions: List[str]
1301+
):
1302+
with open(config_path, "a") as f:
1303+
f.write(config_file)
1304+
1305+
# Create 4 tags with one valid feature each
1306+
create_file_and_commit("feat: initial")
1307+
git.tag("v1.0.0")
1308+
create_file_and_commit("feat: add 1")
1309+
git.tag("v1.1.0-beta")
1310+
create_file_and_commit("feat: add 2")
1311+
git.tag("custom-tag")
1312+
create_file_and_commit("feat: add 3")
1313+
git.tag("v1.1.0")
1314+
1315+
# call CLI
1316+
with patch.object(sys, "argv", ["cz", "changelog"]):
1317+
cli.main()
1318+
1319+
# open CLI output
1320+
with open(changelog_path, "r") as f:
1321+
out = f.read()
1322+
1323+
headings = [line for line in out.splitlines() if line.startswith("## ")]
1324+
changelog_versions = [heading[3:].split()[0] for heading in headings]
1325+
assert changelog_versions == expected_versions

tests/test_git.py

Lines changed: 57 additions & 2 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy