python-semantic-release 9.15.0__py3-none-any.whl → 9.15.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-semantic-release
3
- Version: 9.15.0
3
+ Version: 9.15.2
4
4
  Summary: Automatic Semantic Versioning for Python projects
5
5
  Author-email: Rolf Erik Lekang <me@rolflekang.com>
6
6
  License: MIT
@@ -1,20 +1,20 @@
1
- semantic_release/__init__.py,sha256=Q0ZC22VGUXA_6IkFcuHHN5K6oxt97OYKhhUKMhX-6Ok,1229
2
- semantic_release/__main__.py,sha256=kuotDU7aFKrCBeAJUPWrbIxgJWAmrXUMnztCqWMDMPY,1292
1
+ semantic_release/__init__.py,sha256=PaDXUxHfhYwLilGM9c0vX8F6Jooc8hMhPm3CL8UIX0M,1229
2
+ semantic_release/__main__.py,sha256=KOIBOvLruqfi5ArXcWK3ucIZ7NB55kfCbycJaxx6aQg,1485
3
3
  semantic_release/const.py,sha256=Z1o2QNh60wSLeF-_1TemMBjU3ZXbV0XghnUFsbTVfOs,831
4
- semantic_release/enums.py,sha256=D5B_reQGGKQQT22HO5PUtvn2Bok3fkht6TfJtXkmAUg,1020
4
+ semantic_release/enums.py,sha256=vrEw1UNRcNrFjPqOFnuUzfeoqKj0ChixVVlyk5fqbng,1744
5
5
  semantic_release/errors.py,sha256=PY9rmviSFBZkqawW6VXbUfmF9C_RNOIObcmeGxLefMo,2904
6
- semantic_release/gitproject.py,sha256=XR7DfLxLZudEtfFnqnzvCDTVY8vC7TFRM0JyBJOkS5w,8584
6
+ semantic_release/gitproject.py,sha256=G4XrucN-ZwT1Kj4RMrABcr1vWb0bjKgurEeJjcL-61c,9422
7
7
  semantic_release/globals.py,sha256=imI9WKGa6MS2pTRAZiWZ2qIJup2eWnBz3OZmIj2YIHM,158
8
8
  semantic_release/helpers.py,sha256=d1jOX0SNyqPc_3wr14xR25FfpqhMd4Ev7MNBOWlScc0,5581
9
9
  semantic_release/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  semantic_release/changelog/__init__.py,sha256=Bg6Xe5Vt32rWoMscW-hd4sUwiZqzWmsg4CD1EhMesMY,262
11
11
  semantic_release/changelog/context.py,sha256=jyVluJq8Vu6TyyzQQrsBIQRKm7kEnh1GZt8ObwibR5k,5374
12
12
  semantic_release/changelog/release_history.py,sha256=nbd-WYVfQSWN4SKPPWEGU6QgNZLBrNxKKxfhPGKqnKc,8499
13
- semantic_release/changelog/template.py,sha256=ZgIWznm_Ik8jgLtCGxPv605TnSRTtFGyMvvEZRbe-Zk,5705
13
+ semantic_release/changelog/template.py,sha256=O4EKXVJtN1z6FowcRUiZdZmi9u_TsTiXcHmYJnGyt94,5721
14
14
  semantic_release/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
15
  semantic_release/cli/changelog_writer.py,sha256=RupwYqApOeAMidIvjCttnyyGNRxtLftmxDBmEu5azX0,9049
16
16
  semantic_release/cli/cli_context.py,sha256=Nop71LdVCJOeSUHgTXunMyK3xAu_QKQC2cRp1QBVkX0,4134
17
- semantic_release/cli/config.py,sha256=1PStwoY4OdWggD6MkCWLxoEqCzy18Jd84DTNh7FT0zk,30907
17
+ semantic_release/cli/config.py,sha256=kb8r20PJ-oEJIS69zOQotbHcDpppsRuqwuazA7gQl0Y,30976
18
18
  semantic_release/cli/const.py,sha256=h7XE2D0D__TAZSrUUtVszwvzpkHTMOiQCf97XQNbEvA,163
19
19
  semantic_release/cli/github_actions_output.py,sha256=6oNwjnQBg9XF5QgGc4TgbwX_-W0aj65VwGSL4ALvqVg,2296
20
20
  semantic_release/cli/masking_filter.py,sha256=DxqjiJyABlzwwwZ1r8JGQpb6QrF00StJFm0-2-s5Fv0,3071
@@ -22,23 +22,23 @@ semantic_release/cli/util.py,sha256=FyXaBkeL7nXKjy3X9rQLEwvn7p46xPekp2V8Z-5MVrk,
22
22
  semantic_release/cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
23
  semantic_release/cli/commands/changelog.py,sha256=w_sXuFJHIqcueBQdeNeWn6fqbLVPPl_c-dEhv3Pb_BA,3870
24
24
  semantic_release/cli/commands/generate_config.py,sha256=2xZOu3NpyhBp0pWr7d8ugKl_kjqQgpSsSMHq5wHTfrE,1699
25
- semantic_release/cli/commands/main.py,sha256=wHG51tbaAHKra21nRe3-FAfQT81-e5PGhv0KnerS15c,4100
25
+ semantic_release/cli/commands/main.py,sha256=237rn_Od4LOWfjUjiUKI_jSV820MfcCtRpwPjxjLbyU,4312
26
26
  semantic_release/cli/commands/publish.py,sha256=y_LalPti_kZeQJzl2CR2pTZUK8DCMvNSTe4NaMC5TJA,2875
27
- semantic_release/cli/commands/version.py,sha256=dsPVyKELn_GSoNeX0YVVvLKDPmWbEpLXgxWyObKIl20,24170
27
+ semantic_release/cli/commands/version.py,sha256=vC4myixs_w8UPj58ZqJmmochl8RWUBmCbseoOEnVmD8,24220
28
28
  semantic_release/commit_parser/__init__.py,sha256=cv5HFBdw7OJd4Laj4Ex8ZZ5Tml8GwXgQcXW6Pasr2Ao,615
29
29
  semantic_release/commit_parser/_base.py,sha256=LAscBtS3_28jebRCeR-eGo3UtAsuxCWBzgb7FF4n4Vo,3046
30
- semantic_release/commit_parser/angular.py,sha256=1lTU73k-DXiMHHdQ8iNS4L2cPnns49KFq6BNc8CiCZo,10441
31
- semantic_release/commit_parser/emoji.py,sha256=zkHOGfupnmO3EAILZM4yqEkHgYLrXzn8mcdHnB0V4S4,9974
30
+ semantic_release/commit_parser/angular.py,sha256=zDYYOK1itsYJ0Ar7-cf29MnfrEpbQTeQCAcExWPH1fM,10486
31
+ semantic_release/commit_parser/emoji.py,sha256=6HtvKvJZwPAvY2fNOXU_gpKf1rGrMiDd0rYamyrdQZ8,10002
32
32
  semantic_release/commit_parser/scipy.py,sha256=Fm_6WUaliLmqD397uVXwpOSDZ7LpMFu59oz-inKeHko,4526
