From 5fae5ad773ecc5e65c8dc909bf32b309457205d2 Mon Sep 17 00:00:00 2001 From: CentOS Sources Date: Feb 04 2021 06:11:16 +0000 Subject: import dnf-4.4.2-5.el8 --- 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/SPECS/dnf.spec b/SPECS/dnf.spec index ad6967f..bd3656c 100644 --- a/SPECS/dnf.spec +++ b/SPECS/dnf.spec @@ -82,7 +82,7 @@ It supports RPMs, modules and comps groups & environments. Name: dnf Version: 4.4.2 -Release: 3%{?dist} +Release: 5%{?dist} Summary: %{pkg_summary} # For a breakdown of the licensing, see PACKAGE-LICENSING License: GPLv2+ and GPLv2 and GPL @@ -96,6 +96,15 @@ Patch2: 0002-Revert-Fix-setopt-cachedir-writing-outside-of-installroot.p 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 BuildArch: noarch BuildRequires: cmake @@ -516,6 +525,14 @@ ln -sr %{buildroot}%{confdir}/vars %{buildroot}%{_sysconfdir}/yum/vars %endif %changelog +* 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)