comver 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- comver/__init__.py +33 -0
- comver/__main__.py +13 -0
- comver/_cli.py +38 -0
- comver/_parser.py +132 -0
- comver/_regex/__init__.py +17 -0
- comver/_regex/_process.py +41 -0
- comver/_regex/match.py +83 -0
- comver/_regex/semantic.py +97 -0
- comver/_subcommand.py +174 -0
- comver/_version.py +772 -0
- comver/error.py +82 -0
- comver/plugin.py +241 -0
- comver/py.typed +0 -0
- comver/py.typed.license +4 -0
- comver/type_definitions.py +27 -0
- comver-0.2.0.dist-info/METADATA +329 -0
- comver-0.2.0.dist-info/RECORD +19 -0
- comver-0.2.0.dist-info/WHEEL +4 -0
- comver-0.2.0.dist-info/entry_points.txt +5 -0
comver/_version.py
ADDED
|
@@ -0,0 +1,772 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2025 open-nudge <https://github.com/open-nudge>
|
|
2
|
+
# SPDX-FileContributor: szymonmaszke <github@maszke.co>
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
"""Versioning related core functionality of `comver`.
|
|
7
|
+
|
|
8
|
+
This module is responsible for heavy-lifting with respect to:
|
|
9
|
+
|
|
10
|
+
- version calculations based on multiple factors (from `string`s,
|
|
11
|
+
`git` commits etc.)
|
|
12
|
+
- filtering based on user/config provided info (e.g. exclusion of
|
|
13
|
+
specific committer name)
|
|
14
|
+
|
|
15
|
+
Usually `Version.from_git_configured()` is an entrypoint one
|
|
16
|
+
should be interested in.
|
|
17
|
+
|
|
18
|
+
Important:
|
|
19
|
+
Check out guidelines and tutorials for information about CLI/plugin
|
|
20
|
+
usage and suggested configuration. This section should be of interest
|
|
21
|
+
to people wanting to use the API directly (e.g. new integrations).
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import collections
|
|
28
|
+
import dataclasses
|
|
29
|
+
import functools
|
|
30
|
+
import typing
|
|
31
|
+
|
|
32
|
+
import git
|
|
33
|
+
import loadfig
|
|
34
|
+
|
|
35
|
+
from comver import _regex, error
|
|
36
|
+
|
|
37
|
+
if typing.TYPE_CHECKING:
|
|
38
|
+
from collections.abc import Iterable, Iterator
|
|
39
|
+
|
|
40
|
+
from comver.type_definitions import OptionalStringsOrPatterns
|
|
41
|
+
|
|
42
|
+
from importlib.metadata import version
|
|
43
|
+
|
|
44
|
+
_version = version("comver")
|
|
45
|
+
"""Current comver version."""
|
|
46
|
+
|
|
47
|
+
del version
|
|
48
|
+
|
|
49
|
+
T = typing.TypeVar("T")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@functools.total_ordering
|
|
53
|
+
@dataclasses.dataclass(frozen=True)
|
|
54
|
+
class Version:
|
|
55
|
+
"""Immutable class creating and keeping commit-based semantic versioning.
|
|
56
|
+
|
|
57
|
+
Tip:
|
|
58
|
+
This class will likely be instantiated by one of creation
|
|
59
|
+
`classmethod`s, these are ordered by an abstraction level
|
|
60
|
+
and the higher ups are composed of from the ones below them.
|
|
61
|
+
|
|
62
|
+
Warning:
|
|
63
|
+
This class is immutable, generator methods
|
|
64
|
+
__will return new instances__.
|
|
65
|
+
|
|
66
|
+
Attributes:
|
|
67
|
+
major:
|
|
68
|
+
The major version.
|
|
69
|
+
minor:
|
|
70
|
+
The minor version.
|
|
71
|
+
patch:
|
|
72
|
+
The patch version.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
major: int = 0
|
|
76
|
+
minor: int = 0
|
|
77
|
+
patch: int = 0
|
|
78
|
+
|
|
79
|
+
# Add commit message includes and commit message excludes
|
|
80
|
+
@classmethod
|
|
81
|
+
def from_git_configured( # noqa: PLR0913
|
|
82
|
+
cls,
|
|
83
|
+
message_includes: OptionalStringsOrPatterns = None,
|
|
84
|
+
message_excludes: OptionalStringsOrPatterns = None,
|
|
85
|
+
path_includes: OptionalStringsOrPatterns = None,
|
|
86
|
+
path_excludes: OptionalStringsOrPatterns = None,
|
|
87
|
+
author_name_includes: OptionalStringsOrPatterns = None,
|
|
88
|
+
author_name_excludes: OptionalStringsOrPatterns = None,
|
|
89
|
+
author_email_includes: OptionalStringsOrPatterns = None,
|
|
90
|
+
author_email_excludes: OptionalStringsOrPatterns = None,
|
|
91
|
+
major_regexes: OptionalStringsOrPatterns = None,
|
|
92
|
+
minor_regexes: OptionalStringsOrPatterns = None,
|
|
93
|
+
patch_regexes: OptionalStringsOrPatterns = None,
|
|
94
|
+
unrecognized_message: typing.Literal["ignore", "error"] | None = None,
|
|
95
|
+
repository: str | git.Repo | None = None,
|
|
96
|
+
) -> Iterator[VersionCommit]:
|
|
97
|
+
r"""Yield version and its respective commit.
|
|
98
|
+
|
|
99
|
+
Important:
|
|
100
|
+
This `classmethod`'s arguments, if not provided, will be
|
|
101
|
+
inferred from `[tool.comver]` section in `pyproject.toml`
|
|
102
|
+
(see [loadfig](https://github.com/open-nudge/loadfig)
|
|
103
|
+
for more information)
|
|
104
|
+
|
|
105
|
+
Example configuration (`pyproject.toml` in your project's git root):
|
|
106
|
+
|
|
107
|
+
```toml
|
|
108
|
+
[tool.comver]
|
|
109
|
+
# All commits will be included EXCEPT the ones with matching scopes
|
|
110
|
+
# e.g. ("feat: add versioning [no version]")
|
|
111
|
+
# Note: These are raw strings
|
|
112
|
+
message_excludes = [
|
|
113
|
+
".*\[no version\].*",
|
|
114
|
+
".*\[skip version\].*",
|
|
115
|
+
".*\[version skip\].*",
|
|
116
|
+
]
|
|
117
|
+
# Only changes to the src/* folder count or `pyproject.toml`
|
|
118
|
+
# For version calculations
|
|
119
|
+
path_includes = [
|
|
120
|
+
"src/*",
|
|
121
|
+
"pyproject.toml",
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
# Commits done by GitHub bot are excluded from versioning
|
|
125
|
+
author_name_excludes = [
|
|
126
|
+
"github-actions[bot]",
|
|
127
|
+
]
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Example usage:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
import comver
|
|
134
|
+
|
|
135
|
+
# Every value taken from the configuration (if available)
|
|
136
|
+
for output in comver.Version.git_configured():
|
|
137
|
+
print(output.commit.hexsha, output.version)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Commit messages are used to calculate versions based on the
|
|
141
|
+
regexes (`major_regexes`, `minor_regexes` and `patch_regexes`).
|
|
142
|
+
|
|
143
|
+
Tip:
|
|
144
|
+
You can also configure this function via `.comver.toml`
|
|
145
|
+
instead of `pyproject.toml`, in such case
|
|
146
|
+
remove the `[tool.comver]` header, rest stays the same.
|
|
147
|
+
|
|
148
|
+
Warning:
|
|
149
|
+
`*_exclude` regexes take precedence over `*_include` regexes,
|
|
150
|
+
the `*_include` regexes are checked first, then the `*_exclude`
|
|
151
|
+
regexes might disinclude the `*_include` match
|
|
152
|
+
|
|
153
|
+
Warning:
|
|
154
|
+
`author_name`, `author_email` and `path` exclusions
|
|
155
|
+
__will exclude them from output__ (unlike message based
|
|
156
|
+
filtering). If the commit does not match it will not be yielded.
|
|
157
|
+
|
|
158
|
+
Warning:
|
|
159
|
+
Message based filtering will not change the version
|
|
160
|
+
(the same will be returned),
|
|
161
|
+
__but the Version-Commit pair will be returned__.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
message_includes:
|
|
165
|
+
Commit message regexes against which the commit is included.
|
|
166
|
+
Default: From config OR all paths are included.
|
|
167
|
+
message_excludes:
|
|
168
|
+
Commit message regexes against which the commit is excluded.
|
|
169
|
+
Default: From config OR no paths are excluded.
|
|
170
|
+
path_includes:
|
|
171
|
+
Path regexes against which the commit is included.
|
|
172
|
+
Default: From config OR all paths are included.
|
|
173
|
+
path_excludes:
|
|
174
|
+
Path regexes against which the commit is excluded.
|
|
175
|
+
Default: From config OR no paths are excluded.
|
|
176
|
+
author_name_includes:
|
|
177
|
+
Commit author names regexes against
|
|
178
|
+
which the commit is included.
|
|
179
|
+
Default: From config OR all names are included.
|
|
180
|
+
author_name_excludes:
|
|
181
|
+
Commit author names regexes against
|
|
182
|
+
which the commit is excluded.
|
|
183
|
+
Default: From config OR no names are excluded.
|
|
184
|
+
author_email_includes:
|
|
185
|
+
Commit author email regexes against
|
|
186
|
+
which the commit is included.
|
|
187
|
+
Default: From config OR all emails are included.
|
|
188
|
+
author_email_excludes:
|
|
189
|
+
Commit author email regexes against
|
|
190
|
+
which the commit is excluded.
|
|
191
|
+
Default: From config OR no emails are excluded.
|
|
192
|
+
major_regexes:
|
|
193
|
+
The regexes by which the major version is found.
|
|
194
|
+
Default: From config OR matches `feat!:` and `fix!:`
|
|
195
|
+
messages OR `BREAKING CHANGE` anywhere in the message.
|
|
196
|
+
minor_regexes:
|
|
197
|
+
The regex for the minor version.
|
|
198
|
+
Default: From config OR matches messages starting with `feat:`.
|
|
199
|
+
patch_regexes:
|
|
200
|
+
The regex for the patch version.
|
|
201
|
+
Default: From config OR matches messages starting with `fix:`.
|
|
202
|
+
unrecognized_message:
|
|
203
|
+
The behavior for unrecognized messages. It can be
|
|
204
|
+
either "exclude" or "error".
|
|
205
|
+
Default: From config OR "ignore"
|
|
206
|
+
repository:
|
|
207
|
+
The `git` repository.
|
|
208
|
+
Default: From config OR will be searched
|
|
209
|
+
in the parent directories.
|
|
210
|
+
|
|
211
|
+
Yields:
|
|
212
|
+
Version and its respective commit
|
|
213
|
+
|
|
214
|
+
"""
|
|
215
|
+
config = collections.defaultdict(lambda: None, loadfig.config("comver"))
|
|
216
|
+
|
|
217
|
+
yield from cls.from_git(
|
|
218
|
+
message_includes=message_includes or config["message_includes"],
|
|
219
|
+
message_excludes=message_excludes or config["message_excludes"],
|
|
220
|
+
path_includes=path_includes or config["path_includes"],
|
|
221
|
+
path_excludes=path_excludes or config["path_excludes"],
|
|
222
|
+
author_name_includes=author_name_includes
|
|
223
|
+
or config["author_name_includes"],
|
|
224
|
+
author_name_excludes=author_name_excludes
|
|
225
|
+
or config["author_name_excludes"],
|
|
226
|
+
author_email_includes=author_email_includes
|
|
227
|
+
or config["author_email_includes"],
|
|
228
|
+
author_email_excludes=author_email_excludes
|
|
229
|
+
or config["author_email_excludes"],
|
|
230
|
+
major_regexes=major_regexes or config["major_regexes"],
|
|
231
|
+
minor_regexes=minor_regexes or config["minor_regexes"],
|
|
232
|
+
patch_regexes=patch_regexes or config["patch_regexes"],
|
|
233
|
+
unrecognized_message=unrecognized_message
|
|
234
|
+
or config["unrecognized_message"],
|
|
235
|
+
repository=repository,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
@classmethod
|
|
239
|
+
def from_git( # noqa: PLR0913
|
|
240
|
+
cls,
|
|
241
|
+
message_includes: OptionalStringsOrPatterns = None,
|
|
242
|
+
message_excludes: OptionalStringsOrPatterns = None,
|
|
243
|
+
path_includes: OptionalStringsOrPatterns = None,
|
|
244
|
+
path_excludes: OptionalStringsOrPatterns = None,
|
|
245
|
+
author_name_includes: OptionalStringsOrPatterns = None,
|
|
246
|
+
author_name_excludes: OptionalStringsOrPatterns = None,
|
|
247
|
+
author_email_includes: OptionalStringsOrPatterns = None,
|
|
248
|
+
author_email_excludes: OptionalStringsOrPatterns = None,
|
|
249
|
+
major_regexes: OptionalStringsOrPatterns = None,
|
|
250
|
+
minor_regexes: OptionalStringsOrPatterns = None,
|
|
251
|
+
patch_regexes: OptionalStringsOrPatterns = None,
|
|
252
|
+
unrecognized_message: typing.Literal["ignore", "error"] | None = None,
|
|
253
|
+
repository: str | git.Repo | None = None,
|
|
254
|
+
) -> Iterator[VersionCommit]:
|
|
255
|
+
"""Yield version and its respective commit.
|
|
256
|
+
|
|
257
|
+
Commit messages are used to calculate versions based on the
|
|
258
|
+
regexes (`major_regexes`, `minor_regexes` and `patch_regexes`).
|
|
259
|
+
|
|
260
|
+
Example usage:
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
import comver
|
|
264
|
+
|
|
265
|
+
# Will not take commits done by anyone with @foo.com email
|
|
266
|
+
# into the account when creating versions
|
|
267
|
+
for output in comver.Version.from_git(
|
|
268
|
+
author_email_excludes=(r".*@foo.com",)
|
|
269
|
+
):
|
|
270
|
+
print(output.commit.hexsha, output.version)
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Warning:
|
|
274
|
+
`author_name`, `author_email` and `path` exclusions
|
|
275
|
+
__will exclude them from output__ (unlike message based
|
|
276
|
+
filtering). If the commit does not match it will not be yielded.
|
|
277
|
+
|
|
278
|
+
Warning:
|
|
279
|
+
Message based filtering will not change the version
|
|
280
|
+
(the same will be returned),
|
|
281
|
+
__but the Version-Commit pair will be returned__.
|
|
282
|
+
|
|
283
|
+
Warning:
|
|
284
|
+
`*_exclude` regexes take precedence over `*_include` regexes,
|
|
285
|
+
the `*_include` regexes are checked first, then the `*_exclude`
|
|
286
|
+
regexes might disinclude the `*_include` match
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
message_includes:
|
|
290
|
+
Commit message regexes against which the commit is included.
|
|
291
|
+
Default: All paths are included.
|
|
292
|
+
message_excludes:
|
|
293
|
+
Commit message regexes against which the commit is
|
|
294
|
+
excluded. Default: No paths are excluded.
|
|
295
|
+
path_includes:
|
|
296
|
+
Path regexes against which the commit is included.
|
|
297
|
+
Default: All paths are included.
|
|
298
|
+
path_excludes:
|
|
299
|
+
Path regexes against which the commit is excluded.
|
|
300
|
+
Default: No paths are excluded.
|
|
301
|
+
author_name_includes:
|
|
302
|
+
Commit author names regexes against
|
|
303
|
+
which the commit is included.
|
|
304
|
+
Default: All names are included.
|
|
305
|
+
author_name_excludes:
|
|
306
|
+
Commit author names regexes against
|
|
307
|
+
which the commit is excluded.
|
|
308
|
+
Default: No names are excluded.
|
|
309
|
+
author_email_includes:
|
|
310
|
+
Commit author email regexes against
|
|
311
|
+
which the commit is included.
|
|
312
|
+
Default: All emails are included.
|
|
313
|
+
author_email_excludes:
|
|
314
|
+
Commit author email regexes against
|
|
315
|
+
which the commit is excluded.
|
|
316
|
+
Default: No emails are excluded.
|
|
317
|
+
major_regexes:
|
|
318
|
+
The regexes by which the major version is found.
|
|
319
|
+
Default: Commit messages starting with `feat!:` and `fix!:`
|
|
320
|
+
OR `BREAKING CHANGE` anywhere in the message.
|
|
321
|
+
minor_regexes:
|
|
322
|
+
The regex for the minor version.
|
|
323
|
+
Default: Messages starting with `feat:`.
|
|
324
|
+
patch_regexes:
|
|
325
|
+
The regex for the patch version.
|
|
326
|
+
Default: Matches messages starting with `fix:`.
|
|
327
|
+
unrecognized_message:
|
|
328
|
+
The behavior for unrecognized messages. It can be
|
|
329
|
+
either "exclude" or "error".
|
|
330
|
+
Default: "ignore"
|
|
331
|
+
repository:
|
|
332
|
+
The `git` repository.
|
|
333
|
+
Default: Searched in the parent directories.
|
|
334
|
+
|
|
335
|
+
Yields:
|
|
336
|
+
Version and its respective commit
|
|
337
|
+
|
|
338
|
+
"""
|
|
339
|
+
if isinstance(repository, str):
|
|
340
|
+
repository = git.Repo(repository)
|
|
341
|
+
if repository is None:
|
|
342
|
+
repository = git.Repo(search_parent_directories=True)
|
|
343
|
+
|
|
344
|
+
commits = [
|
|
345
|
+
commit
|
|
346
|
+
for commit in repository.iter_commits(reverse=True)
|
|
347
|
+
if _include_commit(
|
|
348
|
+
commit,
|
|
349
|
+
path_includes,
|
|
350
|
+
path_excludes,
|
|
351
|
+
author_name_includes,
|
|
352
|
+
author_name_excludes,
|
|
353
|
+
author_email_includes,
|
|
354
|
+
author_email_excludes,
|
|
355
|
+
)
|
|
356
|
+
]
|
|
357
|
+
|
|
358
|
+
for version, commit in zip(
|
|
359
|
+
cls.from_messages(
|
|
360
|
+
messages=(str(commit.message) for commit in commits),
|
|
361
|
+
message_includes=message_includes,
|
|
362
|
+
message_excludes=message_excludes,
|
|
363
|
+
major_regexes=major_regexes,
|
|
364
|
+
minor_regexes=minor_regexes,
|
|
365
|
+
patch_regexes=patch_regexes,
|
|
366
|
+
unrecognized_message=unrecognized_message,
|
|
367
|
+
),
|
|
368
|
+
commits,
|
|
369
|
+
strict=False,
|
|
370
|
+
):
|
|
371
|
+
yield VersionCommit(version, commit)
|
|
372
|
+
|
|
373
|
+
@classmethod
|
|
374
|
+
def from_messages( # noqa: PLR0913
|
|
375
|
+
cls,
|
|
376
|
+
messages: Iterable[str],
|
|
377
|
+
message_includes: OptionalStringsOrPatterns = None,
|
|
378
|
+
message_excludes: OptionalStringsOrPatterns = None,
|
|
379
|
+
major_regexes: OptionalStringsOrPatterns = None,
|
|
380
|
+
minor_regexes: OptionalStringsOrPatterns = None,
|
|
381
|
+
patch_regexes: OptionalStringsOrPatterns = None,
|
|
382
|
+
unrecognized_message: typing.Literal["ignore", "error"] | None = None,
|
|
383
|
+
) -> Iterator[Version]:
|
|
384
|
+
"""Yield versions from an iterable of messages.
|
|
385
|
+
|
|
386
|
+
Warning:
|
|
387
|
+
Every message will be returned, even if it is excluded
|
|
388
|
+
(e.g. by `message_excludes` patterns). In such case
|
|
389
|
+
the version __will not change__ (the same, previous one,
|
|
390
|
+
is returned).
|
|
391
|
+
|
|
392
|
+
Warning:
|
|
393
|
+
`*_exclude` regexes take precedence over `*_include` regexes,
|
|
394
|
+
the `*_include` regexes are checked first, then the `*_exclude`
|
|
395
|
+
regexes might disinclude the `*_include` match
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
messages:
|
|
399
|
+
Iterable containing messages from which versions
|
|
400
|
+
are calculated.
|
|
401
|
+
message_includes:
|
|
402
|
+
Commit message regexes against which the commit is included.
|
|
403
|
+
Default: All paths are included.
|
|
404
|
+
message_excludes:
|
|
405
|
+
Commit message regexes against which the commit is
|
|
406
|
+
excluded. Default: No paths are excluded.
|
|
407
|
+
major_regexes:
|
|
408
|
+
The regexes by which the major version is found.
|
|
409
|
+
Default: Commit messages starting with `feat!:` and `fix!:`
|
|
410
|
+
OR `BREAKING CHANGE` anywhere in the message.
|
|
411
|
+
minor_regexes:
|
|
412
|
+
The regex for the minor version.
|
|
413
|
+
Default: Messages starting with `feat:`.
|
|
414
|
+
patch_regexes:
|
|
415
|
+
The regex for the patch version.
|
|
416
|
+
Default: Matches messages starting with `fix:`.
|
|
417
|
+
unrecognized_message:
|
|
418
|
+
The behavior for unrecognized messages. It can be
|
|
419
|
+
either "exclude" or "error".
|
|
420
|
+
Default: "ignore"
|
|
421
|
+
|
|
422
|
+
Yields:
|
|
423
|
+
Version (one for each message).
|
|
424
|
+
"""
|
|
425
|
+
version = None
|
|
426
|
+
|
|
427
|
+
for message in messages:
|
|
428
|
+
version = cls.from_message(
|
|
429
|
+
message,
|
|
430
|
+
message_includes,
|
|
431
|
+
message_excludes,
|
|
432
|
+
major_regexes,
|
|
433
|
+
minor_regexes,
|
|
434
|
+
patch_regexes,
|
|
435
|
+
unrecognized_message,
|
|
436
|
+
version=version,
|
|
437
|
+
)
|
|
438
|
+
yield version
|
|
439
|
+
|
|
440
|
+
@classmethod
|
|
441
|
+
def from_message( # noqa: PLR0913
|
|
442
|
+
cls,
|
|
443
|
+
message: str,
|
|
444
|
+
message_includes: OptionalStringsOrPatterns = None,
|
|
445
|
+
message_excludes: OptionalStringsOrPatterns = None,
|
|
446
|
+
major_regexes: OptionalStringsOrPatterns = None,
|
|
447
|
+
minor_regexes: OptionalStringsOrPatterns = None,
|
|
448
|
+
patch_regexes: OptionalStringsOrPatterns = None,
|
|
449
|
+
unrecognized_message: typing.Literal["ignore", "error"] | None = None,
|
|
450
|
+
version: Version | None = None,
|
|
451
|
+
) -> Version:
|
|
452
|
+
"""Bump the version based on a message.
|
|
453
|
+
|
|
454
|
+
Warning:
|
|
455
|
+
The message will be returned, even if it should be excluded
|
|
456
|
+
(e.g. by `message_excludes` patterns). In such case
|
|
457
|
+
the version __will not change__ (the same, previous one,
|
|
458
|
+
is returned).
|
|
459
|
+
|
|
460
|
+
Args:
|
|
461
|
+
message:
|
|
462
|
+
Message from which the version is calculated.
|
|
463
|
+
message_includes:
|
|
464
|
+
Commit message regexes against which the commit is included.
|
|
465
|
+
Default: All paths are included.
|
|
466
|
+
message_excludes:
|
|
467
|
+
Commit message regexes against which the commit is
|
|
468
|
+
excluded. Default: No paths are excluded.
|
|
469
|
+
major_regexes:
|
|
470
|
+
The regexes by which the major version is found.
|
|
471
|
+
Default: Commit messages starting with `feat!:` and `fix!:`
|
|
472
|
+
OR `BREAKING CHANGE` anywhere in the message.
|
|
473
|
+
minor_regexes:
|
|
474
|
+
The regex for the minor version.
|
|
475
|
+
Default: Messages starting with `feat:`.
|
|
476
|
+
patch_regexes:
|
|
477
|
+
The regex for the patch version.
|
|
478
|
+
Default: Matches messages starting with `fix:`.
|
|
479
|
+
unrecognized_message:
|
|
480
|
+
The behavior for unrecognized messages. It can be
|
|
481
|
+
either "exclude" or "error".
|
|
482
|
+
Default: "ignore"
|
|
483
|
+
version:
|
|
484
|
+
Starting version from which a new version is calculated
|
|
485
|
+
(version from which to bump). Default: `0.0.0` version.
|
|
486
|
+
|
|
487
|
+
Raises:
|
|
488
|
+
MessageUnrecognizedError: If the message is not recognized
|
|
489
|
+
by any of the regexes and `unrecognized_message`
|
|
490
|
+
is set to "error".
|
|
491
|
+
|
|
492
|
+
Returns:
|
|
493
|
+
Version corresponding to the message (possibly starting from
|
|
494
|
+
initial version provided).
|
|
495
|
+
"""
|
|
496
|
+
version = cls() if version is None else version
|
|
497
|
+
|
|
498
|
+
if not _regex.match.item(
|
|
499
|
+
what=message,
|
|
500
|
+
include=_regex.process(message_includes),
|
|
501
|
+
exclude=_regex.process(message_excludes),
|
|
502
|
+
):
|
|
503
|
+
return version
|
|
504
|
+
|
|
505
|
+
for semantic_component, regex in _regex.semantic.components(
|
|
506
|
+
major_regexes, minor_regexes, patch_regexes
|
|
507
|
+
):
|
|
508
|
+
if regex is not None and regex.match(message):
|
|
509
|
+
return getattr(version, f"bump_{semantic_component}")()
|
|
510
|
+
|
|
511
|
+
if unrecognized_message == "error":
|
|
512
|
+
raise error.MessageUnrecognizedError(message)
|
|
513
|
+
|
|
514
|
+
# Based on hypothesis testing this line may not run
|
|
515
|
+
return version # pragma: no cover
|
|
516
|
+
|
|
517
|
+
@classmethod
|
|
518
|
+
def from_string(cls, version: str) -> Version:
|
|
519
|
+
"""Create a new version from a string.
|
|
520
|
+
|
|
521
|
+
Version should be provided in `MAJOR.MINOR.PATCH` format.
|
|
522
|
+
|
|
523
|
+
Args:
|
|
524
|
+
version:
|
|
525
|
+
The version as a string.
|
|
526
|
+
|
|
527
|
+
Raises:
|
|
528
|
+
VersionFormatError:
|
|
529
|
+
When `version` does not comprise of `3` elements
|
|
530
|
+
(e.g. `1.3` instead of `1.3.0`)
|
|
531
|
+
VersionNotNumericError:
|
|
532
|
+
When `version` elements are not numeric
|
|
533
|
+
(e.g. `1.3.2a1` instead of `1.3.2`)
|
|
534
|
+
|
|
535
|
+
Returns:
|
|
536
|
+
Version object
|
|
537
|
+
"""
|
|
538
|
+
try:
|
|
539
|
+
major, minor, patch = version.split(".")
|
|
540
|
+
except ValueError as e:
|
|
541
|
+
raise error.VersionFormatError(version) from e
|
|
542
|
+
try:
|
|
543
|
+
return cls(
|
|
544
|
+
major=int(major),
|
|
545
|
+
minor=int(minor),
|
|
546
|
+
patch=int(patch),
|
|
547
|
+
)
|
|
548
|
+
except ValueError as e:
|
|
549
|
+
raise error.VersionNotNumericError(version) from e
|
|
550
|
+
|
|
551
|
+
def bump_major(self) -> Version:
|
|
552
|
+
"""Bump the major version.
|
|
553
|
+
|
|
554
|
+
Returns:
|
|
555
|
+
Version with major bumped and the rest zeroed out.
|
|
556
|
+
|
|
557
|
+
"""
|
|
558
|
+
return type(self)(1 + self.major, 0, 0)
|
|
559
|
+
|
|
560
|
+
def bump_minor(self) -> Version:
|
|
561
|
+
"""Bump the minor version.
|
|
562
|
+
|
|
563
|
+
Returns:
|
|
564
|
+
Version with minor bumped and the patch zeroed out.
|
|
565
|
+
|
|
566
|
+
"""
|
|
567
|
+
return type(self)(self.major, 1 + self.minor, 0)
|
|
568
|
+
|
|
569
|
+
def bump_patch(self) -> Version:
|
|
570
|
+
"""Bump the patch version.
|
|
571
|
+
|
|
572
|
+
Returns:
|
|
573
|
+
Version with patch bumped.
|
|
574
|
+
|
|
575
|
+
"""
|
|
576
|
+
return type(self)(self.major, self.minor, 1 + self.patch)
|
|
577
|
+
|
|
578
|
+
def __str__(self) -> str: # pyright: ignore [reportImplicitOverride]
|
|
579
|
+
"""Return the version as a string.
|
|
580
|
+
|
|
581
|
+
Returns:
|
|
582
|
+
String representation of version (e.g. `"1.37.21"`).
|
|
583
|
+
|
|
584
|
+
"""
|
|
585
|
+
return f"{self.major}.{self.minor}.{self.patch}"
|
|
586
|
+
|
|
587
|
+
def __hash__(self) -> int: # pyright: ignore [reportImplicitOverride]
|
|
588
|
+
"""Unique hash of the version.
|
|
589
|
+
|
|
590
|
+
Returns:
|
|
591
|
+
Hashed major, minor and patch
|
|
592
|
+
"""
|
|
593
|
+
return hash((self.major, self.minor, self.patch))
|
|
594
|
+
|
|
595
|
+
def __eq__(self, other: object) -> bool: # pyright: ignore [reportImplicitOverride]
|
|
596
|
+
"""Check if two versions are equal.
|
|
597
|
+
|
|
598
|
+
Important:
|
|
599
|
+
Versions are equal when their `major`, `minor`
|
|
600
|
+
and `patch` are equal.
|
|
601
|
+
|
|
602
|
+
Args:
|
|
603
|
+
other:
|
|
604
|
+
Object to compare against. Should be
|
|
605
|
+
an instance of `Version` or string
|
|
606
|
+
(e.g. `"2.3.7"`)
|
|
607
|
+
|
|
608
|
+
Raises:
|
|
609
|
+
NotImplementedError:
|
|
610
|
+
Raised when comparing to object which is neither
|
|
611
|
+
of `str`, nor `Version` type.
|
|
612
|
+
|
|
613
|
+
Returns:
|
|
614
|
+
Whether the object is equal
|
|
615
|
+
|
|
616
|
+
"""
|
|
617
|
+
other = self._cast(other)
|
|
618
|
+
|
|
619
|
+
return (
|
|
620
|
+
self.major == other.major
|
|
621
|
+
and self.minor == other.minor
|
|
622
|
+
and self.patch == other.patch
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
def __lt__(self, other: object) -> bool:
|
|
626
|
+
"""Check if this version is smaller than `other`.
|
|
627
|
+
|
|
628
|
+
Important:
|
|
629
|
+
Version is smaller if its `major` is smaller
|
|
630
|
+
or `fix` is smaller (and `major` equal) or `patch` is smaller
|
|
631
|
+
(and `major` and `fix` equal)
|
|
632
|
+
|
|
633
|
+
Args:
|
|
634
|
+
other:
|
|
635
|
+
Object to compare against. Should be
|
|
636
|
+
an instance of `Version` or string
|
|
637
|
+
(e.g. `"2.3.7"`).
|
|
638
|
+
|
|
639
|
+
Raises:
|
|
640
|
+
NotImplementedError:
|
|
641
|
+
Raised when comparing to object which is neither
|
|
642
|
+
of `str`, nor `Version` type.
|
|
643
|
+
|
|
644
|
+
Returns:
|
|
645
|
+
Whether the object is smaller
|
|
646
|
+
|
|
647
|
+
"""
|
|
648
|
+
other = self._cast(other)
|
|
649
|
+
|
|
650
|
+
return (
|
|
651
|
+
self.major < other.major
|
|
652
|
+
or (self.major == other.major and self.minor < other.minor)
|
|
653
|
+
or (
|
|
654
|
+
self.major == other.major
|
|
655
|
+
and self.minor == other.minor
|
|
656
|
+
and self.patch < other.patch
|
|
657
|
+
)
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
def _cast(self, other: typing.Any) -> Version:
|
|
661
|
+
"""Cast `other` to `Version` (if possible).
|
|
662
|
+
|
|
663
|
+
Raises:
|
|
664
|
+
NotImplementedError:
|
|
665
|
+
Raised when comparing to object which is neither
|
|
666
|
+
of `str`, nor `Version` type.
|
|
667
|
+
|
|
668
|
+
Returns:
|
|
669
|
+
Casted version
|
|
670
|
+
|
|
671
|
+
"""
|
|
672
|
+
if isinstance(other, str):
|
|
673
|
+
return type(self).from_string(other)
|
|
674
|
+
if not isinstance(other, Version):
|
|
675
|
+
error = "Comparison of Version only works for strings (e.g. `2.37.1`) and other Version objects"
|
|
676
|
+
raise NotImplementedError(error)
|
|
677
|
+
|
|
678
|
+
return other
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
@dataclasses.dataclass(frozen=True)
|
|
682
|
+
class VersionCommit:
|
|
683
|
+
"""POD containing `Version` and its respective `git.Commit`.
|
|
684
|
+
|
|
685
|
+
This container is returned from `git` related functionalities
|
|
686
|
+
of `Version`.
|
|
687
|
+
"""
|
|
688
|
+
|
|
689
|
+
version: Version = Version(0, 0, 0)
|
|
690
|
+
commit: git.Commit | None = None
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def _include_commit( # noqa: PLR0913
|
|
694
|
+
commit: git.Commit,
|
|
695
|
+
path_includes: OptionalStringsOrPatterns = None,
|
|
696
|
+
path_excludes: OptionalStringsOrPatterns = None,
|
|
697
|
+
author_name_includes: OptionalStringsOrPatterns = None,
|
|
698
|
+
author_name_excludes: OptionalStringsOrPatterns = None,
|
|
699
|
+
author_email_includes: OptionalStringsOrPatterns = None,
|
|
700
|
+
author_email_excludes: OptionalStringsOrPatterns = None,
|
|
701
|
+
) -> bool:
|
|
702
|
+
"""Check whether to include a given commit.
|
|
703
|
+
|
|
704
|
+
Args:
|
|
705
|
+
commit:
|
|
706
|
+
Commit to verify.
|
|
707
|
+
path_includes:
|
|
708
|
+
Path regexes against which the commit is included.
|
|
709
|
+
Default: All paths are included.
|
|
710
|
+
path_excludes:
|
|
711
|
+
Path regexes against which the commit is excluded.
|
|
712
|
+
Default: No paths are excluded.
|
|
713
|
+
author_name_includes:
|
|
714
|
+
Commit author names regexes against
|
|
715
|
+
which the commit is included.
|
|
716
|
+
Default: All names are included.
|
|
717
|
+
author_name_excludes:
|
|
718
|
+
Commit author names regexes against
|
|
719
|
+
which the commit is excluded.
|
|
720
|
+
Default: No names are excluded.
|
|
721
|
+
author_email_includes:
|
|
722
|
+
Commit author email regexes against
|
|
723
|
+
which the commit is included.
|
|
724
|
+
Default: All emails are included.
|
|
725
|
+
author_email_excludes:
|
|
726
|
+
Commit author email regexes against
|
|
727
|
+
which the commit is excluded.
|
|
728
|
+
Default: No emails are excluded.
|
|
729
|
+
|
|
730
|
+
"""
|
|
731
|
+
return (
|
|
732
|
+
_maybe_match(
|
|
733
|
+
commit.author.name, author_name_includes, author_name_excludes
|
|
734
|
+
)
|
|
735
|
+
and _maybe_match(
|
|
736
|
+
commit.author.email, author_email_includes, author_email_excludes
|
|
737
|
+
)
|
|
738
|
+
and _regex.match.path(
|
|
739
|
+
commit, _regex.process(path_includes), _regex.process(path_excludes)
|
|
740
|
+
)
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
def _maybe_match(
|
|
745
|
+
variable: str | None,
|
|
746
|
+
includes: OptionalStringsOrPatterns,
|
|
747
|
+
excludes: OptionalStringsOrPatterns,
|
|
748
|
+
) -> bool:
|
|
749
|
+
"""Optionally match variable against includes and excludes.
|
|
750
|
+
|
|
751
|
+
Important:
|
|
752
|
+
`None` is considered as matching.
|
|
753
|
+
|
|
754
|
+
Args:
|
|
755
|
+
variable:
|
|
756
|
+
Variable to be matched
|
|
757
|
+
includes:
|
|
758
|
+
Optional list of includes
|
|
759
|
+
excludes:
|
|
760
|
+
Optional list of excludes
|
|
761
|
+
|
|
762
|
+
Returns:
|
|
763
|
+
`True` if the variable matches the constraints.
|
|
764
|
+
|
|
765
|
+
"""
|
|
766
|
+
# Escape hatch if commit's author name or email is missing
|
|
767
|
+
# This situation is highly unlikely to happen
|
|
768
|
+
if variable is None: # pragma: no cover
|
|
769
|
+
return True
|
|
770
|
+
return _regex.match.item(
|
|
771
|
+
variable, _regex.process(includes), _regex.process(excludes)
|
|
772
|
+
)
|