Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

feat(changelog): add support for single version and version range #400

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

Merged
merged 4 commits into from
Mar 29, 2022
Merged
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
6 changes: 4 additions & 2 deletions commitizen/bump.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def find_increment(
# Most important cases are major and minor.
# Everything else will be considered patch.
select_pattern = re.compile(regex)
increment = None
increment: Optional[str] = None

for commit in commits:
for message in commit.message.split("\n"):
Expand Down Expand Up @@ -196,7 +196,9 @@ def _version_to_regex(version: str):
return re.compile(f"{clean_regex}")


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

That's why this function exists.
Expand Down
71 changes: 70 additions & 1 deletion commitizen/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@
import re
from collections import OrderedDict, defaultdict
from datetime import date
from typing import Callable, Dict, Iterable, List, Optional
from typing import Callable, Dict, Iterable, List, Optional, Tuple

from jinja2 import Environment, PackageLoader

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

Expand Down Expand Up @@ -281,3 +282,71 @@ def incremental_build(new_content: str, lines: List, metadata: Dict) -> List:
if not isinstance(latest_version_position, int):
output_lines.append(new_content)
return output_lines


def get_smart_tag_range(
tags: List[GitTag], newest: str, oldest: Optional[str] = None
) -> List[GitTag]:
"""Smart because it finds the N+1 tag.

This is because we need to find until the next tag
"""
accumulator = []
keep = False
if not oldest:
oldest = newest
for index, tag in enumerate(tags):
if tag.name == newest:
keep = True
if keep:
accumulator.append(tag)
if tag.name == oldest:
keep = False
try:
accumulator.append(tags[index + 1])
except IndexError:
pass
break
return accumulator


def get_oldest_and_newest_rev(
tags: List[GitTag], version: str, tag_format: str
) -> Tuple[Optional[str], Optional[str]]:
"""Find the tags for the given version.

`version` may come in different formats:
- `0.1.0..0.4.0`: as a range
- `0.3.0`: as a single version
"""
oldest: Optional[str] = None
newest: Optional[str] = None
try:
oldest, newest = version.split("..")
except ValueError:
newest = version

newest_tag = normalize_tag(newest, tag_format=tag_format)

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

tags_range = get_smart_tag_range(tags, newest=newest_tag, oldest=oldest_tag)
if not tags_range:
return None, None

oldest_rev: Optional[str] = tags_range[-1].name
newest_rev = newest_tag

# check if it's the first tag created
# and it's also being requested as part of the range
if oldest_rev == tags[-1].name and oldest_rev == oldest_tag:
return None, newest_rev

# when they are the same, and it's also the
# first tag created
if oldest_rev == newest_rev:
return None, newest_rev

return oldest_rev, newest_rev
6 changes: 6 additions & 0 deletions commitizen/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,12 @@
"useful if the changelog has been manually modified"
),
},
{
"name": "rev_range",
"type": str,
"nargs": "?",
"help": "generates changelog for the given version (e.g: 1.5.3) or version range (e.g: 1.5.3..1.7.9)",
},
{
"name": "--start-rev",
"default": None,
Expand Down
4 changes: 2 additions & 2 deletions commitizen/commands/bump.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def __call__(self): # noqa: C901
is_files_only: Optional[bool] = self.arguments["files_only"]
is_local_version: Optional[bool] = self.arguments["local_version"]

current_tag_version: str = bump.create_tag(
current_tag_version: str = bump.normalize_tag(
current_version, tag_format=tag_format
)

Expand Down Expand Up @@ -149,7 +149,7 @@ def __call__(self): # noqa: C901
is_local_version=is_local_version,
)

new_tag_version = bump.create_tag(new_version, tag_format=tag_format)
new_tag_version = bump.normalize_tag(new_version, tag_format=tag_format)
message = bump.create_commit_message(
current_version, new_version, bump_commit_message
)
Expand Down
60 changes: 46 additions & 14 deletions commitizen/commands/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
NoPatternMapError,
NoRevisionError,
NotAGitProjectError,
NotAllowed,
)
from commitizen.git import GitTag

