From 862ba956956b46214e1a75666c6c1099f4e379a3 Mon Sep 17 00:00:00 2001 From: CentOS Sources Date: Mar 30 2021 12:48:58 +0000 Subject: import dnf-4.4.2-10.el8 --- diff --git a/.dnf.metadata b/.dnf.metadata index 0bcc7b5..9125cc8 100644 --- a/.dnf.metadata +++ b/.dnf.metadata @@ -1 +1 @@ -0da07a3e6ff19430ffe39699e474439eab63ee7d SOURCES/dnf-4.2.23.tar.gz +5941a49cfd466aeed4ec882a33647912c2a89245 SOURCES/dnf-4.4.2.tar.gz diff --git a/.gitignore b/.gitignore index c0cb815..378380e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -SOURCES/dnf-4.2.23.tar.gz +SOURCES/dnf-4.4.2.tar.gz diff --git a/SOURCES/0001-Handle-empty-comps-group-name-RhBug1826198.patch b/SOURCES/0001-Handle-empty-comps-group-name-RhBug1826198.patch deleted file mode 100644 index f6266a4..0000000 --- a/SOURCES/0001-Handle-empty-comps-group-name-RhBug1826198.patch +++ /dev/null @@ -1,238 +0,0 @@ -From 3c758a4ea670fab1f4b55fa878ebf2b2ff4b678b Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hr=C3=A1zk=C3=BD?= -Date: Tue, 28 Apr 2020 09:08:05 +0200 -Subject: [PATCH] Handle empty comps group name (RhBug:1826198) - -Don't crash on empty comps group/environment name. In outputs, use the -"" placeholder instead of the name. - -https://bugzilla.redhat.com/show_bug.cgi?id=1826198 ---- - dnf/cli/commands/group.py | 4 ++-- - dnf/cli/output.py | 16 ++++++++++------ - dnf/comps.py | 11 ++++++++++- - dnf/db/group.py | 12 ++++++++---- - tests/repos/main_comps.xml | 7 +++++++ - tests/support.py | 2 +- - tests/test_comps.py | 6 +++--- - tests/test_groups.py | 9 +++++++++ - 8 files changed, 50 insertions(+), 17 deletions(-) - -diff --git a/dnf/cli/commands/group.py b/dnf/cli/commands/group.py -index f535a50980..4ffd3b89c8 100644 ---- a/dnf/cli/commands/group.py -+++ b/dnf/cli/commands/group.py -@@ -177,7 +177,7 @@ def _list(self, userlist): - def _out_grp(sect, group): - if not done: - print(sect) -- msg = ' %s' % group.ui_name -+ msg = ' %s' % (group.ui_name if group.ui_name is not None else _("")) - if print_ids: - msg += ' (%s)' % group.id - if group.lang_only: -@@ -188,7 +188,7 @@ def _out_env(sect, envs): - if envs: - print(sect) - for e in envs: -- msg = ' %s' % e.ui_name -+ msg = ' %s' % (e.ui_name if e.ui_name is not None else _("")) - if print_ids: - msg += ' (%s)' % e.id - print(msg) -diff --git a/dnf/cli/output.py b/dnf/cli/output.py -index 67eab80b19..2585a5c773 100644 ---- a/dnf/cli/output.py -+++ b/dnf/cli/output.py -@@ -1221,47 +1221,51 @@ def _add_line(lines, data, a_wid, po, obsoletes=[]): - lines.append((name, "", "", "", "", "", "")) - pkglist_lines.append((action, lines)) - if self.base._history: -+ def format_line(group): -+ name = group.getName() -+ return (name if name else _(""), "", "", "", "", "", "") -+ - install_env_group = self.base._history.env._installed - if install_env_group: - action = _("Installing Environment Groups") - lines = [] - for group in install_env_group.values(): -- lines.append((group.getName(), "", "", "", "", "", "")) -+ lines.append(format_line(group)) - pkglist_lines.append((action, lines)) - upgrade_env_group = self.base._history.env._upgraded - if upgrade_env_group: - action = _("Upgrading Environment Groups") - lines = [] - for group in upgrade_env_group.values(): -- lines.append((group.getName(), "", "", "", "", "", "")) -+ lines.append(format_line(group)) - pkglist_lines.append((action, lines)) - remove_env_group = self.base._history.env._removed - if remove_env_group: - action = _("Removing Environment Groups") - lines = [] - for group in remove_env_group.values(): -- lines.append((group.getName(), "", "", "", "", "", "")) -+ lines.append(format_line(group)) - pkglist_lines.append((action, lines)) - install_group = self.base._history.group._installed - if install_group: - action = _("Installing Groups") - lines = [] - for group in install_group.values(): -- lines.append((group.getName(), "", "", "", "", "", "")) -+ lines.append(format_line(group)) - pkglist_lines.append((action, lines)) - upgrade_group = self.base._history.group._upgraded - if upgrade_group: - action = _("Upgrading Groups") - lines = [] - for group in upgrade_group.values(): -- lines.append((group.getName(), "", "", "", "", "", "")) -+ lines.append(format_line(group)) - pkglist_lines.append((action, lines)) - remove_group = self.base._history.group._removed - if remove_group: - action = _("Removing Groups") - lines = [] - for group in remove_group.values(): -- lines.append((group.getName(), "", "", "", "", "", "")) -+ lines.append(format_line(group)) - pkglist_lines.append((action, lines)) - # show skipped conflicting packages - if not self.conf.best and self.base._goal.actions & forward_actions: -diff --git a/dnf/comps.py b/dnf/comps.py -index 316d647087..4ca15b1e07 100644 ---- a/dnf/comps.py -+++ b/dnf/comps.py -@@ -75,7 +75,16 @@ def _by_pattern(pattern, case_sensitive, sqn): - else: - match = re.compile(fnmatch.translate(pattern), flags=re.I).match - -- return {g for g in sqn if match(g.name) or match(g.id) or match(g.ui_name)} -+ ret = set() -+ for g in sqn: -+ if match(g.id): -+ ret.add(g) -+ elif g.name is not None and match(g.name): -+ ret.add(g) -+ elif g.ui_name is not None and match(g.ui_name): -+ ret.add(g) -+ -+ return ret - - - def _fn_display_order(group): -diff --git a/dnf/db/group.py b/dnf/db/group.py -index e3a087760b..5d7e18d1a8 100644 ---- a/dnf/db/group.py -+++ b/dnf/db/group.py -@@ -78,8 +78,10 @@ def _get_obj_id(self, obj): - def new(self, obj_id, name, translated_name, pkg_types): - swdb_group = self.history.swdb.createCompsGroupItem() - swdb_group.setGroupId(obj_id) -- swdb_group.setName(name) -- swdb_group.setTranslatedName(translated_name) -+ if name is not None: -+ swdb_group.setName(name) -+ if translated_name is not None: -+ swdb_group.setTranslatedName(translated_name) - swdb_group.setPackageTypes(pkg_types) - return swdb_group - -@@ -136,8 +138,10 @@ def _get_obj_id(self, obj): - def new(self, obj_id, name, translated_name, pkg_types): - swdb_env = self.history.swdb.createCompsEnvironmentItem() - swdb_env.setEnvironmentId(obj_id) -- swdb_env.setName(name) -- swdb_env.setTranslatedName(translated_name) -+ if name is not None: -+ swdb_env.setName(name) -+ if translated_name is not None: -+ swdb_env.setTranslatedName(translated_name) - swdb_env.setPackageTypes(pkg_types) - return swdb_env - -diff --git a/tests/repos/main_comps.xml b/tests/repos/main_comps.xml -index 9e694d13a5..584bb25b3a 100644 ---- a/tests/repos/main_comps.xml -+++ b/tests/repos/main_comps.xml -@@ -49,6 +49,13 @@ - brokendeps - - -+ -+ missing-name-group -+ -+ -+ meaning-of-life -+ -+ - - base-system - 99 -diff --git a/tests/support.py b/tests/support.py -index e549ba5b95..a7d6a8542c 100644 ---- a/tests/support.py -+++ b/tests/support.py -@@ -94,7 +94,7 @@ def mock_open(mock=None, data=None): - MAIN_NSOLVABLES = 9 - UPDATES_NSOLVABLES = 4 - AVAILABLE_NSOLVABLES = MAIN_NSOLVABLES + UPDATES_NSOLVABLES --TOTAL_GROUPS = 4 -+TOTAL_GROUPS = 5 - TOTAL_NSOLVABLES = SYSTEM_NSOLVABLES + AVAILABLE_NSOLVABLES - - -diff --git a/tests/test_comps.py b/tests/test_comps.py -index 30d468e3af..763218587f 100644 ---- a/tests/test_comps.py -+++ b/tests/test_comps.py -@@ -107,7 +107,7 @@ def test_group_packages(self): - def test_iteration(self): - comps = self.comps - self.assertEqual([g.name for g in comps.groups_iter()], -- ['Base', 'Solid Ground', "Pepper's", "Broken Group"]) -+ ['Base', 'Solid Ground', "Pepper's", "Broken Group", None]) - self.assertEqual([c.name for c in comps.categories_iter()], - ['Base System']) - g = dnf.util.first(comps.groups_iter()) -@@ -115,7 +115,7 @@ def test_iteration(self): - - def test_group_display_order(self): - self.assertEqual([g.name for g in self.comps.groups], -- ["Pepper's", 'Base', 'Solid Ground', 'Broken Group']) -+ ["Pepper's", 'Base', 'Solid Ground', 'Broken Group', None]) - - def test_packages(self): - comps = self.comps -@@ -127,7 +127,7 @@ def test_packages(self): - - def test_size(self): - comps = self.comps -- self.assertLength(comps, 6) -+ self.assertLength(comps, 7) - self.assertLength(comps.groups, tests.support.TOTAL_GROUPS) - self.assertLength(comps.categories, 1) - self.assertLength(comps.environments, 1) -diff --git a/tests/test_groups.py b/tests/test_groups.py -index fe388f96c0..8972da687e 100644 ---- a/tests/test_groups.py -+++ b/tests/test_groups.py -@@ -295,6 +295,15 @@ def test_group_install_broken_optional_nonstrict(self): - self.assertLength(inst, 1) - self.assertEmpty(removed) - -+ def test_group_install_missing_name(self): -+ comps_group = self.base.comps.group_by_pattern('missing-name-group') -+ -+ cnt = self.base.group_install(comps_group.id, ('mandatory', 'default', 'optional'), -+ strict=False) -+ self._swdb_commit() -+ self.base.resolve() -+ self.assertEqual(cnt, 1) -+ - - class EnvironmentInstallTest(tests.support.ResultTestCase): - """Set up a test where sugar is considered not installed.""" diff --git a/SOURCES/0001-tests-SQL-write-a-readonly-folder.patch b/SOURCES/0001-tests-SQL-write-a-readonly-folder.patch new file mode 100644 index 0000000..39780f4 --- /dev/null +++ b/SOURCES/0001-tests-SQL-write-a-readonly-folder.patch @@ -0,0 +1,45 @@ +From 66e08009b8254462cb2c454ff2320355633c20d6 Mon Sep 17 00:00:00 2001 +From: Nicola Sella +Date: Tue, 10 Nov 2020 12:11:17 +0100 +Subject: [PATCH 1/1] [tests] SQL write a readonly folder + +fixes on rhel8.4 for test_dnf_base and test_dnf_db_group +libdnf._error.Error: SQLite error on "/var/lib/dnf/history.sqlite": +Executing an SQL statement failed: attempt to write a readonly +database + +=changelog= +msg: fixes SQL write a readonly folder +type: bugfix +--- + tests/api/test_dnf_base.py | 1 + + tests/api/test_dnf_db_group.py | 1 + + 2 files changed, 2 insertions(+) + +diff --git a/tests/api/test_dnf_base.py b/tests/api/test_dnf_base.py +index b1cf49fb..ca71b75c 100644 +--- a/tests/api/test_dnf_base.py ++++ b/tests/api/test_dnf_base.py +@@ -14,6 +14,7 @@ from .common import TOUR_4_4 + class DnfBaseApiTest(TestCase): + def setUp(self): + self.base = dnf.Base(dnf.conf.Conf()) ++ self.base.conf.persistdir = "/tmp/tests" + + def tearDown(self): + self.base.close() +diff --git a/tests/api/test_dnf_db_group.py b/tests/api/test_dnf_db_group.py +index 447fd121..e1828cb4 100644 +--- a/tests/api/test_dnf_db_group.py ++++ b/tests/api/test_dnf_db_group.py +@@ -12,6 +12,7 @@ from .common import TestCase + class DnfRPMTransactionApiTest(TestCase): + def setUp(self): + self.base = dnf.Base(dnf.conf.Conf()) ++ self.base.conf.persistdir = "/tmp/tests" + self.base.fill_sack(False, False) + self.base.resolve() + self.rpmTrans = self.base.transaction +-- +2.26.2 + diff --git a/SOURCES/0002-Revert-Fix-setopt-cachedir-writing-outside-of-installroot.patch b/SOURCES/0002-Revert-Fix-setopt-cachedir-writing-outside-of-installroot.patch new file mode 100644 index 0000000..a844311 --- /dev/null +++ b/SOURCES/0002-Revert-Fix-setopt-cachedir-writing-outside-of-installroot.patch @@ -0,0 +1,26 @@ +From c2e4901cec947e5be2e5ff5afa22691841d00bdc Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hr=C3=A1zk=C3=BD?= +Date: Tue, 10 Nov 2020 13:46:57 +0100 +Subject: [PATCH] Revert "Fix --setopt=cachedir writing outside of installroot" + +This reverts commit 70fffff61f7a006d406b46adc592d21a685c12a8. + +The commit breaks resetting excludes with an empty --exclude= CLI switch +if the excludes were set in the config file. +--- + dnf/cli/cli.py | 2 ++ + 1 file changed, 2 insertions(+) + +diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py +index b5f7bca07b..5878c2aa15 100644 +--- a/dnf/cli/cli.py ++++ b/dnf/cli/cli.py +@@ -974,6 +974,8 @@ def configure(self, args, option_parser=None): + + self.base.configure_plugins() + ++ self.base.conf._configure_from_options(opts) ++ + self.command.configure() + + if self.base.conf.destdir: diff --git a/SOURCES/0003-Post-transaction-summary-is-logged-for-API-users-RhBug-1855158.patch b/SOURCES/0003-Post-transaction-summary-is-logged-for-API-users-RhBug-1855158.patch new file mode 100644 index 0000000..d34928f --- /dev/null +++ b/SOURCES/0003-Post-transaction-summary-is-logged-for-API-users-RhBug-1855158.patch @@ -0,0 +1,567 @@ +From 9ed390d08a9f2b66f4e352532fa526fc64e329d4 Mon Sep 17 00:00:00 2001 +From: Marek Blaha +Date: Tue, 28 Jul 2020 09:50:10 +0200 +Subject: [PATCH 1/3] Remove unused loops attribute from + DepSolveProgressCallBack + +--- + dnf/cli/output.py | 5 ----- + 1 file changed, 5 deletions(-) + +diff --git a/dnf/cli/output.py b/dnf/cli/output.py +index de188ffbd1..44d5f8b89f 100644 +--- a/dnf/cli/output.py ++++ b/dnf/cli/output.py +@@ -1987,10 +1987,6 @@ def historyInfoCmdPkgsAltered(self, old, pats=[]): + class DepSolveProgressCallBack(dnf.callback.Depsolve): + """Provides text output callback functions for Dependency Solver callback.""" + +- def __init__(self): +- """requires yum-cli log and errorlog functions as arguments""" +- self.loops = 0 +- + def pkg_added(self, pkg, mode): + """Print information about a package being added to the + transaction set. +@@ -2037,7 +2033,6 @@ def start(self): + process. + """ + logger.debug(_('--> Starting dependency resolution')) +- self.loops += 1 + + def end(self): + """Output a message stating that dependency resolution has finished.""" + +From 0ee646f4965c597f2832ed3df9d9f0e6546dcc54 Mon Sep 17 00:00:00 2001 +From: Marek Blaha +Date: Wed, 21 Oct 2020 11:47:43 +0200 +Subject: [PATCH 2/3] Remove unused parameter of _make_lists() + +--- + dnf/cli/output.py | 7 ++++--- + 1 file changed, 4 insertions(+), 3 deletions(-) + +diff --git a/dnf/cli/output.py b/dnf/cli/output.py +index 44d5f8b89f..af8a968770 100644 +--- a/dnf/cli/output.py ++++ b/dnf/cli/output.py +@@ -52,7 +52,8 @@ + + logger = logging.getLogger('dnf') + +-def _make_lists(transaction, goal): ++ ++def _make_lists(transaction): + b = dnf.util.Bunch({ + 'downgraded': [], + 'erased': [], +@@ -1101,7 +1102,7 @@ def list_transaction(self, transaction, total_width=None): + # in order to display module changes when RPM transaction is empty + transaction = [] + +- list_bunch = _make_lists(transaction, self.base._goal) ++ list_bunch = _make_lists(transaction) + pkglist_lines = [] + data = {'n' : {}, 'v' : {}, 'r' : {}} + a_wid = 0 # Arch can't get "that big" ... so always use the max. +@@ -1488,7 +1489,7 @@ def _tsi_or_pkg_nevra_cmp(item1, item2): + return (item1.arch > item2.arch) - (item1.arch < item2.arch) + + out = '' +- list_bunch = _make_lists(transaction, self.base._goal) ++ list_bunch = _make_lists(transaction) + + skipped_conflicts, skipped_broken = self._skipped_packages( + report_problems=False, transaction=transaction) + +From 865b7183453684de6a25e77fddf5a2d11fbffba8 Mon Sep 17 00:00:00 2001 +From: Marek Blaha +Date: Wed, 21 Oct 2020 17:59:46 +0200 +Subject: [PATCH 3/3] Post transaction summary is logged for API users + (RhBug:1855158) + +Post transaction summary is always logged into /var/log/dnf.log. +When transaction is called from cli, the summary is also printed to +stdout in columns (as previously). + += changelog = +msg: Packages installed/removed via DNF API are logged into dnf.log +type: enhancement +resolves: https://bugzilla.redhat.com/show_bug.cgi?id=1855158 +--- + dnf/base.py | 46 ++++++++++++- + dnf/cli/cli.py | 8 ++- + dnf/cli/output.py | 167 ++++++++-------------------------------------- + dnf/util.py | 102 +++++++++++++++++++++++++++- + 4 files changed, 177 insertions(+), 146 deletions(-) + +diff --git a/dnf/base.py b/dnf/base.py +index 075e74265a..c0d7712605 100644 +--- a/dnf/base.py ++++ b/dnf/base.py +@@ -28,12 +28,12 @@ + import dnf + import libdnf.transaction + ++from copy import deepcopy + from dnf.comps import CompsQuery + from dnf.i18n import _, P_, ucd + from dnf.util import _parse_specs + from dnf.db.history import SwdbInterface + from dnf.yum import misc +-from functools import reduce + try: + from collections.abc import Sequence + except ImportError: +@@ -549,7 +549,7 @@ def _ts(self): + if self.conf.ignorearch: + self._rpm_probfilter.add(rpm.RPMPROB_FILTER_IGNOREARCH) + +- probfilter = reduce(operator.or_, self._rpm_probfilter, 0) ++ probfilter = functools.reduce(operator.or_, self._rpm_probfilter, 0) + self._priv_ts.setProbFilter(probfilter) + return self._priv_ts + +@@ -890,6 +890,15 @@ def do_transaction(self, display=()): + self._plugins.unload_removed_plugins(self.transaction) + self._plugins.run_transaction() + ++ # log post transaction summary ++ def _pto_callback(action, tsis): ++ msgs = [] ++ for tsi in tsis: ++ msgs.append('{}: {}'.format(action, str(tsi))) ++ return msgs ++ for msg in dnf.util._post_transaction_output(self, self.transaction, _pto_callback): ++ logger.debug(msg) ++ + return tid + + def _trans_error_summary(self, errstring): +@@ -1311,7 +1320,7 @@ def _do_package_lists(self, pkgnarrow='all', patterns=None, showdups=None, + if patterns is None or len(patterns) == 0: + return list_fn(None) + yghs = map(list_fn, patterns) +- return reduce(lambda a, b: a.merge_lists(b), yghs) ++ return functools.reduce(lambda a, b: a.merge_lists(b), yghs) + + def _list_pattern(self, pkgnarrow, pattern, showdups, ignore_case, + reponame=None): +@@ -2579,6 +2588,37 @@ def setup_loggers(self): + """ + self._logging._setup_from_dnf_conf(self.conf, file_loggers_only=True) + ++ def _skipped_packages(self, report_problems, transaction): ++ """returns set of conflicting packages and set of packages with broken dependency that would ++ be additionally installed when --best and --allowerasing""" ++ if self._goal.actions & (hawkey.INSTALL | hawkey.UPGRADE | hawkey.UPGRADE_ALL): ++ best = True ++ else: ++ best = False ++ ng = deepcopy(self._goal) ++ params = {"allow_uninstall": self._allow_erasing, ++ "force_best": best, ++ "ignore_weak": True} ++ ret = ng.run(**params) ++ if not ret and report_problems: ++ msg = dnf.util._format_resolve_problems(ng.problem_rules()) ++ logger.warning(msg) ++ problem_conflicts = set(ng.problem_conflicts(available=True)) ++ problem_dependency = set(ng.problem_broken_dependency(available=True)) - problem_conflicts ++ ++ def _nevra(item): ++ return hawkey.NEVRA(name=item.name, epoch=item.epoch, version=item.version, ++ release=item.release, arch=item.arch) ++ ++ # Sometimes, pkg is not in transaction item, therefore, comparing by nevra ++ transaction_nevras = [_nevra(tsi) for tsi in transaction] ++ skipped_conflicts = set( ++ [pkg for pkg in problem_conflicts if _nevra(pkg) not in transaction_nevras]) ++ skipped_dependency = set( ++ [pkg for pkg in problem_dependency if _nevra(pkg) not in transaction_nevras]) ++ ++ return skipped_conflicts, skipped_dependency ++ + + def _msg_installed(pkg): + name = ucd(pkg) +diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py +index 0bc2c119d0..334000362c 100644 +--- a/dnf/cli/cli.py ++++ b/dnf/cli/cli.py +@@ -252,8 +252,12 @@ def do_transaction(self, display=()): + trans = None + + if trans: +- msg = self.output.post_transaction_output(trans) +- logger.info(msg) ++ # the post transaction summary is already written to log during ++ # Base.do_transaction() so here only print the messages to the ++ # user arranged in columns ++ print() ++ print('\n'.join(self.output.post_transaction_output(trans))) ++ print() + for tsi in trans: + if tsi.state == libdnf.transaction.TransactionItemState_ERROR: + raise dnf.exceptions.Error(_('Transaction failed')) +diff --git a/dnf/cli/output.py b/dnf/cli/output.py +index af8a968770..6d729b63ba 100644 +--- a/dnf/cli/output.py ++++ b/dnf/cli/output.py +@@ -21,9 +21,7 @@ + from __future__ import print_function + from __future__ import unicode_literals + +-from copy import deepcopy + import fnmatch +-import functools + import hawkey + import itertools + import libdnf.transaction +@@ -53,51 +51,6 @@ + logger = logging.getLogger('dnf') + + +-def _make_lists(transaction): +- b = dnf.util.Bunch({ +- 'downgraded': [], +- 'erased': [], +- 'erased_clean': [], +- 'erased_dep': [], +- 'installed': [], +- 'installed_group': [], +- 'installed_dep': [], +- 'installed_weak': [], +- 'reinstalled': [], +- 'upgraded': [], +- 'failed': [], +- }) +- +- for tsi in transaction: +- if tsi.state == libdnf.transaction.TransactionItemState_ERROR: +- b.failed.append(tsi) +- elif tsi.action == libdnf.transaction.TransactionItemAction_DOWNGRADE: +- b.downgraded.append(tsi) +- elif tsi.action == libdnf.transaction.TransactionItemAction_INSTALL: +- if tsi.reason == libdnf.transaction.TransactionItemReason_GROUP: +- b.installed_group.append(tsi) +- elif tsi.reason == libdnf.transaction.TransactionItemReason_DEPENDENCY: +- b.installed_dep.append(tsi) +- elif tsi.reason == libdnf.transaction.TransactionItemReason_WEAK_DEPENDENCY: +- b.installed_weak.append(tsi) +- else: +- # TransactionItemReason_USER +- b.installed.append(tsi) +- elif tsi.action == libdnf.transaction.TransactionItemAction_REINSTALL: +- b.reinstalled.append(tsi) +- elif tsi.action == libdnf.transaction.TransactionItemAction_REMOVE: +- if tsi.reason == libdnf.transaction.TransactionItemReason_CLEAN: +- b.erased_clean.append(tsi) +- elif tsi.reason == libdnf.transaction.TransactionItemReason_DEPENDENCY: +- b.erased_dep.append(tsi) +- else: +- b.erased.append(tsi) +- elif tsi.action == libdnf.transaction.TransactionItemAction_UPGRADE: +- b.upgraded.append(tsi) +- +- return b +- +- + def _spread_in_columns(cols_count, label, lst): + left = itertools.chain((label,), itertools.repeat('')) + lst_length = len(lst) +@@ -1057,37 +1010,6 @@ def list_group_transaction(self, comps, history, diff): + out[0:0] = self._banner(col_data, (_('Group'), _('Packages'), '', '')) + return '\n'.join(out) + +- def _skipped_packages(self, report_problems, transaction): +- """returns set of conflicting packages and set of packages with broken dependency that would +- be additionally installed when --best and --allowerasing""" +- if self.base._goal.actions & (hawkey.INSTALL | hawkey.UPGRADE | hawkey.UPGRADE_ALL): +- best = True +- else: +- best = False +- ng = deepcopy(self.base._goal) +- params = {"allow_uninstall": self.base._allow_erasing, +- "force_best": best, +- "ignore_weak": True} +- ret = ng.run(**params) +- if not ret and report_problems: +- msg = dnf.util._format_resolve_problems(ng.problem_rules()) +- logger.warning(msg) +- problem_conflicts = set(ng.problem_conflicts(available=True)) +- problem_dependency = set(ng.problem_broken_dependency(available=True)) - problem_conflicts +- +- def _nevra(item): +- return hawkey.NEVRA(name=item.name, epoch=item.epoch, version=item.version, +- release=item.release, arch=item.arch) +- +- # Sometimes, pkg is not in transaction item, therefore, comparing by nevra +- transaction_nevras = [_nevra(tsi) for tsi in transaction] +- skipped_conflicts = set( +- [pkg for pkg in problem_conflicts if _nevra(pkg) not in transaction_nevras]) +- skipped_dependency = set( +- [pkg for pkg in problem_dependency if _nevra(pkg) not in transaction_nevras]) +- +- return skipped_conflicts, skipped_dependency +- + def list_transaction(self, transaction, total_width=None): + """Return a string representation of the transaction in an + easy-to-read format. +@@ -1102,7 +1024,7 @@ def list_transaction(self, transaction, total_width=None): + # in order to display module changes when RPM transaction is empty + transaction = [] + +- list_bunch = _make_lists(transaction) ++ list_bunch = dnf.util._make_lists(transaction) + pkglist_lines = [] + data = {'n' : {}, 'v' : {}, 'r' : {}} + a_wid = 0 # Arch can't get "that big" ... so always use the max. +@@ -1271,7 +1193,7 @@ def format_line(group): + # show skipped conflicting packages + if not self.conf.best and self.base._goal.actions & forward_actions: + lines = [] +- skipped_conflicts, skipped_broken = self._skipped_packages( ++ skipped_conflicts, skipped_broken = self.base._skipped_packages( + report_problems=True, transaction=transaction) + skipped_broken = dict((str(pkg), pkg) for pkg in skipped_broken) + for pkg in sorted(skipped_conflicts): +@@ -1436,13 +1358,8 @@ def format_line(group): + max_msg_count, count, msg_pkgs)) + return ''.join(out) + +- def post_transaction_output(self, transaction): +- """Returns a human-readable summary of the results of the +- transaction. + +- :return: a string containing a human-readable summary of the +- results of the transaction +- """ ++ def _pto_callback(self, action, tsis): + # Works a bit like calcColumns, but we never overflow a column we just + # have a dynamic number of columns. + def _fits_in_cols(msgs, num): +@@ -1472,61 +1389,33 @@ def _fits_in_cols(msgs, num): + col_lens[col] *= -1 + return col_lens + +- def _tsi_or_pkg_nevra_cmp(item1, item2): +- """Compares two transaction items or packages by nevra. +- Used as a fallback when tsi does not contain package object. +- """ +- ret = (item1.name > item2.name) - (item1.name < item2.name) +- if ret != 0: +- return ret +- nevra1 = hawkey.NEVRA(name=item1.name, epoch=item1.epoch, version=item1.version, +- release=item1.release, arch=item1.arch) +- nevra2 = hawkey.NEVRA(name=item2.name, epoch=item2.epoch, version=item2.version, +- release=item2.release, arch=item2.arch) +- ret = nevra1.evr_cmp(nevra2, self.sack) +- if ret != 0: +- return ret +- return (item1.arch > item2.arch) - (item1.arch < item2.arch) +- +- out = '' +- list_bunch = _make_lists(transaction) +- +- skipped_conflicts, skipped_broken = self._skipped_packages( +- report_problems=False, transaction=transaction) +- skipped = skipped_conflicts.union(skipped_broken) +- +- for (action, tsis) in [(_('Upgraded'), list_bunch.upgraded), +- (_('Downgraded'), list_bunch.downgraded), +- (_('Installed'), list_bunch.installed + +- list_bunch.installed_group + +- list_bunch.installed_weak + +- list_bunch.installed_dep), +- (_('Reinstalled'), list_bunch.reinstalled), +- (_('Skipped'), skipped), +- (_('Removed'), list_bunch.erased + +- list_bunch.erased_dep + +- list_bunch.erased_clean), +- (_('Failed'), list_bunch.failed)]: +- if not tsis: +- continue +- msgs = [] +- out += '\n%s:\n' % action +- for tsi in sorted(tsis, key=functools.cmp_to_key(_tsi_or_pkg_nevra_cmp)): +- msgs.append(str(tsi)) +- for num in (8, 7, 6, 5, 4, 3, 2): +- cols = _fits_in_cols(msgs, num) +- if cols: +- break +- if not cols: +- cols = [-(self.term.columns - 2)] +- while msgs: +- current_msgs = msgs[:len(cols)] +- out += ' ' +- out += self.fmtColumns(zip(current_msgs, cols), end=u'\n') +- msgs = msgs[len(cols):] +- ++ if not tsis: ++ return '' ++ out = [] ++ msgs = [] ++ out.append('{}:'.format(action)) ++ for tsi in tsis: ++ msgs.append(str(tsi)) ++ for num in (8, 7, 6, 5, 4, 3, 2): ++ cols = _fits_in_cols(msgs, num) ++ if cols: ++ break ++ if not cols: ++ cols = [-(self.term.columns - 2)] ++ while msgs: ++ current_msgs = msgs[:len(cols)] ++ out.append(' {}'.format(self.fmtColumns(zip(current_msgs, cols)))) ++ msgs = msgs[len(cols):] + return out + ++ ++ def post_transaction_output(self, transaction): ++ """ ++ Return a human-readable summary of the transaction. Packages in sections ++ are arranged to columns. ++ """ ++ return dnf.util._post_transaction_output(self.base, transaction, self._pto_callback) ++ + def setup_progress_callbacks(self): + """Set up the progress callbacks and various + output bars based on debug level. +diff --git a/dnf/util.py b/dnf/util.py +index 8cf362706d..0beb04424d 100644 +--- a/dnf/util.py ++++ b/dnf/util.py +@@ -24,13 +24,14 @@ + + from .pycomp import PY3, basestring + from dnf.i18n import _, ucd +-from functools import reduce + import argparse + import dnf + import dnf.callback + import dnf.const + import dnf.pycomp + import errno ++import functools ++import hawkey + import itertools + import locale + import logging +@@ -41,6 +42,7 @@ + import tempfile + import time + import libdnf.repo ++import libdnf.transaction + + logger = logging.getLogger('dnf') + +@@ -195,7 +197,7 @@ def group_by_filter(fn, iterable): + def splitter(acc, item): + acc[not bool(fn(item))].append(item) + return acc +- return reduce(splitter, iterable, ([], [])) ++ return functools.reduce(splitter, iterable, ([], [])) + + def insert_if(item, iterable, condition): + """Insert an item into an iterable by a condition.""" +@@ -504,3 +506,99 @@ def __setattr__(self, what, val): + def setter(item): + setattr(item, what, val) + return list(map(setter, self)) ++ ++ ++def _make_lists(transaction): ++ b = Bunch({ ++ 'downgraded': [], ++ 'erased': [], ++ 'erased_clean': [], ++ 'erased_dep': [], ++ 'installed': [], ++ 'installed_group': [], ++ 'installed_dep': [], ++ 'installed_weak': [], ++ 'reinstalled': [], ++ 'upgraded': [], ++ 'failed': [], ++ }) ++ ++ for tsi in transaction: ++ if tsi.state == libdnf.transaction.TransactionItemState_ERROR: ++ b.failed.append(tsi) ++ elif tsi.action == libdnf.transaction.TransactionItemAction_DOWNGRADE: ++ b.downgraded.append(tsi) ++ elif tsi.action == libdnf.transaction.TransactionItemAction_INSTALL: ++ if tsi.reason == libdnf.transaction.TransactionItemReason_GROUP: ++ b.installed_group.append(tsi) ++ elif tsi.reason == libdnf.transaction.TransactionItemReason_DEPENDENCY: ++ b.installed_dep.append(tsi) ++ elif tsi.reason == libdnf.transaction.TransactionItemReason_WEAK_DEPENDENCY: ++ b.installed_weak.append(tsi) ++ else: ++ # TransactionItemReason_USER ++ b.installed.append(tsi) ++ elif tsi.action == libdnf.transaction.TransactionItemAction_REINSTALL: ++ b.reinstalled.append(tsi) ++ elif tsi.action == libdnf.transaction.TransactionItemAction_REMOVE: ++ if tsi.reason == libdnf.transaction.TransactionItemReason_CLEAN: ++ b.erased_clean.append(tsi) ++ elif tsi.reason == libdnf.transaction.TransactionItemReason_DEPENDENCY: ++ b.erased_dep.append(tsi) ++ else: ++ b.erased.append(tsi) ++ elif tsi.action == libdnf.transaction.TransactionItemAction_UPGRADE: ++ b.upgraded.append(tsi) ++ ++ return b ++ ++ ++def _post_transaction_output(base, transaction, action_callback): ++ """Returns a human-readable summary of the results of the ++ transaction. ++ ++ :param action_callback: function generating output for specific action. It ++ takes two parameters - action as a string and list of affected packages for ++ this action ++ :return: a list of lines containing a human-readable summary of the ++ results of the transaction ++ """ ++ def _tsi_or_pkg_nevra_cmp(item1, item2): ++ """Compares two transaction items or packages by nevra. ++ Used as a fallback when tsi does not contain package object. ++ """ ++ ret = (item1.name > item2.name) - (item1.name < item2.name) ++ if ret != 0: ++ return ret ++ nevra1 = hawkey.NEVRA(name=item1.name, epoch=item1.epoch, version=item1.version, ++ release=item1.release, arch=item1.arch) ++ nevra2 = hawkey.NEVRA(name=item2.name, epoch=item2.epoch, version=item2.version, ++ release=item2.release, arch=item2.arch) ++ ret = nevra1.evr_cmp(nevra2, base.sack) ++ if ret != 0: ++ return ret ++ return (item1.arch > item2.arch) - (item1.arch < item2.arch) ++ ++ list_bunch = dnf.util._make_lists(transaction) ++ ++ skipped_conflicts, skipped_broken = base._skipped_packages( ++ report_problems=False, transaction=transaction) ++ skipped = skipped_conflicts.union(skipped_broken) ++ ++ out = [] ++ for (action, tsis) in [(_('Upgraded'), list_bunch.upgraded), ++ (_('Downgraded'), list_bunch.downgraded), ++ (_('Installed'), list_bunch.installed + ++ list_bunch.installed_group + ++ list_bunch.installed_weak + ++ list_bunch.installed_dep), ++ (_('Reinstalled'), list_bunch.reinstalled), ++ (_('Skipped'), skipped), ++ (_('Removed'), list_bunch.erased + ++ list_bunch.erased_dep + ++ list_bunch.erased_clean), ++ (_('Failed'), list_bunch.failed)]: ++ out.extend(action_callback( ++ action, sorted(tsis, key=functools.cmp_to_key(_tsi_or_pkg_nevra_cmp)))) ++ ++ return out diff --git a/SOURCES/0004-Log-scriptlets-output-also-for-API-users-RhBug-1847340.patch b/SOURCES/0004-Log-scriptlets-output-also-for-API-users-RhBug-1847340.patch new file mode 100644 index 0000000..8447353 --- /dev/null +++ b/SOURCES/0004-Log-scriptlets-output-also-for-API-users-RhBug-1847340.patch @@ -0,0 +1,130 @@ +From df64fd36d7fefe39a96fea3f41e35785bebd37ec Mon Sep 17 00:00:00 2001 +From: Marek Blaha +Date: Wed, 2 Dec 2020 16:33:26 +0100 +Subject: [PATCH 1/2] Log scriptlets output also for API users (RhBug:1847340) + +Messages logged into /var/log/dnf.rpm.log are now the same for both +command line and API usage. + +https://bugzilla.redhat.com/show_bug.cgi?id=1847340 +--- + dnf/cli/output.py | 7 +------ + dnf/yum/rpmtrans.py | 9 ++++++++- + 2 files changed, 9 insertions(+), 7 deletions(-) + +diff --git a/dnf/cli/output.py b/dnf/cli/output.py +index 51d6829ca6..86260661fc 100644 +--- a/dnf/cli/output.py ++++ b/dnf/cli/output.py +@@ -2151,12 +2151,7 @@ def error(self, message): + pass + + def scriptout(self, msgs): +- """Print messages originating from a package script. +- +- :param msgs: the messages coming from the script +- """ +- if msgs: +- self.rpm_logger.info(ucd(msgs)) ++ pass + + def _makefmt(self, percent, ts_done, ts_total, progress=True, + pkgname=None, wid1=15): +diff --git a/dnf/yum/rpmtrans.py b/dnf/yum/rpmtrans.py +index 447639a476..d6c549d2ed 100644 +--- a/dnf/yum/rpmtrans.py ++++ b/dnf/yum/rpmtrans.py +@@ -113,7 +113,10 @@ def progress(self, package, action, ti_done, ti_total, ts_done, ts_total): + pass + + def scriptout(self, msgs): +- """msgs is the messages that were output (if any).""" ++ """Hook for reporting an rpm scriptlet output. ++ ++ :param msgs: the scriptlet output ++ """ + pass + + def error(self, message): +@@ -156,6 +159,10 @@ def filelog(self, package, action): + msg = '%s: %s' % (action_str, package) + self.rpm_logger.log(dnf.logging.SUBDEBUG, msg) + ++ def scriptout(self, msgs): ++ if msgs: ++ self.rpm_logger.info(ucd(msgs)) ++ + + class RPMTransaction(object): + def __init__(self, base, test=False, displays=()): + +From ee6ffcf640180b2b08d2db50b4b81d2bdefb1f2f Mon Sep 17 00:00:00 2001 +From: Marek Blaha +Date: Thu, 3 Dec 2020 10:08:09 +0100 +Subject: [PATCH 2/2] Straighten inheritance of *Display classes + +--- + dnf/cli/output.py | 15 +++------------ + dnf/yum/rpmtrans.py | 2 +- + 2 files changed, 4 insertions(+), 13 deletions(-) + +diff --git a/dnf/cli/output.py b/dnf/cli/output.py +index 86260661fc..de188ffbd1 100644 +--- a/dnf/cli/output.py ++++ b/dnf/cli/output.py +@@ -37,7 +37,7 @@ + from dnf.cli.format import format_number, format_time + from dnf.i18n import _, C_, P_, ucd, fill_exact_width, textwrap_fill, exact_width, select_short_long + from dnf.pycomp import xrange, basestring, long, unicode, sys_maxsize +-from dnf.yum.rpmtrans import LoggingTransactionDisplay ++from dnf.yum.rpmtrans import TransactionDisplay + from dnf.db.history import MergedTransactionWrapper + import dnf.base + import dnf.callback +@@ -2071,7 +2071,7 @@ def short_id(id): + return self.output.userconfirm() + + +-class CliTransactionDisplay(LoggingTransactionDisplay): ++class CliTransactionDisplay(TransactionDisplay): + """A YUM specific callback class for RPM operations.""" + + width = property(lambda self: dnf.cli.term._term_width()) +@@ -2093,7 +2093,7 @@ def progress(self, package, action, ti_done, ti_total, ts_done, ts_total): + :param package: the package involved in the event + :param action: the type of action that is taking place. Valid + values are given by +- :func:`rpmtrans.LoggingTransactionDisplay.action.keys()` ++ :func:`rpmtrans.TransactionDisplay.action.keys()` + :param ti_done: a number representing the amount of work + already done in the current transaction + :param ti_total: a number representing the total amount of work +@@ -2144,15 +2144,6 @@ def _out_progress(self, ti_done, ti_total, ts_done, ts_total, + if ti_done == ti_total: + print(" ") + +- def filelog(self, package, action): +- pass +- +- def error(self, message): +- pass +- +- def scriptout(self, msgs): +- pass +- + def _makefmt(self, percent, ts_done, ts_total, progress=True, + pkgname=None, wid1=15): + l = len(str(ts_total)) +diff --git a/dnf/yum/rpmtrans.py b/dnf/yum/rpmtrans.py +index d6c549d2ed..51fa921d3e 100644 +--- a/dnf/yum/rpmtrans.py ++++ b/dnf/yum/rpmtrans.py +@@ -143,7 +143,7 @@ def error(self, message): + dnf.util._terminal_messenger('print', message, sys.stderr) + + +-class LoggingTransactionDisplay(ErrorTransactionDisplay): ++class LoggingTransactionDisplay(TransactionDisplay): + ''' + Base class for a RPMTransaction display callback class + ''' diff --git a/SOURCES/0005-dnf-history-operations-that-work-with-comps-correctly.patch b/SOURCES/0005-dnf-history-operations-that-work-with-comps-correctly.patch new file mode 100644 index 0000000..233a2fc --- /dev/null +++ b/SOURCES/0005-dnf-history-operations-that-work-with-comps-correctly.patch @@ -0,0 +1,1411 @@ +From b9a8226185f3ab58e3551b315af2b11a8b2f2ebe Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hr=C3=A1zk=C3=BD?= +Date: Tue, 8 Sep 2020 17:02:59 +0200 +Subject: [PATCH 01/17] Add a get_current() method to SwdbInterface + +The method returns the transaction that is currently being created in +Swdb, before it is stored to sqlite. +--- + VERSION.cmake | 2 +- + dnf.spec | 2 +- + dnf/db/history.py | 3 +++ + 3 files changed, 5 insertions(+), 2 deletions(-) + +diff --git a/dnf/db/history.py b/dnf/db/history.py +index 4d355f95..994cdb01 100644 +--- a/dnf/db/history.py ++++ b/dnf/db/history.py +@@ -381,6 +381,9 @@ class SwdbInterface(object): + prev_trans.altered_gt_rpmdb = True + return result[::-1] + ++ def get_current(self): ++ return TransactionWrapper(self.swdb.getCurrent()) ++ + def set_reason(self, pkg, reason): + """Set reason for package""" + rpm_item = self.rpm._pkg_to_swdb_rpm_item(pkg) +-- +2.26.2 + + +From 3bcf90aadfea98da1397b570fcb3ecc20a89c15d Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hr=C3=A1zk=C3=BD?= +Date: Fri, 2 Oct 2020 15:52:19 +0200 +Subject: [PATCH 02/17] transaction-sr: Prefer installing from the original + transaction repository + +In case a package exists in the same repo_id as from which it was +originally installed, prefer the package from that repo when replaying +the transaction. + +Makes a difference in e.g. the system-upgrade plugin, where it ensures +the package is installed from the same repo from which it was downloaded +during the download step. +--- + dnf/transaction_sr.py | 13 +++++++++++++ + 1 file changed, 13 insertions(+) + +diff --git a/dnf/transaction_sr.py b/dnf/transaction_sr.py +index 9b9b0749..45ca2ef7 100644 +--- a/dnf/transaction_sr.py ++++ b/dnf/transaction_sr.py +@@ -257,6 +257,7 @@ class TransactionReplay(object): + try: + action = pkg_data["action"] + nevra = pkg_data["nevra"] ++ repo_id = pkg_data["repo_id"] + reason = libdnf.transaction.StringToTransactionItemReason(pkg_data["reason"]) + except KeyError as e: + raise TransactionError( +@@ -282,6 +283,18 @@ class TransactionReplay(object): + epoch = parsed_nevra.epoch if parsed_nevra.epoch is not None else 0 + query = query_na.filter(epoch=epoch, version=parsed_nevra.version, release=parsed_nevra.release) + ++ # In case the package is found in the same repo as in the original ++ # transaction, limit the query to that plus installed packages. IOW ++ # remove packages with the same NEVRA in case they are found in ++ # multiple repos and the repo the package came from originally is one ++ # of them. ++ # This can e.g. make a difference in the system-upgrade plugin, in case ++ # the same NEVRA is in two repos, this makes sure the same repo is used ++ # for both download and upgrade steps of the plugin. ++ query_repo = query.filter(reponame=repo_id) ++ if query_repo: ++ query = query_repo.union(query.installed()) ++ + if not query: + self._raise_or_warn(self._skip_unavailable, _('Cannot find rpm nevra "{nevra}".').format(nevra=nevra)) + return +-- +2.26.2 + + +From acfd6310131769f33165c8de1d064889a80fc259 Mon Sep 17 00:00:00 2001 +From: Daniel Mach +Date: Tue, 24 Nov 2020 10:57:21 +0100 +Subject: [PATCH 03/17] transaction_sr: Enable loading transactions from dict + +--- + dnf/cli/commands/history.py | 2 +- + dnf/transaction_sr.py | 42 +++++++++++++++++++++++++------------ + 2 files changed, 30 insertions(+), 14 deletions(-) + +diff --git a/dnf/cli/commands/history.py b/dnf/cli/commands/history.py +index e381f902..0a6dad9b 100644 +--- a/dnf/cli/commands/history.py ++++ b/dnf/cli/commands/history.py +@@ -270,7 +270,7 @@ class HistoryCommand(commands.Command): + + self.replay = TransactionReplay( + self.base, +- self.opts.transaction_filename, ++ filename=self.opts.transaction_filename, + ignore_installed = self.opts.ignore_installed, + ignore_extras = self.opts.ignore_extras, + skip_unavailable = self.opts.skip_unavailable +diff --git a/dnf/transaction_sr.py b/dnf/transaction_sr.py +index 45ca2ef7..e6b06665 100644 +--- a/dnf/transaction_sr.py ++++ b/dnf/transaction_sr.py +@@ -187,21 +187,23 @@ class TransactionReplay(object): + def __init__( + self, + base, +- fn, ++ filename="", ++ data=None, + ignore_extras=False, + ignore_installed=False, + skip_unavailable=False + ): + """ + :param base: the dnf base +- :param fn: the filename to load the transaction from ++ :param filename: the filename to load the transaction from (conflicts with the 'data' argument) ++ :param data: the dictionary to load the transaction from (conflicts with the 'filename' argument) + :param ignore_extras: whether to ignore extra package pulled into the transaction + :param ignore_installed: whether to ignore installed versions of packages + :param skip_unavailable: whether to skip transaction packages that aren't available + """ + + self._base = base +- self._filename = fn ++ self._filename = filename + self._ignore_installed = ignore_installed + self._ignore_extras = ignore_extras + self._skip_unavailable = skip_unavailable +@@ -213,25 +215,39 @@ class TransactionReplay(object): + self._nevra_reason_cache = {} + self._warnings = [] + ++ if filename and data: ++ raise ValueError(_("Conflicting TransactionReplay arguments have been specified: filename, data")) ++ elif filename: ++ self._load_from_file(filename) ++ else: ++ self._load_from_data(data) ++ ++ ++ def _load_from_file(self, fn): ++ self._filename = fn + with open(fn, "r") as f: + try: +- self._replay_data = json.load(f) ++ replay_data = json.load(f) + except json.decoder.JSONDecodeError as e: + raise TransactionFileError(fn, str(e) + ".") + + try: +- self._verify_toplevel_json(self._replay_data) ++ self._load_from_data(replay_data) ++ except TransactionError as e: ++ raise TransactionFileError(fn, e) + +- self._rpms = self._replay_data.get("rpms", []) +- self._assert_type(self._rpms, list, "rpms", "array") ++ def _load_from_data(self, data): ++ self._replay_data = data ++ self._verify_toplevel_json(self._replay_data) + +- self._groups = self._replay_data.get("groups", []) +- self._assert_type(self._groups, list, "groups", "array") ++ self._rpms = self._replay_data.get("rpms", []) ++ self._assert_type(self._rpms, list, "rpms", "array") + +- self._environments = self._replay_data.get("environments", []) +- self._assert_type(self._environments, list, "environments", "array") +- except TransactionError as e: +- raise TransactionFileError(fn, e) ++ self._groups = self._replay_data.get("groups", []) ++ self._assert_type(self._groups, list, "groups", "array") ++ ++ self._environments = self._replay_data.get("environments", []) ++ self._assert_type(self._environments, list, "environments", "array") + + def _raise_or_warn(self, warn_only, msg): + if warn_only: +-- +2.26.2 + + +From 90d4a2fd72b30b295adcb6da66b8043a70561b33 Mon Sep 17 00:00:00 2001 +From: Daniel Mach +Date: Fri, 20 Nov 2020 19:36:49 +0100 +Subject: [PATCH 04/17] transaction_sr: Store exception attributes for future + use + +--- + dnf/transaction_sr.py | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/dnf/transaction_sr.py b/dnf/transaction_sr.py +index e6b06665..36787de4 100644 +--- a/dnf/transaction_sr.py ++++ b/dnf/transaction_sr.py +@@ -55,6 +55,10 @@ class TransactionFileError(dnf.exceptions.Error): + :param errors: a list of error classes or a string with an error description + """ + ++ # store args in case someone wants to read them from a caught exception ++ self.filename = filename ++ self.errors = errors ++ + if isinstance(errors, (list, tuple)): + if len(errors) > 1: + msg = _('Errors in "{filename}":').format(filename=filename) +-- +2.26.2 + + +From 0ffa7ed9ea73035acaec2c4f916d967701fddda2 Mon Sep 17 00:00:00 2001 +From: Daniel Mach +Date: Fri, 20 Nov 2020 19:04:59 +0100 +Subject: [PATCH 05/17] transaction_sr: Handle serialize_transaction(None) + +--- + dnf/transaction_sr.py | 3 +++ + 1 file changed, 3 insertions(+) + +diff --git a/dnf/transaction_sr.py b/dnf/transaction_sr.py +index 36787de4..41ddee1f 100644 +--- a/dnf/transaction_sr.py ++++ b/dnf/transaction_sr.py +@@ -120,6 +120,9 @@ def serialize_transaction(transaction): + groups = [] + environments = [] + ++ if transaction is None: ++ return data ++ + for tsi in transaction.packages(): + if tsi.is_package(): + rpms.append({ +-- +2.26.2 + + +From c4bae459caef1d5128bd7ed43fcbb749608449f4 Mon Sep 17 00:00:00 2001 +From: Daniel Mach +Date: Mon, 23 Nov 2020 16:23:53 +0100 +Subject: [PATCH 06/17] transaction_sr: Skip preferred repo lookup if repoid is + empty + +--- + dnf/transaction_sr.py | 7 ++++--- + 1 file changed, 4 insertions(+), 3 deletions(-) + +diff --git a/dnf/transaction_sr.py b/dnf/transaction_sr.py +index 41ddee1f..9926bebd 100644 +--- a/dnf/transaction_sr.py ++++ b/dnf/transaction_sr.py +@@ -314,9 +314,10 @@ class TransactionReplay(object): + # This can e.g. make a difference in the system-upgrade plugin, in case + # the same NEVRA is in two repos, this makes sure the same repo is used + # for both download and upgrade steps of the plugin. +- query_repo = query.filter(reponame=repo_id) +- if query_repo: +- query = query_repo.union(query.installed()) ++ if repo_id: ++ query_repo = query.filter(reponame=repo_id) ++ if query_repo: ++ query = query_repo.union(query.installed()) + + if not query: + self._raise_or_warn(self._skip_unavailable, _('Cannot find rpm nevra "{nevra}".').format(nevra=nevra)) +-- +2.26.2 + + +From 3f82f871170be871ce8ec9d509306d751890ac9e Mon Sep 17 00:00:00 2001 +From: Daniel Mach +Date: Fri, 20 Nov 2020 17:44:28 +0100 +Subject: [PATCH 07/17] history: Refactor redo code to use transaction + store/replay + += changelog = +msg: Support comps groups in history redo +type: enhancement +resolves: https://bugzilla.redhat.com/show_bug.cgi?id=1657123 +resolves: https://bugzilla.redhat.com/show_bug.cgi?id=1809565 +resolves: https://bugzilla.redhat.com/show_bug.cgi?id=1809639 +--- + dnf/cli/commands/history.py | 40 +++++++++++++++---------------------- + 1 file changed, 16 insertions(+), 24 deletions(-) + +diff --git a/dnf/cli/commands/history.py b/dnf/cli/commands/history.py +index 0a6dad9b..c28a136a 100644 +--- a/dnf/cli/commands/history.py ++++ b/dnf/cli/commands/history.py +@@ -120,6 +120,10 @@ class HistoryCommand(commands.Command): + if not self.opts.transactions: + raise dnf.cli.CliError(_('No transaction ID or package name given.')) + elif self.opts.transactions_action in ['redo', 'undo', 'rollback']: ++ demands.available_repos = True ++ demands.resolving = True ++ demands.root_user = True ++ + self._require_one_transaction_id = True + if not self.opts.transactions: + msg = _('No transaction ID or package name given.') +@@ -157,28 +161,16 @@ class HistoryCommand(commands.Command): + old = self.base.history_get_transaction(extcmds) + if old is None: + return 1, ['Failed history redo'] +- tm = dnf.util.normalize_time(old.beg_timestamp) +- print('Repeating transaction %u, from %s' % (old.tid, tm)) +- self.output.historyInfoCmdPkgsAltered(old) +- +- for i in old.packages(): +- pkgs = list(self.base.sack.query().filter(nevra=str(i), reponame=i.from_repo)) +- if i.action in dnf.transaction.FORWARD_ACTIONS: +- if not pkgs: +- logger.info(_('No package %s available.'), +- self.output.term.bold(ucd(str(i)))) +- return 1, ['An operation cannot be redone'] +- pkg = pkgs[0] +- self.base.install(str(pkg)) +- elif i.action == libdnf.transaction.TransactionItemAction_REMOVE: +- if not pkgs: +- # package was removed already, we can skip removing it again +- continue +- pkg = pkgs[0] +- self.base.remove(str(pkg)) +- +- self.base.resolve() +- self.base.do_transaction() ++ ++ data = serialize_transaction(old) ++ self.replay = TransactionReplay( ++ self.base, ++ data=data, ++ ignore_installed=True, ++ ignore_extras=True, ++ skip_unavailable=self.opts.skip_unavailable ++ ) ++ self.replay.run() + + def _hcmd_undo(self, extcmds): + try: +@@ -326,13 +318,13 @@ class HistoryCommand(commands.Command): + raise dnf.exceptions.Error(strs[0]) + + def run_resolved(self): +- if self.opts.transactions_action != "replay": ++ if self.opts.transactions_action not in ("replay", "redo"): + return + + self.replay.post_transaction() + + def run_transaction(self): +- if self.opts.transactions_action != "replay": ++ if self.opts.transactions_action not in ("replay", "redo"): + return + + warnings = self.replay.get_warnings() +-- +2.26.2 + + +From d1b78ba8449b319121b5208c5b39609b1c6b61de Mon Sep 17 00:00:00 2001 +From: Daniel Mach +Date: Fri, 20 Nov 2020 19:07:50 +0100 +Subject: [PATCH 08/17] history: Refactor rollback code to use transaction + store/replay + += changelog = +msg: Support comps groups in history rollback +type: enhancement +resolves: https://bugzilla.redhat.com/show_bug.cgi?id=1657123 +resolves: https://bugzilla.redhat.com/show_bug.cgi?id=1809565 +resolves: https://bugzilla.redhat.com/show_bug.cgi?id=1809639 +--- + dnf/cli/cli.py | 56 ----------------------------- + dnf/cli/commands/history.py | 72 ++++++++++++++++++++++++++++++++++--- + 2 files changed, 67 insertions(+), 61 deletions(-) + +diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py +index cd720a97..36671fd8 100644 +--- a/dnf/cli/cli.py ++++ b/dnf/cli/cli.py +@@ -627,62 +627,6 @@ class BaseCli(dnf.Base): + logger.critical(_('Found more than one transaction ID!')) + return old[0] + +- def history_rollback_transaction(self, extcmd): +- """Rollback given transaction.""" +- old = self.history_get_transaction((extcmd,)) +- if old is None: +- return 1, ['Failed history rollback, no transaction'] +- last = self.history.last() +- if last is None: +- return 1, ['Failed history rollback, no last?'] +- if old.tid == last.tid: +- return 0, ['Rollback to current, nothing to do'] +- +- mobj = None +- for trans in self.history.old(list(range(old.tid + 1, last.tid + 1))): +- if trans.altered_lt_rpmdb: +- logger.warning(_('Transaction history is incomplete, before %u.'), trans.tid) +- elif trans.altered_gt_rpmdb: +- logger.warning(_('Transaction history is incomplete, after %u.'), trans.tid) +- +- if mobj is None: +- mobj = dnf.db.history.MergedTransactionWrapper(trans) +- else: +- mobj.merge(trans) +- +- tm = dnf.util.normalize_time(old.beg_timestamp) +- print("Rollback to transaction %u, from %s" % (old.tid, tm)) +- print(self.output.fmtKeyValFill(" Undoing the following transactions: ", +- ", ".join((str(x) for x in mobj.tids())))) +- self.output.historyInfoCmdPkgsAltered(mobj) # :todo +- +-# history = dnf.history.open_history(self.history) # :todo +-# m = libdnf.transaction.MergedTransaction() +- +-# return +- +-# operations = dnf.history.NEVRAOperations() +-# for id_ in range(old.tid + 1, last.tid + 1): +-# operations += history.transaction_nevra_ops(id_) +- +- try: +- self._history_undo_operations(mobj, old.tid + 1, True, strict=self.conf.strict) +- except dnf.exceptions.PackagesNotInstalledError as err: +- raise +- logger.info(_('No package %s installed.'), +- self.output.term.bold(ucd(err.pkg_spec))) +- return 1, ['A transaction cannot be undone'] +- except dnf.exceptions.PackagesNotAvailableError as err: +- raise +- logger.info(_('No package %s available.'), +- self.output.term.bold(ucd(err.pkg_spec))) +- return 1, ['A transaction cannot be undone'] +- except dnf.exceptions.MarkingError: +- raise +- assert False +- else: +- return 2, ["Rollback to transaction %u" % (old.tid,)] +- + def history_undo_transaction(self, extcmd): + """Undo given transaction.""" + old = self.history_get_transaction((extcmd,)) +diff --git a/dnf/cli/commands/history.py b/dnf/cli/commands/history.py +index c28a136a..a450aaab 100644 +--- a/dnf/cli/commands/history.py ++++ b/dnf/cli/commands/history.py +@@ -20,6 +20,7 @@ from __future__ import print_function + from __future__ import unicode_literals + + import libdnf ++import hawkey + + from dnf.i18n import _, ucd + from dnf.cli import commands +@@ -33,6 +34,7 @@ import dnf.util + import json + import logging + import os ++import sys + + + logger = logging.getLogger('dnf') +@@ -179,10 +181,70 @@ class HistoryCommand(commands.Command): + return 1, [str(err)] + + def _hcmd_rollback(self, extcmds): ++ old = self.base.history_get_transaction(extcmds) ++ if old is None: ++ return 1, ['Failed history rollback'] ++ last = self.base.history.last() ++ ++ merged_trans = None ++ if old.tid != last.tid: ++ # history.old([]) returns all transactions and we don't want that ++ # so skip merging the transactions when trying to rollback to the last transaction ++ # which is the current system state and rollback is not applicable ++ for trans in self.base.history.old(list(range(old.tid + 1, last.tid + 1))): ++ if trans.altered_lt_rpmdb: ++ logger.warning(_('Transaction history is incomplete, before %u.'), trans.tid) ++ elif trans.altered_gt_rpmdb: ++ logger.warning(_('Transaction history is incomplete, after %u.'), trans.tid) ++ ++ if merged_trans is None: ++ merged_trans = dnf.db.history.MergedTransactionWrapper(trans) ++ else: ++ merged_trans.merge(trans) ++ ++ return self._revert_transaction(merged_trans) ++ ++ def _revert_transaction(self, trans): ++ action_map = { ++ "Install": "Removed", ++ "Removed": "Install", ++ "Upgrade": "Downgraded", ++ "Upgraded": "Downgrade", ++ "Downgrade": "Upgraded", ++ "Downgraded": "Upgrade", ++ "Reinstalled": "Reinstall", ++ "Reinstall": "Reinstalled", ++ "Obsoleted": "Install", ++ "Obsolete": "Obsoleted", ++ } ++ ++ data = serialize_transaction(trans) ++ ++ # revert actions in the serialized transaction data to perform rollback/undo ++ for content_type in ("rpms", "groups", "environments"): ++ for ti in data.get(content_type, []): ++ ti["action"] = action_map[ti["action"]] ++ ++ if ti["action"] == "Install" and ti.get("reason", None) == "clean": ++ ti["reason"] = "dependency" ++ ++ if ti.get("repo_id") == hawkey.SYSTEM_REPO_NAME: ++ # erase repo_id, because it's not possible to perform forward actions from the @System repo ++ ti["repo_id"] = None ++ ++ self.replay = TransactionReplay( ++ self.base, ++ data=data, ++ ignore_installed=True, ++ ignore_extras=True, ++ skip_unavailable=self.opts.skip_unavailable ++ ) + try: +- return self.base.history_rollback_transaction(extcmds[0]) +- except dnf.exceptions.Error as err: +- return 1, [str(err)] ++ self.replay.run() ++ except dnf.transaction_sr.TransactionFileError as ex: ++ for error in ex.errors: ++ print(str(error), file=sys.stderr) ++ raise dnf.exceptions.PackageNotFoundError(_('no package matched')) + + def _hcmd_userinstalled(self): + """Execute history userinstalled command.""" +@@ -318,13 +380,13 @@ class HistoryCommand(commands.Command): + raise dnf.exceptions.Error(strs[0]) + + def run_resolved(self): +- if self.opts.transactions_action not in ("replay", "redo"): ++ if self.opts.transactions_action not in ("replay", "redo", "rollback"): + return + + self.replay.post_transaction() + + def run_transaction(self): +- if self.opts.transactions_action not in ("replay", "redo"): ++ if self.opts.transactions_action not in ("replay", "redo", "rollback"): + return + + warnings = self.replay.get_warnings() +-- +2.26.2 + + +From a59a57ce456682e85e86ee362aab4eecc19dbc81 Mon Sep 17 00:00:00 2001 +From: Daniel Mach +Date: Thu, 3 Dec 2020 15:56:52 +0100 +Subject: [PATCH 09/17] history: Refactor undo code to use transaction + store/replay + += changelog = +msg: Support comps groups in history undo +type: enhancement +resolves: https://bugzilla.redhat.com/show_bug.cgi?id=1657123 +resolves: https://bugzilla.redhat.com/show_bug.cgi?id=1809565 +resolves: https://bugzilla.redhat.com/show_bug.cgi?id=1809639 +--- + dnf/cli/cli.py | 28 ---------------------------- + dnf/cli/commands/history.py | 12 ++++++------ + 2 files changed, 6 insertions(+), 34 deletions(-) + +diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py +index 36671fd8..e4fd39c6 100644 +--- a/dnf/cli/cli.py ++++ b/dnf/cli/cli.py +@@ -627,34 +627,6 @@ class BaseCli(dnf.Base): + logger.critical(_('Found more than one transaction ID!')) + return old[0] + +- def history_undo_transaction(self, extcmd): +- """Undo given transaction.""" +- old = self.history_get_transaction((extcmd,)) +- if old is None: +- return 1, ['Failed history undo'] +- +- tm = dnf.util.normalize_time(old.beg_timestamp) +- msg = _("Undoing transaction {}, from {}").format(old.tid, ucd(tm)) +- logger.info(msg) +- self.output.historyInfoCmdPkgsAltered(old) # :todo +- +- +- mobj = dnf.db.history.MergedTransactionWrapper(old) +- +- try: +- self._history_undo_operations(mobj, old.tid, strict=self.conf.strict) +- except dnf.exceptions.PackagesNotInstalledError as err: +- logger.info(_('No package %s installed.'), +- self.output.term.bold(ucd(err.pkg_spec))) +- return 1, ['An operation cannot be undone'] +- except dnf.exceptions.PackagesNotAvailableError as err: +- logger.info(_('No package %s available.'), +- self.output.term.bold(ucd(err.pkg_spec))) +- return 1, ['An operation cannot be undone'] +- except dnf.exceptions.MarkingError: +- raise +- else: +- return 2, ["Undoing transaction %u" % (old.tid,)] + + class Cli(object): + def __init__(self, base): +diff --git a/dnf/cli/commands/history.py b/dnf/cli/commands/history.py +index a450aaab..d60d3f25 100644 +--- a/dnf/cli/commands/history.py ++++ b/dnf/cli/commands/history.py +@@ -175,10 +175,10 @@ class HistoryCommand(commands.Command): + self.replay.run() + + def _hcmd_undo(self, extcmds): +- try: +- return self.base.history_undo_transaction(extcmds[0]) +- except dnf.exceptions.Error as err: +- return 1, [str(err)] ++ old = self.base.history_get_transaction(extcmds) ++ if old is None: ++ return 1, ['Failed history undo'] ++ return self._revert_transaction(old) + + def _hcmd_rollback(self, extcmds): + old = self.base.history_get_transaction(extcmds) +@@ -380,13 +380,13 @@ class HistoryCommand(commands.Command): + raise dnf.exceptions.Error(strs[0]) + + def run_resolved(self): +- if self.opts.transactions_action not in ("replay", "redo", "rollback"): ++ if self.opts.transactions_action not in ("replay", "redo", "rollback", "undo"): + return + + self.replay.post_transaction() + + def run_transaction(self): +- if self.opts.transactions_action not in ("replay", "redo", "rollback"): ++ if self.opts.transactions_action not in ("replay", "redo", "rollback", "undo"): + return + + warnings = self.replay.get_warnings() +-- +2.26.2 + + +From 5a0b6cc00420fd6559a1fd611de1417ea90b1bfc Mon Sep 17 00:00:00 2001 +From: Daniel Mach +Date: Fri, 20 Nov 2020 19:54:54 +0100 +Subject: [PATCH 10/17] Remove Base._history_undo_operations() as it was + replaced with transaction_sr code + +--- + dnf/base.py | 59 ----------------------------------------------------- + 1 file changed, 59 deletions(-) + +diff --git a/dnf/base.py b/dnf/base.py +index ec41ab01..a2955051 100644 +--- a/dnf/base.py ++++ b/dnf/base.py +@@ -2218,65 +2218,6 @@ class Base(object): + for prefix in ['/bin/', '/sbin/', '/usr/bin/', '/usr/sbin/']] + return self.sack.query().filterm(file__glob=binary_provides), binary_provides + +- def _history_undo_operations(self, operations, first_trans, rollback=False, strict=True): +- """Undo the operations on packages by their NEVRAs. +- +- :param operations: a NEVRAOperations to be undone +- :param first_trans: first transaction id being undone +- :param rollback: True if transaction is performing a rollback +- :param strict: if True, raise an exception on any errors +- """ +- +- # map actions to their opposites +- action_map = { +- libdnf.transaction.TransactionItemAction_DOWNGRADE: None, +- libdnf.transaction.TransactionItemAction_DOWNGRADED: libdnf.transaction.TransactionItemAction_UPGRADE, +- libdnf.transaction.TransactionItemAction_INSTALL: libdnf.transaction.TransactionItemAction_REMOVE, +- libdnf.transaction.TransactionItemAction_OBSOLETE: None, +- libdnf.transaction.TransactionItemAction_OBSOLETED: libdnf.transaction.TransactionItemAction_INSTALL, +- libdnf.transaction.TransactionItemAction_REINSTALL: None, +- # reinstalls are skipped as they are considered as no-operation from history perspective +- libdnf.transaction.TransactionItemAction_REINSTALLED: None, +- libdnf.transaction.TransactionItemAction_REMOVE: libdnf.transaction.TransactionItemAction_INSTALL, +- libdnf.transaction.TransactionItemAction_UPGRADE: None, +- libdnf.transaction.TransactionItemAction_UPGRADED: libdnf.transaction.TransactionItemAction_DOWNGRADE, +- libdnf.transaction.TransactionItemAction_REASON_CHANGE: None, +- } +- +- failed = False +- for ti in operations.packages(): +- try: +- action = action_map[ti.action] +- except KeyError: +- raise RuntimeError(_("Action not handled: {}".format(action))) +- +- if action is None: +- continue +- +- if action == libdnf.transaction.TransactionItemAction_REMOVE: +- query = self.sack.query().installed().filterm(nevra_strict=str(ti)) +- if not query: +- logger.error(_('No package %s installed.'), ucd(str(ti))) +- failed = True +- continue +- else: +- query = self.sack.query().filterm(nevra_strict=str(ti)) +- if not query: +- logger.error(_('No package %s available.'), ucd(str(ti))) +- failed = True +- continue +- +- if action == libdnf.transaction.TransactionItemAction_REMOVE: +- for pkg in query: +- self._goal.erase(pkg) +- else: +- selector = dnf.selector.Selector(self.sack) +- selector.set(pkg=query) +- self._goal.install(select=selector, optional=(not strict)) +- +- if strict and failed: +- raise dnf.exceptions.PackageNotFoundError(_('no package matched')) +- + def _merge_update_filters(self, q, pkg_spec=None, warning=True): + """ + Merge Queries in _update_filters and return intersection with q Query +-- +2.26.2 + + +From c5a02f21d1a7b3be9ace78364ce234d853118574 Mon Sep 17 00:00:00 2001 +From: Daniel Mach +Date: Wed, 2 Dec 2020 08:57:15 +0100 +Subject: [PATCH 11/17] history: Move history methods from BaseCli to + HistoryCommand + +--- + dnf/cli/cli.py | 19 ------------- + dnf/cli/commands/history.py | 53 +++++++++++++++---------------------- + 2 files changed, 22 insertions(+), 50 deletions(-) + +diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py +index e4fd39c6..3080ae64 100644 +--- a/dnf/cli/cli.py ++++ b/dnf/cli/cli.py +@@ -608,25 +608,6 @@ class BaseCli(dnf.Base): + return False + return True + +- def _history_get_transactions(self, extcmds): +- if not extcmds: +- logger.critical(_('No transaction ID given')) +- return None +- +- old = self.history.old(extcmds) +- if not old: +- logger.critical(_('Not found given transaction ID')) +- return None +- return old +- +- def history_get_transaction(self, extcmds): +- old = self._history_get_transactions(extcmds) +- if old is None: +- return None +- if len(old) > 1: +- logger.critical(_('Found more than one transaction ID!')) +- return old[0] +- + + class Cli(object): + def __init__(self, base): +diff --git a/dnf/cli/commands/history.py b/dnf/cli/commands/history.py +index d60d3f25..dfd954ee 100644 +--- a/dnf/cli/commands/history.py ++++ b/dnf/cli/commands/history.py +@@ -34,7 +34,6 @@ import dnf.util + import json + import logging + import os +-import sys + + + logger = logging.getLogger('dnf') +@@ -160,10 +159,7 @@ class HistoryCommand(commands.Command): + return dnf.cli.commands.Command.get_error_output(self, error) + + def _hcmd_redo(self, extcmds): +- old = self.base.history_get_transaction(extcmds) +- if old is None: +- return 1, ['Failed history redo'] +- ++ old = self._history_get_transaction(extcmds) + data = serialize_transaction(old) + self.replay = TransactionReplay( + self.base, +@@ -174,16 +170,27 @@ class HistoryCommand(commands.Command): + ) + self.replay.run() + ++ def _history_get_transactions(self, extcmds): ++ if not extcmds: ++ raise dnf.cli.CliError(_('No transaction ID given')) ++ ++ old = self.base.history.old(extcmds) ++ if not old: ++ raise dnf.cli.CliError(_('Transaction ID "{0}" not found.').format(extcmds[0])) ++ return old ++ ++ def _history_get_transaction(self, extcmds): ++ old = self._history_get_transactions(extcmds) ++ if len(old) > 1: ++ raise dnf.cli.CliError(_('Found more than one transaction ID!')) ++ return old[0] ++ + def _hcmd_undo(self, extcmds): +- old = self.base.history_get_transaction(extcmds) +- if old is None: +- return 1, ['Failed history undo'] ++ old = self._history_get_transaction(extcmds) + return self._revert_transaction(old) + + def _hcmd_rollback(self, extcmds): +- old = self.base.history_get_transaction(extcmds) +- if old is None: +- return 1, ['Failed history rollback'] ++ old = self._history_get_transaction(extcmds) + last = self.base.history.last() + + merged_trans = None +@@ -239,12 +246,7 @@ class HistoryCommand(commands.Command): + ignore_extras=True, + skip_unavailable=self.opts.skip_unavailable + ) +- try: +- self.replay.run() +- except dnf.transaction_sr.TransactionFileError as ex: +- for error in ex.errors: +- print(str(error), file=sys.stderr) +- raise dnf.exceptions.PackageNotFoundError(_('no package matched')) ++ self.replay.run() + + def _hcmd_userinstalled(self): + """Execute history userinstalled command.""" +@@ -346,11 +348,8 @@ class HistoryCommand(commands.Command): + elif vcmd == 'userinstalled': + ret = self._hcmd_userinstalled() + elif vcmd == 'store': +- transactions = self.output.history.old(tids) +- if not transactions: +- raise dnf.cli.CliError(_('Transaction ID "{id}" not found.').format(id=tids[0])) +- +- data = serialize_transaction(transactions[0]) ++ tid = self._history_get_transaction(tids) ++ data = serialize_transaction(tid) + try: + filename = self.opts.output if self.opts.output is not None else "transaction.json" + +@@ -371,14 +370,6 @@ class HistoryCommand(commands.Command): + except OSError as e: + raise dnf.cli.CliError(_('Error storing transaction: {}').format(str(e))) + +- if ret is None: +- return +- (code, strs) = ret +- if code == 2: +- self.cli.demands.resolving = True +- elif code != 0: +- raise dnf.exceptions.Error(strs[0]) +- + def run_resolved(self): + if self.opts.transactions_action not in ("replay", "redo", "rollback", "undo"): + return +@@ -393,7 +384,7 @@ class HistoryCommand(commands.Command): + if warnings: + logger.log( + dnf.logging.WARNING, +- _("Warning, the following problems occurred while replaying the transaction:") ++ _("Warning, the following problems occurred while running a transaction:") + ) + for w in warnings: + logger.log(dnf.logging.WARNING, " " + w) +-- +2.26.2 + + +From 917f9f3b0fc418492293e08fa7db053b0c490d8f Mon Sep 17 00:00:00 2001 +From: Daniel Mach +Date: Thu, 10 Dec 2020 13:36:52 +0100 +Subject: [PATCH 12/17] transaction_sr: Simplify error reporting, unify with + history + +--- + dnf/transaction_sr.py | 20 +++++++++----------- + 1 file changed, 9 insertions(+), 11 deletions(-) + +diff --git a/dnf/transaction_sr.py b/dnf/transaction_sr.py +index 9926bebd..2122aba4 100644 +--- a/dnf/transaction_sr.py ++++ b/dnf/transaction_sr.py +@@ -57,21 +57,19 @@ class TransactionFileError(dnf.exceptions.Error): + + # store args in case someone wants to read them from a caught exception + self.filename = filename +- self.errors = errors +- + if isinstance(errors, (list, tuple)): +- if len(errors) > 1: +- msg = _('Errors in "{filename}":').format(filename=filename) +- for error in errors: +- msg += "\n " + str(error) ++ self.errors = errors ++ else: ++ self.errors = [errors] + +- super(TransactionFileError, self).__init__(msg) +- return ++ if filename: ++ msg = _('The following problems occurred while replaying the transaction from file "{filename}":').format(filename=filename) ++ else: ++ msg = _('The following problems occurred while running a transaction:') + +- else: +- errors = str(errors[0]) ++ for error in self.errors: ++ msg += "\n " + str(error) + +- msg = _('Error in "{filename}": {error}').format(filename=filename, error=errors) + super(TransactionFileError, self).__init__(msg) + + +-- +2.26.2 + + +From d2fb741829445efee3187553cf7960f7bc2f643e Mon Sep 17 00:00:00 2001 +From: Daniel Mach +Date: Thu, 17 Dec 2020 16:37:01 +0100 +Subject: [PATCH 13/17] transaction_sr: TransactionFileError exception to + TransactionReplayError + +--- + dnf/transaction_sr.py | 20 ++++++++++---------- + 1 file changed, 10 insertions(+), 10 deletions(-) + +diff --git a/dnf/transaction_sr.py b/dnf/transaction_sr.py +index 2122aba4..e4974eb9 100644 +--- a/dnf/transaction_sr.py ++++ b/dnf/transaction_sr.py +@@ -48,7 +48,7 @@ class TransactionError(dnf.exceptions.Error): + super(TransactionError, self).__init__(msg) + + +-class TransactionFileError(dnf.exceptions.Error): ++class TransactionReplayError(dnf.exceptions.Error): + def __init__(self, filename, errors): + """ + :param filename: The name of the transaction file being replayed +@@ -70,10 +70,10 @@ class TransactionFileError(dnf.exceptions.Error): + for error in self.errors: + msg += "\n " + str(error) + +- super(TransactionFileError, self).__init__(msg) ++ super(TransactionReplayError, self).__init__(msg) + + +-class IncompatibleTransactionVersionError(TransactionFileError): ++class IncompatibleTransactionVersionError(TransactionReplayError): + def __init__(self, filename, msg): + super(IncompatibleTransactionVersionError, self).__init__(filename, msg) + +@@ -84,7 +84,7 @@ def _check_version(version, filename): + try: + major = int(major) + except ValueError as e: +- raise TransactionFileError( ++ raise TransactionReplayError( + filename, + _('Invalid major version "{major}", number expected.').format(major=major) + ) +@@ -92,7 +92,7 @@ def _check_version(version, filename): + try: + int(minor) # minor is unused, just check it's a number + except ValueError as e: +- raise TransactionFileError( ++ raise TransactionReplayError( + filename, + _('Invalid minor version "{minor}", number expected.').format(minor=minor) + ) +@@ -234,12 +234,12 @@ class TransactionReplay(object): + try: + replay_data = json.load(f) + except json.decoder.JSONDecodeError as e: +- raise TransactionFileError(fn, str(e) + ".") ++ raise TransactionReplayError(fn, str(e) + ".") + + try: + self._load_from_data(replay_data) + except TransactionError as e: +- raise TransactionFileError(fn, e) ++ raise TransactionReplayError(fn, e) + + def _load_from_data(self, data): + self._replay_data = data +@@ -268,7 +268,7 @@ class TransactionReplay(object): + fn = self._filename + + if "version" not in replay_data: +- raise TransactionFileError(fn, _('Missing key "{key}".'.format(key="version"))) ++ raise TransactionReplayError(fn, _('Missing key "{key}".'.format(key="version"))) + + self._assert_type(replay_data["version"], str, "version", "string") + +@@ -580,7 +580,7 @@ class TransactionReplay(object): + errors.append(e) + + if errors: +- raise TransactionFileError(fn, errors) ++ raise TransactionReplayError(fn, errors) + + def post_transaction(self): + """ +@@ -635,4 +635,4 @@ class TransactionReplay(object): + pass + + if errors: +- raise TransactionFileError(self._filename, errors) ++ raise TransactionReplayError(self._filename, errors) +-- +2.26.2 + + +From 1182143e58d4fda530d5dfd19f0d9c9406e8eff3 Mon Sep 17 00:00:00 2001 +From: Daniel Mach +Date: Thu, 17 Dec 2020 16:55:39 +0100 +Subject: [PATCH 14/17] transaction_sr: Don't return if there's a mismatch in + actions + +When _ignore_installed == True, then an exception is raised anyway. +When _ignore_installed == False, get the requested package to the system +regardless the action. +--- + dnf/transaction_sr.py | 1 - + 1 file changed, 1 deletion(-) + +diff --git a/dnf/transaction_sr.py b/dnf/transaction_sr.py +index e4974eb9..dae8d300 100644 +--- a/dnf/transaction_sr.py ++++ b/dnf/transaction_sr.py +@@ -334,7 +334,6 @@ class TransactionReplay(object): + if action == "Install" and query_na.installed() and not self._base._get_installonly_query(query_na): + self._raise_or_warn(self._ignore_installed, + _('Package "{na}" is already installed for action "{action}".').format(na=na, action=action)) +- return + + sltr = dnf.selector.Selector(self._base.sack).set(pkg=query) + self._base.goal.install(select=sltr, optional=not self._base.conf.strict) +-- +2.26.2 + + +From ff32a3c68fa853b53084a1a4947f345062056f23 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hr=C3=A1zk=C3=BD?= +Date: Fri, 8 Jan 2021 13:37:45 +0100 +Subject: [PATCH 15/17] cli/output: Return number of listed packages from + listPkgs() + +Instead of an error status and message. +--- + dnf/cli/cli.py | 5 ++--- + dnf/cli/commands/history.py | 4 +++- + dnf/cli/output.py | 14 ++------------ + 3 files changed, 7 insertions(+), 16 deletions(-) + +diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py +index 3080ae64..be737ed3 100644 +--- a/dnf/cli/cli.py ++++ b/dnf/cli/cli.py +@@ -505,7 +505,7 @@ class BaseCli(dnf.Base): + # XXX put this into the ListCommand at some point + if len(ypl.obsoletes) > 0 and basecmd == 'list': + # if we've looked up obsolete lists and it's a list request +- rop = [0, ''] ++ rop = len(ypl.obsoletes) + print(_('Obsoleting Packages')) + for obtup in sorted(ypl.obsoletesTuples, + key=operator.itemgetter(0)): +@@ -517,8 +517,7 @@ class BaseCli(dnf.Base): + rrap = self.output.listPkgs(ypl.recent, _('Recently Added Packages'), + basecmd, columns=columns) + if len(patterns) and \ +- rrap[0] and rop[0] and rup[0] and rep[0] and rap[0] and \ +- raep[0] and rip[0]: ++ rrap == 0 and rop == 0 and rup == 0 and rep == 0 and rap == 0 and raep == 0 and rip == 0: + raise dnf.exceptions.Error(_('No matching Packages to list')) + + def returnPkgLists(self, pkgnarrow='all', patterns=None, +diff --git a/dnf/cli/commands/history.py b/dnf/cli/commands/history.py +index dfd954ee..e9b91d0f 100644 +--- a/dnf/cli/commands/history.py ++++ b/dnf/cli/commands/history.py +@@ -251,7 +251,9 @@ class HistoryCommand(commands.Command): + def _hcmd_userinstalled(self): + """Execute history userinstalled command.""" + pkgs = tuple(self.base.iter_userinstalled()) +- return self.output.listPkgs(pkgs, 'Packages installed by user', 'nevra') ++ n_listed = self.output.listPkgs(pkgs, 'Packages installed by user', 'nevra') ++ if n_listed == 0: ++ raise dnf.cli.CliError(_('No packages to list')) + + def _args2transaction_ids(self): + """Convert commandline arguments to transaction ids""" +diff --git a/dnf/cli/output.py b/dnf/cli/output.py +index 6d729b63..6cfc9e22 100644 +--- a/dnf/cli/output.py ++++ b/dnf/cli/output.py +@@ -597,18 +597,10 @@ class Output(object): + number + '>' - highlighting used when the package has a higher version + number +- :return: (exit_code, [errors]) +- +- exit_code is:: +- +- 0 = we're done, exit +- 1 = we've errored, exit with error string +- ++ :return: number of packages listed + """ + if outputType in ['list', 'info', 'name', 'nevra']: +- thingslisted = 0 + if len(lst) > 0: +- thingslisted = 1 + print('%s' % description) + info_set = set() + if outputType == 'list': +@@ -645,9 +637,7 @@ class Output(object): + if info_set: + print("\n".join(sorted(info_set))) + +- if thingslisted == 0: +- return 1, [_('No packages to list')] +- return 0, [] ++ return len(lst) + + def userconfirm(self, msg=None, defaultyes_msg=None): + """Get a yes or no from the user, and default to No +-- +2.26.2 + + +From 0226da7351eb97cd9c4c6739725b1f77d445764e Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hr=C3=A1zk=C3=BD?= +Date: Fri, 8 Jan 2021 13:44:27 +0100 +Subject: [PATCH 16/17] Clean up history command error handling + +The removal of `ret` value error handling which was removed previously was not +complete. Most of it is was no-op as no errors were really propagated through +it, but the `history userinstalled` command was still relying on it. + +The commit removes the last bit and replaces it with raising an exception. +--- + dnf/cli/commands/history.py | 17 ++++++++--------- + 1 file changed, 8 insertions(+), 9 deletions(-) + +diff --git a/dnf/cli/commands/history.py b/dnf/cli/commands/history.py +index e9b91d0f..7b38cb60 100644 +--- a/dnf/cli/commands/history.py ++++ b/dnf/cli/commands/history.py +@@ -187,7 +187,7 @@ class HistoryCommand(commands.Command): + + def _hcmd_undo(self, extcmds): + old = self._history_get_transaction(extcmds) +- return self._revert_transaction(old) ++ self._revert_transaction(old) + + def _hcmd_rollback(self, extcmds): + old = self._history_get_transaction(extcmds) +@@ -209,7 +209,7 @@ class HistoryCommand(commands.Command): + else: + merged_trans.merge(trans) + +- return self._revert_transaction(merged_trans) ++ self._revert_transaction(merged_trans) + + def _revert_transaction(self, trans): + action_map = { +@@ -321,7 +321,6 @@ class HistoryCommand(commands.Command): + + def run(self): + vcmd = self.opts.transactions_action +- ret = None + + if vcmd == 'replay': + self.base.read_comps(arch_filter=True) +@@ -338,17 +337,17 @@ class HistoryCommand(commands.Command): + tids, merged_tids = self._args2transaction_ids() + + if vcmd == 'list' and (tids or not self.opts.transactions): +- ret = self.output.historyListCmd(tids, reverse=self.opts.reverse) ++ self.output.historyListCmd(tids, reverse=self.opts.reverse) + elif vcmd == 'info' and (tids or not self.opts.transactions): +- ret = self.output.historyInfoCmd(tids, self.opts.transactions, merged_tids) ++ self.output.historyInfoCmd(tids, self.opts.transactions, merged_tids) + elif vcmd == 'undo': +- ret = self._hcmd_undo(tids) ++ self._hcmd_undo(tids) + elif vcmd == 'redo': +- ret = self._hcmd_redo(tids) ++ self._hcmd_redo(tids) + elif vcmd == 'rollback': +- ret = self._hcmd_rollback(tids) ++ self._hcmd_rollback(tids) + elif vcmd == 'userinstalled': +- ret = self._hcmd_userinstalled() ++ self._hcmd_userinstalled() + elif vcmd == 'store': + tid = self._history_get_transaction(tids) + data = serialize_transaction(tid) +-- +2.26.2 + + +From 7e862711b3d7b9b444d966594630b49bf3761faf Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hr=C3=A1zk=C3=BD?= +Date: Mon, 23 Nov 2020 16:32:16 +0100 +Subject: [PATCH 17/17] Lazy-load base.comps instead of explicitly + +Loading base.comps was done by calling a method at arbitrary places in +the code, this is hard to maintain and get right. The method could be +inadvertedly called multiple times per dnf run too. + +Instead load the comps data lazily on first access. In case of the +shell, using "repo enable/disable" can cause the comps data to change +mid-run. Instead of explicitly reloading, clear the comps attribute and +let it be lazy-loaded again when needed. + +Closes: #1690 +Approved by: j-mracek +--- + dnf/base.py | 4 ++-- + dnf/cli/commands/group.py | 5 ----- + dnf/cli/commands/history.py | 2 -- + dnf/cli/commands/install.py | 1 - + dnf/cli/commands/remove.py | 1 - + dnf/cli/commands/repoquery.py | 1 - + dnf/cli/commands/shell.py | 3 +++ + dnf/cli/commands/upgrade.py | 1 - + tests/api/test_dnf_base.py | 4 +--- + 9 files changed, 6 insertions(+), 16 deletions(-) + +diff --git a/dnf/base.py b/dnf/base.py +index a2955051..39c21c33 100644 +--- a/dnf/base.py ++++ b/dnf/base.py +@@ -242,6 +242,8 @@ class Base(object): + @property + def comps(self): + # :api ++ if self._comps is None: ++ self.read_comps(arch_filter=True) + return self._comps + + @property +@@ -1881,7 +1883,6 @@ class Base(object): + no_match_module_specs = install_specs.grp_specs + + if no_match_module_specs: +- self.read_comps(arch_filter=True) + exclude_specs.grp_specs = self._expand_groups(exclude_specs.grp_specs) + self._install_groups(no_match_module_specs, exclude_specs, no_match_group_specs, strict) + +@@ -2084,7 +2085,6 @@ class Base(object): + msg = _('Not a valid form: %s') + logger.warning(msg, grp_spec) + elif grp_specs: +- self.read_comps(arch_filter=True) + if self.env_group_remove(grp_specs): + done = True + +diff --git a/dnf/cli/commands/group.py b/dnf/cli/commands/group.py +index bd17f80f..cf542799 100644 +--- a/dnf/cli/commands/group.py ++++ b/dnf/cli/commands/group.py +@@ -110,9 +110,6 @@ class GroupCommand(commands.Command): + + return installed, available + +- def _grp_setup(self): +- self.base.read_comps(arch_filter=True) +- + def _info(self, userlist): + for strng in userlist: + group_matched = False +@@ -370,8 +367,6 @@ class GroupCommand(commands.Command): + cmd = self.opts.subcmd + extcmds = self.opts.args + +- self._grp_setup() +- + if cmd == 'summary': + return self._summary(extcmds) + if cmd == 'list': +diff --git a/dnf/cli/commands/history.py b/dnf/cli/commands/history.py +index 7b38cb60..293d93fc 100644 +--- a/dnf/cli/commands/history.py ++++ b/dnf/cli/commands/history.py +@@ -323,8 +323,6 @@ class HistoryCommand(commands.Command): + vcmd = self.opts.transactions_action + + if vcmd == 'replay': +- self.base.read_comps(arch_filter=True) +- + self.replay = TransactionReplay( + self.base, + filename=self.opts.transaction_filename, +diff --git a/dnf/cli/commands/install.py b/dnf/cli/commands/install.py +index 38a90b61..b637af0b 100644 +--- a/dnf/cli/commands/install.py ++++ b/dnf/cli/commands/install.py +@@ -151,7 +151,6 @@ class InstallCommand(commands.Command): + return err_pkgs + + def _install_groups(self, grp_specs): +- self.base.read_comps(arch_filter=True) + try: + self.base.env_group_install(grp_specs, + tuple(self.base.conf.group_package_types), +diff --git a/dnf/cli/commands/remove.py b/dnf/cli/commands/remove.py +index f50dbd91..e455ba6e 100644 +--- a/dnf/cli/commands/remove.py ++++ b/dnf/cli/commands/remove.py +@@ -142,7 +142,6 @@ class RemoveCommand(commands.Command): + skipped_grps = self.opts.grp_specs + + if skipped_grps: +- self.base.read_comps(arch_filter=True) + for group in skipped_grps: + try: + if self.base.env_group_remove([group]): +diff --git a/dnf/cli/commands/repoquery.py b/dnf/cli/commands/repoquery.py +index 099a9312..b0d06a90 100644 +--- a/dnf/cli/commands/repoquery.py ++++ b/dnf/cli/commands/repoquery.py +@@ -632,7 +632,6 @@ class RepoQueryCommand(commands.Command): + print("\n".join(sorted(pkgs))) + + def _group_member_report(self, query): +- self.base.read_comps(arch_filter=True) + package_conf_dict = {} + for group in self.base.comps.groups: + package_conf_dict[group.id] = set([pkg.name for pkg in group.packages_iter()]) +diff --git a/dnf/cli/commands/shell.py b/dnf/cli/commands/shell.py +index 431fe502..18c886ff 100644 +--- a/dnf/cli/commands/shell.py ++++ b/dnf/cli/commands/shell.py +@@ -239,6 +239,9 @@ exit (or quit) exit the shell""") + if fill_sack: + self.base.fill_sack() + ++ # reset base._comps, as it has changed due to changing the repos ++ self.base._comps = None ++ + else: + self._help('repo') + +diff --git a/dnf/cli/commands/upgrade.py b/dnf/cli/commands/upgrade.py +index 44789c9a..f62cfcc1 100644 +--- a/dnf/cli/commands/upgrade.py ++++ b/dnf/cli/commands/upgrade.py +@@ -124,7 +124,6 @@ class UpgradeCommand(commands.Command): + + def _update_groups(self): + if self.skipped_grp_specs: +- self.base.read_comps(arch_filter=True) + self.base.env_group_upgrade(self.skipped_grp_specs) + return True + return False +diff --git a/tests/api/test_dnf_base.py b/tests/api/test_dnf_base.py +index ca71b75c..656bd225 100644 +--- a/tests/api/test_dnf_base.py ++++ b/tests/api/test_dnf_base.py +@@ -34,9 +34,7 @@ class DnfBaseApiTest(TestCase): + def test_comps(self): + # Base.comps + self.assertHasAttr(self.base, "comps") +- +- # blank initially +- self.assertEqual(self.base.comps, None) ++ self.assertHasType(self.base.comps, dnf.comps.Comps) + + self.base.read_comps() + self.assertHasType(self.base.comps, dnf.comps.Comps) +-- +2.26.2 + diff --git a/SOURCES/0006-Remove-sourcepackages-from-install-upgrade-set.patch b/SOURCES/0006-Remove-sourcepackages-from-install-upgrade-set.patch new file mode 100644 index 0000000..1ef5a4e --- /dev/null +++ b/SOURCES/0006-Remove-sourcepackages-from-install-upgrade-set.patch @@ -0,0 +1,150 @@ +From 8f3ce4868ac009976da7323ea39ebcd9a062e32d Mon Sep 17 00:00:00 2001 +From: Jaroslav Mracek +Date: Mon, 23 Nov 2020 17:00:01 +0100 +Subject: [PATCH 1/3] Remove source packages from install/upgrade set + (RhBug:1898548) + +It prevents Error: Will not install a source rpm package () + +https://bugzilla.redhat.com/show_bug.cgi?id=1898548 +--- + dnf/module/module_base.py | 16 ++++++++++------ + 1 file changed, 10 insertions(+), 6 deletions(-) + +diff --git a/dnf/module/module_base.py b/dnf/module/module_base.py +index 04701b9d..49c871c4 100644 +--- a/dnf/module/module_base.py ++++ b/dnf/module/module_base.py +@@ -140,20 +140,21 @@ class ModuleBase(object): + if fail_safe_repo_used: + raise dnf.exceptions.Error(_( + "Installing module from Fail-Safe repository is not allowed")) +- install_base_query = self.base.sack.query().filterm( +- nevra_strict=install_set_artefacts).apply() ++ # Remove source packages they cannot be installed or upgraded ++ base_no_source_query = self.base.sack.query().filterm(arch__neq=['src', 'nosrc']).apply() ++ install_base_query = base_no_source_query.filter(nevra_strict=install_set_artefacts) + + # add hot-fix packages + hot_fix_repos = [i.id for i in self.base.repos.iter_enabled() if i.module_hotfixes] +- hotfix_packages = self.base.sack.query().filterm(reponame=hot_fix_repos).filterm( +- name=install_dict.keys()) ++ hotfix_packages = base_no_source_query.filter( ++ reponame=hot_fix_repos, name=install_dict.keys()) + install_base_query = install_base_query.union(hotfix_packages) + + for pkg_name, set_specs in install_dict.items(): + query = install_base_query.filter(name=pkg_name) + if not query: + # package can also be non-modular or part of another stream +- query = self.base.sack.query().filterm(name=pkg_name) ++ query = base_no_source_query.filter(name=pkg_name) + if not query: + for spec in set_specs: + logger.error(_("Unable to resolve argument {}").format(spec)) +@@ -182,6 +183,9 @@ class ModuleBase(object): + fail_safe_repo = hawkey.MODULE_FAIL_SAFE_REPO_NAME + fail_safe_repo_used = False + ++ # Remove source packages they cannot be installed or upgraded ++ base_no_source_query = self.base.sack.query().filterm(arch__neq=['src', 'nosrc']).apply() ++ + for spec in module_specs: + module_list, nsvcap = self._get_modules(spec) + if not module_list: +@@ -221,7 +225,7 @@ class ModuleBase(object): + + if not upgrade_package_set: + logger.error(_("Unable to match profile in argument {}").format(spec)) +- query = self.base.sack.query().filterm(name=upgrade_package_set) ++ query = base_no_source_query.filter(name=upgrade_package_set) + if query: + sltr = dnf.selector.Selector(self.base.sack) + sltr.set(pkg=query) +-- +2.26.2 + + +From c42680b292b2cca38b24fb18f46f06f800c1934f Mon Sep 17 00:00:00 2001 +From: Jaroslav Mracek +Date: Mon, 23 Nov 2020 17:04:05 +0100 +Subject: [PATCH 2/3] Remove all source packages from query + +--- + dnf/base.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/dnf/base.py b/dnf/base.py +index a2955051..a3d9b63f 100644 +--- a/dnf/base.py ++++ b/dnf/base.py +@@ -1550,7 +1550,7 @@ class Base(object): + if (comps_pkg.basearchonly): + query_args.update({'arch': basearch}) + q = self.sack.query().filterm(**query_args).apply() +- q.filterm(arch__neq="src") ++ q.filterm(arch__neq=["src", "nosrc"]) + if not q: + package_string = comps_pkg.name + if comps_pkg.basearchonly: +-- +2.26.2 + + +From 1f68fa6dc59fb350e71a24e787135475f3fb5b4c Mon Sep 17 00:00:00 2001 +From: Jaroslav Mracek +Date: Mon, 23 Nov 2020 17:29:45 +0100 +Subject: [PATCH 3/3] Run tests with sack in tmp directory + +--- + tests/api/test_dnf_module_base.py | 17 +++++++++++------ + 1 file changed, 11 insertions(+), 6 deletions(-) + +diff --git a/tests/api/test_dnf_module_base.py b/tests/api/test_dnf_module_base.py +index aa47555b..18dd080d 100644 +--- a/tests/api/test_dnf_module_base.py ++++ b/tests/api/test_dnf_module_base.py +@@ -7,16 +7,26 @@ from __future__ import unicode_literals + import dnf + import dnf.module.module_base + ++import os ++import shutil ++import tempfile ++ + from .common import TestCase + + + class DnfModuleBaseApiTest(TestCase): + def setUp(self): + self.base = dnf.Base(dnf.conf.Conf()) ++ self._installroot = tempfile.mkdtemp(prefix="dnf_test_installroot_") ++ self.base.conf.installroot = self._installroot ++ self.base.conf.cachedir = os.path.join(self._installroot, "var/cache/dnf") ++ self.base._sack = dnf.sack._build_sack(self.base) + self.moduleBase = dnf.module.module_base.ModuleBase(self.base) + + def tearDown(self): + self.base.close() ++ if self._installroot.startswith("/tmp/"): ++ shutil.rmtree(self._installroot) + + def test_init(self): + moduleBase = dnf.module.module_base.ModuleBase(self.base) +@@ -51,12 +61,7 @@ class DnfModuleBaseApiTest(TestCase): + def test_install(self): + # ModuleBase.install() + self.assertHasAttr(self.moduleBase, "install") +- self.assertRaises( +- AttributeError, +- self.moduleBase.install, +- module_specs=[], +- strict=False, +- ) ++ self.moduleBase.install(module_specs=[], strict=False) + + def test_remove(self): + # ModuleBase.remove() +-- +2.26.2 + diff --git a/SOURCES/0007-Fix-documentation-of-globs-not-supporting-curly-brackets.patch b/SOURCES/0007-Fix-documentation-of-globs-not-supporting-curly-brackets.patch new file mode 100644 index 0000000..2daa3b2 --- /dev/null +++ b/SOURCES/0007-Fix-documentation-of-globs-not-supporting-curly-brackets.patch @@ -0,0 +1,30 @@ +From f3c254581bcb0591a543aee0c7e031c3c9d0a9a1 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hr=C3=A1zk=C3=BD?= +Date: Mon, 11 Jan 2021 16:43:25 +0100 +Subject: [PATCH] Fix documentation of globs not supporting curly brackets + += changelog = +msg: Fix documentation of globs not supporting curly brackets +type: bugfix +resolves: https://bugzilla.redhat.com/show_bug.cgi?id=1913418 +--- + doc/command_ref.rst | 6 ++++-- + 1 file changed, 4 insertions(+), 2 deletions(-) + +diff --git a/doc/command_ref.rst b/doc/command_ref.rst +index bbce3ddcf4..d11e8dd502 100644 +--- a/doc/command_ref.rst ++++ b/doc/command_ref.rst +@@ -1763,8 +1763,10 @@ The following patterns are supported: + those two characters, inclusive, is matched. If the first character + following the ``[`` is a ``!`` or a ``^`` then any character not enclosed + is matched. +-``{}`` +- Matches any of the comma separated list of enclosed strings. ++ ++Note: Curly brackets (``{}``) are not supported. You can still use them in ++shells that support them and let the shell do the expansion, but if quoted or ++escaped, dnf will not expand them. + + -------------- + NEVRA Matching diff --git a/SOURCES/0008-Module-switch-command.patch b/SOURCES/0008-Module-switch-command.patch new file mode 100644 index 0000000..f464e15 --- /dev/null +++ b/SOURCES/0008-Module-switch-command.patch @@ -0,0 +1,507 @@ +From 6ed0458744090ab307da9d9118690372b2e66ca8 Mon Sep 17 00:00:00 2001 +From: Jaroslav Mracek +Date: Wed, 11 Nov 2020 12:47:21 +0100 +Subject: [PATCH 1/5] Make module_base better industrialized for method reuse + +It will allow to use internal for module switch command. +--- + dnf/module/module_base.py | 29 ++++++++++++++++++++--------- + 1 file changed, 20 insertions(+), 9 deletions(-) + +diff --git a/dnf/module/module_base.py b/dnf/module/module_base.py +index 49c871c4..0da4fab1 100644 +--- a/dnf/module/module_base.py ++++ b/dnf/module/module_base.py +@@ -323,7 +323,7 @@ class ModuleBase(object): + assert len(streamDict) == 1 + return moduleDict + +- def _resolve_specs_enable_update_sack(self, module_specs): ++ def _resolve_specs_enable(self, module_specs): + no_match_specs = [] + error_spec = [] + module_dicts = {} +@@ -339,6 +339,9 @@ class ModuleBase(object): + error_spec.append(spec) + logger.error(ucd(e)) + logger.error(_("Unable to resolve argument {}").format(spec)) ++ return no_match_specs, error_spec, module_dicts ++ ++ def _update_sack(self): + hot_fix_repos = [i.id for i in self.base.repos.iter_enabled() if i.module_hotfixes] + try: + solver_errors = self.base.sack.filter_modules( +@@ -347,6 +350,10 @@ class ModuleBase(object): + debugsolver=self.base.conf.debug_solver) + except hawkey.Exception as e: + raise dnf.exceptions.Error(ucd(e)) ++ return solver_errors ++ ++ def _enable_dependencies(self, module_dicts): ++ error_spec = [] + for spec, (nsvcap, moduleDict) in module_dicts.items(): + for streamDict in moduleDict.values(): + for modules in streamDict.values(): +@@ -357,6 +364,17 @@ class ModuleBase(object): + error_spec.append(spec) + logger.error(ucd(e)) + logger.error(_("Unable to resolve argument {}").format(spec)) ++ return error_spec ++ ++ def _resolve_specs_enable_update_sack(self, module_specs): ++ no_match_specs, error_spec, module_dicts = self._resolve_specs_enable(module_specs) ++ ++ solver_errors = self._update_sack() ++ ++ dependency_error_spec = self._enable_dependencies(module_dicts) ++ if dependency_error_spec: ++ error_spec.extend(dependency_error_spec) ++ + return no_match_specs, error_spec, solver_errors, module_dicts + + def _modules_reset_or_disable(self, module_specs, to_state): +@@ -379,14 +397,7 @@ class ModuleBase(object): + if to_state == STATE_DISABLED: + self.base._moduleContainer.disable(name) + +- hot_fix_repos = [i.id for i in self.base.repos.iter_enabled() if i.module_hotfixes] +- try: +- solver_errors = self.base.sack.filter_modules( +- self.base._moduleContainer, hot_fix_repos, self.base.conf.installroot, +- self.base.conf.module_platform_id, update_only=True, +- debugsolver=self.base.conf.debug_solver) +- except hawkey.Exception as e: +- raise dnf.exceptions.Error(ucd(e)) ++ solver_errors = self._update_sack() + return no_match_specs, solver_errors + + def _get_package_name_set_and_remove_profiles(self, module_list, nsvcap, remove=False): +-- +2.26.2 + + +From e6473f4e6f17bb635e023b8905f29b318b8795bf Mon Sep 17 00:00:00 2001 +From: Jaroslav Mracek +Date: Wed, 11 Nov 2020 17:09:16 +0100 +Subject: [PATCH 2/5] Add module switch-to support (RhBug:1792020) + +It is a combination of module rpm distrosync, module profile switch and +module stream switch. + += changelog = +msg: Add new `module switch-to` command for switching content +of module streams +type: enhancement +resolves: https://bugzilla.redhat.com/show_bug.cgi?id=1792020 +--- + VERSION.cmake | 2 +- + dnf.spec | 2 +- + dnf/cli/commands/module.py | 24 ++++- + dnf/module/module_base.py | 182 ++++++++++++++++++++++++++++++++----- + 4 files changed, 185 insertions(+), 25 deletions(-) + +diff --git a/dnf/cli/commands/module.py b/dnf/cli/commands/module.py +index 5a6c0069..4cdc915e 100644 +--- a/dnf/cli/commands/module.py ++++ b/dnf/cli/commands/module.py +@@ -271,6 +271,28 @@ class ModuleCommand(commands.Command): + + logger.error(dnf.exceptions.MarkingErrors(no_match_group_specs=skipped_groups)) + ++ class SwitchToSubCommand(SubCommand): ++ ++ aliases = ('switch-to',) ++ summary = _('switch a module to a stream and distrosync rpm packages') ++ ++ def configure(self): ++ demands = self.cli.demands ++ demands.available_repos = True ++ demands.sack_activation = True ++ demands.resolving = True ++ demands.root_user = True ++ self.base.conf.module_stream_switch = True ++ ++ def run_on_module(self): ++ try: ++ self.module_base.switch_to(self.opts.module_spec, strict=self.base.conf.strict) ++ except dnf.exceptions.MarkingErrors as e: ++ if self.base.conf.strict: ++ if e.no_match_group_specs or e.error_group_specs: ++ raise e ++ logger.error(str(e)) ++ + class ProvidesSubCommand(SubCommand): + + aliases = ("provides", ) +@@ -319,7 +341,7 @@ class ModuleCommand(commands.Command): + + SUBCMDS = {ListSubCommand, InfoSubCommand, EnableSubCommand, + DisableSubCommand, ResetSubCommand, InstallSubCommand, UpdateSubCommand, +- RemoveSubCommand, ProvidesSubCommand, RepoquerySubCommand} ++ RemoveSubCommand, SwitchToSubCommand, ProvidesSubCommand, RepoquerySubCommand} + + SUBCMDS_NOT_REQUIRED_ARG = {ListSubCommand} + +diff --git a/dnf/module/module_base.py b/dnf/module/module_base.py +index 0da4fab1..03d54f72 100644 +--- a/dnf/module/module_base.py ++++ b/dnf/module/module_base.py +@@ -140,31 +140,140 @@ class ModuleBase(object): + if fail_safe_repo_used: + raise dnf.exceptions.Error(_( + "Installing module from Fail-Safe repository is not allowed")) +- # Remove source packages they cannot be installed or upgraded +- base_no_source_query = self.base.sack.query().filterm(arch__neq=['src', 'nosrc']).apply() +- install_base_query = base_no_source_query.filter(nevra_strict=install_set_artefacts) ++ __, profiles_errors = self._install_profiles_internal( ++ install_set_artefacts, install_dict, strict) ++ if profiles_errors: ++ error_specs.extend(profiles_errors) + +- # add hot-fix packages +- hot_fix_repos = [i.id for i in self.base.repos.iter_enabled() if i.module_hotfixes] +- hotfix_packages = base_no_source_query.filter( +- reponame=hot_fix_repos, name=install_dict.keys()) +- install_base_query = install_base_query.union(hotfix_packages) ++ if no_match_specs or error_specs or solver_errors: ++ raise dnf.exceptions.MarkingErrors(no_match_group_specs=no_match_specs, ++ error_group_specs=error_specs, ++ module_depsolv_errors=solver_errors) + +- for pkg_name, set_specs in install_dict.items(): +- query = install_base_query.filter(name=pkg_name) +- if not query: +- # package can also be non-modular or part of another stream +- query = base_no_source_query.filter(name=pkg_name) +- if not query: +- for spec in set_specs: +- logger.error(_("Unable to resolve argument {}").format(spec)) +- logger.error(_("No match for package {}").format(pkg_name)) +- error_specs.extend(set_specs) +- continue +- self.base._goal.group_members.add(pkg_name) ++ def switch_to(self, module_specs, strict=True): ++ # :api ++ no_match_specs, error_specs, module_dicts = self._resolve_specs_enable(module_specs) ++ # collect name of artifacts from new modules for distrosync ++ new_artifacts_names = set() ++ # collect name of artifacts from active modules for distrosync before sack update ++ active_artifacts_names = set() ++ src_arches = {"nosrc", "src"} ++ for spec, (nsvcap, moduledict) in module_dicts.items(): ++ for name in moduledict.keys(): ++ for module in self.base._moduleContainer.query(name, "", "", "", ""): ++ if self.base._moduleContainer.isModuleActive(module): ++ for artifact in module.getArtifacts(): ++ arch = artifact.rsplit(".", 1)[1] ++ if arch in src_arches: ++ continue ++ pkg_name = artifact.rsplit("-", 2)[0] ++ active_artifacts_names.add(pkg_name) ++ ++ solver_errors = self._update_sack() ++ ++ dependency_error_spec = self._enable_dependencies(module_dicts) ++ if dependency_error_spec: ++ error_specs.extend(dependency_error_spec) ++ ++ # ++ fail_safe_repo = hawkey.MODULE_FAIL_SAFE_REPO_NAME ++ install_dict = {} ++ install_set_artifacts = set() ++ fail_safe_repo_used = False ++ ++ # list of name: [profiles] for module profiles being removed ++ removed_profiles = self.base._moduleContainer.getRemovedProfiles() ++ ++ for spec, (nsvcap, moduledict) in module_dicts.items(): ++ for name, streamdict in moduledict.items(): ++ for stream, module_list in streamdict.items(): ++ install_module_list = [x for x in module_list ++ if self.base._moduleContainer.isModuleActive(x.getId())] ++ if not install_module_list: ++ "No active matches for argument '{0}' in module '{1}:{2}'" ++ logger.error(_("No active matches for argument '{0}' in module " ++ "'{1}:{2}'").format(spec, name, stream)) ++ error_specs.append(spec) ++ continue ++ profiles = [] ++ latest_module = self._get_latest(install_module_list) ++ if latest_module.getRepoID() == fail_safe_repo: ++ msg = _( ++ "Installing module '{0}' from Fail-Safe repository {1} is not allowed") ++ logger.critical(msg.format(latest_module.getNameStream(), fail_safe_repo)) ++ fail_safe_repo_used = True ++ if nsvcap.profile: ++ profiles.extend(latest_module.getProfiles(nsvcap.profile)) ++ if not profiles: ++ available_profiles = latest_module.getProfiles() ++ if available_profiles: ++ profile_names = ", ".join(sorted( ++ [profile.getName() for profile in available_profiles])) ++ msg = _("Unable to match profile for argument {}. Available " ++ "profiles for '{}:{}': {}").format( ++ spec, name, stream, profile_names) ++ else: ++ msg = _("Unable to match profile for argument {}").format(spec) ++ logger.error(msg) ++ no_match_specs.append(spec) ++ continue ++ elif name in removed_profiles: ++ ++ for profile in removed_profiles[name]: ++ module_profiles = latest_module.getProfiles(profile) ++ if not module_profiles: ++ logger.warning( ++ _("Installed profile '{0}' is not available in module " ++ "'{1}' stream '{2}'").format(profile, name, stream)) ++ continue ++ profiles.extend(module_profiles) ++ for profile in profiles: ++ self.base._moduleContainer.install(latest_module, profile.getName()) ++ for pkg_name in profile.getContent(): ++ install_dict.setdefault(pkg_name, set()).add(spec) ++ for module in install_module_list: ++ artifacts = module.getArtifacts() ++ install_set_artifacts.update(artifacts) ++ for artifact in artifacts: ++ arch = artifact.rsplit(".", 1)[1] ++ if arch in src_arches: ++ continue ++ pkg_name = artifact.rsplit("-", 2)[0] ++ new_artifacts_names.add(pkg_name) ++ if fail_safe_repo_used: ++ raise dnf.exceptions.Error(_( ++ "Installing module from Fail-Safe repository is not allowed")) ++ install_base_query, profiles_errors = self._install_profiles_internal( ++ install_set_artifacts, install_dict, strict) ++ if profiles_errors: ++ error_specs.extend(profiles_errors) ++ ++ # distrosync module name ++ all_names = set() ++ all_names.update(new_artifacts_names) ++ all_names.update(active_artifacts_names) ++ remove_query = self.base.sack.query().filterm(empty=True) ++ for pkg_name in all_names: ++ query = self.base.sack.query().filterm(name=pkg_name) ++ installed = query.installed() ++ if not installed: ++ continue ++ available = query.available() ++ if not available: ++ logger.warning(_("No packages available to distrosync for package name " ++ "'{}'").format(pkg_name)) ++ if pkg_name not in new_artifacts_names: ++ remove_query = remove_query.union(query) ++ continue ++ ++ only_new_module = query.intersection(install_base_query) ++ if only_new_module: ++ query = only_new_module + sltr = dnf.selector.Selector(self.base.sack) + sltr.set(pkg=query) +- self.base._goal.install(select=sltr, optional=(not strict)) ++ self.base._goal.distupgrade(select=sltr) ++ self.base._remove_if_unneeded(remove_query) ++ + if no_match_specs or error_specs or solver_errors: + raise dnf.exceptions.MarkingErrors(no_match_group_specs=no_match_specs, + error_group_specs=error_specs, +@@ -183,7 +292,7 @@ class ModuleBase(object): + fail_safe_repo = hawkey.MODULE_FAIL_SAFE_REPO_NAME + fail_safe_repo_used = False + +- # Remove source packages they cannot be installed or upgraded ++ # Remove source packages because they cannot be installed or upgraded + base_no_source_query = self.base.sack.query().filterm(arch__neq=['src', 'nosrc']).apply() + + for spec in module_specs: +@@ -694,6 +803,35 @@ class ModuleBase(object): + def _format_repoid(self, repo_name): + return "{}\n".format(self.base.output.term.bold(repo_name)) + ++ def _install_profiles_internal(self, install_set_artifacts, install_dict, strict): ++ # Remove source packages because they cannot be installed or upgraded ++ base_no_source_query = self.base.sack.query().filterm(arch__neq=['src', 'nosrc']).apply() ++ install_base_query = base_no_source_query.filter(nevra_strict=install_set_artifacts) ++ error_specs = [] ++ ++ # add hot-fix packages ++ hot_fix_repos = [i.id for i in self.base.repos.iter_enabled() if i.module_hotfixes] ++ hotfix_packages = base_no_source_query.filter( ++ reponame=hot_fix_repos, name=install_dict.keys()) ++ install_base_query = install_base_query.union(hotfix_packages) ++ ++ for pkg_name, set_specs in install_dict.items(): ++ query = install_base_query.filter(name=pkg_name) ++ if not query: ++ # package can also be non-modular or part of another stream ++ query = base_no_source_query.filter(name=pkg_name) ++ if not query: ++ for spec in set_specs: ++ logger.error(_("Unable to resolve argument {}").format(spec)) ++ logger.error(_("No match for package {}").format(pkg_name)) ++ error_specs.extend(set_specs) ++ continue ++ self.base._goal.group_members.add(pkg_name) ++ sltr = dnf.selector.Selector(self.base.sack) ++ sltr.set(pkg=query) ++ self.base._goal.install(select=sltr, optional=(not strict)) ++ return install_base_query, error_specs ++ + + def format_modular_solver_errors(errors): + msg = dnf.util._format_resolve_problems(errors) +-- +2.26.2 + + +From df8c74679193bf27db584b3ad225997b2f5f4b87 Mon Sep 17 00:00:00 2001 +From: Jaroslav Mracek +Date: Thu, 12 Nov 2020 13:51:02 +0100 +Subject: [PATCH 3/5] [minor] Rename all variables with artefact to artifact + +--- + dnf/module/module_base.py | 10 +++++----- + 1 file changed, 5 insertions(+), 5 deletions(-) + +diff --git a/dnf/module/module_base.py b/dnf/module/module_base.py +index 03d54f72..7298c9a3 100644 +--- a/dnf/module/module_base.py ++++ b/dnf/module/module_base.py +@@ -73,7 +73,7 @@ class ModuleBase(object): + # + fail_safe_repo = hawkey.MODULE_FAIL_SAFE_REPO_NAME + install_dict = {} +- install_set_artefacts = set() ++ install_set_artifacts = set() + fail_safe_repo_used = False + for spec, (nsvcap, moduledict) in module_dicts.items(): + for name, streamdict in moduledict.items(): +@@ -136,12 +136,12 @@ class ModuleBase(object): + for pkg_name in profile.getContent(): + install_dict.setdefault(pkg_name, set()).add(spec) + for module in install_module_list: +- install_set_artefacts.update(module.getArtifacts()) ++ install_set_artifacts.update(module.getArtifacts()) + if fail_safe_repo_used: + raise dnf.exceptions.Error(_( + "Installing module from Fail-Safe repository is not allowed")) + __, profiles_errors = self._install_profiles_internal( +- install_set_artefacts, install_dict, strict) ++ install_set_artifacts, install_dict, strict) + if profiles_errors: + error_specs.extend(profiles_errors) + +@@ -326,8 +326,8 @@ class ModuleBase(object): + else: + for profile in latest_module.getProfiles(): + upgrade_package_set.update(profile.getContent()) +- for artefact in latest_module.getArtifacts(): +- subj = hawkey.Subject(artefact) ++ for artifact in latest_module.getArtifacts(): ++ subj = hawkey.Subject(artifact) + for nevra_obj in subj.get_nevra_possibilities( + forms=[hawkey.FORM_NEVRA]): + upgrade_package_set.add(nevra_obj.name) +-- +2.26.2 + + +From 0818bb80fc0846f602f338a2119671be97c47217 Mon Sep 17 00:00:00 2001 +From: Jaroslav Mracek +Date: Thu, 12 Nov 2020 15:11:29 +0100 +Subject: [PATCH 4/5] [doc] Add description of dnf module switch-to + +--- + doc/command_ref.rst | 30 ++++++++++++++++++++++-------- + 1 file changed, 22 insertions(+), 8 deletions(-) + +diff --git a/doc/command_ref.rst b/doc/command_ref.rst +index 83879013..c12837ea 100644 +--- a/doc/command_ref.rst ++++ b/doc/command_ref.rst +@@ -979,15 +979,31 @@ Module subcommands take :ref:`\\ `... arg + In case no profile was provided, all default profiles get installed. + Module streams get enabled accordingly. + +- This command cannot be used for switching module streams. It is recommended to remove all +- installed content from the module and reset the module using the +- :ref:`reset ` command. After you reset the module, you can install +- the other stream. ++ This command cannot be used for switching module streams. Use the ++ :ref:`dnf module switch-to ` command for that. + + ``dnf [options] module update ...`` + Update packages associated with an active module stream, optionally restricted to a profile. + If the `profile_name` is provided, only the packages referenced by that profile will be updated. + ++.. _module_switch_to_command-label: ++ ++``dnf [options] module switch-to ...`` ++ Switch to or enable a module stream, change versions of installed packages to versions provided ++ by the new stream, and remove packages from the old stream that are no longer available. It also ++ updates installed profiles if they are available for the new stream. When a profile was ++ provided, it installs that profile and does not update any already installed profiles. ++ ++ This command can be used as a stronger version of the ++ :ref:`dnf module enable ` command, which not only enables modules, ++ but also does a `distrosync` to all modular packages in the enabled modules. ++ ++ It can also be used as a stronger version of the ++ :ref:`dnf module install ` command, but it requires to specify ++ profiles that are supposed to be installed, because `switch-to` command does not use `default ++ profiles`. The `switch-to` command doesn't only install profiles, it also makes a `distrosync` ++ to all modular packages in the installed module. ++ + ``dnf [options] module remove ...`` + Remove installed module profiles, including packages that were installed with the + :ref:`dnf module install ` command. Will not remove packages +@@ -1010,10 +1026,8 @@ Module subcommands take :ref:`\\ `... arg + of modular dependency issue the operation will be rejected. To perform the action anyway please use + \-\ :ref:`-skip-broken ` option. + +- This command cannot be used for switching module streams. It is recommended to remove all +- installed content from the module, and reset the module using the +- :ref:`reset ` command. After you reset the module, you can enable +- the other stream. ++ This command cannot be used for switching module streams. Use the ++ :ref:`dnf module switch-to ` command for that. + + .. _module_disable_command-label: + +-- +2.26.2 + + +From 6b0b2b99e40c20706145e774626658825f5bc55d Mon Sep 17 00:00:00 2001 +From: Jaroslav Mracek +Date: Wed, 25 Nov 2020 12:34:30 +0100 +Subject: [PATCH 5/5] Do not use source rpms for module switch + +It prevents misleading message from libsolv that it tries to install +source rpm. +--- + dnf/module/module_base.py | 4 +++- + 1 file changed, 3 insertions(+), 1 deletion(-) + +diff --git a/dnf/module/module_base.py b/dnf/module/module_base.py +index 7298c9a3..02d5d5a3 100644 +--- a/dnf/module/module_base.py ++++ b/dnf/module/module_base.py +@@ -253,8 +253,10 @@ class ModuleBase(object): + all_names.update(new_artifacts_names) + all_names.update(active_artifacts_names) + remove_query = self.base.sack.query().filterm(empty=True) ++ base_no_source_query = self.base.sack.query().filterm(arch__neq=['src', 'nosrc']).apply() ++ + for pkg_name in all_names: +- query = self.base.sack.query().filterm(name=pkg_name) ++ query = base_no_source_query.filter(name=pkg_name) + installed = query.installed() + if not installed: + continue +-- +2.26.2 + diff --git a/SOURCES/0009-yum.misc.decompress-to-handle-uncompressed-files-RhBug-1895059.patch b/SOURCES/0009-yum.misc.decompress-to-handle-uncompressed-files-RhBug-1895059.patch new file mode 100644 index 0000000..240495d --- /dev/null +++ b/SOURCES/0009-yum.misc.decompress-to-handle-uncompressed-files-RhBug-1895059.patch @@ -0,0 +1,107 @@ +From de8bbccc4e035a9a9b5baa3aeb0dbf0cb12f1fe2 Mon Sep 17 00:00:00 2001 +From: Marek Blaha +Date: Wed, 9 Dec 2020 13:45:46 +0100 +Subject: [PATCH 1/1] yum.misc.decompress() to handle uncompressed files + (RhBug:1895059) + +The underlying libdnf function is capable to handle even uncompressed +files - so now uncompressed files are just copied to the destination. +Also unused fn_only parameter of the function was removed. + +This fixes issue with "reposync -m" command when the group metadata file +in the repository is a plain xml file (not compressed). + +https://bugzilla.redhat.com/show_bug.cgi?id=1895059 +--- + dnf/yum/misc.py | 60 +++++++++++++++++++++++++++---------------------- + 1 file changed, 33 insertions(+), 27 deletions(-) + +diff --git a/dnf/yum/misc.py b/dnf/yum/misc.py +index 0f922350..3e3905fe 100644 +--- a/dnf/yum/misc.py ++++ b/dnf/yum/misc.py +@@ -386,34 +386,39 @@ def getloginuid(): + _cached_getloginuid = _getloginuid() + return _cached_getloginuid + +-def decompress(filename, dest=None, fn_only=False, check_timestamps=False): +- """take a filename and decompress it into the same relative location. +- if the file is not compressed just return the file""" +- +- ztype = None +- out = filename # If the file is not compressed, it returns the same file + +- dot_pos = filename.rfind('.') +- if dot_pos > 0: +- ext = filename[dot_pos:] +- if ext in ('.zck', '.xz', '.bz2', '.gz'): +- ztype = ext +- out = dest if dest else filename[:dot_pos] +- +- if ztype and not fn_only: +- if check_timestamps: +- fi = stat_f(filename) +- fo = stat_f(out) +- if fi and fo and fo.st_mtime == fi.st_mtime: +- return out ++def decompress(filename, dest=None, check_timestamps=False): ++ """take a filename and decompress it into the same relative location. ++ When the compression type is not recognized (or file is not compressed), ++ the content of the file is copied to the destination""" ++ ++ if dest: ++ out = dest ++ else: ++ out = None ++ dot_pos = filename.rfind('.') ++ if dot_pos > 0: ++ ext = filename[dot_pos:] ++ if ext in ('.zck', '.xz', '.bz2', '.gz', '.lzma', '.zst'): ++ out = filename[:dot_pos] ++ if out is None: ++ raise dnf.exceptions.MiscError("Could not determine destination filename") ++ ++ if check_timestamps: ++ fi = stat_f(filename) ++ fo = stat_f(out) ++ if fi and fo and fo.st_mtime == fi.st_mtime: ++ return out + +- try: +- libdnf.utils.decompress(filename, out, 0o644, ztype) +- except RuntimeError as e: +- raise dnf.exceptions.MiscError(str(e)) ++ try: ++ # libdnf.utils.decompress either decompress file to the destination or ++ # copy the content if the compression type is not recognized ++ libdnf.utils.decompress(filename, out, 0o644) ++ except RuntimeError as e: ++ raise dnf.exceptions.MiscError(str(e)) + +- if check_timestamps and fi: +- os.utime(out, (fi.st_mtime, fi.st_mtime)) ++ if check_timestamps and fi: ++ os.utime(out, (fi.st_mtime, fi.st_mtime)) + + return out + +@@ -424,13 +429,14 @@ def calculate_repo_gen_dest(filename, generated_name): + os.makedirs(dest, mode=0o755) + return dest + '/' + generated_name + +-def repo_gen_decompress(filename, generated_name, cached=False): ++ ++def repo_gen_decompress(filename, generated_name): + """ This is a wrapper around decompress, where we work out a cached + generated name, and use check_timestamps. filename _must_ be from + a repo. and generated_name is the type of the file. """ + + dest = calculate_repo_gen_dest(filename, generated_name) +- return decompress(filename, dest=dest, check_timestamps=True, fn_only=cached) ++ return decompress(filename, dest=dest, check_timestamps=True) + + def read_in_items_from_dot_dir(thisglob, line_as_list=True): + """ Takes a glob of a dir (like /etc/foo.d/\\*.foo) returns a list of all +-- +2.26.2 + diff --git a/SOURCES/0010-Make-log-rotated-permissions-match-initial-log-permissions-RhBug-1894344.patch b/SOURCES/0010-Make-log-rotated-permissions-match-initial-log-permissions-RhBug-1894344.patch new file mode 100644 index 0000000..3bfdafb --- /dev/null +++ b/SOURCES/0010-Make-log-rotated-permissions-match-initial-log-permissions-RhBug-1894344.patch @@ -0,0 +1,22 @@ +From 04b1a90bb24b7e98d4e001c44f8b3f563ad5f0f6 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Ale=C5=A1=20Mat=C4=9Bj?= +Date: Tue, 24 Nov 2020 14:31:21 +0100 +Subject: [PATCH] Make rotated log file (mode, owner, group) match previous log + settings (RhBug:1894344) + +https://bugzilla.redhat.com/show_bug.cgi?id=1894344 +--- + etc/logrotate.d/dnf | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/etc/logrotate.d/dnf b/etc/logrotate.d/dnf +index b96c6ff9b4..0ce2629f1b 100644 +--- a/etc/logrotate.d/dnf ++++ b/etc/logrotate.d/dnf +@@ -3,5 +3,5 @@ + notifempty + rotate 4 + weekly +- create 0600 root root ++ create + } diff --git a/SOURCES/0011-Add-new-attribute-for-Package--from-repo.patch b/SOURCES/0011-Add-new-attribute-for-Package--from-repo.patch new file mode 100644 index 0000000..49c2a1a --- /dev/null +++ b/SOURCES/0011-Add-new-attribute-for-Package--from-repo.patch @@ -0,0 +1,117 @@ +From eb2aa8c14208da7a567a0d79a8baa9f5201640cd Mon Sep 17 00:00:00 2001 +From: Jaroslav Mracek +Date: Tue, 24 Nov 2020 09:17:41 +0100 +Subject: [PATCH 1/3] Add `from_repo` attribute for Package class + (RhBug:1898968,1879168) + +It as an alias for private attribute _from_repo. + +https://bugzilla.redhat.com/show_bug.cgi?id=1898968 +https://bugzilla.redhat.com/show_bug.cgi?id=1879168 +--- + dnf/cli/commands/repoquery.py | 2 +- + dnf/package.py | 7 +++++-- + doc/api_package.rst | 6 ++++++ + 3 files changed, 12 insertions(+), 3 deletions(-) + +diff --git a/dnf/cli/commands/repoquery.py b/dnf/cli/commands/repoquery.py +index 099a9312d9..a11b440525 100644 +--- a/dnf/cli/commands/repoquery.py ++++ b/dnf/cli/commands/repoquery.py +@@ -44,7 +44,7 @@ + QFORMAT_MATCH = re.compile(r'%(-?\d*?){([:.\w]+?)}') + + QUERY_TAGS = """\ +-name, arch, epoch, version, release, reponame (repoid), evr, ++name, arch, epoch, version, release, reponame (repoid), from_repo, evr, + debug_name, source_name, source_debug_name, + installtime, buildtime, size, downloadsize, installsize, + provides, requires, obsoletes, conflicts, sourcerpm, +diff --git a/dnf/package.py b/dnf/package.py +index d44ce6706c..f647df6bff 100644 +--- a/dnf/package.py ++++ b/dnf/package.py +@@ -76,12 +76,15 @@ def _from_repo(self): + pkgrepo = None + if self._from_system: + pkgrepo = self.base.history.repo(self) +- else: +- pkgrepo = {} + if pkgrepo: + return '@' + pkgrepo + return self.reponame + ++ @property ++ def from_repo(self): ++ # :api ++ return self._from_repo ++ + @property + def _header(self): + return dnf.rpm._header(self.localPkg()) +diff --git a/doc/api_package.rst b/doc/api_package.rst +index 95df5d4b23..48ef8f1d22 100644 +--- a/doc/api_package.rst ++++ b/doc/api_package.rst +@@ -74,6 +74,12 @@ + + Files the package provides (list of strings). + ++ .. attribute:: from_repo ++ ++ For installed packages returns id of repository from which the package was installed prefixed ++ with '@' (if such information is available in the history database). Otherwise returns id of ++ repository the package belongs to (@System for installed packages of unknown origin) (string). ++ + .. attribute:: group + + Group of the package (string). + +From 1a933f8e036cd704fa6e7f77a8448263e93e540f Mon Sep 17 00:00:00 2001 +From: Jaroslav Mracek +Date: Tue, 24 Nov 2020 09:19:42 +0100 +Subject: [PATCH 2/3] Correct description of Package().reponane attribute + +--- + doc/api_package.rst | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/doc/api_package.rst b/doc/api_package.rst +index 48ef8f1d22..a78897babe 100644 +--- a/doc/api_package.rst ++++ b/doc/api_package.rst +@@ -138,7 +138,7 @@ + + .. attribute:: reponame + +- Id of repository the package was installed from (string). ++ Id of repository the package belongs to (@System for installed packages) (string). + + .. attribute:: requires + + +From 24cdb68776507fdae25bed0e82d80df3018aecfc Mon Sep 17 00:00:00 2001 +From: Jaroslav Mracek +Date: Tue, 24 Nov 2020 09:22:07 +0100 +Subject: [PATCH 3/3] Add unittest for new API + +--- + tests/api/test_dnf_package.py | 5 +++++ + 1 file changed, 5 insertions(+) + +diff --git a/tests/api/test_dnf_package.py b/tests/api/test_dnf_package.py +index 04cddc7ecc..5952352bb5 100644 +--- a/tests/api/test_dnf_package.py ++++ b/tests/api/test_dnf_package.py +@@ -163,6 +163,11 @@ def test_reponame(self): + self.assertHasAttr(self.package, "reponame") + self.assertHasType(self.package.reponame, str) + ++ def test_from_repo(self): ++ # Package.reponame ++ self.assertHasAttr(self.package, "from_repo") ++ self.assertHasType(self.package.from_repo, str) ++ + def test_requires(self): + # Package.requires + self.assertHasAttr(self.package, "requires") diff --git a/SOURCES/0012-Change-behaviour-of-Package-.from-repo.patch b/SOURCES/0012-Change-behaviour-of-Package-.from-repo.patch new file mode 100644 index 0000000..b81646b --- /dev/null +++ b/SOURCES/0012-Change-behaviour-of-Package-.from-repo.patch @@ -0,0 +1,80 @@ +From ca06d200d738fd6b23cb05b9776c9fd29288665f Mon Sep 17 00:00:00 2001 +From: Jaroslav Mracek +Date: Wed, 25 Nov 2020 13:00:22 +0100 +Subject: [PATCH 1/2] Change behaviour of Package().from_repo + +The change makes a difference between private attribute _from_repo and +API attribute. _from_repo is required for `dnf info` and we have to keep +it, but for API the magic handling behind could be confusing. +--- + dnf/package.py | 8 +++++++- + doc/api_package.rst | 5 ++--- + 2 files changed, 9 insertions(+), 4 deletions(-) + +diff --git a/dnf/package.py b/dnf/package.py +index f647df6bff..28ca5ef760 100644 +--- a/dnf/package.py ++++ b/dnf/package.py +@@ -73,6 +73,12 @@ def _from_system(self): + + @property + def _from_repo(self): ++ """ ++ For installed packages returns id of repository from which the package was installed ++ prefixed with '@' (if such information is available in the history database). Otherwise ++ returns id of repository the package belongs to (@System for installed packages of unknown ++ origin) ++ """ + pkgrepo = None + if self._from_system: + pkgrepo = self.base.history.repo(self) +@@ -83,7 +89,7 @@ def _from_repo(self): + @property + def from_repo(self): + # :api +- return self._from_repo ++ return self.base.history.repo(self) + + @property + def _header(self): +diff --git a/doc/api_package.rst b/doc/api_package.rst +index a78897babe..634f504ca6 100644 +--- a/doc/api_package.rst ++++ b/doc/api_package.rst +@@ -76,9 +76,8 @@ + + .. attribute:: from_repo + +- For installed packages returns id of repository from which the package was installed prefixed +- with '@' (if such information is available in the history database). Otherwise returns id of +- repository the package belongs to (@System for installed packages of unknown origin) (string). ++ For installed packages returns id of repository from which the package was installed if such ++ information is available in the history database. Otherwise returns an empty string (string). + + .. attribute:: group + + +From 895e61a1281db753dd28f01c20816e83c5316cdd Mon Sep 17 00:00:00 2001 +From: Jaroslav Mracek +Date: Thu, 26 Nov 2020 10:02:08 +0100 +Subject: [PATCH 2/2] fixup! Change behaviour of Package().from_repo + +--- + dnf/package.py | 4 +++- + 1 file changed, 3 insertions(+), 1 deletion(-) + +diff --git a/dnf/package.py b/dnf/package.py +index 28ca5ef760..baef04fa5b 100644 +--- a/dnf/package.py ++++ b/dnf/package.py +@@ -89,7 +89,9 @@ def _from_repo(self): + @property + def from_repo(self): + # :api +- return self.base.history.repo(self) ++ if self._from_system: ++ return self.base.history.repo(self) ++ return "" + + @property + def _header(self): diff --git a/SOURCES/0013-Package-add-a-get-header--method.patch b/SOURCES/0013-Package-add-a-get-header--method.patch new file mode 100644 index 0000000..a5f8478 --- /dev/null +++ b/SOURCES/0013-Package-add-a-get-header--method.patch @@ -0,0 +1,94 @@ +From 38cc67385fb1b36aa0881bc5982bc58d75dac464 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hr=C3=A1zk=C3=BD?= +Date: Wed, 11 Nov 2020 18:45:11 +0100 +Subject: [PATCH] Package: add a get_header() method (RhBug:1876606) + +Adds get_header() method to the Package class, which returns the rpm +header of an installed package. + += changelog = +msg: Add get_header() method to the Package class +type: enhancement +resolves: https://bugzilla.redhat.com/show_bug.cgi?id=1876606 +--- + dnf/package.py | 24 ++++++++++++++++++++++++ + tests/test_package.py | 12 ++++++++++++ + 2 files changed, 36 insertions(+) + +diff --git a/dnf/package.py b/dnf/package.py +index baef04fa5b..836e0e4989 100644 +--- a/dnf/package.py ++++ b/dnf/package.py +@@ -26,11 +26,13 @@ + from dnf.i18n import _ + + import binascii ++import dnf.exceptions + import dnf.rpm + import dnf.yum.misc + import hawkey + import logging + import os ++import rpm + + logger = logging.getLogger("dnf") + +@@ -95,6 +97,11 @@ def from_repo(self): + + @property + def _header(self): ++ """ ++ Returns the header of a locally present rpm package file. As opposed to ++ self.get_header(), which retrieves the header of an installed package ++ from rpmdb. ++ """ + return dnf.rpm._header(self.localPkg()) + + @property +@@ -164,6 +171,23 @@ def debugsource_name(self): + src_name = self.source_name if self.source_name is not None else self.name + return src_name + self.DEBUGSOURCE_SUFFIX + ++ def get_header(self): ++ """ ++ Returns the rpm header of the package if it is installed. If not ++ installed, returns None. The header is not cached, it is retrieved from ++ rpmdb on every call. In case of a failure (e.g. when the rpmdb changes ++ between loading the data and calling this method), raises an instance ++ of PackageNotFoundError. ++ """ ++ if not self._from_system: ++ return None ++ ++ try: ++ # RPMDBI_PACKAGES stands for the header of the package ++ return next(self.base._ts.dbMatch(rpm.RPMDBI_PACKAGES, self.rpmdbid)) ++ except StopIteration: ++ raise dnf.exceptions.PackageNotFoundError("Package not found when attempting to retrieve header", str(self)) ++ + @property + def source_debug_name(self): + # :api +diff --git a/tests/test_package.py b/tests/test_package.py +index cd4872e631..514e5bf099 100644 +--- a/tests/test_package.py ++++ b/tests/test_package.py +@@ -68,6 +68,18 @@ def fn_getter(): + with self.assertRaises(IOError): + pkg._header + ++ # rpm.hdr() is not easy to construct with custom data, we just return a string ++ # instead, as we don't actually need an instance of rpm.hdr for the test ++ @mock.patch("rpm.TransactionSet.dbMatch", lambda self, a, b: iter(["package_header_test_data"])) ++ def test_get_header(self): ++ pkg = self.sack.query().installed().filter(name="pepper")[0] ++ header = pkg.get_header() ++ self.assertEqual(header, "package_header_test_data") ++ ++ pkg = self.sack.query().available().filter(name="pepper")[0] ++ header = pkg.get_header() ++ self.assertEqual(header, None) ++ + @mock.patch("dnf.package.Package.rpmdbid", long(3)) + def test_idx(self): + """ pkg.idx is an int. """ diff --git a/SOURCES/0014-Add-api-function-fill-sack-from-repos-in-cache-RhBug-1865803.patch b/SOURCES/0014-Add-api-function-fill-sack-from-repos-in-cache-RhBug-1865803.patch new file mode 100644 index 0000000..d912d69 --- /dev/null +++ b/SOURCES/0014-Add-api-function-fill-sack-from-repos-in-cache-RhBug-1865803.patch @@ -0,0 +1,115 @@ +From b3542a96c6f77e5cc0b5217e586fcc56fde074d8 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Ale=C5=A1=20Mat=C4=9Bj?= +Date: Wed, 2 Dec 2020 15:27:13 +0100 +Subject: [PATCH 1/2] Add api function: fill_sack_from_repos_in_cache + (RhBug:1865803) + += changelog = +msg: Add api function fill_sack_from_repos_in_cache to allow loading a repo cache with repomd and (solv file or primary xml) only +type: enhancement +resolves: https://bugzilla.redhat.com/show_bug.cgi?id=1865803 +--- + dnf.spec | 2 +- + dnf/base.py | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++++ + 2 files changed, 63 insertions(+), 1 deletion(-) + +diff --git a/dnf/base.py b/dnf/base.py +index 075e74265a..a10b837340 100644 +--- a/dnf/base.py ++++ b/dnf/base.py +@@ -425,6 +425,68 @@ def fill_sack(self, load_system_repo=True, load_available_repos=True): + self._plugins.run_sack() + return self._sack + ++ def fill_sack_from_repos_in_cache(self, load_system_repo=True): ++ # :api ++ """ ++ Prepare Sack and Goal objects and also load all enabled repositories from cache only, ++ it doesn't download anything and it doesn't check if metadata are expired. ++ If there is not enough metadata present (repond.xml or both primary.xml and solv file ++ are missing) given repo is either skipped or it throws a RepoError exception depending ++ on skip_if_unavailable configuration. ++ """ ++ timer = dnf.logging.Timer('sack setup') ++ self.reset(sack=True, goal=True) ++ self._sack = dnf.sack._build_sack(self) ++ lock = dnf.lock.build_metadata_lock(self.conf.cachedir, self.conf.exit_on_lock) ++ with lock: ++ if load_system_repo is not False: ++ try: ++ # FIXME: If build_cache=True, @System.solv is incorrectly updated in install- ++ # remove loops ++ self._sack.load_system_repo(build_cache=False) ++ except IOError: ++ if load_system_repo != 'auto': ++ raise ++ ++ error_repos = [] ++ # Iterate over installed GPG keys and check their validity using DNSSEC ++ if self.conf.gpgkey_dns_verification: ++ dnf.dnssec.RpmImportedKeys.check_imported_keys_validity() ++ for repo in self.repos.iter_enabled(): ++ try: ++ repo._repo.loadCache(throwExcept=True, ignoreMissing=True) ++ mdload_flags = dict(load_filelists=True, ++ load_presto=repo.deltarpm, ++ load_updateinfo=True) ++ if repo.load_metadata_other: ++ mdload_flags["load_other"] = True ++ ++ self._sack.load_repo(repo._repo, **mdload_flags) ++ ++ logger.debug(_("%s: using metadata from %s."), repo.id, ++ dnf.util.normalize_time( ++ repo._repo.getMaxTimestamp())) ++ except (RuntimeError, hawkey.Exception) as e: ++ if repo.skip_if_unavailable is False: ++ raise dnf.exceptions.RepoError( ++ _("loading repo '{}' failure: {}").format(repo.id, e)) ++ else: ++ logger.debug(_("loading repo '{}' failure: {}").format(repo.id, e)) ++ error_repos.append(repo.id) ++ repo.disable() ++ if error_repos: ++ logger.warning( ++ _("Ignoring repositories: %s"), ', '.join(error_repos)) ++ ++ conf = self.conf ++ self._sack._configure(conf.installonlypkgs, conf.installonly_limit, conf.allow_vendor_change) ++ self._setup_excludes_includes() ++ timer() ++ self._goal = dnf.goal.Goal(self._sack) ++ self._goal.protect_running_kernel = conf.protect_running_kernel ++ self._plugins.run_sack() ++ return self._sack ++ + def _finalize_base(self): + self._tempfile_persistor = dnf.persistor.TempfilePersistor( + self.conf.cachedir) + +From 29ae53918d4a0b65a917aca2f8f43416fee15dfd Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Ale=C5=A1=20Mat=C4=9Bj?= +Date: Thu, 10 Dec 2020 14:54:16 +0100 +Subject: [PATCH 2/2] Add api test for new fill_sack_from_repos_in_cache + +--- + tests/api/test_dnf_base.py | 6 ++++++ + 1 file changed, 6 insertions(+) + +diff --git a/tests/api/test_dnf_base.py b/tests/api/test_dnf_base.py +index 656bd22584..335981897e 100644 +--- a/tests/api/test_dnf_base.py ++++ b/tests/api/test_dnf_base.py +@@ -107,6 +107,12 @@ def test_fill_sack(self): + + self.base.fill_sack(load_system_repo=False, load_available_repos=False) + ++ def test_fill_sack_from_repos_in_cache(self): ++ # Base.fill_sack_from_repos_in_cache(self, load_system_repo=True): ++ self.assertHasAttr(self.base, "fill_sack_from_repos_in_cache") ++ ++ self.base.fill_sack_from_repos_in_cache(load_system_repo=False) ++ + def test_close(self): + # Base.close() + self.assertHasAttr(self.base, "close") diff --git a/SOURCES/0015-Add-tests-and-docs-for-fill-sack-from-repos-in-cache-RhBug-1865803.patch b/SOURCES/0015-Add-tests-and-docs-for-fill-sack-from-repos-in-cache-RhBug-1865803.patch new file mode 100644 index 0000000..30dbab6 --- /dev/null +++ b/SOURCES/0015-Add-tests-and-docs-for-fill-sack-from-repos-in-cache-RhBug-1865803.patch @@ -0,0 +1,366 @@ +From a777ff01c79d5e0e2cf3ae7b0652795577253bc3 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Ale=C5=A1=20Mat=C4=9Bj?= +Date: Thu, 14 Jan 2021 09:58:30 +0100 +Subject: [PATCH 1/3] Fix recreate script + +--- + tests/repos/rpm/recreate | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/tests/repos/rpm/recreate b/tests/repos/rpm/recreate +index da348d9799..0fbb9396bd 100755 +--- a/tests/repos/rpm/recreate ++++ b/tests/repos/rpm/recreate +@@ -1,6 +1,6 @@ + #!/bin/bash + +-THISDIR="$( readlink -f "$( dirname "$0 )" )" ++THISDIR="$( readlink -f "$( dirname "$0" )" )" + cd "$THISDIR" + git rm -rf repodata/ + createrepo --no-database -o . .. + +From 5d4c0266f6967c7cd5f0e675b13fa3e9b395e4dd Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Ale=C5=A1=20Mat=C4=9Bj?= +Date: Thu, 14 Jan 2021 10:28:53 +0100 +Subject: [PATCH 2/3] Add unit test for fill_sack_from_repos_in_cache + (RhBug:1865803) + +https://bugzilla.redhat.com/show_bug.cgi?id=1865803 +--- + tests/test_fill_sack_from_repos_in_cache.py | 262 ++++++++++++++++++++ + 1 file changed, 262 insertions(+) + create mode 100644 tests/test_fill_sack_from_repos_in_cache.py + +diff --git a/tests/test_fill_sack_from_repos_in_cache.py b/tests/test_fill_sack_from_repos_in_cache.py +new file mode 100644 +index 0000000000..24b0d4598d +--- /dev/null ++++ b/tests/test_fill_sack_from_repos_in_cache.py +@@ -0,0 +1,262 @@ ++# -*- coding: utf-8 -*- ++ ++# Copyright (C) 2012-2021 Red Hat, Inc. ++# ++# This copyrighted material is made available to anyone wishing to use, ++# modify, copy, or redistribute it subject to the terms and conditions of ++# the GNU General Public License v.2, or (at your option) any later version. ++# This program is distributed in the hope that it will be useful, but WITHOUT ++# ANY WARRANTY expressed or implied, including the implied warranties of ++# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General ++# Public License for more details. You should have received a copy of the ++# GNU General Public License along with this program; if not, write to the ++# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA ++# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the ++# source code or documentation are not subject to the GNU General Public ++# License and may only be used or replicated with the express permission of ++# Red Hat, Inc. ++# ++ ++from __future__ import absolute_import ++from __future__ import unicode_literals ++ ++import os ++import tempfile ++import glob ++import shutil ++import unittest ++ ++import dnf.exceptions ++import dnf.repo ++import dnf.sack ++ ++import hawkey ++ ++import tests.support ++from tests.support import mock ++ ++TEST_REPO_NAME = "test-repo" ++ ++ ++class FillSackFromReposInCacheTest(unittest.TestCase): ++ def _create_cache_for_repo(self, repopath, tmpdir): ++ conf = dnf.conf.MainConf() ++ conf.cachedir = os.path.join(tmpdir, "cache") ++ ++ base = dnf.Base(conf=conf) ++ ++ repoconf = dnf.repo.Repo(TEST_REPO_NAME, base.conf) ++ repoconf.baseurl = repopath ++ repoconf.enable() ++ ++ base.repos.add(repoconf) ++ ++ base.fill_sack(load_system_repo=False) ++ base.close() ++ ++ def _setUp_from_repo_path(self, original_repo_path): ++ self.tmpdir = tempfile.mkdtemp(prefix="dnf_test_") ++ ++ self.repo_copy_path = os.path.join(self.tmpdir, "repo") ++ shutil.copytree(original_repo_path, self.repo_copy_path) ++ ++ self._create_cache_for_repo(self.repo_copy_path, self.tmpdir) ++ ++ # Just to be sure remove repo (it shouldn't be used) ++ shutil.rmtree(self.repo_copy_path) ++ ++ # Prepare base for the actual test ++ conf = dnf.conf.MainConf() ++ conf.cachedir = os.path.join(self.tmpdir, "cache") ++ self.test_base = dnf.Base(conf=conf) ++ repoconf = dnf.repo.Repo(TEST_REPO_NAME, conf) ++ repoconf.baseurl = self.repo_copy_path ++ repoconf.enable() ++ self.test_base.repos.add(repoconf) ++ ++ def tearDown(self): ++ self.test_base.close() ++ shutil.rmtree(self.tmpdir) ++ ++ def test_with_solv_solvx_repomd(self): ++ self._setUp_from_repo_path(os.path.join(os.path.abspath(os.path.dirname(__file__)), "repos/rpm")) ++ ++ # Remove xml metadata except repomd ++ # repomd.xml is not compressed and doesn't end with .gz ++ repodata_without_repomd = glob.glob(os.path.join(self.tmpdir, "cache/test-repo-*/repodata/*.gz")) ++ for f in repodata_without_repomd: ++ os.remove(f) ++ ++ # Now we only have cache with just solv, solvx files and repomd.xml ++ ++ self.test_base.fill_sack_from_repos_in_cache(load_system_repo=False) ++ ++ q = self.test_base.sack.query() ++ packages = q.run() ++ self.assertEqual(len(packages), 9) ++ self.assertEqual(packages[0].evr, "4-4") ++ ++ # Use *-updateinfo.solvx ++ adv_pkgs = q.get_advisory_pkgs(hawkey.LT | hawkey.EQ | hawkey.GT) ++ adv_titles = set() ++ for pkg in adv_pkgs: ++ adv_titles.add(pkg.get_advisory(self.test_base.sack).title) ++ self.assertEqual(len(adv_titles), 3) ++ ++ def test_with_just_solv_repomd(self): ++ self._setUp_from_repo_path(os.path.join(os.path.abspath(os.path.dirname(__file__)), "repos/rpm")) ++ ++ # Remove xml metadata except repomd ++ # repomd.xml is not compressed and doesn't end with .gz ++ repodata_without_repomd = glob.glob(os.path.join(self.tmpdir, "cache/test-repo-*/repodata/*.gz")) ++ for f in repodata_without_repomd: ++ os.remove(f) ++ ++ # Remove solvx files ++ solvx = glob.glob(os.path.join(self.tmpdir, "cache/*.solvx")) ++ for f in solvx: ++ os.remove(f) ++ ++ # Now we only have cache with just solv files and repomd.xml ++ ++ self.test_base.fill_sack_from_repos_in_cache(load_system_repo=False) ++ ++ q = self.test_base.sack.query() ++ packages = q.run() ++ self.assertEqual(len(packages), 9) ++ self.assertEqual(packages[0].evr, "4-4") ++ ++ # No *-updateinfo.solvx -> we get no advisory packages ++ adv_pkgs = q.get_advisory_pkgs(hawkey.LT | hawkey.EQ | hawkey.GT) ++ self.assertEqual(len(adv_pkgs), 0) ++ ++ def test_with_xml_metadata(self): ++ self._setUp_from_repo_path(os.path.join(os.path.abspath(os.path.dirname(__file__)), "repos/rpm")) ++ ++ # Remove all solv and solvx files ++ solvx = glob.glob(os.path.join(self.tmpdir, "cache/*.solv*")) ++ for f in solvx: ++ os.remove(f) ++ ++ # Now we only have cache with just xml metadata ++ ++ self.test_base.fill_sack_from_repos_in_cache(load_system_repo=False) ++ ++ q = self.test_base.sack.query() ++ packages = q.run() ++ self.assertEqual(len(packages), 9) ++ self.assertEqual(packages[0].evr, "4-4") ++ ++ def test_exception_without_repomd(self): ++ self._setUp_from_repo_path(os.path.join(os.path.abspath(os.path.dirname(__file__)), "repos/rpm")) ++ ++ # Remove xml metadata ++ repodata_without_repomd = glob.glob(os.path.join(self.tmpdir, "cache/test-repo-*/repodata/*")) ++ for f in repodata_without_repomd: ++ os.remove(f) ++ ++ # Now we only have cache with just solv and solvx files ++ # Since we don't have repomd we cannot verify checksums -> fail (exception) ++ ++ self.assertRaises(dnf.exceptions.RepoError, ++ self.test_base.fill_sack_from_repos_in_cache, load_system_repo=False) ++ ++ def test_exception_with_just_repomd(self): ++ self._setUp_from_repo_path(os.path.join(os.path.abspath(os.path.dirname(__file__)), "repos/rpm")) ++ ++ # Remove xml metadata except repomd ++ # repomd.xml is not compressed and doesn't end with .gz ++ repodata_without_repomd = glob.glob(os.path.join(self.tmpdir, "cache/test-repo-*/repodata/*.gz")) ++ for f in repodata_without_repomd: ++ os.remove(f) ++ ++ # Remove all solv and solvx files ++ solvx = glob.glob(os.path.join(self.tmpdir, "cache/*.solv*")) ++ for f in solvx: ++ os.remove(f) ++ ++ # Now we only have cache with just repomd ++ # repomd is not enough, it doesn't contain the metadata it self -> fail (exception) ++ ++ self.assertRaises(dnf.exceptions.RepoError, ++ self.test_base.fill_sack_from_repos_in_cache, load_system_repo=False) ++ ++ def test_exception_with_checksum_mismatch_and_only_repomd(self): ++ self._setUp_from_repo_path(os.path.join(os.path.abspath(os.path.dirname(__file__)), "repos/rpm")) ++ ++ # Remove xml metadata except repomd ++ # repomd.xml is not compressed and doesn't end with .gz ++ repodata_without_repomd = glob.glob(os.path.join(self.tmpdir, "cache/test-repo-*/repodata/*.gz")) ++ for f in repodata_without_repomd: ++ os.remove(f) ++ ++ # Modify checksum of solv file so it doesn't match with repomd ++ solv = glob.glob(os.path.join(self.tmpdir, "cache/*.solv"))[0] ++ with open(solv, "a") as opensolv: ++ opensolv.write("appended text to change checksum") ++ ++ # Now we only have cache with solvx, modified solv file and just repomd ++ # Since we don't have original xml metadata we cannot regenerate solv -> fail (exception) ++ ++ self.assertRaises(dnf.exceptions.RepoError, ++ self.test_base.fill_sack_from_repos_in_cache, load_system_repo=False) ++ ++ def test_checksum_mistmatch_regenerates_solv(self): ++ self._setUp_from_repo_path(os.path.join(os.path.abspath(os.path.dirname(__file__)), "repos/rpm")) ++ ++ # Modify checksum of solv file so it doesn't match with repomd ++ solv = glob.glob(os.path.join(self.tmpdir, "cache/*.solv"))[0] ++ with open(solv, "a") as opensolv: ++ opensolv.write("appended text to change checksum") ++ ++ # Now we only have cache with solvx, modified solv file and xml metadata. ++ # Checksum mistmatch causes regeneration of solv file and repo works. ++ ++ self.test_base.fill_sack_from_repos_in_cache(load_system_repo=False) ++ ++ q = self.test_base.sack.query() ++ packages = q.run() ++ self.assertEqual(len(packages), 9) ++ self.assertEqual(packages[0].evr, "4-4") ++ ++ def test_with_modules_yaml(self): ++ self._setUp_from_repo_path(os.path.join(os.path.abspath(os.path.dirname(__file__)), ++ "modules/modules/_all/x86_64")) ++ ++ # Now we have full cache (also with modules.yaml) ++ ++ self.test_base.fill_sack_from_repos_in_cache(load_system_repo=False) ++ ++ q = self.test_base.sack.query() ++ packages = q.run() ++ self.assertEqual(len(packages), 8) ++ self.assertEqual(packages[0].evr, "2.02-0.40") ++ ++ self.module_base = dnf.module.module_base.ModuleBase(self.test_base) ++ modules, _ = self.module_base._get_modules("base-runtime*") ++ self.assertEqual(len(modules), 3) ++ self.assertEqual(modules[0].getFullIdentifier(), "base-runtime:f26:1::") ++ ++ def test_with_modular_repo_without_modules_yaml(self): ++ self._setUp_from_repo_path(os.path.join(os.path.abspath(os.path.dirname(__file__)), ++ "modules/modules/_all/x86_64")) ++ ++ # Remove xml and yaml metadata except repomd ++ # repomd.xml is not compressed and doesn't end with .gz ++ repodata_without_repomd = glob.glob(os.path.join(self.tmpdir, "cache/test-repo-*/repodata/*.gz")) ++ for f in repodata_without_repomd: ++ os.remove(f) ++ ++ # Now we have just solv, *-filenames.solvx and repomd.xml (modules.yaml are not processed into *-modules.solvx) ++ ++ self.test_base.fill_sack_from_repos_in_cache(load_system_repo=False) ++ ++ q = self.test_base.sack.query() ++ packages = q.run() ++ # We have many more packages because they are not hidden by modules ++ self.assertEqual(len(packages), 44) ++ self.assertEqual(packages[0].evr, "10.0-7") ++ ++ self.module_base = dnf.module.module_base.ModuleBase(self.test_base) ++ modules, _ = self.module_base._get_modules("base-runtime*") ++ self.assertEqual(len(modules), 0) + +From de6177dba3dc20191e275eec14672570a0c4f4a8 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Ale=C5=A1=20Mat=C4=9Bj?= +Date: Thu, 14 Jan 2021 12:29:06 +0100 +Subject: [PATCH 3/3] Add docs and examples for fill_sack_from_repos_in_cache + (RhBug:1865803) + +https://bugzilla.redhat.com/show_bug.cgi?id=1865803 +--- + doc/api_base.rst | 41 +++++++++++++++++++++++++++++++++++++++++ + 1 file changed, 41 insertions(+) + +diff --git a/doc/api_base.rst b/doc/api_base.rst +index 24ecb50e43..f0b1992e88 100644 +--- a/doc/api_base.rst ++++ b/doc/api_base.rst +@@ -111,6 +111,47 @@ + print("id: {}".format(repo.id)) + print("baseurl: {}".format(repo.baseurl)) + ++ .. method:: fill_sack_from_repos_in_cache(load_system_repo=True) ++ ++ Prepare Sack and Goal objects and load all enabled repositories from cache only, it doesn't download anything and it doesn't check if metadata are expired. ++ To successfully load a repository cache it requires repond.xml plus metadata (xml, yaml) or repond.xml plus generated cache files (solv, solvx). ++ If there is not enough metadata given repo is either skipped or it throws a :exc:`dnf.exceptions.RepoError` exception depending on :attr:`dnf.conf.Conf.skip_if_unavailable` configuration. ++ ++ All additional metadata are loaded if present but are not generally required. Note that some metadata like updateinfo.xml get processed into a solvx cache file and its sufficient to have either xml or solvx. Module metadata represented by modules.yaml are not processed therefore they are needed when they are defined in repomd.xml. ++ ++ Example of loading all configured repositories from cache and printing available packages' names:: ++ ++ #!/usr/bin/python3 ++ import dnf ++ ++ with dnf.Base() as base: ++ base.read_all_repos() ++ ++ base.fill_sack_from_repos_in_cache(load_system_repo=False) ++ ++ query = base.sack.query().available() ++ for pkg in query.run(): ++ print(pkg.name) ++ ++ Example of loading a single repository and printing available packages' names without reading repository configuration:: ++ ++ #!/usr/bin/python3 ++ import dnf ++ ++ with dnf.Base() as base: ++ repo = dnf.repo.Repo("rawhide", base.conf) ++ ++ # Repository cache is also identified by its source therefore to find it you need to ++ # set metalink, mirrorlist or baseurl to the same value from which it was created. ++ repo.metalink = "https://mirrors.fedoraproject.org/metalink?repo=rawhide&arch=x86_64" ++ ++ base.repos.add(repo) ++ ++ base.fill_sack_from_repos_in_cache(load_system_repo=False) ++ ++ query = base.sack.query().available() ++ for pkg in query.run(): ++ print(pkg.name) + + .. method:: do_transaction([display]) + diff --git a/SOURCES/0016-Run-tests-for-fill-sack-from-repos-in-cache-in-installroot..patch b/SOURCES/0016-Run-tests-for-fill-sack-from-repos-in-cache-in-installroot..patch new file mode 100644 index 0000000..2c8f13b --- /dev/null +++ b/SOURCES/0016-Run-tests-for-fill-sack-from-repos-in-cache-in-installroot..patch @@ -0,0 +1,43 @@ +From 291071a937a1de398641f02002413678398e473c Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Ale=C5=A1=20Mat=C4=9Bj?= +Date: Mon, 8 Feb 2021 08:25:46 +0100 +Subject: [PATCH] Run tests for fill_sack_from_repos_in_cache in installroot + (RhBug:1865803) + +This prevents loading data (like failsafe) from host. + +It also allows testing that there are no modules in the installroot not just +no base-runtime* in test_with_modular_repo_without_modules_yaml. + +https://bugzilla.redhat.com/show_bug.cgi?id=1865803 +--- + tests/test_fill_sack_from_repos_in_cache.py | 4 +++- + 1 file changed, 3 insertions(+), 1 deletion(-) + +diff --git a/tests/test_fill_sack_from_repos_in_cache.py b/tests/test_fill_sack_from_repos_in_cache.py +index 24b0d4598d..f27235bf84 100644 +--- a/tests/test_fill_sack_from_repos_in_cache.py ++++ b/tests/test_fill_sack_from_repos_in_cache.py +@@ -42,6 +42,7 @@ class FillSackFromReposInCacheTest(unittest.TestCase): + def _create_cache_for_repo(self, repopath, tmpdir): + conf = dnf.conf.MainConf() + conf.cachedir = os.path.join(tmpdir, "cache") ++ conf.installroot = os.path.join(tmpdir) + + base = dnf.Base(conf=conf) + +@@ -68,6 +69,7 @@ def _setUp_from_repo_path(self, original_repo_path): + # Prepare base for the actual test + conf = dnf.conf.MainConf() + conf.cachedir = os.path.join(self.tmpdir, "cache") ++ conf.installroot = os.path.join(self.tmpdir) + self.test_base = dnf.Base(conf=conf) + repoconf = dnf.repo.Repo(TEST_REPO_NAME, conf) + repoconf.baseurl = self.repo_copy_path +@@ -258,5 +260,5 @@ def test_with_modular_repo_without_modules_yaml(self): + self.assertEqual(packages[0].evr, "10.0-7") + + self.module_base = dnf.module.module_base.ModuleBase(self.test_base) +- modules, _ = self.module_base._get_modules("base-runtime*") ++ modules, _ = self.module_base._get_modules("*") + self.assertEqual(len(modules), 0) diff --git a/SOURCES/0017-Set-persistdir-for-fill-sack-from-repos-in-cache-tests-RhBug-1865803.patch b/SOURCES/0017-Set-persistdir-for-fill-sack-from-repos-in-cache-tests-RhBug-1865803.patch new file mode 100644 index 0000000..b6dee78 --- /dev/null +++ b/SOURCES/0017-Set-persistdir-for-fill-sack-from-repos-in-cache-tests-RhBug-1865803.patch @@ -0,0 +1,61 @@ +From 40e762da5cd2d876b6424f4c25b77e8dc2422a0f Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Ale=C5=A1=20Mat=C4=9Bj?= +Date: Mon, 8 Feb 2021 08:25:46 +0100 +Subject: [PATCH] Set persistdir and substitutions for + fill_sack_from_repos_in_cache tests (RhBug:1865803) + +Setting just installroot is not enough because persistdir is not +automatically prepended with installroot if set via API. + +Also assert exact package names which is more useful output in case the +test fails. + +https://bugzilla.redhat.com/show_bug.cgi?id=1865803 +--- + tests/test_fill_sack_from_repos_in_cache.py | 19 +++++++++++++++---- + 1 file changed, 15 insertions(+), 4 deletions(-) + +diff --git a/tests/test_fill_sack_from_repos_in_cache.py b/tests/test_fill_sack_from_repos_in_cache.py +index f27235bf84..23fd2a4337 100644 +--- a/tests/test_fill_sack_from_repos_in_cache.py ++++ b/tests/test_fill_sack_from_repos_in_cache.py +@@ -42,7 +42,10 @@ class FillSackFromReposInCacheTest(unittest.TestCase): + def _create_cache_for_repo(self, repopath, tmpdir): + conf = dnf.conf.MainConf() + conf.cachedir = os.path.join(tmpdir, "cache") +- conf.installroot = os.path.join(tmpdir) ++ conf.installroot = tmpdir ++ conf.persistdir = os.path.join(conf.installroot, conf.persistdir.lstrip("/")) ++ conf.substitutions["arch"] = "x86_64" ++ conf.substitutions["basearch"] = dnf.rpm.basearch(conf.substitutions["arch"]) + + base = dnf.Base(conf=conf) + +@@ -69,7 +72,10 @@ def _setUp_from_repo_path(self, original_repo_path): + # Prepare base for the actual test + conf = dnf.conf.MainConf() + conf.cachedir = os.path.join(self.tmpdir, "cache") +- conf.installroot = os.path.join(self.tmpdir) ++ conf.installroot = self.tmpdir ++ conf.persistdir = os.path.join(conf.installroot, conf.persistdir.lstrip("/")) ++ conf.substitutions["arch"] = "x86_64" ++ conf.substitutions["basearch"] = dnf.rpm.basearch(conf.substitutions["arch"]) + self.test_base = dnf.Base(conf=conf) + repoconf = dnf.repo.Repo(TEST_REPO_NAME, conf) + repoconf.baseurl = self.repo_copy_path +@@ -231,8 +237,13 @@ def test_with_modules_yaml(self): + + q = self.test_base.sack.query() + packages = q.run() +- self.assertEqual(len(packages), 8) +- self.assertEqual(packages[0].evr, "2.02-0.40") ++ ++ pkg_names = [] ++ for pkg in packages: ++ pkg_names.append(pkg.name) ++ ++ self.assertEqual(pkg_names, ['grub2', 'httpd', 'httpd', 'httpd-doc', 'httpd-doc', 'httpd-provides-name-doc', ++ 'httpd-provides-name-version-release-doc', 'libnghttp2']) + + self.module_base = dnf.module.module_base.ModuleBase(self.test_base) + modules, _ = self.module_base._get_modules("base-runtime*") diff --git a/SOURCES/0018-Allow-stream-switching-if-option-enabled.patch b/SOURCES/0018-Allow-stream-switching-if-option-enabled.patch new file mode 100644 index 0000000..0fd4828 --- /dev/null +++ b/SOURCES/0018-Allow-stream-switching-if-option-enabled.patch @@ -0,0 +1,56 @@ +From 9ceb74f77479910f7844a9a87d4b7623687076be Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Ale=C5=A1=20Mat=C4=9Bj?= +Date: Fri, 24 Jul 2020 07:59:38 +0200 +Subject: [PATCH] Allow stream switching if option enabled + += changelog = +msg: New config option module_allow_stream_switch allows switching enabled streams +type: enhancement +--- + dnf.spec | 2 +- + dnf/cli/cli.py | 19 ++++++++++--------- + 2 files changed, 11 insertions(+), 10 deletions(-) + +diff --git a/dnf.spec b/dnf.spec +index 0e63b2b422..04f6f104c7 100644 +--- a/dnf.spec ++++ b/dnf.spec +@@ -2,7 +2,7 @@ + %undefine __cmake_in_source_build + + # default dependencies +-%global hawkey_version 0.54.4 ++%global hawkey_version 0.55.0 + %global libcomps_version 0.1.8 + %global libmodulemd_version 1.4.0 + %global rpm_version 4.14.0 +diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py +index be737ed3b7..29d7373fa3 100644 +--- a/dnf/cli/cli.py ++++ b/dnf/cli/cli.py +@@ -166,15 +166,16 @@ def do_transaction(self, display=()): + :return: history database transaction ID or None + """ + if dnf.base.WITH_MODULES: +- switchedModules = dict(self._moduleContainer.getSwitchedStreams()) +- if switchedModules: +- report_module_switch(switchedModules) +- msg = _("It is not possible to switch enabled streams of a module.\n" +- "It is recommended to remove all installed content from the module, and " +- "reset the module using '{prog} module reset ' command. After " +- "you reset the module, you can install the other stream.").format( +- prog=dnf.util.MAIN_PROG) +- raise dnf.exceptions.Error(msg) ++ if not self.conf.module_stream_switch: ++ switchedModules = dict(self._moduleContainer.getSwitchedStreams()) ++ if switchedModules: ++ report_module_switch(switchedModules) ++ msg = _("It is not possible to switch enabled streams of a module.\n" ++ "It is recommended to remove all installed content from the module, and " ++ "reset the module using '{prog} module reset ' command. After " ++ "you reset the module, you can install the other stream.").format( ++ prog=dnf.util.MAIN_PROG) ++ raise dnf.exceptions.Error(msg) + + trans = self.transaction + pkg_str = self.output.list_transaction(trans) diff --git a/SPECS/dnf.spec b/SPECS/dnf.spec index 5207358..137e6ff 100644 --- a/SPECS/dnf.spec +++ b/SPECS/dnf.spec @@ -1,5 +1,5 @@ # default dependencies -%global hawkey_version 0.48.0 +%global hawkey_version 0.55.0-5 %global libcomps_version 0.1.8 %global libmodulemd_version 1.4.0 %global rpm_version 4.14.2-35 @@ -81,14 +81,52 @@ It supports RPMs, modules and comps groups & environments. Name: dnf -Version: 4.2.23 -Release: 2%{?dist} +Version: 4.4.2 +Release: 10%{?dist} Summary: %{pkg_summary} # For a breakdown of the licensing, see PACKAGE-LICENSING License: GPLv2+ and GPLv2 and GPL URL: https://github.com/rpm-software-management/dnf Source0: %{url}/archive/%{version}/%{name}-%{version}.tar.gz -Patch1: 0001-Handle-empty-comps-group-name-RhBug1826198.patch +# https://github.com/rpm-software-management/dnf/commit/f6d1e4308769efaa6175f70d52bfd784c62fbf98 +Patch1: 0001-tests-SQL-write-a-readonly-folder.patch +# https://github.com/rpm-software-management/dnf/commit/c2e4901cec947e5be2e5ff5afa22691841d00bdc +Patch2: 0002-Revert-Fix-setopt-cachedir-writing-outside-of-installroot.patch +# https://github.com/rpm-software-management/dnf/pull/1675 +Patch3: 0003-Post-transaction-summary-is-logged-for-API-users-RhBug-1855158.patch +# https://github.com/rpm-software-management/dnf/pull/1698 +Patch4: 0004-Log-scriptlets-output-also-for-API-users-RhBug-1847340.patch +# https://github.com/rpm-software-management/dnf/pull/1659 +# https://github.com/rpm-software-management/dnf/pull/1689 +# https://github.com/rpm-software-management/dnf/pull/1709 +# https://github.com/rpm-software-management/dnf/pull/1690 +Patch5: 0005-dnf-history-operations-that-work-with-comps-correctly.patch +# https://github.com/rpm-software-management/dnf/pull/1691 +Patch6: 0006-Remove-sourcepackages-from-install-upgrade-set.patch +# https://github.com/rpm-software-management/dnf/pull/1710 +Patch7: 0007-Fix-documentation-of-globs-not-supporting-curly-brackets.patch +# https://github.com/rpm-software-management/dnf/pull/1685 +Patch8: 0008-Module-switch-command.patch +# https://github.com/rpm-software-management/dnf/pull/1702 +Patch9: 0009-yum.misc.decompress-to-handle-uncompressed-files-RhBug-1895059.patch +# https://github.com/rpm-software-management/dnf/pull/1693 +Patch10: 0010-Make-log-rotated-permissions-match-initial-log-permissions-RhBug-1894344.patch +# https://github.com/rpm-software-management/dnf/pull/1692 +Patch11: 0011-Add-new-attribute-for-Package--from-repo.patch +# https://github.com/rpm-software-management/dnf/pull/1695 +Patch12: 0012-Change-behaviour-of-Package-.from-repo.patch +# https://github.com/rpm-software-management/dnf/pull/1686 +Patch13: 0013-Package-add-a-get-header--method.patch +# https://github.com/rpm-software-management/dnf/pull/1703 +Patch14: 0014-Add-api-function-fill-sack-from-repos-in-cache-RhBug-1865803.patch +# https://github.com/rpm-software-management/dnf/pull/1711 +Patch15: 0015-Add-tests-and-docs-for-fill-sack-from-repos-in-cache-RhBug-1865803.patch +# https://github.com/rpm-software-management/dnf/pull/1721 +Patch16: 0016-Run-tests-for-fill-sack-from-repos-in-cache-in-installroot..patch +#https://github.com/rpm-software-management/dnf/pull/1723 +Patch17: 0017-Set-persistdir-for-fill-sack-from-repos-in-cache-tests-RhBug-1865803.patch +# https://github.com/rpm-software-management/dnf/pull/1725 +Patch18: 0018-Allow-stream-switching-if-option-enabled.patch BuildArch: noarch BuildRequires: cmake @@ -407,6 +445,7 @@ ln -sr %{buildroot}%{confdir}/vars %{buildroot}%{_sysconfdir}/yum/vars %{_mandir}/man8/%{name}.8* %{_mandir}/man8/yum2dnf.8* %{_mandir}/man7/dnf.modularity.7* +%{_mandir}/man5/dnf-transaction-json.5* %{_unitdir}/%{name}-makecache.service %{_unitdir}/%{name}-makecache.timer %{_var}/cache/%{name}/ @@ -508,6 +547,58 @@ ln -sr %{buildroot}%{confdir}/vars %{buildroot}%{_sysconfdir}/yum/vars %endif %changelog +* Thu Feb 11 2021 Nicola Sella - 4.4.2-10 +- Allow stream switching if option enabled + +* Tue Feb 09 2021 Nicola Sella - 4.4.2-9 +- Set persistdir for fill_sack_from_repos_in_cache tests (RhBug:1865803) + +* Mon Feb 08 2021 Nicola Sella - 4.4.2-8 +- Add api function: fill_sack_from_repos_in_cache (RhBug:1865803) +- Add tests and docs for fill_sack_from_repos_in_cache (RhBug:1865803) +- Run tests for fill_sack_from_repos_in_cache in installroot + +* Fri Feb 05 2021 Nicola Sella - 4.4.2-7 +- Make log rotated permissions match initial log permissions (RhBug:1894344) +- Add new attribute for Package - from_repo +- Change behaviour of Package().from_repo +- Package: add a get_header() method + +* Fri Jan 29 2021 Nicola Sella - 4.4.2-6 +- yum.misc.decompress() to handle uncompressed files (RhBug:1895059) +- Module switch command + +* Fri Jan 15 2021 Nicola Sella - 4.4.2-5 +- Fix patch for dnf history operations + +* Thu Jan 14 2021 Nicola Sella - 4.4.2-4 +- `dnf history` operations that work with comps correctly +- Remove sourcepackages from install/upgrade set +- Fix documentation of globs not supporting curly brackets + +* Thu Jan 07 2021 Nicola Sella - 4.4.2-3 +- Backport patches +- Log scriptlets output also for API users (RhBug:1847340) +- Post transaction summary is logged for API users (RhBug:1855158) + +* Wed Nov 11 2020 Nicola Sella - 4.4.2-2 +- Backport patch Revert "Fix --setopt=cachedir writing outside of installroot" + +* Tue Nov 10 2020 Nicola Sella - 4.4.2-1 +- Update to 4.4.2 +- spec: Fix building with new cmake macros (backport from downstream) +- Warn about key retrieval over http: +- Fix --setopt=cachedir writing outside of installroot +- Add vendor to dnf API (RhBug:1876561) +- Add allow_vendor_change option (RhBug:1788371) (RhBug:1788371) + +* Tue Jul 28 2020 Marek Blaha - 4.2.23-4 +- Update translations + +* Fri Jul 17 2020 Nicola Sella - 4.2.23-3 +- Add logfilelevel configuration (RhBug:1802074) +- [doc] Enhance repo variables documentation (RhBug:1848161,1848615) + * Wed Jun 10 2020 Ales Matej - 4.2.23-2 - Handle empty comps group name (RhBug:1826198)