Skip to content

Commit e94eae9

Browse files
woileLee-W
authored andcommitted
feat(changelog): add support for single version and version range
Closes #399 #225
1 parent 6efad39 commit e94eae9

15 files changed

+502
-21
lines changed

commitizen/bump.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def find_increment(
2121
# Most important cases are major and minor.
2222
# Everything else will be considered patch.
2323
select_pattern = re.compile(regex)
24-
increment = None
24+
increment: Optional[str] = None
2525

2626
for commit in commits:
2727
for message in commit.message.split("\n"):

commitizen/changelog.py

+70-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
import re
3030
from collections import OrderedDict, defaultdict
3131
from datetime import date
32-
from typing import Callable, Dict, Iterable, List, Optional
32+
from typing import Callable, Dict, Iterable, List, Optional, Tuple
3333

3434
from jinja2 import Environment, PackageLoader
3535

@@ -281,3 +281,72 @@ def incremental_build(new_content: str, lines: List, metadata: Dict) -> List:
281281
if not isinstance(latest_version_position, int):
282282
output_lines.append(new_content)
283283
return output_lines
284+
285+
286+
def get_smart_tag_range(
287+
tags: List[GitTag], start: str, end: Optional[str] = None
288+
) -> List[GitTag]:
289+
"""Smart because it finds the N+1 tag.
290+
291+
This is because we need to find until the next tag
292+
"""
293+
accumulator = []
294+
keep = False
295+
if not end:
296+
end = start
297+
for index, tag in enumerate(tags):
298+
if tag.name == start:
299+
keep = True
300+
if keep:
301+
accumulator.append(tag)
302+
if tag.name == end:
303+
keep = False
304+
try:
305+
accumulator.append(tags[index + 1])
306+
except IndexError:
307+
pass
308+
break
309+
return accumulator
310+
311+
312+
def get_start_and_end_rev(
313+
tags: List[GitTag], version: str, tag_format: str, create_tag: Callable
314+
) -> Tuple[Optional[str], Optional[str]]:
315+
"""Find the tags for the given version.
316+
317+
`version` may come in different formats:
318+
- `0.1.0..0.4.0`: as a range
319+
- `0.3.0`: as a single version
320+
"""
321+
start: Optional[str] = None
322+
end: Optional[str] = None
323+
324+
try:
325+
start, end = version.split("..")
326+
except ValueError:
327+
end = version
328+
329+
end_tag = create_tag(end, tag_format=tag_format)
330+
331+
start_tag = None
332+
if start:
333+
start_tag = create_tag(start, tag_format=tag_format)
334+
335+
tags_range = get_smart_tag_range(tags, start=end_tag, end=start_tag)
336+
if len(tags_range) == 0:
337+
return None, None
338+
339+
start_rev: Optional[str] = tags_range[-1].name
340+
end_rev = end_tag
341+
342+
# check if it's the first tag created
343+
# and it's also being requested as part of the range
344+
if start_rev == tags[-1].name and start_rev == start_tag:
345+
return None, end_rev
346+
347+
# when they are the same, and it's also the
348+
# first tag crated
349+
if start_rev == end_rev:
350+
return None, end_rev
351+
352+
return start_rev, end_rev

commitizen/cli.py

+6
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,12 @@
188188
"useful if the changelog has been manually modified"
189189
),
190190
},
191+
{
192+
"name": "rev_range",
193+
"type": str,
194+
"nargs": "?",
195+
"help": "generates changelog for the given version (e.g: 1.5.3) or version range (e.g: 1.5.3..1.7.9)",
196+
},
191197
{
192198
"name": "--start-rev",
193199
"default": None,

commitizen/commands/changelog.py

+48-15
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33
from operator import itemgetter
44
from typing import Callable, Dict, List, Optional
55

6-
from commitizen import changelog, factory, git, out
6+
from commitizen import bump, changelog, factory, git, out
77
from commitizen.config import BaseConfig
88
from commitizen.exceptions import (
99
DryRunExit,
1010
NoCommitsFoundError,
1111
NoPatternMapError,
1212
NoRevisionError,
1313
NotAGitProjectError,
14+
NotAllowed,
1415
)
1516
from commitizen.git import GitTag
1617

@@ -46,6 +47,10 @@ def __init__(self, config: BaseConfig, args):
4647
self.change_type_order = (
4748
self.config.settings.get("change_type_order") or self.cz.change_type_order
4849
)
50+
self.rev_range = args.get("rev_range")
51+
self.tag_format = args.get("tag_format") or self.config.settings.get(
52+
"tag_format"
53+
)
4954

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

81+
def write_changelog(
82+
self, changelog_out: str, lines: List[str], changelog_meta: Dict
83+
):
84+
if not isinstance(self.file_name, str):
85+
raise NotAllowed(
86+
"Changelog file name is broken.\n"
87+
"Check the flag `--file-name` in the terminal "
88+
f"or the setting `changelog_file` in {self.config.path}"
89+
)
90+
91+
changelog_hook: Optional[Callable] = self.cz.changelog_hook
92+
with open(self.file_name, "w") as changelog_file:
93+
partial_changelog: Optional[str] = None
94+
if self.incremental:
95+
new_lines = changelog.incremental_build(
96+
changelog_out, lines, changelog_meta
97+
)
98+
changelog_out = "".join(new_lines)
99+
partial_changelog = changelog_out
100+
101+
if changelog_hook:
102+
changelog_out = changelog_hook(changelog_out, partial_changelog)
103+
changelog_file.write(changelog_out)
104+
76105
def __call__(self):
77106
commit_parser = self.cz.commit_parser
78107
changelog_pattern = self.cz.changelog_pattern
@@ -83,23 +112,38 @@ def __call__(self):
83112
changelog_message_builder_hook: Optional[
84113
Callable
85114
] = self.cz.changelog_message_builder_hook
86-
changelog_hook: Optional[Callable] = self.cz.changelog_hook
115+
87116
if not changelog_pattern or not commit_parser:
88117
raise NoPatternMapError(
89118
f"'{self.config.settings['name']}' rule does not support changelog"
90119
)
91120

121+
if self.incremental and self.rev_range:
122+
raise NotAllowed("--incremental cannot be combined with a rev_range")
123+
92124
tags = git.get_tags()
93125
if not tags:
94126
tags = []
95127

128+
end_rev = "HEAD"
129+
96130
if self.incremental:
97131
changelog_meta = changelog.get_metadata(self.file_name)
98132
latest_version = changelog_meta.get("latest_version")
99133
if latest_version:
100134
start_rev = self._find_incremental_rev(latest_version, tags)
101135

102-
commits = git.get_commits(start=start_rev, args="--author-date-order")
136+
if self.rev_range and self.tag_format:
137+
start_rev, end_rev = changelog.get_start_and_end_rev(
138+
tags,
139+
version=self.rev_range,
140+
tag_format=self.tag_format,
141+
create_tag=bump.create_tag,
142+
)
143+
144+
commits = git.get_commits(
145+
start=start_rev, end=end_rev, args="--author-date-order"
146+
)
103147
if not commits:
104148
raise NoCommitsFoundError("No commits found")
105149

@@ -126,15 +170,4 @@ def __call__(self):
126170
with open(self.file_name, "r") as changelog_file:
127171
lines = changelog_file.readlines()
128172

129-
with open(self.file_name, "w") as changelog_file:
130-
partial_changelog: Optional[str] = None
131-
if self.incremental:
132-
new_lines = changelog.incremental_build(
133-
changelog_out, lines, changelog_meta
134-
)
135-
changelog_out = "".join(new_lines)
136-
partial_changelog = changelog_out
137-
138-
if changelog_hook:
139-
changelog_out = changelog_hook(changelog_out, partial_changelog)
140-
changelog_file.write(changelog_out)
173+
self.write_changelog(changelog_out, lines, changelog_meta)

commitizen/cz/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def discover_plugins(path: Iterable[str] = None) -> Dict[str, Type[BaseCommitize
2323
for finder, name, ispkg in pkgutil.iter_modules(path):
2424
try:
2525
if name.startswith("cz_"):
26-
plugins[name] = importlib.import_module(name).discover_this # type: ignore
26+
plugins[name] = importlib.import_module(name).discover_this
2727
except AttributeError as e:
2828
warnings.warn(UserWarning(e.args[0]))
2929
continue

commitizen/exceptions.py

+5
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class ExitCode(enum.IntEnum):
2424
CURRENT_VERSION_NOT_FOUND = 17
2525
INVALID_COMMAND_ARGUMENT = 18
2626
INVALID_CONFIGURATION = 19
27+
NOT_ALLOWED = 20
2728

2829

2930
class CommitizenException(Exception):
@@ -142,3 +143,7 @@ class InvalidCommandArgumentError(CommitizenException):
142143

143144
class InvalidConfigurationError(CommitizenException):
144145
exit_code = ExitCode.INVALID_CONFIGURATION
146+
147+
148+
class NotAllowed(CommitizenException):
149+
exit_code = ExitCode.NOT_ALLOWED

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ isort = "^5.7.0"
7272
# linter
7373
flake8 = "^3.6"
7474
pre-commit = "^2.6.0"
75-
mypy = "0.910"
75+
mypy = "^0.931"
7676
types-PyYAML = "^5.4.3"
7777
types-termcolor = "^0.1.1"
7878
# documentation

0 commit comments

Comments
 (0)