Skip to content

Commit e141d90

Browse files
Merge branch 'master' into bump-doc-version-files
2 parents 3a23bcd + f4e8fcb commit e141d90

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+4714
-1420
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ repos:
4848
- tomli
4949

5050
- repo: https://github.com/commitizen-tools/commitizen
51-
rev: v4.9.1 # automatically updated by Commitizen
51+
rev: v4.10.0 # automatically updated by Commitizen
5252
hooks:
5353
- id: commitizen
5454
- id: commitizen-branch

CHANGELOG.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,36 @@
1+
## v4.10.0 (2025-11-10)
2+
3+
### Feat
4+
5+
- add config option for line length warning
6+
- **conventional_commits**: allow exclamation in title on BC
7+
- **version**: add the ability to just print major or minor version
8+
- allow `amend!` prefix as created by `git --fixup=reword:<commit>`
9+
10+
### Fix
11+
12+
- **commands/version**: add missing return
13+
- **test**: set terminal width for cli tests
14+
- **Init**: raise InitFailedError on keyboard interrupt on pre-commit hook question, simplify logic, remove unreachable code path
15+
16+
### Refactor
17+
18+
- **bump**: cleanup related to update_version_file
19+
- **RestructuredTest**: rename variable, fix typo and remove unnecessary string copy
20+
- **TomlConfig**: minor cleanups for DX
21+
- **Commit**: refactor _prompt_commit_questions and fix some type hint
22+
- **hooks**: refactor to improve readability
23+
- **Init**: make project_info a module and remove self.project_info
24+
- **BaseConfig**: update docstring, extract factory method and remove unnecessary variable assignment
25+
- remove self.encoding for better maintainability
26+
- **utils**: make get_backup_file_path to return a path for semantic correctness
27+
- remove unnecessary class member tag_format
28+
- **Bump**: remove use of getattr
29+
- **ConventionalCommitsCz**: rewrite message method to make the pattern more clear
30+
- **cmd**: unnest try except
31+
- **BaseCommitizen**: remove NotImplementedError and make them abstract method
32+
- **BaseCommitizen**: construct Style object directly to get rid of potential type error
33+
134
## v4.9.1 (2025-09-10)
235

336
### Fix

commitizen/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "4.9.1"
1+
__version__ = "4.10.0"

commitizen/bump.py

Lines changed: 33 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
import os
44
import re
55
from collections import OrderedDict
6-
from collections.abc import Iterable
6+
from collections.abc import Generator, Iterable
77
from glob import iglob
88
from logging import getLogger
99
from string import Template
1010
from typing import cast
1111

12-
from commitizen.defaults import BUMP_MESSAGE, ENCODING, MAJOR, MINOR, PATCH
12+
from commitizen.defaults import BUMP_MESSAGE, MAJOR, MINOR, PATCH
1313
from commitizen.exceptions import CurrentVersionNotFoundError
1414
from commitizen.git import GitCommit, smart_open
1515
from commitizen.version_schemes import Increment, Version
@@ -64,8 +64,8 @@ def update_version_in_files(
6464
new_version: str,
6565
files: Iterable[str],
6666
*,
67-
check_consistency: bool = False,
68-
encoding: str = ENCODING,
67+
check_consistency: bool,
68+
encoding: str,
6969
) -> list[str]:
7070
"""Change old version to the new one in every file given.
7171
@@ -75,16 +75,22 @@ def update_version_in_files(
7575
7676
Returns the list of updated files.
7777
"""
78-
# TODO: separate check step and write step
79-
updated = []
80-
for path, regex in _files_and_regexes(files, current_version):
81-
current_version_found, version_file = _bump_with_regex(
82-
path,
83-
current_version,
84-
new_version,
85-
regex,
86-
encoding=encoding,
87-
)
78+
updated_files = []
79+
80+
for path, pattern in _resolve_files_and_regexes(files, current_version):
81+
current_version_found = False
82+
bumped_lines = []
83+
84+
with open(path, encoding=encoding) as version_file:
85+
for line in version_file:
86+
bumped_line = (
87+
line.replace(current_version, new_version)
88+
if pattern.search(line)
89+
else line
90+
)
91+
92+
current_version_found = current_version_found or bumped_line != line
93+
bumped_lines.append(bumped_line)
8894

