Skip to content

feat: Introduce tag_regex option with smart default #692

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
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)
  • Loading branch information
robertschweizer committed May 2, 2023
commit e58e56f81777bc27e55be12be6edffddb9d70406
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]:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make the pattern filtering optional:

Suggested change
def get_tags(dateformat: str = "%Y-%m-%d", *, pattern: re.Pattern) -> List[GitTag]:
def get_tags(dateformat: str = "%Y-%m-%d", *, pattern: re.Pattern | None = None) -> 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)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we can use git command to filter . but it's not requried.


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`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Default: Based on `tag_format`
Default: Computed from `tag_format`

or

Suggested change
Default: Based on `tag_format`
Default: Extrapolated from `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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not related to this PR, can you remove this ?

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