33
33
  semantic_release/commit_parser/tag.py,sha256=4uwIKBqUM2SE6UTGIw-a7B6Jg1OONXmGwXsTyL3yZBA,3490
34
34
  semantic_release/commit_parser/token.py,sha256=RXdoCmKMTKMmUJGEUvfCiAOCPRrV0WubXojwa6FbvFg,7363
35
- semantic_release/commit_parser/util.py,sha256=wsormGXONAIFip4AUUYveY_UwnnUFNqItLA7nRGYLjk,2255
35
+ semantic_release/commit_parser/util.py,sha256=KmJ-M-CJbc7q37GG7o595SmeBwrRrrnjP4q1C05jGYI,2463
36
36
  semantic_release/data/templates/angular/md/.release_notes.md.j2,sha256=BzpatS7WYBcpHii8qiDemyI1ygXM6Q1nbxdcdcps__U,2107
37
37
  semantic_release/data/templates/angular/md/CHANGELOG.md.j2,sha256=FZmrQ-qOIoSoJmAa_NFaRelfmqUpypU2xlDeScdGOf4,729
38
38
  semantic_release/data/templates/angular/md/.components/changelog_header.md.j2,sha256=qNxTuSr59CV_yyimVU_RYp5azCnK0l6nJ03Zf0u5Ugg,166
39
39
  semantic_release/data/templates/angular/md/.components/changelog_init.md.j2,sha256=MyEQdWUemGXKWDhvpTmubZdozd3iaLECZAI1VRlE7mg,862
40
40
  semantic_release/data/templates/angular/md/.components/changelog_update.md.j2,sha256=uVF4wbbjTMvl6Kbsq9xy3YIrj-uhBnHylEfA7S76aHI,2606
41
- semantic_release/data/templates/angular/md/.components/changes.md.j2,sha256=85LrnuHZhwKfdWhtqg_en6tl5UU2MzZnIJKb79j59Og,3996
41
+ semantic_release/data/templates/angular/md/.components/changes.md.j2,sha256=AnF6sEsdaOeCS6E0_Q1PS430jufC1wg4ZMIJdMuBuNI,4000
42
42
  semantic_release/data/templates/angular/md/.components/first_release.md.j2,sha256=jMUZiLwhMqAOjdYOCphJxJr8C411cKRzhsabyebj_AY,162
43
43
  semantic_release/data/templates/angular/md/.components/macros.md.j2,sha256=GEql_lFHWqtfZUMYo8WT3E_YOTf0gvGhjqRQw0c7mlk,5890
44
44
  semantic_release/data/templates/angular/md/.components/unreleased_changes.md.j2,sha256=HRLj6cyRfPZXC0s-0Av6s0Gp3jKxWg9AIEtIXBVqJuY,177
@@ -62,14 +62,14 @@ semantic_release/hvcs/remote_hvcs_base.py,sha256=cV8qYHtP47bmfIZqV4K2EiMHskFEoIo
62
62
  semantic_release/hvcs/token_auth.py,sha256=ZjT56-NIPB4OKIt1qwHCu1TavXnrWFIBl9ARlg56hgU,663
63
63
  semantic_release/hvcs/util.py,sha256=guxisysY_IW5tv7aaV-iVPEVJzgbOs375kiRRpSquTI,2879
64
64
  semantic_release/version/__init__.py,sha256=CLhtGQry9dLIij5XyRa9ZevxU_1p8tjMTSQ-K_GMpWM,270
65
- semantic_release/version/algorithm.py,sha256=XI9nKhmcS6T62b5kRRna9CZl7gTRw98hdetFT31fe_o,16890
65
+ semantic_release/version/algorithm.py,sha256=UiJch0cpMxbuL-K_yaah8VF3PVy8mdwj1K8hArcwvdo,15183
66
66
  semantic_release/version/declaration.py,sha256=f6Ld7hIhrqvDrRBapJHr-KDimuyo-4IG8009Zu9BIgU,7357
67
67
  semantic_release/version/translator.py,sha256=P1noIsVBn8u6zNOFjG0xKYOWapxqf_PHSMvMeLJ9kXg,3050
68
68
  semantic_release/version/version.py,sha256=6PCtSbLP88U1daoxnCwHc--YguZo4waGNLqJ5JfeczE,14175
69
- python_semantic_release-9.15.0.dist-info/AUTHORS.rst,sha256=XOReVvpymEFUPsS2QPH97jlfJBVrxwS2eu8-jVAe4gk,230
70
- python_semantic_release-9.15.0.dist-info/LICENSE,sha256=NE85nszX252sdQdu0xgS9qwfYES0k8qS6gW3uO4jRGE,1083
71
- python_semantic_release-9.15.0.dist-info/METADATA,sha256=i-z285QdMbctU1GapnRvsTz0o1HQPqtDIOiqdbx6_GM,3812
72
- python_semantic_release-9.15.0.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
73
- python_semantic_release-9.15.0.dist-info/entry_points.txt,sha256=r2Jql3GTQyugQnvf34l2eXk1O_Qx6llR_xixG1ZWgD0,105
74
- python_semantic_release-9.15.0.dist-info/top_level.txt,sha256=qYA24nyg3eP-ti5UW7Vuj2aXVmM0wqVHx4mREdRZNAA,17
75
- python_semantic_release-9.15.0.dist-info/RECORD,,
69
+ python_semantic_release-9.15.2.dist-info/AUTHORS.rst,sha256=XOReVvpymEFUPsS2QPH97jlfJBVrxwS2eu8-jVAe4gk,230
70
+ python_semantic_release-9.15.2.dist-info/LICENSE,sha256=NE85nszX252sdQdu0xgS9qwfYES0k8qS6gW3uO4jRGE,1083
71
+ python_semantic_release-9.15.2.dist-info/METADATA,sha256=yD3qvMg3m175Z40HU3Gj5P8Z7z9Z8q4jhcwX3KS3JNM,3812
72
+ python_semantic_release-9.15.2.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
73
+ python_semantic_release-9.15.2.dist-info/entry_points.txt,sha256=r2Jql3GTQyugQnvf34l2eXk1O_Qx6llR_xixG1ZWgD0,105
74
+ python_semantic_release-9.15.2.dist-info/top_level.txt,sha256=qYA24nyg3eP-ti5UW7Vuj2aXVmM0wqVHx4mREdRZNAA,17
75
+ python_semantic_release-9.15.2.dist-info/RECORD,,
@@ -24,7 +24,7 @@ from semantic_release.version import (
24
24
  tags_and_versions,
25
25
  )
26
26
 
27
- __version__ = "9.15.0"
27
+ __version__ = "9.15.2"
28
28
 
