Skip to content
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

feat: Introduce tag_regex option with smart default #692

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
36 changes: 0 additions & 36 deletions commitizen/bump.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,42 +217,6 @@ def _version_to_regex(version: str) -> str:
return version.replace(".", r"\.").replace("+", r"\+")


def normalize_tag(
version: Union[VersionProtocol, str],
tag_format: Optional[str] = None,
version_type_cls: Optional[Type[VersionProtocol]] = None,
) -> str:
"""The tag and the software version might be different.

That's why this function exists.

Example:
| tag | version (PEP 0440) |
| --- | ------- |
| v0.9.0 | 0.9.0 |
| ver1.0.0 | 1.0.0 |
| ver1.0.0.a0 | 1.0.0a0 |
"""
if version_type_cls is None:
version_type_cls = Version
if isinstance(version, str):
version = version_type_cls(version)

if not tag_format:
return str(version)

major, minor, patch = version.release
prerelease = ""
# version.pre is needed for mypy check
if version.is_prerelease and version.pre:
prerelease = f"{version.pre[0]}{version.pre[1]}"

t = Template(tag_format)
return t.safe_substitute(
version=version, major=major, minor=minor, patch=patch, prerelease=prerelease
)


def create_commit_message(
current_version: Union[Version, str],
new_version: Union[Version, str],
Expand Down
6 changes: 3 additions & 3 deletions commitizen/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@
from packaging.version import InvalidVersion, Version

from commitizen import defaults
from commitizen.bump import normalize_tag
from commitizen.exceptions import InvalidConfigurationError, NoCommitsFoundError
from commitizen.git import GitCommit, GitTag
from commitizen.tags import tag_from_version

if sys.version_info >= (3, 8):
from commitizen.version_types import VersionProtocol
Expand Down Expand Up @@ -341,13 +341,13 @@ def get_oldest_and_newest_rev(
except ValueError:
newest = version

newest_tag = normalize_tag(
newest_tag = tag_from_version(
newest, tag_format=tag_format, version_type_cls=version_type_cls
)

oldest_tag = None
if oldest:
oldest_tag = normalize_tag(
oldest_tag = tag_from_version(
oldest, tag_format=tag_format, version_type_cls=version_type_cls
)

Expand Down
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
5 changes: 3 additions & 2 deletions commitizen/commands/bump.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
NoVersionSpecifiedError,
)
from commitizen.providers import get_provider
from commitizen.tags import tag_from_version

logger = getLogger("commitizen")

Expand Down Expand Up @@ -161,7 +162,7 @@ def __call__(self): # noqa: C901
f"--major-version-zero is meaningless for current version {current_version}"
)

current_tag_version: str = bump.normalize_tag(
current_tag_version: str = tag_from_version(
current_version,
tag_format=tag_format,
version_type_cls=self.version_type,
Expand Down Expand Up @@ -223,7 +224,7 @@ def __call__(self): # noqa: C901
version_type_cls=self.version_type,
)

new_tag_version = bump.normalize_tag(
new_tag_version = tag_from_version(
new_version,
tag_format=tag_format,
version_type_cls=self.version_type,
Expand Down
18 changes: 13 additions & 5 deletions commitizen/commands/changelog.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import os.path
import re
from difflib import SequenceMatcher
from operator import itemgetter
from typing import Callable, Dict, List, Optional

from packaging.version import parse

from commitizen import bump, changelog, defaults, factory, git, out, version_types
from commitizen import changelog, defaults, factory, git, out, version_types
from commitizen.config import BaseConfig
from commitizen.defaults import DEFAULT_SETTINGS
from commitizen.exceptions import (
DryRunExit,
NoCommitsFoundError,
Expand All @@ -16,6 +18,7 @@
NotAllowed,
)
from commitizen.git import GitTag, smart_open
from commitizen.tags import make_tag_pattern, tag_from_version


class Changelog:
Expand Down Expand Up @@ -55,8 +58,8 @@ def __init__(self, config: BaseConfig, args):
or defaults.change_type_order
)
self.rev_range = args.get("rev_range")
self.tag_format = args.get("tag_format") or self.config.settings.get(
"tag_format"
self.tag_format: str = args.get("tag_format") or self.config.settings.get(
"tag_format", DEFAULT_SETTINGS["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.

You don't need to inject default there, default are already loaded and overwritten by actual settings

)
self.merge_prerelease = args.get(
"merge_prerelease"
Expand All @@ -65,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 @@ -138,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 All @@ -148,7 +156,7 @@ def __call__(self):
changelog_meta = changelog.get_metadata(self.file_name)
latest_version = changelog_meta.get("latest_version")
if latest_version:
latest_tag_version: str = bump.normalize_tag(
latest_tag_version: str = tag_from_version(
latest_version,
tag_format=self.tag_format,
version_type_cls=self.version_type,
Expand Down
7 changes: 4 additions & 3 deletions commitizen/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from commitizen.__version__ import __version__
from commitizen.config import BaseConfig, JsonConfig, TomlConfig, YAMLConfig
from commitizen.cz import registry
from commitizen.defaults import config_files
from commitizen.defaults import DEFAULT_SETTINGS, config_files
from commitizen.exceptions import InitFailedError, NoAnswersError
from commitizen.git import get_latest_tag_name, get_tag_names, smart_open
from commitizen.version_types import VERSION_TYPES
Expand Down Expand Up @@ -203,14 +203,15 @@ def _ask_tag_format(self, latest_tag) -> str:
f'Is "{tag_format}" the correct tag format?', style=self.cz.style
).unsafe_ask()

default_format = DEFAULT_SETTINGS["tag_format"]
if not is_correct_format:
tag_format = questionary.text(
'Please enter the correct version format: (default: "$version")',
f'Please enter the correct version format: (default: "{default_format}")',
style=self.cz.style,
).unsafe_ask()

if not tag_format:
tag_format = "$version"
tag_format = default_format
return tag_format

def _ask_version_provider(self) -> str:
Expand Down
4 changes: 2 additions & 2 deletions commitizen/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class Settings(TypedDict, total=False):
version: Optional[str]
version_files: List[str]
version_provider: Optional[str]
tag_format: Optional[str]
tag_format: str
bump_message: Optional[str]
allow_abort: bool
changelog_file: str
Expand Down Expand Up @@ -68,7 +68,7 @@ class Settings(TypedDict, total=False):
"version": None,
"version_files": [],
"version_provider": "commitizen",
"tag_format": None, # example v$version
"tag_format": "$version", # example v$version
"bump_message": None, # bumped v$current_version to $new_version
"allow_abort": False,
"changelog_file": "CHANGELOG.md",
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
65 changes: 65 additions & 0 deletions commitizen/tags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import re
import sys
from string import Template
from typing import Any, Optional, Type, Union

from packaging.version import VERSION_PATTERN, Version

if sys.version_info >= (3, 8):
from commitizen.version_types import VersionProtocol
else:
# workaround mypy issue for 3.7 python
VersionProtocol = Any


def tag_from_version(
Copy link
Member

Choose a reason for hiding this comment

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

version_to_tag looks more straightforward to me

Suggested change
def tag_from_version(
def veresion_to_tag(

version: Union[VersionProtocol, str],
tag_format: str,
version_type_cls: Optional[Type[VersionProtocol]] = None,
) -> str:
"""The tag and the software version might be different.

That's why this function exists.

Example:
| tag | version (PEP 0440) |
| --- | ------- |
| v0.9.0 | 0.9.0 |
| ver1.0.0 | 1.0.0 |
| ver1.0.0.a0 | 1.0.0a0 |
"""
if version_type_cls is None:
version_type_cls = Version
if isinstance(version, str):
version = version_type_cls(version)

major, minor, patch = version.release
prerelease = ""
# version.pre is needed for mypy check
if version.is_prerelease and version.pre:
prerelease = f"{version.pre[0]}{version.pre[1]}"

t = Template(tag_format)
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
2 changes: 1 addition & 1 deletion docs/bump.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ In your `pyproject.toml` or `.cz.toml`
tag_format = "v$major.$minor.$patch$prerelease"
```

The variables must be preceded by a `$` sign.
The variables must be preceded by a `$` sign. Default is `$version`.

Supported variables:

Expand Down
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
11 changes: 10 additions & 1 deletion docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,18 @@ Version provider used to read and write version [Read more](#version-providers)

Type: `str`

Default: `None`
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
Loading