8995
if check_consistency and not current_version_found:
9096
raise CurrentVersionNotFoundError(
@@ -93,53 +99,32 @@ def update_version_in_files(
9399
"version_files are possibly inconsistent."
94100
)
95101

102+
bumped_version_file_content = "".join(bumped_lines)
103+
96104
# Write the file out again
97105
with smart_open(path, "w", encoding=encoding) as file:
98-
file.write(version_file)
99-
updated.append(path)
100-
return updated
106+
file.write(bumped_version_file_content)
107+
updated_files.append(path)
108+
109+
return updated_files
101110

102111

103-
def _files_and_regexes(patterns: Iterable[str], version: str) -> list[tuple[str, str]]:
112+
def _resolve_files_and_regexes(
113+
patterns: Iterable[str], version: str
114+
) -> Generator[tuple[str, re.Pattern], None, None]:
104115
"""
105116
Resolve all distinct files with their regexp from a list of glob patterns with optional regexp
106117
"""
107-
out: set[tuple[str, str]] = set()
118+
filepath_set: set[tuple[str, str]] = set()
108119
for pattern in patterns:
109120
drive, tail = os.path.splitdrive(pattern)
110121
path, _, regex = tail.partition(":")
111122
filepath = drive + path
112-
if not regex:
113-
regex = re.escape(version)
123+
regex = regex or re.escape(version)
114124

115-
for file in iglob(filepath):
116-
out.add((file, regex))
125+
filepath_set.update((path, regex) for path in iglob(filepath))
117126

118-
return sorted(out)
119-
120-
121-
def _bump_with_regex(
122-
version_filepath: str,
123-
current_version: str,
124-
new_version: str,
125-
regex: str,
126-
encoding: str = ENCODING,
127-
) -> tuple[bool, str]:
128-
current_version_found = False
129-
lines = []
130-
pattern = re.compile(regex)
131-
with open(version_filepath, encoding=encoding) as f:
132-
for line in f:
133-
if not pattern.search(line):
134-
lines.append(line)
135-
continue
136-
137-
bumped_line = line.replace(current_version, new_version)
138-
if bumped_line != line:
139-
current_version_found = True
140-
lines.append(bumped_line)
141-
142-
return current_version_found, "".join(lines)
127+
return ((path, re.compile(regex)) for path, regex in sorted(filepath_set))
143128

144129

145130
def create_commit_message(

commitizen/changelog_formats/base.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,9 @@ def __init__(self, config: BaseConfig) -> None:
2424
# Constructor needs to be redefined because `Protocol` prevent instantiation by default
2525
# See: https://bugs.python.org/issue44807
2626
self.config = config
27-
self.encoding = self.config.settings["encoding"]
28-
self.tag_format = self.config.settings["tag_format"]
2927
self.tag_rules = TagRules(
3028
scheme=get_version_scheme(self.config.settings),
31-
tag_format=self.tag_format,
29+
tag_format=self.config.settings["tag_format"],
3230
legacy_tag_formats=self.config.settings["legacy_tag_formats"],
3331
ignored_tag_formats=self.config.settings["ignored_tag_formats"],
3432
)
@@ -37,7 +35,9 @@ def get_metadata(self, filepath: str) -> Metadata:
3735
if not os.path.isfile(filepath):
3836
return Metadata()
3937

40-
with open(filepath, encoding=self.encoding) as changelog_file:
38+
with open(
39+
filepath, encoding=self.config.settings["encoding"]
40+
) as changelog_file:
4141
return self.get_metadata_from_file(changelog_file)
4242

4343
def get_metadata_from_file(self, file: IO[Any]) -> Metadata:
Lines changed: 57 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,92 +1,88 @@
11
from __future__ import annotations
22

3-
import sys
43
from itertools import zip_longest
5-
from typing import IO, TYPE_CHECKING, Any, Union
4+
from typing import IO
65

76
from commitizen.changelog import Metadata
87

98
from .base import BaseFormat
109

11-
if TYPE_CHECKING:
12-
# TypeAlias is Python 3.10+ but backported in typing-extensions
13-
if sys.version_info >= (3, 10):
14-
from typing import TypeAlias
15-
else:
16-
from typing_extensions import TypeAlias
17-
18-
19-
# Can't use `|` operator and native type because of https://bugs.python.org/issue42233 only fixed in 3.10
20-
TitleKind: TypeAlias = Union[str, tuple[str, str]]
21-
2210

2311
class RestructuredText(BaseFormat):
2412
extension = "rst"
2513

26-
def get_metadata_from_file(self, file: IO[Any]) -> Metadata:
14+
def get_metadata_from_file(self, file: IO[str]) -> Metadata:
2715
"""
2816
RestructuredText section titles are not one-line-based,
2917
they spread on 2 or 3 lines and levels are not predefined
30-
but determined byt their occurrence order.
18+
but determined by their occurrence order.
3119
3220
It requires its own algorithm.
3321
3422
For a more generic approach, you need to rely on `docutils`.
3523
"""
36-
meta = Metadata()
37-
unreleased_title_kind: TitleKind | None = None
38-
in_overlined_title = False
39-
lines = file.readlines()
24+
out_metadata = Metadata()
25+
unreleased_title_kind: str | tuple[str, str] | None = None
26+
is_overlined_title = False
27+
lines = [line.strip().lower() for line in file.readlines()]
28+
4029
for index, (first, second, third) in enumerate(
4130
zip_longest(lines, lines[1:], lines[2:], fillvalue="")
4231
):
43-
first = first.strip().lower()
44-
second = second.strip().lower()
45-
third = third.strip().lower()
4632
title: str | None = None
47-
kind: TitleKind | None = None
48-
if self.is_overlined_title(first, second, third):
33+
kind: str | tuple[str, str] | None = None
34+
if _is_overlined_title(first, second, third):
4935
title = second
5036
kind = (first[0], third[0])
51-
in_overlined_title = True
52-
elif not in_overlined_title and self.is_underlined_title(first, second):
37+
is_overlined_title = True
38+
elif not is_overlined_title and _is_underlined_title(first, second):
5339
title = first
5440
kind = second[0]
5541
else:
56-
in_overlined_title = False
57-
58-
if title:
59-
if "unreleased" in title:
60-
unreleased_title_kind = kind
61-
meta.unreleased_start = index
62-
continue
63-
elif unreleased_title_kind and unreleased_title_kind == kind:
64-
meta.unreleased_end = index
65-
# Try to find the latest release done
66-
if version := self.tag_rules.search_version(title):
67-
meta.latest_version = version[0]
68-
meta.latest_version_tag = version[1]
69-
meta.latest_version_position = index
70-
break
71-
if meta.unreleased_start is not None and meta.unreleased_end is None:
72-
meta.unreleased_end = (
73-
meta.latest_version_position if meta.latest_version else index + 1
42+
is_overlined_title = False
43+
44+
if not title:
45+
continue
46+
47+
if "unreleased" in title:
48+
unreleased_title_kind = kind
49+
out_metadata.unreleased_start = index
50+
continue
51+
52+
if unreleased_title_kind and unreleased_title_kind == kind:
53+
out_metadata.unreleased_end = index
54+
# Try to find the latest release done
55+
if version := self.tag_rules.search_version(title):
56+
out_metadata.latest_version = version[0]
57+
out_metadata.latest_version_tag = version[1]
58+
out_metadata.latest_version_position = index
59+
break
60+
61+
if (
62+
out_metadata.unreleased_start is not None
63+
and out_metadata.unreleased_end is None
64+
):
65+
out_metadata.unreleased_end = (
66+
out_metadata.latest_version_position
67+
if out_metadata.latest_version
68+
else len(lines)
7469
)
7570

76-
return meta
77-
78-
def is_overlined_title(self, first: str, second: str, third: str) -> bool:
79-
return (
80-
len(first) >= len(second)
81-
and len(first) == len(third)
82-
and all(char == first[0] for char in first[1:])
83-
and first[0] == third[0]
84-
and self.is_underlined_title(second, third)
85-
)
86-
87-
def is_underlined_title(self, first: str, second: str) -> bool:
88-
return (
89-
len(second) >= len(first)
90-
and not second.isalnum()
91-
and all(char == second[0] for char in second[1:])
92-
)
71+
return out_metadata
72+
73+
74+
def _is_overlined_title(first: str, second: str, third: str) -> bool:
75+
return (
76+
len(first) == len(third) >= len(second)
77+
and first[0] == third[0]
78+
and all(char == first[0] for char in first)
79+
and _is_underlined_title(second, third)
80+
)
81+
82+
83+
def _is_underlined_title(first: str, second: str) -> bool:
84+
return (
85+
len(second) >= len(first)
86+
and not second.isalnum()
87+
and all(char == second[0] for char in second)
88+
)

commitizen/cli.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,6 @@ def __call__(
160160
{
161161
"name": ["-l", "--message-length-limit"],
162162
"type": int,
163-
"default": 0,
164163
"help": "length limit of the commit message; 0 for no limit",
165164
},
166165
{
@@ -499,7 +498,6 @@ def __call__(
499498
{
500499
"name": ["-l", "--message-length-limit"],
501500
"type": int,
502-
"default": 0,
503501
"help": "length limit of the commit message; 0 for no limit",
504502
},
505503
],
@@ -543,6 +541,18 @@ def __call__(
543541
"action": "store_true",
544542
"exclusive_group": "group1",
545543
},
544+
{
545+
"name": ["--major"],
546+
"help": "get just the major version",
547+
"action": "store_true",
548+
"exclusive_group": "group2",
549+
},
550+
{
551+
"name": ["--minor"],
552+
"help": "get just the minor version",
553+
"action": "store_true",
554+
"exclusive_group": "group2",
555+
},
546556
],
547557
},
548558
],

0 commit comments

Comments
 (0)