Skip to content
Merged
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
76 changes: 70 additions & 6 deletions specfile/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import re
import shutil
import subprocess
from enum import Enum, auto, unique
from typing import List, Optional, Union, overload

from specfile.exceptions import SpecfileException
Expand All @@ -34,6 +35,24 @@
"Dec",
)

_OPENSUSE_CHANGELOG_SEPARATOR = 67 * "-"


@unique
class ChangelogStyle(Enum):
"""Style of changelog entries"""

#: standard changelog entries parseable by RPM (used in Fedora, RHEL, etc.):
#: * $DATE $AUTHOR <$EMAIL> - $EVR
#: $ENTRY
standard = auto()

#: openSUSE/SUSE style detached changelog:
#: -------------------------------------------------------------------
#: $DATE - $AUTHOR <$EMAIL>
#: $ENTRY
openSUSE = auto()


class ChangelogEntry:
"""
Expand Down Expand Up @@ -168,6 +187,13 @@ def day_of_month_padding(self) -> str:
return ""
return m.group("wsp") + (m.group("zp") or "")

@property
def style(self) -> ChangelogStyle:
"""Style of this changelog entry (standard vs openSUSE)."""
if self.header.startswith(_OPENSUSE_CHANGELOG_SEPARATOR):
return ChangelogStyle.openSUSE
return ChangelogStyle.standard

@classmethod
def assemble(
cls,
Expand All @@ -177,37 +203,63 @@ def assemble(
evr: Optional[str] = None,
day_of_month_padding: str = "0",
append_newline: bool = True,
style: ChangelogStyle = ChangelogStyle.standard,
) -> "ChangelogEntry":
"""
Assembles a changelog entry.

Args:
timestamp: Timestamp of the entry.
Supply `datetime` rather than `date` for extended format.
openSUSE-style changelog entries mandate extended format, so if a `date`
is supplied, the timestamp will be set to noon of that day.
author: Author of the entry.
content: List of lines forming the content of the entry.
evr: EVR (epoch, version, release) of the entry.
Ignored if `style` is `ChangelogStyle.openSUSE`.
day_of_month_padding: Padding to apply to day of month in the timestamp.
append_newline: Whether the entry should be followed by an empty line.
style: Which style of changelog should be created.

