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: Add --major-version-zero #58

Merged
merged 16 commits into from
Nov 4, 2023
Merged
Show file tree
Hide file tree
Changes from 11 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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@ options:
for 0.x versions), else if there are new features,
bump the minor number, else just bump the patch number.
Default: None.
-Z, --no-zerover By default, breaking changes on a 0.x don't bump the
major version, maintaining it at 0. With this option, a
breaking change will bump a 0.x version to 1.0.
-h, --help Show this help message and exit.
-i, --in-place Insert new entries (versions missing from changelog)
in-place. An output file must be specified. With
Expand Down
17 changes: 15 additions & 2 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,20 +241,31 @@ git-changelog --bump minor # 1.2.3 -> 1.3.0
git-changelog --bump patch # 1.2.3 -> 1.2.4
```

Note that the major number won't be bumped if the latest version is 0.x.
Note that, by default the major number won't be bumped if the latest version is 0.x.
Instead, the minor number will be bumped:

```bash
git-changelog --bump major # 0.1.2 -> 0.2.0, same as minor because 0.x
git-changelog --bump minor # 0.1.2 -> 0.2.0
```

In that case, when you are ready to bump to 1.0.0, just pass this version as value:
In that case, when you are ready to bump to 1.0.0,
just pass this version as value, or use the `-Z`, `--no-zerover` flag:

```bash
git-changelog --bump 1.0.0
git-changelog --bump auto --no-zerover
git-changelog --bump major --no-zerover
```

If you use *git-changelog* in CI, to update your changelog automatically,
it is recommended to use a configuration file instead of the CLI option.
On a fresh project, start by setting `zerover = true` in one of the supported
[configuration files](#configuration-files). Then, once you are ready
to bump to v1, set `zerover = false` and commit it as a breaking change.
Once v1 is released, the setting has no use anymore, and you can remove it
from your configuration file.

## Parse additional information in commit messages

*git-changelog* is able to parse the body of commit messages
Expand Down Expand Up @@ -483,6 +494,8 @@ repository = "."
sections = ["fix", "maint"]
template = "angular"
version-regex = "^## \\\\[(?P<version>v?[^\\\\]]+)"
provider = "gitlab"
zerover = true
```

In the case of configuring *git-changelog* within `pyproject.toml`, these
Expand Down
10 changes: 7 additions & 3 deletions src/git_changelog/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,13 @@
ConventionType = Union[str, CommitConvention, Type[CommitConvention]]


def bump(version: str, part: Literal["major", "minor", "patch"] = "patch") -> str:
def bump(version: str, part: Literal["major", "minor", "patch"] = "patch", *, zerover: bool = True) -> str:
"""Bump a version.

Arguments:
version: The version to bump.
part: The part of the version to bump (major, minor, or patch).
zerover: Keep major version at zero, even for breaking changes.

Returns:
The bumped version.
Expand All @@ -43,7 +44,7 @@ def bump(version: str, part: Literal["major", "minor", "patch"] = "patch") -> st
version = version[1:]

semver_version = VersionInfo.parse(version)
if part == "major" and semver_version.major != 0:
if part == "major" and (semver_version.major != 0 or not zerover):
semver_version = semver_version.bump_major()
elif part == "minor" or (part == "major" and semver_version.major == 0):
semver_version = semver_version.bump_minor()
Expand Down Expand Up @@ -170,6 +171,7 @@ def __init__(
sections: list[str] | None = None,
bump_latest: bool = False,
bump: str | None = None,
zerover: bool = True,
):
"""Initialization method.

