Skip to content

Commit a65747b

Browse files
committed
Add method to find the next branch introducing breaking changes
If there is a last tag, use that as a base, otherwise use the current branch as a base. If none is available, return `None`. If there is a base, the major is incremented by one and the minor is set to `None`, unless the major is 0 (an "initial development version" for semver), in which case the major is set to 0 and the minor is incremented by one, as the next minor could be a breaking change. Technically semver allows breaking changes in patches for major version 0, but we assume patches maintain backwards compatibility. For example: - `v0.x.x` -> `v0.2.x` (if the last tag is `v0.1.1` for example) - `v0.2.x` -> `v0.3.x` - `v1.x.x` -> `v2.x.x` - `v1.0.x` -> `v2.x.x` - `v0.0.1` -> `v0.1.x` - `v0.3.1-pre.1+build.3` -> `v0.4.x` - `v1.0.0` -> `v2.x.x` - `v1.1.0` -> `v2.x.x` This is useful to use as a maximum dependency version for a package. Signed-off-by: Leandro Lucarella <[email protected]>
1 parent 2ee53fb commit a65747b

File tree

2 files changed

+112
-0
lines changed

2 files changed

+112
-0
lines changed

src/frequenz/repo/config/version.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,43 @@ def find_last_tag(self) -> semver.Version | None:
347347
return None
348348
return max(tags)
349349