Returns:
New instance of `ChangelogEntry` class.
"""
weekday = WEEKDAYS[timestamp.weekday()]
month = MONTHS[timestamp.month - 1]
header = f"* {weekday} {month}"

if style == ChangelogStyle.standard:
header = "* "
else:
header = _OPENSUSE_CHANGELOG_SEPARATOR + "\n"
header += f"{weekday} {month}"

if day_of_month_padding.endswith("0"):
header += f" {day_of_month_padding[:-1]}{timestamp.day:02}"
else:
header += f" {day_of_month_padding}{timestamp.day}"

# convert to extended format for openSUSE style changelogs
if style == ChangelogStyle.openSUSE and not isinstance(
timestamp, datetime.datetime
):
timestamp = datetime.datetime(
year=timestamp.year, month=timestamp.month, day=timestamp.day, hour=12
)

if isinstance(timestamp, datetime.datetime):
# extended format
if not timestamp.tzinfo:
timestamp = timestamp.replace(tzinfo=datetime.timezone.utc)
header += f" {timestamp:%H:%M:%S %Z}"
header += f" {timestamp:%Y} {author}"
if evr is not None:
header += f" {timestamp:%Y} "

if style == ChangelogStyle.openSUSE:
header += "- "
header += author

if evr is not None and style == ChangelogStyle.standard:
header += f" - {evr}"

return cls(header, content, [""] if append_newline else None)


Expand Down Expand Up @@ -350,13 +402,25 @@ def extract_following_lines(content: List[str]) -> List[str]:
predecessor = []
header = None
content: List[str] = []
for line in section:
if line.startswith("*"):

for i, line in enumerate(section):
if line == _OPENSUSE_CHANGELOG_SEPARATOR:
continue

prev_line_is_opensuse_separator = (
i >= 1 and section[i - 1] == _OPENSUSE_CHANGELOG_SEPARATOR
)
if line.startswith("*") or prev_line_is_opensuse_separator:
if header is None or "".join(content).strip():
if header:
following_lines = extract_following_lines(content)
data.insert(0, ChangelogEntry(header, content, following_lines))
header = line

if prev_line_is_opensuse_separator:
header = _OPENSUSE_CHANGELOG_SEPARATOR + "\n"
else:
header = ""
header += line
content = []
else:
content.append(line)
Expand Down
143 changes: 141 additions & 2 deletions tests/unit/test_changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@

import copy
import datetime
from typing import Optional
from typing import List, Optional, Union

import pytest

from specfile.changelog import Changelog, ChangelogEntry
from specfile.changelog import (
_OPENSUSE_CHANGELOG_SEPARATOR,
Changelog,
ChangelogEntry,
ChangelogStyle,
)
from specfile.sections import Section
from specfile.utils import EVR

Expand Down Expand Up @@ -240,6 +245,140 @@ def test_parse():
]
assert not changelog[6].extended_timestamp

assert all(
changelog_entry.style == ChangelogStyle.standard
for changelog_entry in changelog
)


def test_suse_style_changelog_parse():
changelog = Changelog.parse(
Section(
"changelog",
data=[
"-------------------------------------------------------------------",
(
hdr1 := "Tue Dec 17 14:21:37 UTC 2024 - "
+ (dc := "Dan Čermák <[email protected]>")
),
"",
(content1 := "- First version"),
"",
"-------------------------------------------------------------------",
(hdr2 := f"Mon Nov 4 17:47:23 UTC 2024 - {dc}"),
"",
(content2 := "- # [0.9.37] - September 4th, 2024"),
"",
"-------------------------------------------------------------------",
(
hdr3 := "Fri May 17 09:14:20 UTC 2024 - "
+ "Dominique Leuenberger <[email protected]>"
),
"",
(content3 := "- Use %patch -P N instead of deprecated %patchN syntax."),
"",
"-------------------------------------------------------------------",
(
hdr4 := "Mon Oct 10 13:27:24 UTC 2022 - Stephan Kulow <[email protected]>"
),
"",
(content4_1 := "updated to version 0.9.28"),
(content4_2 := " see installed CHANGELOG.md"),
"",
"",
"-------------------------------------------------------------------",
(
hdr5 := "Fri Jun 25 07:31:34 UTC 2021 - Dan Čermák <[email protected]>"
),
"",
(content5_1 := "- New upstream release 0.9.26"),
"",
(content5_2 := " - Add support for Ruby 3.0 and fix tests"),
(
content5_3 := " - Fix support for `frozen_string_literal: false`"
+ " magic comments (#1363)"
),
"",
"",
],
)
)

assert isinstance(changelog, Changelog)
assert len(changelog) == 5

for changelog_entry, hdr, content in zip(
changelog,
reversed((hdr1, hdr2, hdr3, hdr4, hdr5)),
reversed(
(
[content1],
[content2],
[content3],
[content4_1, content4_2],
[content5_1, "", content5_2, content5_3],
)
),
):

assert isinstance(changelog_entry, ChangelogEntry)
assert changelog_entry.evr is None
assert changelog_entry.header == _OPENSUSE_CHANGELOG_SEPARATOR + "\n" + hdr
assert changelog_entry.content == [""] + content
assert changelog_entry.extended_timestamp
assert changelog_entry.style == ChangelogStyle.openSUSE


@pytest.mark.parametrize(
"timestamp,author,content,entry",
(
[
(
datetime.datetime(2021, 6, 25, 7, 31, 34),
"Dan Čermák <[email protected]>",
content_1 := ["", "New upstream release 0.9.26"],
ChangelogEntry(
header=_OPENSUSE_CHANGELOG_SEPARATOR
+ "\n"
+ "Fri Jun 25 07:31:34 UTC 2021 - Dan Čermák <[email protected]>",
content=content_1,
),
),
(
datetime.date(2021, 6, 25),
"Dan Čermák <[email protected]>",
content_2 := [
"",
"New upstream release 0.26",
"Fixed a major regression in Foo",
],
ChangelogEntry(
header=_OPENSUSE_CHANGELOG_SEPARATOR
+ "\n"
+ "Fri Jun 25 12:00:00 UTC 2021 - Dan Čermák <[email protected]>",
content=content_2,
),
),
]
),
)
def test_create_opensuse_changelog_assemble(
timestamp: Union[datetime.datetime, datetime.date],
author: str,
content: List[str],
entry: ChangelogEntry,
) -> None:
assert (
ChangelogEntry.assemble(
timestamp,
author,
content,
style=ChangelogStyle.openSUSE,
append_newline=False,
)
== entry
)


def test_get_raw_section_data():
tzinfo = datetime.timezone(datetime.timedelta(hours=2), name="CEST")
Expand Down
Loading