Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions doc/autorelease.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ the different portions of the release field:
For instance, this can be used to keep release numbers on older Fedora releases way lower than on
newer ones for the same package version.

* ``-r <minorbump>``: Allows specifying a custom rightmost (minor) release number (the default is 0).

This is only used when previous commit contained ``[start rightmost]`` line or rightmost number is
greater than 0. This provides a different way to ensure release stays lower on older Fedora releases.
Rightmost bumps can be started by explicit ``%autorelease -r 1``.

* ``-n``: Don’t render the dist tag, e.g. for use in macros, if the dist tag is added later.

.. important::
Expand Down Expand Up @@ -54,6 +60,36 @@ seamlessly.
You don’t have to undo this later, when the version changes, the release will be reset to 1 (or the
value specified by ``%autorelease -b …``).

Rightmost minor release numbers
===============================

You can also continue doing only rightmost bumps. Easiest way is to use magic comment in commit::

[start rightmost]

This would freeze current release number and increment separate number on the right of release.

Use case for this would be for multiple supported branches, which contain equivalent changes.
Example might be rebased version in multiple branches. But latest branch build should be considered latest,
even if older stable branch contained more commits modifying spec file. Stable branch can use this
magic comment to be always lower than development branch like rawhide.

This provides similar functionality to ``rpmdev-bumpspec -r`` tool, but generated from commit messages.

Rightmost bumping release numbers
=================================

You can bump also the rightmost number to a higher value. Similar to ``bump release`` magic comment,
you can bump rightmost number to a higher value.::

[bump rightmost: <number>]

This will start rightmost bumps if not yet started already. Sometimes it might be needed to start
different number than trailing ``.1`` on right side of the release.

Alternative to this commit line is using ``%autorelease -r …``. You don't have to undo this later,
it will reset to 1 when version or release number changes.

Examples
========

Expand Down Expand Up @@ -107,6 +143,7 @@ those added fields:
* ``-p``: Designates a pre-release, i.e. ``pkgrel`` will be prefixed with ``0.``.
* ``-e <extraver>``: Allows specifying the ``extraver`` portion of the release.
* ``-s <snapinfo>``: Allows specifying the ``snapinfo`` portion of the release.
* ``-r <minorbase>``: Allows specifying the ``minorbump`` portion of the release.

In the modern versioning, those fields are embedded in the package `Version` instead.

Expand Down
11 changes: 9 additions & 2 deletions rpm_macros.d/macros.rpmautospec
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
%autorelease(e:s:pb:n) %{?-p:0.}%{lua:
%autorelease(e:s:pb:r:n) %{?-p:0.}%{lua:
release_number = tonumber(rpm.expand("%{?_rpmautospec_release_number}%{!?_rpmautospec_release_number:1}"));
base_release_number = tonumber(rpm.expand("%{?-b*}%{!?-b:1}"));
print(release_number + base_release_number - 1);
}%{?-e:.%{-e*}}%{?-s:.%{-s*}}%{!?-n:%{?dist}}
}%{?-e:.%{-e*}}%{?-s:.%{-s*}}%{!?-n:%{?dist}}%{lua:
rightmost_number = tonumber(rpm.expand("%{?_rpmautospec_rightmost_number}%{!?_rpmautospec_rightmost_number:0}"));
rightmost_base_number = tonumber(rpm.expand("%{?-r*}%{!?-r:0}"));
if ((rightmost_number + rightmost_base_number) > 0) then
print(string.format(".%d", rightmost_number + rightmost_base_number));
end;
}

