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

commitizen/changelog.py

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

commitizen/cli.py

Lines changed: 7 additions & 0 deletions
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
{

commitizen/commands/changelog.py

Lines changed: 7 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
@@ -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

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,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

docs/changelog.md

Lines changed: 22 additions & 0 deletions
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:

docs/config.md

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

tests/commands/test_bump_command.py

Lines changed: 18 additions & 0 deletions
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
):

tests/commands/test_changelog_command.py

Lines changed: 52 additions & 0 deletions
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

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