29
29
  __all__ = [
30
30
  "CommitParser",
@@ -34,7 +34,15 @@ def main() -> None:
34
34
  ),
35
35
  file=sys.stderr,
36
36
  )
37
+
37
38
  print(f"::ERROR:: {err}", file=sys.stderr)
39
+
40
+ if not globals.debug:
41
+ print(
42
+ "Run semantic-release in very verbose mode (-vv) to see the full traceback.",
43
+ file=sys.stderr,
44
+ )
45
+
38
46
  sys.exit(1)
39
47
 
40
48
 
@@ -124,9 +124,9 @@ def recursive_render(
124
124
  # is used for inserting into a current changelog. When using stream rendering
125
125
  # of the same file, it always came back empty
126
126
  log.debug("rendering %s to %s", src_file_path, output_file_path)
127
- rendered_file = environment.get_template(src_file_path).render()
127
+ rendered_file = environment.get_template(src_file_path).render().rstrip()
128
128
  with open(output_file_path, "w", encoding="utf-8") as output_file:
129
- output_file.write(rendered_file)
129
+ output_file.write(f"{rendered_file}\n")
130
130
 
131
131
  rendered_paths.append(output_file_path)
132
132
  else:
@@ -15,6 +15,7 @@ from semantic_release.cli.cli_context import CliContextObj
15
15
  from semantic_release.cli.config import GlobalCommandLineOptions
16
16
  from semantic_release.cli.const import DEFAULT_CONFIG_FILE
17
17
  from semantic_release.cli.util import rprint
18
+ from semantic_release.enums import SemanticReleaseLogLevels
18
19
 
19
20
  # if TYPE_CHECKING:
20
21
  # pass
@@ -108,7 +109,15 @@ def main(
108
109
  """
109
110
  console = Console(stderr=True)
110
111
 
111
- log_level = [logging.WARNING, logging.INFO, logging.DEBUG][verbosity]
112
+ log_levels = [
113
+ SemanticReleaseLogLevels.WARNING,
114
+ SemanticReleaseLogLevels.INFO,
115
+ SemanticReleaseLogLevels.DEBUG,
116
+ SemanticReleaseLogLevels.SILLY,
117
+ ]
118
+
119
+ log_level = log_levels[verbosity]
120
+
112
121
  logging.basicConfig(
113
122
  level=log_level,
114
123
  format=FORMAT,
@@ -123,7 +132,7 @@ def main(
123
132
  logger = logging.getLogger(__name__)
124
133
  logger.debug("logging level set to: %s", logging.getLevelName(log_level))
125
134
 
126
- if log_level == logging.DEBUG:
135
+ if log_level <= logging.DEBUG:
127
136
  globals.debug = True
128
137
 
129
138
  if noop:
@@ -653,6 +653,7 @@ def version( # noqa: C901
653
653
  project.git_tag(
654
654
  tag_name=new_version.as_tag(),
655
655
  message=new_version.as_tag(),
656
+ isotimestamp=commit_date.isoformat(),
656
657
  noop=opts.noop,
657
658
  )
658
659
 
@@ -360,9 +360,12 @@ class RawConfig(BaseModel):
360
360
  try:
361
361
  # Check for repository & walk up parent directories
362
362
  with Repo(str(dir_path), search_parent_directories=True) as git_repo:
363
- found_path = Path(
364
- git_repo.working_tree_dir or git_repo.working_dir
365
- ).absolute()
363
+ found_path = (
364
+ Path(git_repo.working_tree_dir or git_repo.working_dir)
365
+ .expanduser()
366
+ .absolute()
367
+ )
368
+
366
369
  except InvalidGitRepositoryError as err:
367
370
  raise InvalidGitRepositoryError("No valid git repository found!") from err
368
371
 
@@ -370,7 +373,8 @@ class RawConfig(BaseModel):
370
373
  logging.warning(
371
374
  "Found .git/ in higher parent directory rather than provided in configuration."
372
375
  )
373
- return found_path
376
+
377
+ return found_path.resolve()
374
378
 
375
379
  @field_validator("commit_parser", mode="after")
376
380
  @classmethod
@@ -21,7 +21,11 @@ from semantic_release.commit_parser.token import (
21
21
  ParseError,
22
22
  ParseResult,
23
23
  )
24
- from semantic_release.commit_parser.util import breaking_re, parse_paragraphs
24
+ from semantic_release.commit_parser.util import (
25
+ breaking_re,
26
+ parse_paragraphs,
27
+ sort_numerically,
28
+ )
25
29
  from semantic_release.enums import LevelBump
26
30
  from semantic_release.errors import InvalidParserOptions
27
31
 
@@ -195,7 +199,7 @@ class AngularCommitParser(CommitParser[ParseResult, AngularParserOptions]):
195
199
  predicate.split(","),
196
200
  )
197
201
  )
198
- accumulator["linked_issues"] = sorted(
202
+ accumulator["linked_issues"] = sort_numerically(
199
203
  set(accumulator["linked_issues"]).union(new_issue_refs)
200
204
  )
201
205
  # TODO: breaking change v10, removes resolution footers from descriptions
@@ -18,7 +18,7 @@ from semantic_release.commit_parser.token import (
18
18
  ParsedMessageResult,
19
19
  ParseResult,
20
20
  )
21
- from semantic_release.commit_parser.util import parse_paragraphs
21
+ from semantic_release.commit_parser.util import parse_paragraphs, sort_numerically
22
22
  from semantic_release.enums import LevelBump
23
23
  from semantic_release.errors import InvalidParserOptions
24
24
 
@@ -186,7 +186,7 @@ class EmojiCommitParser(CommitParser[ParseResult, EmojiParserOptions]):
186
186
  predicate.split(","),
187
187
  )
188
188
  )
189
- accumulator["linked_issues"] = sorted(
189
+ accumulator["linked_issues"] = sort_numerically(
190
190
  set(accumulator["linked_issues"]).union(new_issue_refs)
191
191
  )
192
192
  # TODO: breaking change v10, removes resolution footers from descriptions
@@ -6,28 +6,34 @@ from typing import TYPE_CHECKING
6
6
 
7
7
  if TYPE_CHECKING: # pragma: no cover
8
8
  from re import Pattern
9
- from typing import TypedDict
9
+ from typing import Sequence, TypedDict
10
10
 
11
11
  class RegexReplaceDef(TypedDict):
12
12
  pattern: Pattern
13
13
  repl: str
14
14
 
15
15
 
16
+ number_pattern = regexp(r"(\d+)")
17
+
16
18
  breaking_re = regexp(r"BREAKING[ -]CHANGE:\s?(.*)")
19
+
17
20
  un_word_wrap: RegexReplaceDef = {
18
21
  # Match a line ending where the next line is not indented, or a bullet
19
22
  "pattern": regexp(r"((?<!-)\n(?![\s*-]))"),
20
23
  "repl": r" ", # Replace with a space
21
24
  }
25
+
22
26
  un_word_wrap_hyphen: RegexReplaceDef = {
23
27
  "pattern": regexp(r"((?<=\w)-\n(?=\w))"),
24
28
  "repl": r"-", # Replace with single hyphen
25
29
  }
30
+
26
31
  trim_line_endings: RegexReplaceDef = {
27
32
  # Match line endings with optional whitespace
28
33
  "pattern": regexp(r"[\r\t\f\v ]*\r?\n"),
29
34
  "repl": "\n", # remove the optional whitespace & remove windows newlines
30
35
  }
36
+
31
37
  spread_out_git_footers: RegexReplaceDef = {
32
38
  # Match a git footer line, and add an extra newline after it
33
39
  # only be flexible enough for a double space indent (otherwise its probably on purpose)
@@ -65,3 +71,7 @@ def parse_paragraphs(text: str) -> list[str]:
65
71
  ],
66
72
  )
67
73
  )
74
+
75
+
76
+ def sort_numerically(iterable: Sequence[str] | set[str]) -> list[str]:
77
+ return sorted(iterable, key=lambda x: int((number_pattern.search(x) or [-1])[0]))
@@ -69,7 +69,7 @@ EXAMPLE:
69
69
  #}{% if breaking_commits | length > 0
70
70
  %}{# PREPROCESS COMMITS
71
71
  #}{% set brk_ns = namespace(commits=breaking_commits)
72
- %}{{ apply_alphabetical_ordering_by_descriptions(brk_ns) | default("", true)
72
+ %}{{ apply_alphabetical_ordering_by_brk_descriptions(brk_ns) | default("", true)
73
73
  }}{#
74
74
  #}{% set brking_descriptions = []
75
75
  %}{#
semantic_release/enums.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import logging
3
4
  from enum import IntEnum, unique
4
5
 
5
6
 
@@ -37,3 +38,32 @@ class LevelBump(IntEnum):
37
38
  >>> LevelBump.from_string("minor") == LevelBump.MINOR
38
39
  """
39
40
  return cls[val.upper().replace("-", "_")]
41
+
42
+
43
+ class SemanticReleaseLogLevels(IntEnum):
44
+ """IntEnum representing the log levels used by semantic-release."""
45
+
46
+ FATAL = logging.FATAL
47
+ CRITICAL = logging.CRITICAL
48
+ ERROR = logging.ERROR
49
+ WARNING = logging.WARNING
50
+ INFO = logging.INFO
51
+ DEBUG = logging.DEBUG
52
+ SILLY = 5
53
+
54
+ def __str__(self) -> str:
55
+ """
56
+ Return the level name rather than 'SemanticReleaseLogLevels.<level>'
57
+ E.g.
58
+ >>> str(SemanticReleaseLogLevels.DEBUG)
59
+ 'DEBUG'
60
+ >>> str(SemanticReleaseLogLevels.CRITICAL)
61
+ 'CRITICAL'
62
+ """
63
+ return self.name.upper()
64
+
65
+
66
+ logging.addLevelName(
67
+ SemanticReleaseLogLevels.SILLY,
68
+ str(SemanticReleaseLogLevels.SILLY),
69
+ )
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from contextlib import nullcontext
6
+ from datetime import datetime
6
7
  from logging import getLogger
7
8
  from pathlib import Path
8
9
  from typing import TYPE_CHECKING
@@ -48,7 +49,9 @@ class GitProject:
48
49
  return self._logger
49
50
 
50
51
  def _get_custom_environment(
51
- self, repo: Repo
52
+ self,
53
+ repo: Repo,
54
+ custom_vars: dict[str, str] | None = None,
52
55
  ) -> nullcontext[None] | _GeneratorContextManager[None]:
53
56
  """
54
57
  git.custom_environment is a context manager but
@@ -56,15 +59,26 @@ class GitProject:
56
59
  we need to throw it away and re-create it in
57
60
  order to use it again
58
61
  """
62
+ author_vars = (
63
+ {
64
+ "GIT_AUTHOR_NAME": self._commit_author.name,
65
+ "GIT_AUTHOR_EMAIL": self._commit_author.email,
66
+ "GIT_COMMITTER_NAME": self._commit_author.name,
67
+ "GIT_COMMITTER_EMAIL": self._commit_author.email,
68
+ }
69
+ if self._commit_author
70
+ else {}
71
+ )
72
+
73
+ custom_env_vars = {
74
+ **author_vars,
75
+ **(custom_vars or {}),
76
+ }
77
+
59
78
  return (
60
79
  nullcontext()
61
- if not self._commit_author
62
- else repo.git.custom_environment(
63
- GIT_AUTHOR_NAME=self._commit_author.name,
64
- GIT_AUTHOR_EMAIL=self._commit_author.email,
65
- GIT_COMMITTER_NAME=self._commit_author.name,
66
- GIT_COMMITTER_EMAIL=self._commit_author.email,
67
- )
80
+ if not custom_env_vars
81
+ else repo.git.custom_environment(**custom_env_vars)
68
82
  )
69
83
 
70
84
  def is_dirty(self) -> bool:
@@ -182,19 +196,32 @@ class GitProject:
182
196
  self.logger.exception(str(err))
183
197
  raise GitCommitError("Failed to commit changes") from err
184
198
 
185
- def git_tag(self, tag_name: str, message: str, noop: bool = False) -> None:
199
+ def git_tag(
200
+ self, tag_name: str, message: str, isotimestamp: str, noop: bool = False
201
+ ) -> None:
202
+ try:
203
+ datetime.fromisoformat(isotimestamp)
204
+ except ValueError as err:
205
+ raise ValueError("Invalid timestamp format") from err
206
+
186
207
  if noop:
187
- command = (
188
- f"""\
189
- GIT_AUTHOR_NAME={self._commit_author.name} \\
190
- GIT_AUTHOR_EMAIL={self._commit_author.email} \\
191
- GIT_COMMITTER_NAME={self._commit_author.name} \\
192
- GIT_COMMITTER_EMAIL={self._commit_author.email} \\
193
- """
194
- if self._commit_author
195
- else ""
208
+ command = str.join(
209
+ " ",
210
+ [
211
+ f"GIT_COMMITTER_DATE={isotimestamp}",
212
+ *(
213
+ [
214
+ f"GIT_AUTHOR_NAME={self._commit_author.name}",
215
+ f"GIT_AUTHOR_EMAIL={self._commit_author.email}",
216
+ f"GIT_COMMITTER_NAME={self._commit_author.name}",
217
+ f"GIT_COMMITTER_EMAIL={self._commit_author.email}",
218
+ ]
219
+ if self._commit_author
220
+ else [""]
221
+ ),
222
+ f"git tag -a {tag_name} -m '{message}'",
223
+ ],
196
224
  )
197
- command += f"git tag -a {tag_name} -m '{message}'"
198
225
 
199
226
  noop_report(
200
227
  indented(
@@ -206,7 +233,10 @@ class GitProject:
206
233
  )
207
234
  return
208
235
 
209
- with Repo(str(self.project_root)) as repo, self._get_custom_environment(repo):
236
+ with Repo(str(self.project_root)) as repo, self._get_custom_environment(
237
+ repo,
238
+ {"GIT_COMMITTER_DATE": isotimestamp},
239
+ ):
210
240
  try:
211
241
  repo.git.tag("-a", tag_name, m=message)
212
242
  except GitCommandError as err:
@@ -1,20 +1,19 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
- from queue import Queue
4
+ from contextlib import suppress
5
+ from queue import LifoQueue
5
6
  from typing import TYPE_CHECKING, Iterable
6
7
 
7
8
  from semantic_release.commit_parser import ParsedCommit
8
9
  from semantic_release.const import DEFAULT_VERSION
9
- from semantic_release.enums import LevelBump
10
- from semantic_release.errors import InvalidVersion, MissingMergeBaseError
11
- from semantic_release.version.version import Version
10
+ from semantic_release.enums import LevelBump, SemanticReleaseLogLevels
11
+ from semantic_release.errors import InternalError, InvalidVersion
12
12
 
13
13
  if TYPE_CHECKING: # pragma: no cover
14
- from git.objects.blob import Blob
14
+ from typing import Sequence
15
+
15
16
  from git.objects.commit import Commit
16
- from git.objects.tag import TagObject
17
- from git.objects.tree import Tree
18
17
  from git.refs.tag import Tag
19
18
  from git.repo.base import Repo
20
19
 
@@ -24,8 +23,10 @@ if TYPE_CHECKING: # pragma: no cover
24
23
  ParserOptions,
25
24
  )
26
25
  from semantic_release.version.translator import VersionTranslator
26
+ from semantic_release.version.version import Version
27
+
27
28
 
28
- log = logging.getLogger(__name__)
29
+ logger = logging.getLogger(__name__)
29
30
 
30
31
 
31
32
  def tags_and_versions(
@@ -44,99 +45,78 @@ def tags_and_versions(
44
45
  try:
45
46
  version = translator.from_tag(tag.name)
46
47
  except (NotImplementedError, InvalidVersion) as e:
47
- log.warning(
48
+ logger.warning(
48
49
  "Couldn't parse tag %s as as Version: %s",
49
50
  tag.name,
50
51
  str(e),
51
- exc_info=log.isEnabledFor(logging.DEBUG),
52
+ exc_info=logger.isEnabledFor(logging.DEBUG),
52
53
  )
53
54
  continue
54
55
 
55
56
  if version:
56
57
  ts_and_vs.append((tag, version))
57
58
 
58
- log.info("found %s previous tags", len(ts_and_vs))
59
+ logger.info("found %s previous tags", len(ts_and_vs))
59
60
  return sorted(ts_and_vs, reverse=True, key=lambda v: v[1])
60
61
 
61
62
 
62
- def _bfs_for_latest_version_in_history(
63
- merge_base: Commit | TagObject | Blob | Tree,
64
- full_release_tags_and_versions: list[tuple[Tag, Version]],
65
- ) -> Version | None:
66
- """
67
- Run a breadth-first search through the given `merge_base`'s parents,
68
- looking for the most recent version corresponding to a commit in the
69
- `merge_base`'s parents' history. If no commits in the history correspond
70
- to a released version, return None
71
- """
72
- tag_sha_2_version_lookup = {
73
- tag.commit.hexsha: version for tag, version in full_release_tags_and_versions
74
- }
75
-
76
- # Step 3. Latest full release version within the history of the current branch
77
- # Breadth-first search the merge-base and its parent commits for one which matches
78
- # the tag of the latest full release tag in history
79
- def bfs(start_commit: Commit | TagObject | Blob | Tree) -> Version | None:
80
- # Derived from Geeks for Geeks
81
- # https://www.geeksforgeeks.org/python-program-for-breadth-first-search-or-bfs-for-a-graph/?ref=lbp
82
-
83
- # Create a queue for BFS
84
- q: Queue[Commit | TagObject | Blob | Tree] = Queue()
63
+ def _traverse_graph_for_commits(
64
+ head_commit: Commit,
65
+ latest_release_tag_str: str = "",
66
+ ) -> Sequence[Commit]:
67
+ # Depth-first search
68
+ def dfs(start_commit: Commit, stop_nodes: set[Commit]) -> Sequence[Commit]:
69
+ # Create a stack for DFS
70
+ stack: LifoQueue[Commit] = LifoQueue()
85
71
 
86
72
  # Create a set to store visited graph nodes (commit objects in this case)
87
- visited: set[Commit | TagObject | Blob | Tree] = set()
88
-
89
- # Add the source node in the queue & mark as visited to start the search
90
- q.put(start_commit)
91
- visited.add(start_commit)
73
+ visited: set[Commit] = set()
92
74
 
93
- # Initialize the result to None
94
- result = None
75
+ # Initialize the result
76
+ commits: list[Commit] = []
95
77
 
96
- # Traverse the git history until it finds a version tag if one exists
97
- while not q.empty():
98
- node = q.get()
99
- visited.add(node)
78
+ # Add the source node in the queue to start the search
79
+ stack.put(start_commit)
100
80
 
101
- log.debug("checking if commit %s matches any tags", node.hexsha)
102
- version = tag_sha_2_version_lookup.get(node.hexsha, None)
103
-
104
- if version is not None:
105
- log.info(
106
- "found latest version in branch history: %r (%s)",
107
- str(version),
108
- node.hexsha[:7],
109
- )
110
- result = version
111
- break
81
+ # Traverse the git history capturing each commit found before it reaches a stop node
82
+ while not stack.empty():
83
+ if (node := stack.get()) in visited or node in stop_nodes:
84
+ continue
112
85
 
113
- log.debug("commit %s doesn't match any tags", node.hexsha)
86
+ visited.add(node)
87
+ commits.append(node)
114
88
 
115
- # Add all parent commits to the queue if they haven't been visited
89
+ # Add all parent commits to the stack from left to right so that the rightmost is popped first
90
+ # as the left side is generally the merged into branch
116
91
  for parent in node.parents:
117
- if parent in visited:
118
- log.debug("parent commit %s already visited", node.hexsha)
119
- continue
120
-
121
- log.debug("queuing parent commit %s", parent.hexsha)
122
- q.put(parent)
123
-
124
- return result
92
+ logger.debug("queuing parent commit %s", parent.hexsha[:7])
93
+ stack.put(parent)
94
+
95
+ return commits
96
+
97
+ # Run a Depth First Search to find all the commits since the last release
98
+ commits_since_last_release = dfs(
99
+ start_commit=head_commit,
100
+ stop_nodes=set(
101
+ head_commit.repo.iter_commits(latest_release_tag_str)
102
+ if latest_release_tag_str
103
+ else []
104
+ ),
105
+ )
125
106
 
126
- # Run a Breadth First Search to find the latest version in the history
127
- latest_version = bfs(merge_base)
128
- if latest_version is not None:
129
- log.info("the latest version in this branch's history is %s", latest_version)
130
- else:
131
- log.info("no version tags found in this branch's history")
107
+ log_msg = (
108
+ f"Found {len(commits_since_last_release)} commits since the last release!"
109
+ if len(commits_since_last_release) > 0
110
+ else "No commits found since the last release!"
111
+ )
112
+ logger.info(log_msg)
132
113
 
133
- return latest_version
114
+ return commits_since_last_release
134
115
 
135
116
 
136
117
  def _increment_version(
137
118
  latest_version: Version,
138
119
  latest_full_version: Version,
139
- latest_full_version_in_history: Version,
140
120
  level_bump: LevelBump,
141
121
  prerelease: bool,
142
122
  prerelease_token: str,
@@ -147,23 +127,25 @@ def _increment_version(
147
127
  Using the given versions, along with a given `level_bump`, increment to
148
128
  the next version according to whether or not this is a prerelease.
149
129
 
150
- `latest_version`, `latest_full_version` and `latest_full_version_in_history`
151
- can be the same, but aren't necessarily.
152
-
153
130
  `latest_version` is the most recent version released from this branch's history.
154
- `latest_full_version` is the most recent full release (i.e. not a prerelease)
155
- anywhere in the repository's history, including commits which aren't present on
156
- this branch.
157
- `latest_full_version_in_history`, correspondingly, is the latest full release which
158
- is in this branch's history.
131
+ `latest_full_version`, the most recent full release (i.e. not a prerelease)
132
+ in this branch's history.
133
+
134
+ `latest_version` and `latest_full_version` can be the same, but aren't necessarily.
159
135
  """
160
136
  local_vars = list(locals().items())
161
- log.debug("_increment_version: %s", ", ".join(f"{k} = {v}" for k, v in local_vars))
137
+ logger.log(
138
+ SemanticReleaseLogLevels.SILLY,
139
+ "_increment_version: %s",
140
+ str.join(", ", [f"{k} = {v}" for k, v in local_vars]),
141
+ )
142
+
143
+ # Handle variations where the latest version is 0.x.x
162
144
  if latest_version.major == 0:
163
145
  if not allow_zero_version:
164
146
  # Set up default version to be 1.0.0 if currently 0.x.x which means a commented
165
147
  # breaking change is not required to bump to 1.0.0
166
- log.debug(
148
+ logger.debug(
167
149
  "Bumping major version as 0.x.x versions are disabled because of allow_zero_version=False"
168
150
  )
169
151
  level_bump = LevelBump.MAJOR
@@ -173,93 +155,99 @@ def _increment_version(
173
155
  # breaking changes should increment the minor digit
174
156
  # Correspondingly, we reduce the level that we increment the
175
157
  # version by.
176
- log.debug(
158
+ logger.debug(
177
159
  "reducing version increment due to 0. version and major_on_zero=False"
178
160
  )
179
161
 
180
162
  level_bump = min(level_bump, LevelBump.MINOR)
181
163
 
182
- if prerelease:
183
- log.debug("prerelease=true")
184
- target_final_version = latest_full_version.finalize_version()
185
- diff_with_last_released_version = (
186
- latest_version - latest_full_version_in_history
187
- )
188
- log.debug(
189
- "diff between the latest version %s and the latest full release version %s "
190
- "is: %s",
191
- latest_version,
192
- latest_full_version_in_history,
193
- diff_with_last_released_version,
194
- )
195
- # 6a i) if the level_bump > the level bump introduced by any prerelease tag
196
- # before e.g. 1.2.4-rc.3 -> 1.3.0-rc.1
197
- if level_bump > diff_with_last_released_version:
198
- log.debug(
199
- "this release has a greater bump than any change since the last full "
200
- "release, %s",
201
- latest_full_version_in_history,
202
- )
203
- return target_final_version.bump(level_bump).to_prerelease(
204
- token=prerelease_token
205
- )
164
+ logger.debug(
165
+ "prerelease=%s and the latest version %s %s prerelease",
166
+ prerelease,
167
+ latest_version,
168
+ "is a" if latest_version.is_prerelease else "is not a",
169
+ )
206
170
 
207
- # 6a ii) if level_bump <= the level bump introduced by prerelease tag
208
- log.debug(
209
- "there has already been at least a %s release since the last full "
210
- "release %s",
211
- level_bump,
212
- latest_full_version_in_history,
213
- )
214
- log.debug("this release will increment the prerelease revision")
215
- return latest_version.to_prerelease(
216
- token=prerelease_token,
217
- revision=(
218
- 1
219
- if latest_version.prerelease_token != prerelease_token
220
- else (latest_version.prerelease_revision or 0) + 1
221
- ),
171
+ if level_bump == LevelBump.NO_RELEASE:
172
+ raise ValueError("level_bump must be at least PRERELEASE_REVISION")
173
+
174
+ if level_bump == LevelBump.PRERELEASE_REVISION and not latest_version.is_prerelease:
175
+ raise ValueError(
176
+ "Cannot increment a non-prerelease version with a prerelease level bump"
222
177
  )
223
178
 
224
- # 6b. if not prerelease
225
- # NOTE: These can actually be condensed down to the single line
226
- # 6b. i) if there's been a prerelease
179
+ # assume we always want to increment the version that is the latest in the branch's history
180
+ base_version = latest_version
181
+
182
+ # if the current version is a prerelease & we want a new prerelease, then
183
+ # figure out if we need to bump the prerelease revision or start a new prerelease
227
184
  if latest_version.is_prerelease:
228
- log.debug(
229
- "prerelease=false and the latest version %s is a prerelease", latest_version
230
- )
231
- diff_with_last_released_version = (
232
- latest_version - latest_full_version_in_history
233
- )
234
- log.debug(
235
- "diff between the latest version %s and the latest full release version %s "
236
- "is: %s",
185
+ # find the change since the last full release because if the current version is a prerelease
186
+ # then we need to predict properly the next full version
187
+ diff_with_last_released_version = latest_version - latest_full_version
188
+ logger.debug(
189
+ "the diff b/w the latest version '%s' and the latest full release version '%s' is: %s",
237
190
  latest_version,
238
- latest_full_version_in_history,
191
+ latest_full_version,
239
192
  diff_with_last_released_version,
240
193
  )
241
- if level_bump > diff_with_last_released_version:
242
- log.debug(
243
- "this release has a greater bump than any change since the last full "
244
- "release, %s",
245
- latest_full_version_in_history,
246
- )
247
- return latest_version.bump(level_bump).finalize_version()
248
- log.debug(
249
- "there has already been at least a %s release since the last full "
250
- "release %s",
251
- level_bump,
252
- latest_full_version_in_history,
194
+
195
+ # Since the difference is less than or equal to the level bump and we want a new prerelease,
196
+ # we can abort early and just increment the revision
197
+ if level_bump <= diff_with_last_released_version:
198
+ # 6a ii) if level_bump <= the level bump introduced by the previous tag (latest_version)
199
+ if prerelease:
200
+ logger.debug(
201
+ "there has already been at least a %s release since the last full release %s",
202
+ level_bump,
203
+ latest_full_version,
204
+ )
205
+ logger.debug("Incrementing the prerelease revision...")
206
+ new_revision = base_version.to_prerelease(
207
+ token=prerelease_token,
208
+ revision=(
209
+ 1
210
+ if latest_version.prerelease_token != prerelease_token
211
+ else (latest_version.prerelease_revision or 0) + 1
212
+ ),
213
+ )
214
+ logger.debug("Incremented %s to %s", base_version, new_revision)
215
+ return new_revision
216
+
217
+ # When we don't want a prerelease, but the previous version is a prerelease that
218
+ # had a greater bump than we currently are applying, choose the larger bump instead
219
+ # as it consumes this bump
220
+ logger.debug("Finalizing the prerelease version...")
221
+ return base_version.finalize_version()
222
+
223
+ # Fallthrough to handle all larger level bumps
224
+ logger.debug(
225
+ "this release has a greater bump than any change since the last full release, %s",
226
+ latest_full_version,
253
227
  )
254
- return latest_version.finalize_version()
255
228
 
256
- # 6b. ii) If there's been no prerelease
257
- log.debug(
258
- "prerelease=false and %s is not a prerelease; bumping with a %s release",
259
- latest_version,
260
- level_bump,
229
+ # Fallthrough, if we don't want a prerelease, or if we do but the level bump is greater
230
+ #
231
+ # because the current version is a prerelease, we must start from the last full version
232
+ # Case 1: we identified that the level bump is greater than the change since
233
+ # the last full release, this will also reset the prerelease revision
234
+ # Case 2: we don't want a prerelease, so consider only the last full version in history
235
+ base_version = latest_full_version
236
+
237
+ # From the base version, we can now increment the version according to the level bump
238
+ # regardless of the prerelease status as bump() handles the reset and pass through
239
+ logger.debug("Bumping %s with a %s bump", base_version, level_bump)
240
+ target_next_version = base_version.bump(level_bump)
241
+
242
+ # Converting to/from a prerelease if necessary
243
+ target_next_version = (
244
+ target_next_version.to_prerelease(token=prerelease_token)
245
+ if prerelease
246
+ else target_next_version.finalize_version()
261
247
  )
262
- return latest_version.bump(level_bump)
248
+
249
+ logger.debug("Incremented %s to %s", base_version, target_next_version)
250
+ return target_next_version
263
251
 
264
252
 
265
253
  def next_version(
@@ -274,181 +262,118 @@ def next_version(
274
262
  Evaluate the history within `repo`, and based on the tags and commits in the repo
275
263
  history, identify the next semantic version that should be applied to a release
276
264
  """
277
- # Step 1. All tags, sorted descending by semver ordering rules
278
- all_git_tags_as_versions = tags_and_versions(repo.tags, translator)
279
- all_full_release_tags_and_versions = list(
280
- filter(lambda t_v: not t_v[1].is_prerelease, all_git_tags_as_versions)
281
- )
282
- log.info(
283
- "Found %s full releases (excluding prereleases)",
284
- len(all_full_release_tags_and_versions),
285
- )
286
-
287
265
  # Default initial version
288
- latest_full_release_tag, latest_full_release_version = next(
289
- iter(all_full_release_tags_and_versions),
290
- (None, translator.from_string(DEFAULT_VERSION)),
266
+ # Since the translator is configured by the user, we can't guarantee that it will
267
+ # be able to parse the default version. So we first cast it to a tag using the default
268
+ # value and the users configured tag format, then parse it back to a version object
269
+ default_initial_version = translator.from_tag(
270
+ translator.str_to_tag(DEFAULT_VERSION)
291
271
  )
292
-
293
- # we can safely scan the extra commits on this
294
- # branch if it's never been released, but we have no other
295
- # guarantees that other branches exist
296
- # Note the merge_base might be on our current branch, it's not
297
- # necessarily the merge base of the current branch with `main`
298
- other_ref = (
299
- repo.active_branch
300
- if latest_full_release_tag is None
301
- else latest_full_release_tag.name
302
- )
303
-
304
- # Conditional log message to inform what was chosen as the comparison point
305
- # to find the merge base of the current branch with the latest full release
306
- log_msg = (
307
- str.join(
308
- ", ",
309
- [
310
- "No full releases have been made yet",
311
- f"the default version to use is {latest_full_release_version}",
312
- ],
313
- )
314
- if latest_full_release_tag is None
315
- else str.join(
316
- ", ",
317
- [
318
- f"The last full release was {latest_full_release_version}",
319
- f"tagged as {latest_full_release_tag!r}",
320
- ],
321
- )
322
- )
323
-
324
- log.info(log_msg)
325
- merge_bases = repo.merge_base(other_ref, repo.active_branch)
326
-
327
- if len(merge_bases) < 1:
328
- raise MissingMergeBaseError(
329
- f"Unable to find merge-base between {other_ref} and {repo.active_branch.name}"
330
- )
331
-
332
- if len(merge_bases) > 1:
333
- raise NotImplementedError(
334
- str.join(
335
- " ",
336
- [
337
- "This branch has more than one merge-base with the",
338
- "latest version, which is not yet supported",
339
- ],
340
- )
272
+ if default_initial_version is None:
273
+ raise InternalError(
274
+ "Translator was unable to parse the embedded default version"
341
275
  )
342
276
 
343
- merge_base = merge_bases[0]
277
+ # Step 1. All tags, sorted descending by semver ordering rules
278
+ all_git_tags_as_versions = tags_and_versions(repo.tags, translator)
344
279
 
345
- if merge_base is None:
346
- str_tag_name = (
347
- "None" if latest_full_release_tag is None else latest_full_release_tag.name
348
- )
349
- raise ValueError(
350
- f"The merge_base found by merge_base({str_tag_name}, {repo.active_branch}) "
351
- "is None"
352
- )
280
+ # Retrieve all commit hashes (regardless of merges) in the current branch's history from repo origin
281
+ commit_hash_set = {
282
+ commit.hexsha
283
+ for commit in _traverse_graph_for_commits(head_commit=repo.active_branch.commit)
284
+ }
353
285
 
354
- latest_full_version_in_history = _bfs_for_latest_version_in_history(
355
- merge_base=merge_base,
356
- full_release_tags_and_versions=all_full_release_tags_and_versions,
357
- )
358
- log.info(
359
- "The last full version in this branch's history was %s",
360
- latest_full_version_in_history,
286
+ # Filter all releases that are not found in the current branch's history
287
+ historic_versions: list[Version] = []
288
+ for tag, version in all_git_tags_as_versions:
289
+ # TODO: move this to tags_and_versions() function?
290
+ # Ignore the error that is raised when tag points to a Blob or Tree object rather
291
+ # than a commit object (tags that point to tags that then point to commits are resolved automatically)
292
+ with suppress(ValueError):
293
+ if tag.commit.hexsha in commit_hash_set:
294
+ historic_versions.append(version)
295
+
296
+ # Step 2. Get the latest final release version in the history of the current branch
297
+ # or fallback to the default 0.0.0 starting version value if none are found
298
+ latest_full_release_version = next(
299
+ filter(
300
+ lambda version: not version.is_prerelease,
301
+ historic_versions,
302
+ ),
303
+ default_initial_version,
361
304
  )
362
305
 
363
- commits_since_last_full_release = (
364
- repo.iter_commits()
365
- if latest_full_version_in_history is None
366
- else repo.iter_commits(f"{latest_full_version_in_history.as_tag()}...")
306
+ logger.info(
307
+ f"The last full version in this branch's history was {latest_full_release_version}"
308
+ if latest_full_release_version != default_initial_version
309
+ else "No full releases found in this branch's history"
367
310
  )
368
311
 
369
- # Step 4. Parse each commit since the last release and find any tags that have
370
- # been added since then.
371
- parsed_levels: set[LevelBump] = set()
372
- latest_version = latest_full_version_in_history or Version(
373
- 0,
374
- 0,
375
- 0,
376
- prerelease_token=translator.prerelease_token,
377
- tag_format=translator.tag_format,
312
+ # Step 3. Determine the latest release version in the history of the current branch
313
+ # If we the desired result is a prerelease, we must determine if there was any previous
314
+ # prerelease in the history of the current branch beyond the latest_full_release_version.
315
+ # Important to note that, we only consider prereleases that are of the same prerelease token
316
+ # as the basis of incrementing the prerelease revision.
317
+ # If we are not looking for a prerelease, this is the same as the last full release.
318
+ latest_version = (
319
+ latest_full_release_version
320
+ if not prerelease
321
+ else next(
322
+ filter(
323
+ lambda version: all(
324
+ [
325
+ version.is_prerelease,
326
+ version.prerelease_token == translator.prerelease_token,
327
+ version >= latest_full_release_version,
328
+ ]
329
+ ),
330
+ historic_versions,
331
+ ),
332
+ latest_full_release_version, # default
333
+ )
378
334
  )
379
335
 
380
- # We only include pre-releases here if doing a prerelease.
381
- # If it's not a prerelease, we need to include commits back
382
- # to the last full version in consideration for what kind of
383
- # bump to produce. However if we're doing a prerelease, we can
384
- # include prereleases here to potentially consider a smaller portion
385
- # of history (from a prerelease since the last full release, onwards)
386
-
387
- # Note that a side-effect of this is, if at some point the configuration
388
- # for a particular branch pattern changes w.r.t. prerelease=True/False,
389
- # the new kind of version will be produced from the commits already
390
- # included in a prerelease since the last full release on the branch
391
- tag_sha_2_version_lookup = {
392
- tag.commit.hexsha: (tag, version)
393
- for tag, version in all_git_tags_as_versions
394
- if prerelease or not version.is_prerelease
395
- }
396
-
397
- # N.B. these should be sorted so long as we iterate the commits in reverse order
398
- for commit in commits_since_last_full_release:
399
- parse_result = commit_parser.parse(commit)
400
- if isinstance(parse_result, ParsedCommit):
401
- log.debug(
402
- "adding %s to the levels identified in commits_since_last_full_release",
403
- parse_result.bump,
404
- )
405
- parsed_levels.add(parse_result.bump)
336
+ logger.info("The latest release in this branch's history was %s", latest_version)
406
337
 
407
- log.debug("checking if commit %s matches any tags", commit.hexsha)
408
- t_v = tag_sha_2_version_lookup.get(commit.hexsha, None)
409
-
410
- if t_v is None:
411
- # If we haven't found the latest prerelease on the branch,
412
- # keep the loop going to look for it
413
- log.debug("no tags correspond to commit %s", commit.hexsha)
414
- continue
338
+ # Step 4. Walk the git tree to find all commits that have been made since the last release
339
+ commits_since_last_release = _traverse_graph_for_commits(
340
+ head_commit=repo.active_branch.commit,
341
+ latest_release_tag_str=(
342
+ # NOTE: the default_initial_version should not actually exist on the repository (ie v0.0.0)
343
+ # so we provide an empty tag string when there are no tags on the repository yet
344
+ latest_version.as_tag() if latest_version != default_initial_version else ""
345
+ ),
346
+ )
415
347
 
416
- # Unpack the tuple
417
- tag, latest_version = t_v
418
- log.debug(
419
- "tag %r (%s) matches commit %s. the latest version is %s",
420
- tag.name,
421
- tag.commit.hexsha,
422
- commit.hexsha,
423
- latest_version,
348
+ # Step 5. Parse the commits to determine the bump level that should be applied
349
+ parsed_levels: set[LevelBump] = {
350
+ parsed_result.bump # type: ignore[union-attr] # too complex for type checkers
351
+ for parsed_result in filter(
352
+ lambda parsed_result: isinstance(parsed_result, ParsedCommit),
353
+ map(commit_parser.parse, commits_since_last_release),
424
354
  )
425
- break
355
+ }
426
356
 
427
- log.debug(
428
- "parsed the following distinct levels from the commits since the last release: "
429
- "%s",
357
+ logger.debug(
358
+ "parsed the following distinct levels from the commits since the last release: %s",
430
359
  parsed_levels,
431
360
  )
361
+
432
362
  level_bump = max(parsed_levels, default=LevelBump.NO_RELEASE)
433
- log.info("The type of the next release release is: %s", level_bump)
434
- if level_bump is LevelBump.NO_RELEASE: # noqa: SIM102
435
- if latest_version.major != 0 or allow_zero_version:
436
- log.info("No release will be made")
437
- return latest_version
363
+ logger.info("The type of the next release release is: %s", level_bump)
364
+
365
+ if all(
366
+ [
367
+ level_bump is LevelBump.NO_RELEASE,
368
+ latest_version.major != 0 or allow_zero_version,
369
+ ]
370
+ ):
371
+ logger.info("No release will be made")
372
+ return latest_version
438
373
 
439
374
  return _increment_version(
440
375
  latest_version=latest_version,
441
376
  latest_full_version=latest_full_release_version,
442
- latest_full_version_in_history=(
443
- latest_full_version_in_history
444
- or Version(
445
- 0,
446
- 0,
447
- 0,
448
- prerelease_token=translator.prerelease_token,
449
- tag_format=translator.tag_format,
450
- )
451
- ),
452
377
  level_bump=level_bump,
453
378
  prerelease=prerelease,
454
379
  prerelease_token=translator.prerelease_token,