Expand All @@ -182,10 +184,12 @@ def __init__(
sections: The sections to render (features, bug fixes, etc.).
bump_latest: Deprecated, use `bump="auto"` instead. Whether to try and bump latest version to guess new one.
bump: Whether to try and bump to a given version.
zerover: Keep major version at zero, even for breaking changes.
"""
self.repository: str | Path = repository
self.parse_provider_refs: bool = parse_provider_refs
self.parse_trailers: bool = parse_trailers
self.zerover: bool = zerover

# set provider
if not isinstance(provider, ProviderRefParser):
Expand Down Expand Up @@ -409,7 +413,7 @@ def _bump(self, version: str) -> None:
if version in {"major", "minor", "patch"}:
# bump version (don't fail on non-semver versions)
try:
last_version.planned_tag = bump(last_tag, version) # type: ignore[arg-type]
last_version.planned_tag = bump(last_tag, version, zerover=self.zerover) # type: ignore[arg-type]
except ValueError:
return
else:
Expand Down
14 changes: 13 additions & 1 deletion src/git_changelog/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"sections": None,
"template": "keepachangelog",
"version_regex": DEFAULT_VERSION_REGEX,
"zerover": True,
}


Expand Down Expand Up @@ -165,7 +166,7 @@ def get_parser() -> argparse.ArgumentParser:
metavar="VERSION",
help="Specify the bump from latest version for the set of unreleased commits. "
"Can be one of 'auto', 'major', 'minor', 'patch' or a valid semver version (eg. 1.2.3). "
"With 'auto', if a commit contains breaking changes, bump the major number (or the minor number for 0.x versions), "
"With 'auto', if a commit contains breaking changes, bump the major number, "
pawamoy marked this conversation as resolved.
Show resolved Hide resolved
"else if there are new features, bump the minor number, else just bump the patch number. "
"Default: unset (false).",
)
Expand Down Expand Up @@ -291,6 +292,14 @@ def get_parser() -> argparse.ArgumentParser:
dest="omit_empty_versions",
help="Omit empty versions from the output. Default: unset (false).",
)
parser.add_argument(
"-Z",
"--no-zerover",
action="store_false",
dest="zerover",
help="By default, breaking changes on a 0.x don't bump the major version, maintaining it at 0. "
"With this option, a breaking change will bump a 0.x version to 1.0.",
)
parser.add_argument(
"-v",
"--version",
Expand Down Expand Up @@ -440,6 +449,7 @@ def build_and_render(
omit_empty_versions: bool = False, # noqa: FBT001,FBT002
provider: str | None = None,
bump: str | None = None,
zerover: bool = True, # noqa: FBT001,FBT002
) -> tuple[Changelog, str]:
"""Build a changelog and render it.

Expand All @@ -462,6 +472,7 @@ def build_and_render(
omit_empty_versions: Whether to omit empty versions from the output.
provider: Provider class used by this repository.
bump: Whether to try and bump to a given version.
zerover: Keep major version at zero, even for breaking changes.

Raises:
ValueError: When some arguments are incompatible or missing.
Expand Down Expand Up @@ -504,6 +515,7 @@ def build_and_render(
parse_trailers=parse_trailers,
sections=sections,
bump=bump,
zerover=zerover,
)

# remove empty versions from changelog data
Expand Down
54 changes: 21 additions & 33 deletions src/git_changelog/commit.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,6 @@ def _format_sections_help(cls) -> str:
""",
)


class BasicConvention(CommitConvention):
"""Basic commit message convention."""

Expand All @@ -210,8 +209,6 @@ class BasicConvention(CommitConvention):
"merge": "Merged",
"doc": "Documented",
}

TYPE_REGEX: ClassVar[Pattern] = re.compile(r"^(?P<type>(%s))" % "|".join(TYPES.keys()), re.I)
BREAK_REGEX: ClassVar[Pattern] = re.compile(
r"^break(s|ing changes?)?[ :].+$",
re.I | re.MULTILINE,
Expand All @@ -223,6 +220,11 @@ class BasicConvention(CommitConvention):
TYPES["remove"],
]

@property
def type_regex(self) -> re.Pattern:
"""Type regex."""
return re.compile(r"^(?P<type>(%s))" % "|".join(self.TYPES.keys()), re.I)

def parse_commit(self, commit: Commit) -> dict[str, str | bool]: # noqa: D102
commit_type = self.parse_type(commit.subject)
message = "\n".join([commit.subject, *commit.body])
Expand All @@ -246,22 +248,11 @@ def parse_type(self, commit_subject: str) -> str:
Returns:
The commit type.
"""
type_match = self.TYPE_REGEX.match(commit_subject)
type_match = self.type_regex.match(commit_subject)
if type_match:
return self.TYPES.get(type_match.groupdict()["type"].lower(), "")
return ""

def is_minor(self, commit_type: str) -> bool:
"""Tell if this commit is worth a minor bump.

Arguments:
commit_type: The commit type.

Returns:
Whether it's a minor commit.
"""
return commit_type == self.TYPES["add"]

def is_major(self, commit_message: str) -> bool:
"""Tell if this commit is worth a major bump.

Expand Down Expand Up @@ -294,9 +285,6 @@ class AngularConvention(CommitConvention):
"test": "Tests",
"tests": "Tests",
}
SUBJECT_REGEX: ClassVar[Pattern] = re.compile(
r"^(?P<type>(%s))(?:\((?P<scope>.+)\))?: (?P<subject>.+)$" % ("|".join(TYPES.keys())), # (%)
)
BREAK_REGEX: ClassVar[Pattern] = re.compile(
r"^break(s|ing changes?)?[ :].+$",
re.I | re.MULTILINE,
Expand All @@ -309,6 +297,13 @@ class AngularConvention(CommitConvention):
TYPES["perf"],
]

@property
def subject_regex(self) -> re.Pattern:
"""Subject regex."""
return re.compile(
r"^(?P<type>(%s))(?:\((?P<scope>.+)\))?: (?P<subject>.+)$" % ("|".join(self.TYPES.keys())), # (%)
)

def parse_commit(self, commit: Commit) -> dict[str, str | bool]: # noqa: D102
subject = self.parse_subject(commit.subject)
message = "\n".join([commit.subject, *commit.body])
Expand All @@ -334,24 +329,13 @@ def parse_subject(self, commit_subject: str) -> dict[str, str]:
Returns:
The parsed data.
"""
subject_match = self.SUBJECT_REGEX.match(commit_subject)
subject_match = self.subject_regex.match(commit_subject)
if subject_match:
dct = subject_match.groupdict()
dct["type"] = self.TYPES[dct["type"]]
return dct
return {"type": "", "scope": "", "subject": commit_subject}

def is_minor(self, commit_type: str) -> bool:
"""Tell if this commit is worth a minor bump.

Arguments:
commit_type: The commit type.

Returns:
Whether it's a minor commit.
"""
return commit_type == self.TYPES["feat"]

def is_major(self, commit_message: str) -> bool:
"""Tell if this commit is worth a major bump.

Expand All @@ -369,9 +353,13 @@ class ConventionalCommitConvention(AngularConvention):

TYPES: ClassVar[dict[str, str]] = AngularConvention.TYPES
DEFAULT_RENDER: ClassVar[list[str]] = AngularConvention.DEFAULT_RENDER
SUBJECT_REGEX: ClassVar[Pattern] = re.compile(
r"^(?P<type>(%s))(?:\((?P<scope>.+)\))?(?P<breaking>!)?: (?P<subject>.+)$" % ("|".join(TYPES.keys())), # (%)
)

@property
def subject_regex(self) -> re.Pattern:
"""Subject regex."""
return re.compile(
r"^(?P<type>(%s))(?:\((?P<scope>.+)\))?(?P<breaking>!)?: (?P<subject>.+)$" % ("|".join(self.TYPES.keys())), # (%)
)

def parse_commit(self, commit: Commit) -> dict[str, str | bool]: # noqa: D102
subject = self.parse_subject(commit.subject)
Expand Down
11 changes: 10 additions & 1 deletion tests/test_commit.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,18 @@

from __future__ import annotations

from abc import ABCMeta
from typing import Generator

import pytest

from git_changelog.commit import Commit
from git_changelog.commit import (
AngularConvention,
BasicConvention,
Commit,
CommitConvention,
ConventionalCommitConvention,
)


@pytest.mark.parametrize(
Expand Down
35 changes: 32 additions & 3 deletions tests/test_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,35 @@ def test_bump_minor(version: str, bumped: str) -> None:
assert bump(version, "minor") == bumped


@pytest.mark.parametrize(
("version", "bumped"),
[
("0.0.1", "1.0.0"),
("0.1.0", "1.0.0"),
("0.1.1", "1.0.0"),
("1.0.0", "2.0.0"),
("1.0.1", "2.0.0"),
("1.1.0", "2.0.0"),
("1.1.1", "2.0.0"),
("v0.0.1", "v1.0.0"),
("v0.1.0", "v1.0.0"),
("v0.1.1", "v1.0.0"),
("v1.0.0", "v2.0.0"),
("v1.0.1", "v2.0.0"),
("v1.1.0", "v2.0.0"),
("v1.1.1", "v2.0.0"),
],
)
def test_bump_major_no_zerover(version: str, bumped: str) -> None:
"""Test major version bumping without zerover.

Parameters:
version: The base version.
bumped: The expected, bumped version.
"""
assert bump(version, "major", zerover=False) == bumped


@pytest.mark.parametrize(
("version", "bumped"),
[
Expand All @@ -84,11 +113,11 @@ def test_bump_minor(version: str, bumped: str) -> None:
("v1.1.1", "v2.0.0"),
],
)
def test_bump_major(version: str, bumped: str) -> None:
"""Test major version bumping.
def test_bump_major_zerover(version: str, bumped: str) -> None:
"""Test major version bumping with zerover.

Parameters:
version: The base version.
bumped: The expected, bumped version.
"""
assert bump(version, "major") == bumped
assert bump(version, "major", zerover=True) == bumped