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/_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
+ )