350+
def find_next_breaking_branch(self) -> BranchVersion | None:
351+
"""Find the next branch potentially introducing breaking changes.
352+
353+
Returns:
354+
If there is a last tag, use that as a base, otherwise use the current branch
355+
as a base. If none is available, return `None`. If there is a base, the
356+
major is incremented by one and the minor is set to `None`, unless the
357+
major is 0 (an "initial development version" for semver), in which case
358+
the major is set to 0 and the minor is incremented by one, as the next
359+
minor could be a breaking change. Technically semver allows breaking
360+
changes in patches for major version 0, but we assume patches maintain
361+
backwards compatibility.
362+
"""
363+
v_prefix = "v" if self._ref_name.startswith("v") else ""
364+
last_tag = self.find_last_tag()
365+
if last_tag is None:
366+
branch = self.current_branch
367+
if branch is None:
368+
_logger.warning(
369+
"Trying to get the next breaking branch but there is no (valid) "
370+
"last tag nor current branch for %r",
371+
self._ref,
372+
)
373+
return None
374+
major = branch.major + 1
375+
minor = branch.minor or 1
376+
else:
377+
major = last_tag.major + 1
378+
minor = last_tag.minor + 1
379+
380+
# If the next major is 1, then the current is 0, so the next minor could be
381+
# a breaking change.
382+
if major == 1:
383+
return BranchVersion(major=0, minor=minor, name=f"{v_prefix}0.{minor}.x")
384+
385+
return BranchVersion(major=major, name=f"{v_prefix}{major}.x.x")
386+
350387
def find_next_minor_for_major_branch(self) -> int | None:
351388
"""Find the next minor version for the current major branch.
352389

tests/test_version.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ class _Expected: # pylint: disable=too-many-instance-attributes
182182
is_branch: bool = False
183183
is_branch_latest: bool = False
184184
next_minor_for_major_branch: int | None = None
185+
next_breaking_branch: BranchVersion | None = None
185186

186187

187188
@dataclasses.dataclass(frozen=True, kw_only=True)
@@ -214,6 +215,7 @@ class _TestCase: # pylint: disable=too-many-instance-attributes
214215
is_tag=True,
215216
is_tag_last_minor_for_major=True,
216217
last_tag=semver.Version(1, 0, 1),
218+
next_breaking_branch=BranchVersion(major=2, name="v2.x.x"),
217219
),
218220
),
219221
_TestCase(
@@ -226,6 +228,7 @@ class _TestCase: # pylint: disable=too-many-instance-attributes
226228
is_tag=True,
227229
is_tag_last_minor_for_major=True,
228230
last_tag=semver.Version(1, 1, 0),
231+
next_breaking_branch=BranchVersion(major=2, name="v2.x.x"),
229232
),
230233
),
231234
_TestCase(
@@ -238,6 +241,7 @@ class _TestCase: # pylint: disable=too-many-instance-attributes
238241
is_tag=True,
239242
is_tag_last_minor_for_major=False,
240243
last_tag=semver.Version(2, 0, 1),
244+
next_breaking_branch=BranchVersion(major=3, name="v3.x.x"),
241245
),
242246
),
243247
_TestCase(
@@ -252,6 +256,7 @@ class _TestCase: # pylint: disable=too-many-instance-attributes
252256
is_tag_last_minor_for_major=True,
253257
is_tag_latest=True,
254258
last_tag=semver.Version(3, 0, 0),
259+
next_breaking_branch=BranchVersion(major=4, name="v4.x.x"),
255260
),
256261
),
257262
_TestCase(
@@ -265,6 +270,7 @@ class _TestCase: # pylint: disable=too-many-instance-attributes
265270
is_tag_last_minor_for_major=True,
266271
is_tag_latest=True,
267272
last_tag=semver.Version(2, 1, 0),
273+
next_breaking_branch=BranchVersion(major=3, name="v3.x.x"),
268274
),
269275
),
270276
_TestCase(
@@ -279,6 +285,7 @@ class _TestCase: # pylint: disable=too-many-instance-attributes
279285
is_tag_last_minor_for_major=True,
280286
is_tag_latest=True,
281287
last_tag=semver.Version(2, 1, 1),
288+
next_breaking_branch=BranchVersion(major=3, name="v3.x.x"),
282289
),
283290
),
284291
_TestCase(
@@ -291,6 +298,7 @@ class _TestCase: # pylint: disable=too-many-instance-attributes
291298
current_tag=semver.Version(1, 0, 2, "alpha.1"),
292299
is_tag=True,
293300
last_tag=semver.Version(1, 0, 2, "alpha.1"),
301+
next_breaking_branch=BranchVersion(major=2, name="v2.x.x"),
294302
),
295303
),
296304
_TestCase(
@@ -304,6 +312,7 @@ class _TestCase: # pylint: disable=too-many-instance-attributes
304312
is_tag=True,
305313
is_tag_last_minor_for_major=True,
306314
last_tag=semver.Version(1, 1, 0, "beta"),
315+
next_breaking_branch=BranchVersion(major=2, name="v2.x.x"),
307316
),
308317
),
309318
_TestCase(
@@ -318,6 +327,7 @@ class _TestCase: # pylint: disable=too-many-instance-attributes
318327
is_tag_last_minor_for_major=True,
319328
is_tag_latest=True,
320329
last_tag=semver.Version(3, 0, 0, "rc1"),
330+
next_breaking_branch=BranchVersion(major=4, name="v4.x.x"),
321331
),
322332
),
323333
_TestCase(
@@ -332,6 +342,7 @@ class _TestCase: # pylint: disable=too-many-instance-attributes
332342
is_tag_last_minor_for_major=True,
333343
is_tag_latest=True,
334344
last_tag=semver.Version(2, 1, 0, "alpha.1"),
345+
next_breaking_branch=BranchVersion(major=3, name="v3.x.x"),
335346
),
336347
),
337348
_TestCase(
@@ -346,6 +357,7 @@ class _TestCase: # pylint: disable=too-many-instance-attributes
346357
is_tag_last_minor_for_major=True,
347358
is_tag_latest=True,
348359
last_tag=semver.Version(2, 0, 1, "rc1"),
360+
next_breaking_branch=BranchVersion(major=3, name="v3.x.x"),
349361
),
350362
),
351363
_TestCase(
@@ -368,6 +380,7 @@ class _TestCase: # pylint: disable=too-many-instance-attributes
368380
is_branch=True,
369381
next_minor_for_major_branch=1,
370382
last_tag=semver.Version(1, 1, 0, "rc"),
383+
next_breaking_branch=BranchVersion(major=2, name="v2.x.x"),
371384
),
372385
),
373386
_TestCase(
@@ -379,6 +392,7 @@ class _TestCase: # pylint: disable=too-many-instance-attributes
379392
current_branch=BranchVersion(major=1, minor=0, name="v1.0.x"),
380393
is_branch=True,
381394
last_tag=semver.Version(1, 0, 2, "alpha.1"),
395+
next_breaking_branch=BranchVersion(major=2, name="v2.x.x"),
382396
),
383397
),
384398
_TestCase(
@@ -392,6 +406,7 @@ class _TestCase: # pylint: disable=too-many-instance-attributes
392406
is_tag=True,
393407
is_tag_last_minor_for_major=True,
394408
last_tag=semver.Version(1, 0, 0),
409+
next_breaking_branch=BranchVersion(major=2, name="v2.x.x"),
395410
),
396411
),
397412
_TestCase(
@@ -405,6 +420,7 @@ class _TestCase: # pylint: disable=too-many-instance-attributes
405420
is_tag=True,
406421
is_tag_last_minor_for_major=True,
407422
last_tag=semver.Version(1, 0, 2),
423+
next_breaking_branch=BranchVersion(major=2, name="v2.x.x"),
408424
),
409425
),
410426
_TestCase(
@@ -417,6 +433,7 @@ class _TestCase: # pylint: disable=too-many-instance-attributes
417433
current_tag=semver.Version(2, 0, 1),
418434
is_tag=True,
419435
last_tag=semver.Version(2, 0, 1),
436+
next_breaking_branch=BranchVersion(major=3, name="v3.x.x"),
420437
),
421438
),
422439
_TestCase(
@@ -431,6 +448,7 @@ class _TestCase: # pylint: disable=too-many-instance-attributes
431448
is_tag=True,
432449
is_tag_last_minor_for_major=True,
433450
last_tag=semver.Version(1, 0, 0),
451+
next_breaking_branch=BranchVersion(major=2, name="v2.x.x"),
434452
),
435453
),
436454
_TestCase(
@@ -443,6 +461,7 @@ class _TestCase: # pylint: disable=too-many-instance-attributes
443461
current_branch=BranchVersion(major=1, minor=None, name="v1.x.x"),
444462
is_branch=True,
445463
next_minor_for_major_branch=0,
464+
next_breaking_branch=BranchVersion(major=2, name="v2.x.x"),
446465
),
447466
),
448467
_TestCase(
@@ -456,6 +475,7 @@ class _TestCase: # pylint: disable=too-many-instance-attributes
456475
is_branch=True,
457476
next_minor_for_major_branch=1,
458477
last_tag=semver.Version(1, 1, 0, "rc"),
478+
next_breaking_branch=BranchVersion(major=2, name="v2.x.x"),
459479
),
460480
),
461481
_TestCase(
@@ -469,6 +489,57 @@ class _TestCase: # pylint: disable=too-many-instance-attributes
469489
current_branch=BranchVersion(major=1, minor=None, name="v1.x.x"),
470490
is_branch=True,
471491
next_minor_for_major_branch=0,
492+
next_breaking_branch=BranchVersion(major=2, name="v2.x.x"),
493+
),
494+
),
495+
_TestCase(
496+
title="release-0-major-0-minor",
497+
ref="refs/tags/v0.0.1",
498+
expected=_Expected(
499+
ref="refs/tags/v0.0.1",
500+
ref_name="v0.0.1",
501+
current_tag=semver.Version(0, 0, 1),
502+
is_tag=True,
503+
last_tag=semver.Version(0, 0, 1),
504+
next_breaking_branch=BranchVersion(major=0, minor=1, name="v0.1.x"),
505+
),
506+
),
507+
_TestCase(
508+
title="prerelease-0-major-3-minor",
509+
ref="refs/tags/v0.3.1-pre.1+build.3",
510+
expected=_Expected(
511+
ref="refs/tags/v0.3.1-pre.1+build.3",
512+
ref_name="v0.3.1-pre.1+build.3",
513+
current_tag=semver.Version(0, 3, 1, "pre.1", "build.3"),
514+
is_tag=True,
515+
is_tag_last_minor_for_major=True,
516+
last_tag=semver.Version(0, 3, 1, "pre.1", "build.3"),
517+
next_breaking_branch=BranchVersion(major=0, minor=4, name="v0.4.x"),
518+
),
519+
),
520+
_TestCase(
521+
title="branch-0-major",
522+
ref="refs/heads/v0.x.x",
523+
expected=_Expected(
524+
ref="refs/heads/v0.x.x",
525+
ref_name="v0.x.x",
526+
current_branch=BranchVersion(major=0, name="v0.x.x"),
527+
is_branch=True,
528+
next_minor_for_major_branch=2,
529+
last_tag=semver.Version(0, 1, 0),
530+
next_breaking_branch=BranchVersion(major=0, minor=2, name="v0.2.x"),
531+
),
532+
),
533+
_TestCase(
534+
title="branch-0-major-with-minor",
535+
ref="refs/heads/v0.0.x",
536+
expected=_Expected(
537+
ref="refs/heads/v0.0.x",
538+
ref_name="v0.0.x",
539+
current_branch=BranchVersion(major=0, minor=0, name="v0.0.x"),
540+
is_branch=True,
541+
last_tag=semver.Version(0, 0, 1),
542+
next_breaking_branch=BranchVersion(major=0, minor=1, name="v0.1.x"),
472543
),
473544
),
474545
]
@@ -513,3 +584,7 @@ def test_repo_version(
513584
last_tag = repo_version_info.find_last_tag()
514585
assert last_tag is not None
515586
assert last_tag == case.expected.last_tag
587+
assert (
588+
repo_version_info.find_next_breaking_branch()
589+
== case.expected.next_breaking_branch
590+
)

0 commit comments

Comments
 (0)