%autochangelog %{lua:
locale = os.setlocale(nil)
os.setlocale("C.utf8")
Expand Down
79 changes: 66 additions & 13 deletions rpmautospec/magic_comments.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,79 @@
import logging
import re
from typing import NamedTuple
import typing

log = logging.getLogger(__name__)

magic_comment_re = re.compile(r"^\s*\[(?P<magic>.*)\]\s*$")
skip_changelog_re = re.compile(r"\s*skip\s+changelog\s*")
bump_release_re = re.compile(r"\s*bump\s+release\s*(?::\s*)?(?P<bump_value>\d+)\s*")
intvalue_re = re.compile(r"(?P<keyname>[a-z]+(?:[\s_]+[a-z]+)*)\s*(?::\s*)?(?P<value>\d+)")


class MagicCommentResult(typing.NamedTuple):
"""Stores special commit parameters extracted from a commit message."""

skip_changelog: bool = False
"""Do not include this commit in changelog."""

start_rightmost: bool = False
"""Start increasing number on the right of release string, not left."""

bump_release: int = 0
"""Bump release to specified number."""

bump_rightmost: int = 0
"""Bump rightmost (minorbump) of the release to specified number."""


types = typing.get_type_hints(MagicCommentResult)

class MagicCommentResult(NamedTuple):
skip_changelog: bool
bump_release: int

def _get_keyname(keyname: str) -> str:
return keyname.strip().replace(" ", "_")


def parse_magic_comments(message: str) -> MagicCommentResult:
skip_changelog = False
bump_release = 0
"""Parses commit message for special lines providing extra features.

Reworked to extract keywords from MagicCommentResult directly.
Should reduce code repetition and simplify documentation of supported keywords.

It does so by replacing spaces with _ underscore, then searching in defined
class properties. Do not define extra regex for everything.

Supports parsing of bool presences of:

[skip changelog]
[start rightmost]

Supports parsing of int values of:

[bump release: 3]
[bump rightmost: 4]
"""

values = dict()

for line in message.split("\n"):
if l_match := magic_comment_re.match(line):
for part in l_match.group("magic").split(","):
if skip_changelog_re.match(part):
skip_changelog = True
if br_match := bump_release_re.match(part):
bump_release = max(bump_release, int(br_match.group("bump_value")))
if br_match := intvalue_re.match(part):
keyname = _get_keyname(br_match.group("keyname"))
t = types.get(keyname, None)
value = br_match.group("value")
if types.get(keyname, None) is int:
values[keyname] = int(value)
elif t is not None:
log.warning(f'Parameter "{keyname}" is known, but {t} is not int')
else:
log.warning(f'Parameter "{keyname}" is not int type magic comment')
else:
keyname = _get_keyname(part)
t = types.get(keyname, None)
if t is bool:
values[keyname] = True
elif t is not None:
log.warning(f'Parameter "{keyname}" is known, but {t} is not bool')
else:
log.warning(f'Parameter "{keyname}" is not known magic comment')

return MagicCommentResult(skip_changelog=skip_changelog, bump_release=bump_release)
return MagicCommentResult(**values)
110 changes: 77 additions & 33 deletions rpmautospec/pkg_history.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ def __init__(self, spec_or_path: Union[str, Path]):
self.repo = None

self._rpmverflags_for_commits = {}
self.rightmost_bumps = False

@staticmethod
def _get_rpm_packager() -> str:
Expand All @@ -96,6 +97,19 @@ def _get_rpm_packager() -> str:
except Exception:
return fallback

@staticmethod
def _make_rpmvererror(error, error_detail) -> dict[str, Union[str, int]]:
return {
"epoch-version": None,
"prerelease": False,
"extraver": None,
"snapinfo": None,
"base": 1,
"rightmost-base": 0,
"error": error,
"error-detail": error_detail,
}

def _get_rpmverflags(
self, path: str, name: Optional[str] = None, log_error: bool = True
) -> dict[str, Union[str, int]]:
Expand All @@ -109,7 +123,7 @@ def _get_rpmverflags(

if not specfile.exists():
log.debug("spec file missing: %s", specfile)
return {"error": "specfile-missing", "error-detail": "Spec file is missing."}
return self._make_rpmvererror("specfile-missing", "Spec file is missing.")

with (
specfile.open(mode="rb") as unabridged,
Expand Down Expand Up @@ -144,7 +158,7 @@ def _get_rpmverflags(
if error:
if log_error:
log.debug("spec file query failed: %s", rpmerr_out)
return {"error": "specfile-parse-error", "error-detail": rpmerr_out}
return self._make_rpmvererror("specfile-parse-error", rpmerr_out)

match = self.autorelease_flags_re.match(info)
if match:
Expand Down Expand Up @@ -181,7 +195,7 @@ def _get_rpmverflags_for_commit(self, commit: pygit2.Commit) -> dict[str, Union[
specblob = commit.tree[self.specfile.name]
except KeyError:
# no spec file
error = {"error": "specfile-missing", "error-detail": "Spec file is missing."}
error = self._make_rpmvererror("specfile-missing", "Spec file is missing.")
self._rpmverflags_for_commits[commit] = error
return error

Expand All @@ -198,6 +212,22 @@ def _get_rpmverflags_for_commit(self, commit: pygit2.Commit) -> dict[str, Union[
self._rpmverflags_for_commits[commit] = rpmverflags
return rpmverflags

@staticmethod
def _get_tagstring(verflags):
return "".join(f".{t}" for t in (verflags["extraver"], verflags["snapinfo"]) if t)

@staticmethod
def _get_release_complete(verflags, release_number, rightmost_number=0, tag_string=""):
prerel_str = "0." if verflags["prerelease"] else ""
base = verflags["base"] or 1
rightmost_base = verflags.get("rightmost-base", 0) or 0
release_number_with_base = release_number + base - 1
rightmost_with_base = rightmost_number + rightmost_base
rightmost_str = "" # hide rightmost unless used
if rightmost_with_base > 0:
rightmost_str = "." + str(rightmost_with_base)
return f"{prerel_str}{release_number_with_base}{tag_string}{rightmost_str}"

def release_number_visitor(self, commit: pygit2.Commit, child_info: dict[str, Any]):
"""Visit a commit to determine its release number.

Expand All @@ -213,14 +243,9 @@ def release_number_visitor(self, commit: pygit2.Commit, child_info: dict[str, An

if "error" not in verflags:
epoch_version = verflags["epoch-version"]
prerelease = verflags["prerelease"]
base = verflags["base"]
if base is None:
base = 1
tag_string = "".join(f".{t}" for t in (verflags["extraver"], verflags["snapinfo"]) if t)
tag_string = self._get_tagstring(verflags)
else:
epoch_version = prerelease = None
base = 1
epoch_version = None
tag_string = ""

if not epoch_version:
Expand All @@ -247,13 +272,22 @@ def release_number_visitor(self, commit: pygit2.Commit, child_info: dict[str, An

commit_result["verflags"] = commit_verflags
commit_result["epoch-version"] = epoch_version
commit_result["magic-comment-result"] = parse_magic_comments(commit.message)
magic_comments = parse_magic_comments(commit.message)
commit_result["magic-comment-result"] = magic_comments

if magic_comments.start_rightmost or magic_comments.bump_rightmost > 0:
log.debug("\tstarting rightmost bumps")
self.rightmost_bumps = True

log.debug("\tepoch_version: %s", epoch_version)
log.debug(
"\tparent rel numbers: %s",
", ".join(str(res["release-number"]) if res else "none" for res in parent_results),
)
log.debug(
"\tparent rightmost numbers: %s",
", ".join(str(res["rightmost-number"]) if res else "none" for res in parent_results),
)

# Find the maximum applicable parent release number and increment by one if the
# epoch-version can be parsed from the spec file.
Expand All @@ -272,20 +306,45 @@ def release_number_visitor(self, commit: pygit2.Commit, child_info: dict[str, An
)
for res in parent_results
)

release_number = max(parent_release_numbers, default=0)

parent_rightmost_numbers = tuple(
(
res["rightmost-number"]
if res
and (
# Now increase rightmost only until epoch-version or release-number changes
epoch_version is None
or res["epoch-version"] is None
or epoch_version == res["epoch-version"]
or res["release-number"] == release_number
)
else 0
)
for res in parent_results
)

rightmost_number = max(parent_rightmost_numbers, default=0)

if self.specfile.name in commit.tree:
release_number += 1
if self.rightmost_bumps:
rightmost_number += 1
else:
release_number += 1

release_number = max(release_number, commit_result["magic-comment-result"].bump_release)
release_number = max(release_number, magic_comments.bump_release)
rightmost_number = max(rightmost_number, magic_comments.bump_rightmost)

commit_result["release-number"] = release_number
commit_result["rightmost-number"] = rightmost_number

log.debug("\trelease_number: %s", release_number)
log.debug("\trightmost_number: %s", rightmost_number)

prerel_str = "0." if prerelease else ""
release_number_with_base = release_number + base - 1
commit_result["release-complete"] = f"{prerel_str}{release_number_with_base}{tag_string}"
commit_result["release-complete"] = self._get_release_complete(
verflags, release_number, rightmost_number, tag_string=tag_string
)

yield commit_result

Expand Down Expand Up @@ -699,15 +758,6 @@ def run(
worktree_result = {}

verflags = self._get_rpmverflags(self.path, name=self.name)
if "error" in verflags:
# cringe, but what can you do?
verflags |= {
"epoch-version": None,
"prerelease": False,
"extraver": None,
"snapinfo": None,
"base": 1,
}

# Mimic the bottom half of release_number_visitor
worktree_result["verflags"] = verflags
Expand All @@ -718,14 +768,8 @@ def run(
release_number = 1
worktree_result["release-number"] = release_number

prerel_str = "0." if verflags["prerelease"] else ""
tag_string = "".join(f".{t}" for t in (verflags["extraver"], verflags["snapinfo"]) if t)
base = verflags["base"]
if base is None:
base = 1
release_number_with_base = release_number + base - 1
worktree_result["release-complete"] = release_complete = (
f"{prerel_str}{release_number_with_base}{tag_string}"
worktree_result["release-complete"] = release_complete = self._get_release_complete(
verflags, release_number, tag_string=self._get_tagstring(verflags)
)

# Mimic the bottom half of the changelog visitor for a generic entry
Expand Down
Loading