Expand Down Expand Up @@ -46,6 +47,10 @@ def __init__(self, config: BaseConfig, args):
self.change_type_order = (
self.config.settings.get("change_type_order") or self.cz.change_type_order
)
self.rev_range = args.get("rev_range")
self.tag_format = args.get("tag_format") or self.config.settings.get(
"tag_format"
)

def _find_incremental_rev(self, latest_version: str, tags: List[GitTag]) -> str:
"""Try to find the 'start_rev'.
Expand Down Expand Up @@ -73,6 +78,30 @@ def _find_incremental_rev(self, latest_version: str, tags: List[GitTag]) -> str:
start_rev = tag.name
return start_rev

def write_changelog(
self, changelog_out: str, lines: List[str], changelog_meta: Dict
):
if not isinstance(self.file_name, str):
raise NotAllowed(
"Changelog file name is broken.\n"
"Check the flag `--file-name` in the terminal "
f"or the setting `changelog_file` in {self.config.path}"
)

changelog_hook: Optional[Callable] = self.cz.changelog_hook
with open(self.file_name, "w") as changelog_file:
partial_changelog: Optional[str] = None
if self.incremental:
new_lines = changelog.incremental_build(
changelog_out, lines, changelog_meta
)
changelog_out = "".join(new_lines)
partial_changelog = changelog_out

if changelog_hook:
changelog_out = changelog_hook(changelog_out, partial_changelog)
changelog_file.write(changelog_out)

def __call__(self):
commit_parser = self.cz.commit_parser
changelog_pattern = self.cz.changelog_pattern
Expand All @@ -83,23 +112,37 @@ def __call__(self):
changelog_message_builder_hook: Optional[
Callable
] = self.cz.changelog_message_builder_hook
changelog_hook: Optional[Callable] = self.cz.changelog_hook

if not changelog_pattern or not commit_parser:
raise NoPatternMapError(
f"'{self.config.settings['name']}' rule does not support changelog"
)

if self.incremental and self.rev_range:
raise NotAllowed("--incremental cannot be combined with a rev_range")

tags = git.get_tags()
if not tags:
tags = []

end_rev = ""

if self.incremental:
changelog_meta = changelog.get_metadata(self.file_name)
latest_version = changelog_meta.get("latest_version")
if latest_version:
start_rev = self._find_incremental_rev(latest_version, tags)

commits = git.get_commits(start=start_rev, args="--author-date-order")
if self.rev_range and self.tag_format:
start_rev, end_rev = changelog.get_oldest_and_newest_rev(
tags,
version=self.rev_range,
tag_format=self.tag_format,
)

commits = git.get_commits(
start=start_rev, end=end_rev, args="--author-date-order"
)
if not commits:
raise NoCommitsFoundError("No commits found")

Expand All @@ -126,15 +169,4 @@ def __call__(self):
with open(self.file_name, "r") as changelog_file:
lines = changelog_file.readlines()

with open(self.file_name, "w") as changelog_file:
partial_changelog: Optional[str] = None
if self.incremental:
new_lines = changelog.incremental_build(
changelog_out, lines, changelog_meta
)
changelog_out = "".join(new_lines)
partial_changelog = changelog_out

if changelog_hook:
changelog_out = changelog_hook(changelog_out, partial_changelog)
changelog_file.write(changelog_out)
self.write_changelog(changelog_out, lines, changelog_meta)
2 changes: 1 addition & 1 deletion commitizen/config/toml_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def _parse_setting(self, data: Union[bytes, str]) -> None:
name = "cz_conventional_commits"
```
"""
doc = parse(data) # type: ignore
doc = parse(data)
try:
self.settings.update(doc["tool"]["commitizen"]) # type: ignore
except exceptions.NonExistentKey:
Expand Down
2 changes: 1 addition & 1 deletion commitizen/cz/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def discover_plugins(path: Iterable[str] = None) -> Dict[str, Type[BaseCommitize
for finder, name, ispkg in pkgutil.iter_modules(path):
try:
if name.startswith("cz_"):
plugins[name] = importlib.import_module(name).discover_this # type: ignore
plugins[name] = importlib.import_module(name).discover_this
except AttributeError as e:
warnings.warn(UserWarning(e.args[0]))
continue
Expand Down
5 changes: 5 additions & 0 deletions commitizen/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class ExitCode(enum.IntEnum):
CURRENT_VERSION_NOT_FOUND = 17
INVALID_COMMAND_ARGUMENT = 18
INVALID_CONFIGURATION = 19
NOT_ALLOWED = 20


class CommitizenException(Exception):
Expand Down Expand Up @@ -142,3 +143,7 @@ class InvalidCommandArgumentError(CommitizenException):

class InvalidConfigurationError(CommitizenException):
exit_code = ExitCode.INVALID_CONFIGURATION


class NotAllowed(CommitizenException):
exit_code = ExitCode.NOT_ALLOWED
12 changes: 8 additions & 4 deletions commitizen/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,15 @@ class GitTag(GitObject):
def __init__(self, name, rev, date):
self.rev = rev.strip()
self.name = name.strip()
self.date = date.strip()
self._date = date.strip()

def __repr__(self):
return f"GitTag('{self.name}', '{self.rev}', '{self.date}')"

@property
def date(self):
return self._date

@classmethod
def from_line(cls, line: str, inner_delimiter: str) -> "GitTag":

Expand Down Expand Up @@ -82,10 +86,10 @@ def get_commits(
)

if start:
c = cmd.run(f"{git_log_cmd} {start}..{end}")
command = f"{git_log_cmd} {start}..{end}"
else:
c = cmd.run(f"{git_log_cmd} {end}")

command = f"{git_log_cmd} {end}"
c = cmd.run(command)
if not c.out:
return []

Expand Down
35 changes: 24 additions & 11 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,28 @@ This command will generate a changelog following the committing rules establishe

```bash
$ cz changelog --help
usage: cz changelog [-h] [--dry-run] [--file-name FILE_NAME]
[--unreleased-version UNRELEASED_VERSION] [--incremental]
[--start-rev START_REV]
usage: cz changelog [-h] [--dry-run] [--file-name FILE_NAME] [--unreleased-version UNRELEASED_VERSION] [--incremental] [--start-rev START_REV]
[rev_range]

positional arguments:
rev_range generates changelog for the given version (e.g: 1.5.3) or version range (e.g: 1.5.3..1.7.9)

optional arguments:
-h, --help show this help message and exit
--dry-run show changelog to stdout
--file-name FILE_NAME
file name of changelog (default: 'CHANGELOG.md')
--unreleased-version UNRELEASED_VERSION
set the value for the new version (use the tag value),
instead of using unreleased
--incremental generates changelog from last created version, useful
if the changelog has been manually modified
set the value for the new version (use the tag value), instead of using unreleased
--incremental generates changelog from last created version, useful if the changelog has been manually modified
--start-rev START_REV
start rev of the changelog.If not set, it will
generate changelog from the start
start rev of the changelog.If not set, it will generate changelog from the start
```

### Examples

#### Generate full changelog

```bash
cz changelog
```
Expand All @@ -35,6 +36,18 @@ cz changelog
cz ch
```

#### Get the changelog for the given version

```bash
cz changelog 0.3.0
```

#### Get the changelog for the given version range

```bash
cz changelog 0.3.0..0.4.0
```

## Constrains

changelog generation is constrained only to **markdown** files.
Expand Down Expand Up @@ -66,15 +79,15 @@ and the following variables are expected:
| `change_type` | The group where the commit belongs to, this is optional. Example: fix | `commit regex` |
| `message`\* | Information extracted from the commit message | `commit regex` |
| `scope` | Contextual information. Should be parsed using the regex from the message, it will be **bold** | `commit regex` |
| `breaking` | Whether is a breaking change or not | `commit regex` |
| `breaking` | Whether is a breaking change or not | `commit regex` |

- **required**: is the only one required to be parsed by the regex

## Configuration

### `unreleased_version`

There is usually an egg and chicken situation when automatically
There is usually a chicken and egg situation when automatically
bumping the version and creating the changelog.
If you bump the version first, you have no changelog, you have to
create it later, and it won't be included in
Expand Down
Loading