Blame SOURCES/0005-dnf-history-operations-that-work-with-comps-correctly.patch

5fae5a
From b9a8226185f3ab58e3551b315af2b11a8b2f2ebe Mon Sep 17 00:00:00 2001
5fae5a
From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hr=C3=A1zk=C3=BD?= <lhrazky@redhat.com>
5fae5a
Date: Tue, 8 Sep 2020 17:02:59 +0200
5fae5a
Subject: [PATCH 01/17] Add a get_current() method to SwdbInterface
5fae5a
5fae5a
The method returns the transaction that is currently being created in
5fae5a
Swdb, before it is stored to sqlite.
5fae5a
---
5fae5a
 VERSION.cmake     | 2 +-
5fae5a
 dnf.spec          | 2 +-
5fae5a
 dnf/db/history.py | 3 +++
5fae5a
 3 files changed, 5 insertions(+), 2 deletions(-)
5fae5a
5fae5a
diff --git a/dnf/db/history.py b/dnf/db/history.py
5fae5a
index 4d355f95..994cdb01 100644
5fae5a
--- a/dnf/db/history.py
5fae5a
+++ b/dnf/db/history.py
5fae5a
@@ -381,6 +381,9 @@ class SwdbInterface(object):
5fae5a
                 prev_trans.altered_gt_rpmdb = True
5fae5a
         return result[::-1]
5fae5a
 
5fae5a
+    def get_current(self):
5fae5a
+        return TransactionWrapper(self.swdb.getCurrent())
5fae5a
+
5fae5a
     def set_reason(self, pkg, reason):
5fae5a
         """Set reason for package"""
5fae5a
         rpm_item = self.rpm._pkg_to_swdb_rpm_item(pkg)
5fae5a
-- 
5fae5a
2.26.2
5fae5a
5fae5a
5fae5a
From 3bcf90aadfea98da1397b570fcb3ecc20a89c15d Mon Sep 17 00:00:00 2001
5fae5a
From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hr=C3=A1zk=C3=BD?= <lhrazky@redhat.com>
5fae5a
Date: Fri, 2 Oct 2020 15:52:19 +0200
5fae5a
Subject: [PATCH 02/17] transaction-sr: Prefer installing from the original
5fae5a
 transaction repository
5fae5a
5fae5a
In case a package exists in the same repo_id as from which it was
5fae5a
originally installed, prefer the package from that repo when replaying
5fae5a
the transaction.
5fae5a
5fae5a
Makes a difference in e.g. the system-upgrade plugin, where it ensures
5fae5a
the package is installed from the same repo from which it was downloaded
5fae5a
during the download step.
5fae5a
---
5fae5a
 dnf/transaction_sr.py | 13 +++++++++++++
5fae5a
 1 file changed, 13 insertions(+)
5fae5a
5fae5a
diff --git a/dnf/transaction_sr.py b/dnf/transaction_sr.py
5fae5a
index 9b9b0749..45ca2ef7 100644
5fae5a
--- a/dnf/transaction_sr.py
5fae5a
+++ b/dnf/transaction_sr.py
5fae5a
@@ -257,6 +257,7 @@ class TransactionReplay(object):
5fae5a
         try:
5fae5a
             action = pkg_data["action"]
5fae5a
             nevra = pkg_data["nevra"]
5fae5a
+            repo_id = pkg_data["repo_id"]
5fae5a
             reason = libdnf.transaction.StringToTransactionItemReason(pkg_data["reason"])
5fae5a
         except KeyError as e:
5fae5a
             raise TransactionError(
5fae5a
@@ -282,6 +283,18 @@ class TransactionReplay(object):
5fae5a
         epoch = parsed_nevra.epoch if parsed_nevra.epoch is not None else 0
5fae5a
         query = query_na.filter(epoch=epoch, version=parsed_nevra.version, release=parsed_nevra.release)
5fae5a
 
5fae5a
+        # In case the package is found in the same repo as in the original
5fae5a
+        # transaction, limit the query to that plus installed packages. IOW
5fae5a
+        # remove packages with the same NEVRA in case they are found in
5fae5a
+        # multiple repos and the repo the package came from originally is one
5fae5a
+        # of them.
5fae5a
+        # This can e.g. make a difference in the system-upgrade plugin, in case
5fae5a
+        # the same NEVRA is in two repos, this makes sure the same repo is used
5fae5a
+        # for both download and upgrade steps of the plugin.
5fae5a
+        query_repo = query.filter(reponame=repo_id)
5fae5a
+        if query_repo:
5fae5a
+            query = query_repo.union(query.installed())
5fae5a
+
5fae5a
         if not query:
5fae5a
             self._raise_or_warn(self._skip_unavailable, _('Cannot find rpm nevra "{nevra}".').format(nevra=nevra))
5fae5a
             return
5fae5a
-- 
5fae5a
2.26.2
5fae5a
5fae5a
5fae5a
From acfd6310131769f33165c8de1d064889a80fc259 Mon Sep 17 00:00:00 2001
5fae5a
From: Daniel Mach <dmach@redhat.com>
5fae5a
Date: Tue, 24 Nov 2020 10:57:21 +0100
5fae5a
Subject: [PATCH 03/17] transaction_sr: Enable loading transactions from dict
5fae5a
5fae5a
---
5fae5a
 dnf/cli/commands/history.py |  2 +-
5fae5a
 dnf/transaction_sr.py       | 42 +++++++++++++++++++++++++------------
5fae5a
 2 files changed, 30 insertions(+), 14 deletions(-)
5fae5a
5fae5a
diff --git a/dnf/cli/commands/history.py b/dnf/cli/commands/history.py
5fae5a
index e381f902..0a6dad9b 100644
5fae5a
--- a/dnf/cli/commands/history.py
5fae5a
+++ b/dnf/cli/commands/history.py
5fae5a
@@ -270,7 +270,7 @@ class HistoryCommand(commands.Command):
5fae5a
 
5fae5a
             self.replay = TransactionReplay(
5fae5a
                 self.base,
5fae5a
-                self.opts.transaction_filename,
5fae5a
+                filename=self.opts.transaction_filename,
5fae5a
                 ignore_installed = self.opts.ignore_installed,
5fae5a
                 ignore_extras = self.opts.ignore_extras,
5fae5a
                 skip_unavailable = self.opts.skip_unavailable
5fae5a
diff --git a/dnf/transaction_sr.py b/dnf/transaction_sr.py
5fae5a
index 45ca2ef7..e6b06665 100644
5fae5a
--- a/dnf/transaction_sr.py
5fae5a
+++ b/dnf/transaction_sr.py
5fae5a
@@ -187,21 +187,23 @@ class TransactionReplay(object):
5fae5a
     def __init__(
5fae5a
         self,
5fae5a
         base,
5fae5a
-        fn,
5fae5a
+        filename="",
5fae5a
+        data=None,
5fae5a
         ignore_extras=False,
5fae5a
         ignore_installed=False,
5fae5a
         skip_unavailable=False
5fae5a
     ):
5fae5a
         """
5fae5a
         :param base: the dnf base
5fae5a
-        :param fn: the filename to load the transaction from
5fae5a
+        :param filename: the filename to load the transaction from (conflicts with the 'data' argument)
5fae5a
+        :param data: the dictionary to load the transaction from (conflicts with the 'filename' argument)
5fae5a
         :param ignore_extras: whether to ignore extra package pulled into the transaction
5fae5a
         :param ignore_installed: whether to ignore installed versions of packages
5fae5a
         :param skip_unavailable: whether to skip transaction packages that aren't available
5fae5a
         """
5fae5a
 
5fae5a
         self._base = base
5fae5a
-        self._filename = fn
5fae5a
+        self._filename = filename
5fae5a
         self._ignore_installed = ignore_installed
5fae5a
         self._ignore_extras = ignore_extras
5fae5a
         self._skip_unavailable = skip_unavailable
5fae5a
@@ -213,25 +215,39 @@ class TransactionReplay(object):
5fae5a
         self._nevra_reason_cache = {}
5fae5a
         self._warnings = []
5fae5a
 
5fae5a
+        if filename and data:
5fae5a
+            raise ValueError(_("Conflicting TransactionReplay arguments have been specified: filename, data"))
5fae5a
+        elif filename:
5fae5a
+            self._load_from_file(filename)
5fae5a
+        else:
5fae5a
+            self._load_from_data(data)
5fae5a
+
5fae5a
+
5fae5a
+    def _load_from_file(self, fn):
5fae5a
+        self._filename = fn
5fae5a
         with open(fn, "r") as f:
5fae5a
             try:
5fae5a
-                self._replay_data = json.load(f)
5fae5a
+                replay_data = json.load(f)
5fae5a
             except json.decoder.JSONDecodeError as e:
5fae5a
                 raise TransactionFileError(fn, str(e) + ".")
5fae5a
 
5fae5a
         try:
5fae5a
-            self._verify_toplevel_json(self._replay_data)
5fae5a
+            self._load_from_data(replay_data)
5fae5a
+        except TransactionError as e:
5fae5a
+            raise TransactionFileError(fn, e)
5fae5a
 
5fae5a
-            self._rpms = self._replay_data.get("rpms", [])
5fae5a
-            self._assert_type(self._rpms, list, "rpms", "array")
5fae5a
+    def _load_from_data(self, data):
5fae5a
+        self._replay_data = data
5fae5a
+        self._verify_toplevel_json(self._replay_data)
5fae5a
 
5fae5a
-            self._groups = self._replay_data.get("groups", [])
5fae5a
-            self._assert_type(self._groups, list, "groups", "array")
5fae5a
+        self._rpms = self._replay_data.get("rpms", [])
5fae5a
+        self._assert_type(self._rpms, list, "rpms", "array")
5fae5a
 
5fae5a
-            self._environments = self._replay_data.get("environments", [])
5fae5a
-            self._assert_type(self._environments, list, "environments", "array")
5fae5a
-        except TransactionError as e:
5fae5a
-            raise TransactionFileError(fn, e)
5fae5a
+        self._groups = self._replay_data.get("groups", [])
5fae5a
+        self._assert_type(self._groups, list, "groups", "array")
5fae5a
+
5fae5a
+        self._environments = self._replay_data.get("environments", [])
5fae5a
+        self._assert_type(self._environments, list, "environments", "array")
5fae5a
 
5fae5a
     def _raise_or_warn(self, warn_only, msg):
5fae5a
         if warn_only:
5fae5a
-- 
5fae5a
2.26.2
5fae5a
5fae5a
5fae5a
From 90d4a2fd72b30b295adcb6da66b8043a70561b33 Mon Sep 17 00:00:00 2001
5fae5a
From: Daniel Mach <dmach@redhat.com>
5fae5a
Date: Fri, 20 Nov 2020 19:36:49 +0100
5fae5a
Subject: [PATCH 04/17] transaction_sr: Store exception attributes for future
5fae5a
 use
5fae5a
5fae5a
---
5fae5a
 dnf/transaction_sr.py | 4 ++++
5fae5a
 1 file changed, 4 insertions(+)
5fae5a
5fae5a
diff --git a/dnf/transaction_sr.py b/dnf/transaction_sr.py
5fae5a
index e6b06665..36787de4 100644
5fae5a
--- a/dnf/transaction_sr.py
5fae5a
+++ b/dnf/transaction_sr.py
5fae5a
@@ -55,6 +55,10 @@ class TransactionFileError(dnf.exceptions.Error):
5fae5a
         :param errors: a list of error classes or a string with an error description
5fae5a
         """
5fae5a
 
5fae5a
+        # store args in case someone wants to read them from a caught exception
5fae5a
+        self.filename = filename
5fae5a
+        self.errors = errors
5fae5a
+
5fae5a
         if isinstance(errors, (list, tuple)):
5fae5a
             if len(errors) > 1:
5fae5a
                 msg = _('Errors in "{filename}":').format(filename=filename)
5fae5a
-- 
5fae5a
2.26.2
5fae5a
5fae5a
5fae5a
From 0ffa7ed9ea73035acaec2c4f916d967701fddda2 Mon Sep 17 00:00:00 2001
5fae5a
From: Daniel Mach <dmach@redhat.com>
5fae5a
Date: Fri, 20 Nov 2020 19:04:59 +0100
5fae5a
Subject: [PATCH 05/17] transaction_sr: Handle serialize_transaction(None)
5fae5a
5fae5a
---
5fae5a
 dnf/transaction_sr.py | 3 +++
5fae5a
 1 file changed, 3 insertions(+)
5fae5a
5fae5a
diff --git a/dnf/transaction_sr.py b/dnf/transaction_sr.py
5fae5a
index 36787de4..41ddee1f 100644
5fae5a
--- a/dnf/transaction_sr.py
5fae5a
+++ b/dnf/transaction_sr.py
5fae5a
@@ -120,6 +120,9 @@ def serialize_transaction(transaction):
5fae5a
     groups = []
5fae5a
     environments = []
5fae5a
 
5fae5a
+    if transaction is None:
5fae5a
+        return data
5fae5a
+
5fae5a
     for tsi in transaction.packages():
5fae5a
         if tsi.is_package():
5fae5a
             rpms.append({
5fae5a
-- 
5fae5a
2.26.2
5fae5a
5fae5a
5fae5a
From c4bae459caef1d5128bd7ed43fcbb749608449f4 Mon Sep 17 00:00:00 2001
5fae5a
From: Daniel Mach <dmach@redhat.com>
5fae5a
Date: Mon, 23 Nov 2020 16:23:53 +0100
5fae5a
Subject: [PATCH 06/17] transaction_sr: Skip preferred repo lookup if repoid is
5fae5a
 empty
5fae5a
5fae5a
---
5fae5a
 dnf/transaction_sr.py | 7 ++++---
5fae5a
 1 file changed, 4 insertions(+), 3 deletions(-)
5fae5a
5fae5a
diff --git a/dnf/transaction_sr.py b/dnf/transaction_sr.py
5fae5a
index 41ddee1f..9926bebd 100644
5fae5a
--- a/dnf/transaction_sr.py
5fae5a
+++ b/dnf/transaction_sr.py
5fae5a
@@ -314,9 +314,10 @@ class TransactionReplay(object):
5fae5a
         # This can e.g. make a difference in the system-upgrade plugin, in case
5fae5a
         # the same NEVRA is in two repos, this makes sure the same repo is used
5fae5a
         # for both download and upgrade steps of the plugin.
5fae5a
-        query_repo = query.filter(reponame=repo_id)
5fae5a
-        if query_repo:
5fae5a
-            query = query_repo.union(query.installed())
5fae5a
+        if repo_id:
5fae5a
+            query_repo = query.filter(reponame=repo_id)
5fae5a
+            if query_repo:
5fae5a
+                query = query_repo.union(query.installed())
5fae5a
 
5fae5a
         if not query:
5fae5a
             self._raise_or_warn(self._skip_unavailable, _('Cannot find rpm nevra "{nevra}".').format(nevra=nevra))
5fae5a
-- 
5fae5a
2.26.2
5fae5a
5fae5a
5fae5a
From 3f82f871170be871ce8ec9d509306d751890ac9e Mon Sep 17 00:00:00 2001
5fae5a
From: Daniel Mach <dmach@redhat.com>
5fae5a
Date: Fri, 20 Nov 2020 17:44:28 +0100
5fae5a
Subject: [PATCH 07/17] history: Refactor redo code to use transaction
5fae5a
 store/replay
5fae5a
5fae5a
= changelog =
5fae5a
msg: Support comps groups in history redo
5fae5a
type: enhancement
5fae5a
resolves: https://bugzilla.redhat.com/show_bug.cgi?id=1657123
5fae5a
resolves: https://bugzilla.redhat.com/show_bug.cgi?id=1809565
5fae5a
resolves: https://bugzilla.redhat.com/show_bug.cgi?id=1809639
5fae5a
---
5fae5a
 dnf/cli/commands/history.py | 40 +++++++++++++++----------------------
5fae5a
 1 file changed, 16 insertions(+), 24 deletions(-)
5fae5a
5fae5a
diff --git a/dnf/cli/commands/history.py b/dnf/cli/commands/history.py
5fae5a
index 0a6dad9b..c28a136a 100644
5fae5a
--- a/dnf/cli/commands/history.py
5fae5a
+++ b/dnf/cli/commands/history.py
5fae5a
@@ -120,6 +120,10 @@ class HistoryCommand(commands.Command):
5fae5a
             if not self.opts.transactions:
5fae5a
                 raise dnf.cli.CliError(_('No transaction ID or package name given.'))
5fae5a
         elif self.opts.transactions_action in ['redo', 'undo', 'rollback']:
5fae5a
+            demands.available_repos = True
5fae5a
+            demands.resolving = True
5fae5a
+            demands.root_user = True
5fae5a
+
5fae5a
             self._require_one_transaction_id = True
5fae5a
             if not self.opts.transactions:
5fae5a
                 msg = _('No transaction ID or package name given.')
5fae5a
@@ -157,28 +161,16 @@ class HistoryCommand(commands.Command):
5fae5a
         old = self.base.history_get_transaction(extcmds)
5fae5a
         if old is None:
5fae5a
             return 1, ['Failed history redo']
5fae5a
-        tm = dnf.util.normalize_time(old.beg_timestamp)
5fae5a
-        print('Repeating transaction %u, from %s' % (old.tid, tm))
5fae5a
-        self.output.historyInfoCmdPkgsAltered(old)
5fae5a
-
5fae5a
-        for i in old.packages():
5fae5a
-            pkgs = list(self.base.sack.query().filter(nevra=str(i), reponame=i.from_repo))
5fae5a
-            if i.action in dnf.transaction.FORWARD_ACTIONS:
5fae5a
-                if not pkgs:
5fae5a
-                    logger.info(_('No package %s available.'),
5fae5a
-                    self.output.term.bold(ucd(str(i))))
5fae5a
-                    return 1, ['An operation cannot be redone']
5fae5a
-                pkg = pkgs[0]
5fae5a
-                self.base.install(str(pkg))
5fae5a
-            elif i.action == libdnf.transaction.TransactionItemAction_REMOVE:
5fae5a
-                if not pkgs:
5fae5a
-                    # package was removed already, we can skip removing it again
5fae5a
-                    continue
5fae5a
-                pkg = pkgs[0]
5fae5a
-                self.base.remove(str(pkg))
5fae5a
-
5fae5a
-        self.base.resolve()
5fae5a
-        self.base.do_transaction()
5fae5a
+
5fae5a
+        data = serialize_transaction(old)
5fae5a
+        self.replay = TransactionReplay(
5fae5a
+            self.base,
5fae5a
+            data=data,
5fae5a
+            ignore_installed=True,
5fae5a
+            ignore_extras=True,
5fae5a
+            skip_unavailable=self.opts.skip_unavailable
5fae5a
+        )
5fae5a
+        self.replay.run()
5fae5a
 
5fae5a
     def _hcmd_undo(self, extcmds):
5fae5a
         try:
5fae5a
@@ -326,13 +318,13 @@ class HistoryCommand(commands.Command):
5fae5a
             raise dnf.exceptions.Error(strs[0])
5fae5a
 
5fae5a
     def run_resolved(self):
5fae5a
-        if self.opts.transactions_action != "replay":
5fae5a
+        if self.opts.transactions_action not in ("replay", "redo"):
5fae5a
             return
5fae5a
 
5fae5a
         self.replay.post_transaction()
5fae5a
 
5fae5a
     def run_transaction(self):
5fae5a
-        if self.opts.transactions_action != "replay":
5fae5a
+        if self.opts.transactions_action not in ("replay", "redo"):
5fae5a
             return
5fae5a
 
5fae5a
         warnings = self.replay.get_warnings()
5fae5a
-- 
5fae5a
2.26.2
5fae5a
5fae5a
5fae5a
From d1b78ba8449b319121b5208c5b39609b1c6b61de Mon Sep 17 00:00:00 2001
5fae5a
From: Daniel Mach <dmach@redhat.com>
5fae5a
Date: Fri, 20 Nov 2020 19:07:50 +0100
5fae5a
Subject: [PATCH 08/17] history: Refactor rollback code to use transaction
5fae5a
 store/replay
5fae5a
5fae5a
= changelog =
5fae5a
msg: Support comps groups in history rollback
5fae5a
type: enhancement
5fae5a
resolves: https://bugzilla.redhat.com/show_bug.cgi?id=1657123
5fae5a
resolves: https://bugzilla.redhat.com/show_bug.cgi?id=1809565
5fae5a
resolves: https://bugzilla.redhat.com/show_bug.cgi?id=1809639
5fae5a
---
5fae5a
 dnf/cli/cli.py              | 56 -----------------------------
5fae5a
 dnf/cli/commands/history.py | 72 ++++++++++++++++++++++++++++++++++---
5fae5a
 2 files changed, 67 insertions(+), 61 deletions(-)
5fae5a
5fae5a
diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py
5fae5a
index cd720a97..36671fd8 100644
5fae5a
--- a/dnf/cli/cli.py
5fae5a
+++ b/dnf/cli/cli.py
5fae5a
@@ -627,62 +627,6 @@ class BaseCli(dnf.Base):
5fae5a
             logger.critical(_('Found more than one transaction ID!'))
5fae5a
         return old[0]
5fae5a
 
5fae5a
-    def history_rollback_transaction(self, extcmd):
5fae5a
-        """Rollback given transaction."""
5fae5a
-        old = self.history_get_transaction((extcmd,))
5fae5a
-        if old is None:
5fae5a
-            return 1, ['Failed history rollback, no transaction']
5fae5a
-        last = self.history.last()
5fae5a
-        if last is None:
5fae5a
-            return 1, ['Failed history rollback, no last?']
5fae5a
-        if old.tid == last.tid:
5fae5a
-            return 0, ['Rollback to current, nothing to do']
5fae5a
-
5fae5a
-        mobj = None
5fae5a
-        for trans in self.history.old(list(range(old.tid + 1, last.tid + 1))):
5fae5a
-            if trans.altered_lt_rpmdb:
5fae5a
-                logger.warning(_('Transaction history is incomplete, before %u.'), trans.tid)
5fae5a
-            elif trans.altered_gt_rpmdb:
5fae5a
-                logger.warning(_('Transaction history is incomplete, after %u.'), trans.tid)
5fae5a
-
5fae5a
-            if mobj is None:
5fae5a
-                mobj = dnf.db.history.MergedTransactionWrapper(trans)
5fae5a
-            else:
5fae5a
-                mobj.merge(trans)
5fae5a
-
5fae5a
-        tm = dnf.util.normalize_time(old.beg_timestamp)
5fae5a
-        print("Rollback to transaction %u, from %s" % (old.tid, tm))
5fae5a
-        print(self.output.fmtKeyValFill("  Undoing the following transactions: ",
5fae5a
-                                        ", ".join((str(x) for x in mobj.tids()))))
5fae5a
-        self.output.historyInfoCmdPkgsAltered(mobj)  # :todo
5fae5a
-
5fae5a
-#        history = dnf.history.open_history(self.history)  # :todo
5fae5a
-#        m = libdnf.transaction.MergedTransaction()
5fae5a
-
5fae5a
-#        return
5fae5a
-
5fae5a
-#        operations = dnf.history.NEVRAOperations()
5fae5a
-#        for id_ in range(old.tid + 1, last.tid + 1):
5fae5a
-#            operations += history.transaction_nevra_ops(id_)
5fae5a
-
5fae5a
-        try:
5fae5a
-            self._history_undo_operations(mobj, old.tid + 1, True, strict=self.conf.strict)
5fae5a
-        except dnf.exceptions.PackagesNotInstalledError as err:
5fae5a
-            raise
5fae5a
-            logger.info(_('No package %s installed.'),
5fae5a
-                        self.output.term.bold(ucd(err.pkg_spec)))
5fae5a
-            return 1, ['A transaction cannot be undone']
5fae5a
-        except dnf.exceptions.PackagesNotAvailableError as err:
5fae5a
-            raise
5fae5a
-            logger.info(_('No package %s available.'),
5fae5a
-                        self.output.term.bold(ucd(err.pkg_spec)))
5fae5a
-            return 1, ['A transaction cannot be undone']
5fae5a
-        except dnf.exceptions.MarkingError:
5fae5a
-            raise
5fae5a
-            assert False
5fae5a
-        else:
5fae5a
-            return 2, ["Rollback to transaction %u" % (old.tid,)]
5fae5a
-
5fae5a
     def history_undo_transaction(self, extcmd):
5fae5a
         """Undo given transaction."""
5fae5a
         old = self.history_get_transaction((extcmd,))
5fae5a
diff --git a/dnf/cli/commands/history.py b/dnf/cli/commands/history.py
5fae5a
index c28a136a..a450aaab 100644
5fae5a
--- a/dnf/cli/commands/history.py
5fae5a
+++ b/dnf/cli/commands/history.py
5fae5a
@@ -20,6 +20,7 @@ from __future__ import print_function
5fae5a
 from __future__ import unicode_literals
5fae5a
 
5fae5a
 import libdnf
5fae5a
+import hawkey
5fae5a
 
5fae5a
 from dnf.i18n import _, ucd
5fae5a
 from dnf.cli import commands
5fae5a
@@ -33,6 +34,7 @@ import dnf.util
5fae5a
 import json
5fae5a
 import logging
5fae5a
 import os
5fae5a
+import sys
5fae5a
 
5fae5a
 
5fae5a
 logger = logging.getLogger('dnf')
5fae5a
@@ -179,10 +181,70 @@ class HistoryCommand(commands.Command):
5fae5a
             return 1, [str(err)]
5fae5a
 
5fae5a
     def _hcmd_rollback(self, extcmds):
5fae5a
+        old = self.base.history_get_transaction(extcmds)
5fae5a
+        if old is None:
5fae5a
+            return 1, ['Failed history rollback']
5fae5a
+        last = self.base.history.last()
5fae5a
+
5fae5a
+        merged_trans = None
5fae5a
+        if old.tid != last.tid:
5fae5a
+            # history.old([]) returns all transactions and we don't want that
5fae5a
+            # so skip merging the transactions when trying to rollback to the last transaction
5fae5a
+            # which is the current system state and rollback is not applicable
5fae5a
+            for trans in self.base.history.old(list(range(old.tid + 1, last.tid + 1))):
5fae5a
+                if trans.altered_lt_rpmdb:
5fae5a
+                    logger.warning(_('Transaction history is incomplete, before %u.'), trans.tid)
5fae5a
+                elif trans.altered_gt_rpmdb:
5fae5a
+                    logger.warning(_('Transaction history is incomplete, after %u.'), trans.tid)
5fae5a
+
5fae5a
+                if merged_trans is None:
5fae5a
+                    merged_trans = dnf.db.history.MergedTransactionWrapper(trans)
5fae5a
+                else:
5fae5a
+                    merged_trans.merge(trans)
5fae5a
+
5fae5a
+        return self._revert_transaction(merged_trans)
5fae5a
+
5fae5a
+    def _revert_transaction(self, trans):
5fae5a
+        action_map = {
5fae5a
+            "Install": "Removed",
5fae5a
+            "Removed": "Install",
5fae5a
+            "Upgrade": "Downgraded",
5fae5a
+            "Upgraded": "Downgrade",
5fae5a
+            "Downgrade": "Upgraded",
5fae5a
+            "Downgraded": "Upgrade",
5fae5a
+            "Reinstalled": "Reinstall",
5fae5a
+            "Reinstall": "Reinstalled",
5fae5a
+            "Obsoleted": "Install",
5fae5a
+            "Obsolete": "Obsoleted",
5fae5a
+        }
5fae5a
+
5fae5a
+        data = serialize_transaction(trans)
5fae5a
+
5fae5a
+        # revert actions in the serialized transaction data to perform rollback/undo
5fae5a
+        for content_type in ("rpms", "groups", "environments"):
5fae5a
+            for ti in data.get(content_type, []):
5fae5a
+                ti["action"] = action_map[ti["action"]]
5fae5a
+
5fae5a
+                if ti["action"] == "Install" and ti.get("reason", None) == "clean":
5fae5a
+                    ti["reason"] = "dependency"
5fae5a
+
5fae5a
+                if ti.get("repo_id") == hawkey.SYSTEM_REPO_NAME:
5fae5a
+                    # erase repo_id, because it's not possible to perform forward actions from the @System repo
5fae5a
+                    ti["repo_id"] = None
5fae5a
+
5fae5a
+        self.replay = TransactionReplay(
5fae5a
+            self.base,
5fae5a
+            data=data,
5fae5a
+            ignore_installed=True,
5fae5a
+            ignore_extras=True,
5fae5a
+            skip_unavailable=self.opts.skip_unavailable
5fae5a
+        )
5fae5a
         try:
5fae5a
-            return self.base.history_rollback_transaction(extcmds[0])
5fae5a
-        except dnf.exceptions.Error as err:
5fae5a
-            return 1, [str(err)]
5fae5a
+            self.replay.run()
5fae5a
+        except dnf.transaction_sr.TransactionFileError as ex:
5fae5a
+            for error in ex.errors:
5fae5a
+                print(str(error), file=sys.stderr)
5fae5a
+            raise dnf.exceptions.PackageNotFoundError(_('no package matched'))
5fae5a
 
5fae5a
     def _hcmd_userinstalled(self):
5fae5a
         """Execute history userinstalled command."""
5fae5a
@@ -318,13 +380,13 @@ class HistoryCommand(commands.Command):
5fae5a
             raise dnf.exceptions.Error(strs[0])
5fae5a
 
5fae5a
     def run_resolved(self):
5fae5a
-        if self.opts.transactions_action not in ("replay", "redo"):
5fae5a
+        if self.opts.transactions_action not in ("replay", "redo", "rollback"):
5fae5a
             return
5fae5a
 
5fae5a
         self.replay.post_transaction()
5fae5a
 
5fae5a
     def run_transaction(self):
5fae5a
-        if self.opts.transactions_action not in ("replay", "redo"):
5fae5a
+        if self.opts.transactions_action not in ("replay", "redo", "rollback"):
5fae5a
             return
5fae5a
 
5fae5a
         warnings = self.replay.get_warnings()
5fae5a
-- 
5fae5a
2.26.2
5fae5a
5fae5a
5fae5a
From a59a57ce456682e85e86ee362aab4eecc19dbc81 Mon Sep 17 00:00:00 2001
5fae5a
From: Daniel Mach <dmach@redhat.com>
5fae5a
Date: Thu, 3 Dec 2020 15:56:52 +0100
5fae5a
Subject: [PATCH 09/17] history: Refactor undo code to use transaction
5fae5a
 store/replay
5fae5a
5fae5a
= changelog =
5fae5a
msg: Support comps groups in history undo
5fae5a
type: enhancement
5fae5a
resolves: https://bugzilla.redhat.com/show_bug.cgi?id=1657123
5fae5a
resolves: https://bugzilla.redhat.com/show_bug.cgi?id=1809565
5fae5a
resolves: https://bugzilla.redhat.com/show_bug.cgi?id=1809639
5fae5a
---
5fae5a
 dnf/cli/cli.py              | 28 ----------------------------
5fae5a
 dnf/cli/commands/history.py | 12 ++++++------
5fae5a
 2 files changed, 6 insertions(+), 34 deletions(-)
5fae5a
5fae5a
diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py
5fae5a
index 36671fd8..e4fd39c6 100644
5fae5a
--- a/dnf/cli/cli.py
5fae5a
+++ b/dnf/cli/cli.py
5fae5a
@@ -627,34 +627,6 @@ class BaseCli(dnf.Base):
5fae5a
             logger.critical(_('Found more than one transaction ID!'))
5fae5a
         return old[0]
5fae5a
 
5fae5a
-    def history_undo_transaction(self, extcmd):
5fae5a
-        """Undo given transaction."""
5fae5a
-        old = self.history_get_transaction((extcmd,))
5fae5a
-        if old is None:
5fae5a
-            return 1, ['Failed history undo']
5fae5a
-
5fae5a
-        tm = dnf.util.normalize_time(old.beg_timestamp)
5fae5a
-        msg = _("Undoing transaction {}, from {}").format(old.tid, ucd(tm))
5fae5a
-        logger.info(msg)
5fae5a
-        self.output.historyInfoCmdPkgsAltered(old)  # :todo
5fae5a
-
5fae5a
-
5fae5a
-        mobj = dnf.db.history.MergedTransactionWrapper(old)
5fae5a
-
5fae5a
-        try:
5fae5a
-            self._history_undo_operations(mobj, old.tid, strict=self.conf.strict)
5fae5a
-        except dnf.exceptions.PackagesNotInstalledError as err:
5fae5a
-            logger.info(_('No package %s installed.'),
5fae5a
-                        self.output.term.bold(ucd(err.pkg_spec)))
5fae5a
-            return 1, ['An operation cannot be undone']
5fae5a
-        except dnf.exceptions.PackagesNotAvailableError as err:
5fae5a
-            logger.info(_('No package %s available.'),
5fae5a
-                        self.output.term.bold(ucd(err.pkg_spec)))
5fae5a
-            return 1, ['An operation cannot be undone']
5fae5a
-        except dnf.exceptions.MarkingError:
5fae5a
-            raise
5fae5a
-        else:
5fae5a
-            return 2, ["Undoing transaction %u" % (old.tid,)]
5fae5a
 
5fae5a
 class Cli(object):
5fae5a
     def __init__(self, base):
5fae5a
diff --git a/dnf/cli/commands/history.py b/dnf/cli/commands/history.py
5fae5a
index a450aaab..d60d3f25 100644
5fae5a
--- a/dnf/cli/commands/history.py
5fae5a
+++ b/dnf/cli/commands/history.py
5fae5a
@@ -175,10 +175,10 @@ class HistoryCommand(commands.Command):
5fae5a
         self.replay.run()
5fae5a
 
5fae5a
     def _hcmd_undo(self, extcmds):
5fae5a
-        try:
5fae5a
-            return self.base.history_undo_transaction(extcmds[0])
5fae5a
-        except dnf.exceptions.Error as err:
5fae5a
-            return 1, [str(err)]
5fae5a
+        old = self.base.history_get_transaction(extcmds)
5fae5a
+        if old is None:
5fae5a
+            return 1, ['Failed history undo']
5fae5a
+        return self._revert_transaction(old)
5fae5a
 
5fae5a
     def _hcmd_rollback(self, extcmds):
5fae5a
         old = self.base.history_get_transaction(extcmds)
5fae5a
@@ -380,13 +380,13 @@ class HistoryCommand(commands.Command):
5fae5a
             raise dnf.exceptions.Error(strs[0])
5fae5a
 
5fae5a
     def run_resolved(self):
5fae5a
-        if self.opts.transactions_action not in ("replay", "redo", "rollback"):
5fae5a
+        if self.opts.transactions_action not in ("replay", "redo", "rollback", "undo"):
5fae5a
             return
5fae5a
 
5fae5a
         self.replay.post_transaction()
5fae5a
 
5fae5a
     def run_transaction(self):
5fae5a
-        if self.opts.transactions_action not in ("replay", "redo", "rollback"):
5fae5a
+        if self.opts.transactions_action not in ("replay", "redo", "rollback", "undo"):
5fae5a
             return
5fae5a
 
5fae5a
         warnings = self.replay.get_warnings()
5fae5a
-- 
5fae5a
2.26.2
5fae5a
5fae5a
5fae5a
From 5a0b6cc00420fd6559a1fd611de1417ea90b1bfc Mon Sep 17 00:00:00 2001
5fae5a
From: Daniel Mach <dmach@redhat.com>
5fae5a
Date: Fri, 20 Nov 2020 19:54:54 +0100
5fae5a
Subject: [PATCH 10/17] Remove Base._history_undo_operations() as it was
5fae5a
 replaced with transaction_sr code
5fae5a
5fae5a
---
5fae5a
 dnf/base.py | 59 -----------------------------------------------------
5fae5a
 1 file changed, 59 deletions(-)
5fae5a
5fae5a
diff --git a/dnf/base.py b/dnf/base.py
5fae5a
index ec41ab01..a2955051 100644
5fae5a
--- a/dnf/base.py
5fae5a
+++ b/dnf/base.py
5fae5a
@@ -2218,65 +2218,6 @@ class Base(object):
5fae5a
                                for prefix in ['/bin/', '/sbin/', '/usr/bin/', '/usr/sbin/']]
5fae5a
         return self.sack.query().filterm(file__glob=binary_provides), binary_provides
5fae5a
 
5fae5a
-    def _history_undo_operations(self, operations, first_trans, rollback=False, strict=True):
5fae5a
-        """Undo the operations on packages by their NEVRAs.
5fae5a
-
5fae5a
-        :param operations: a NEVRAOperations to be undone
5fae5a
-        :param first_trans: first transaction id being undone
5fae5a
-        :param rollback: True if transaction is performing a rollback
5fae5a
-        :param strict: if True, raise an exception on any errors
5fae5a
-        """
5fae5a
-
5fae5a
-        # map actions to their opposites
5fae5a
-        action_map = {
5fae5a
-            libdnf.transaction.TransactionItemAction_DOWNGRADE: None,
5fae5a
-            libdnf.transaction.TransactionItemAction_DOWNGRADED: libdnf.transaction.TransactionItemAction_UPGRADE,
5fae5a
-            libdnf.transaction.TransactionItemAction_INSTALL: libdnf.transaction.TransactionItemAction_REMOVE,
5fae5a
-            libdnf.transaction.TransactionItemAction_OBSOLETE: None,
5fae5a
-            libdnf.transaction.TransactionItemAction_OBSOLETED: libdnf.transaction.TransactionItemAction_INSTALL,
5fae5a
-            libdnf.transaction.TransactionItemAction_REINSTALL: None,
5fae5a
-            # reinstalls are skipped as they are considered as no-operation from history perspective
5fae5a
-            libdnf.transaction.TransactionItemAction_REINSTALLED: None,
5fae5a
-            libdnf.transaction.TransactionItemAction_REMOVE: libdnf.transaction.TransactionItemAction_INSTALL,
5fae5a
-            libdnf.transaction.TransactionItemAction_UPGRADE: None,
5fae5a
-            libdnf.transaction.TransactionItemAction_UPGRADED: libdnf.transaction.TransactionItemAction_DOWNGRADE,
5fae5a
-            libdnf.transaction.TransactionItemAction_REASON_CHANGE: None,
5fae5a
-        }
5fae5a
-
5fae5a
-        failed = False
5fae5a
-        for ti in operations.packages():
5fae5a
-            try:
5fae5a
-                action = action_map[ti.action]
5fae5a
-            except KeyError:
5fae5a
-                raise RuntimeError(_("Action not handled: {}".format(action)))
5fae5a
-
5fae5a
-            if action is None:
5fae5a
-                continue
5fae5a
-
5fae5a
-            if action == libdnf.transaction.TransactionItemAction_REMOVE:
5fae5a
-                query = self.sack.query().installed().filterm(nevra_strict=str(ti))
5fae5a
-                if not query:
5fae5a
-                    logger.error(_('No package %s installed.'), ucd(str(ti)))
5fae5a
-                    failed = True
5fae5a
-                    continue
5fae5a
-            else:
5fae5a
-                query = self.sack.query().filterm(nevra_strict=str(ti))
5fae5a
-                if not query:
5fae5a
-                    logger.error(_('No package %s available.'), ucd(str(ti)))
5fae5a
-                    failed = True
5fae5a
-                    continue
5fae5a
-
5fae5a
-            if action == libdnf.transaction.TransactionItemAction_REMOVE:
5fae5a
-                for pkg in query:
5fae5a
-                    self._goal.erase(pkg)
5fae5a
-            else:
5fae5a
-                selector = dnf.selector.Selector(self.sack)
5fae5a
-                selector.set(pkg=query)
5fae5a
-                self._goal.install(select=selector, optional=(not strict))
5fae5a
-
5fae5a
-        if strict and failed:
5fae5a
-            raise dnf.exceptions.PackageNotFoundError(_('no package matched'))
5fae5a
-
5fae5a
     def _merge_update_filters(self, q, pkg_spec=None, warning=True):
5fae5a
         """
5fae5a
         Merge Queries in _update_filters and return intersection with q Query
5fae5a
-- 
5fae5a
2.26.2
5fae5a
5fae5a
5fae5a
From c5a02f21d1a7b3be9ace78364ce234d853118574 Mon Sep 17 00:00:00 2001
5fae5a
From: Daniel Mach <dmach@redhat.com>
5fae5a
Date: Wed, 2 Dec 2020 08:57:15 +0100
5fae5a
Subject: [PATCH 11/17] history: Move history methods from BaseCli to
5fae5a
 HistoryCommand
5fae5a
5fae5a
---
5fae5a
 dnf/cli/cli.py              | 19 -------------
5fae5a
 dnf/cli/commands/history.py | 53 +++++++++++++++----------------------
5fae5a
 2 files changed, 22 insertions(+), 50 deletions(-)
5fae5a
5fae5a
diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py
5fae5a
index e4fd39c6..3080ae64 100644
5fae5a
--- a/dnf/cli/cli.py
5fae5a
+++ b/dnf/cli/cli.py
5fae5a
@@ -608,25 +608,6 @@ class BaseCli(dnf.Base):
5fae5a
             return False
5fae5a
         return True
5fae5a
 
5fae5a
-    def _history_get_transactions(self, extcmds):
5fae5a
-        if not extcmds:
5fae5a
-            logger.critical(_('No transaction ID given'))
5fae5a
-            return None
5fae5a
-
5fae5a
-        old = self.history.old(extcmds)
5fae5a
-        if not old:
5fae5a
-            logger.critical(_('Not found given transaction ID'))
5fae5a
-            return None
5fae5a
-        return old
5fae5a
-
5fae5a
-    def history_get_transaction(self, extcmds):
5fae5a
-        old = self._history_get_transactions(extcmds)
5fae5a
-        if old is None:
5fae5a
-            return None
5fae5a
-        if len(old) > 1:
5fae5a
-            logger.critical(_('Found more than one transaction ID!'))
5fae5a
-        return old[0]
5fae5a
-
5fae5a
 
5fae5a
 class Cli(object):
5fae5a
     def __init__(self, base):
5fae5a
diff --git a/dnf/cli/commands/history.py b/dnf/cli/commands/history.py
5fae5a
index d60d3f25..dfd954ee 100644
5fae5a
--- a/dnf/cli/commands/history.py
5fae5a
+++ b/dnf/cli/commands/history.py
5fae5a
@@ -34,7 +34,6 @@ import dnf.util
5fae5a
 import json
5fae5a
 import logging
5fae5a
 import os
5fae5a
-import sys
5fae5a
 
5fae5a
 
5fae5a
 logger = logging.getLogger('dnf')
5fae5a
@@ -160,10 +159,7 @@ class HistoryCommand(commands.Command):
5fae5a
         return dnf.cli.commands.Command.get_error_output(self, error)
5fae5a
 
5fae5a
     def _hcmd_redo(self, extcmds):
5fae5a
-        old = self.base.history_get_transaction(extcmds)
5fae5a
-        if old is None:
5fae5a
-            return 1, ['Failed history redo']
5fae5a
-
5fae5a
+        old = self._history_get_transaction(extcmds)
5fae5a
         data = serialize_transaction(old)
5fae5a
         self.replay = TransactionReplay(
5fae5a
             self.base,
5fae5a
@@ -174,16 +170,27 @@ class HistoryCommand(commands.Command):
5fae5a
         )
5fae5a
         self.replay.run()
5fae5a
 
5fae5a
+    def _history_get_transactions(self, extcmds):
5fae5a
+        if not extcmds:
5fae5a
+            raise dnf.cli.CliError(_('No transaction ID given'))
5fae5a
+
5fae5a
+        old = self.base.history.old(extcmds)
5fae5a
+        if not old:
5fae5a
+            raise dnf.cli.CliError(_('Transaction ID "{0}" not found.').format(extcmds[0]))
5fae5a
+        return old
5fae5a
+
5fae5a
+    def _history_get_transaction(self, extcmds):
5fae5a
+        old = self._history_get_transactions(extcmds)
5fae5a
+        if len(old) > 1:
5fae5a
+            raise dnf.cli.CliError(_('Found more than one transaction ID!'))
5fae5a
+        return old[0]
5fae5a
+
5fae5a
     def _hcmd_undo(self, extcmds):
5fae5a
-        old = self.base.history_get_transaction(extcmds)
5fae5a
-        if old is None:
5fae5a
-            return 1, ['Failed history undo']
5fae5a
+        old = self._history_get_transaction(extcmds)
5fae5a
         return self._revert_transaction(old)
5fae5a
 
5fae5a
     def _hcmd_rollback(self, extcmds):
5fae5a
-        old = self.base.history_get_transaction(extcmds)
5fae5a
-        if old is None:
5fae5a
-            return 1, ['Failed history rollback']
5fae5a
+        old = self._history_get_transaction(extcmds)
5fae5a
         last = self.base.history.last()
5fae5a
 
5fae5a
         merged_trans = None
5fae5a
@@ -239,12 +246,7 @@ class HistoryCommand(commands.Command):
5fae5a
             ignore_extras=True,
5fae5a
             skip_unavailable=self.opts.skip_unavailable
5fae5a
         )
5fae5a
-        try:
5fae5a
-            self.replay.run()
5fae5a
-        except dnf.transaction_sr.TransactionFileError as ex:
5fae5a
-            for error in ex.errors:
5fae5a
-                print(str(error), file=sys.stderr)
5fae5a
-            raise dnf.exceptions.PackageNotFoundError(_('no package matched'))
5fae5a
+        self.replay.run()
5fae5a
 
5fae5a
     def _hcmd_userinstalled(self):
5fae5a
         """Execute history userinstalled command."""
5fae5a
@@ -346,11 +348,8 @@ class HistoryCommand(commands.Command):
5fae5a
             elif vcmd == 'userinstalled':
5fae5a
                 ret = self._hcmd_userinstalled()
5fae5a
             elif vcmd == 'store':
5fae5a
-                transactions = self.output.history.old(tids)
5fae5a
-                if not transactions:
5fae5a
-                    raise dnf.cli.CliError(_('Transaction ID "{id}" not found.').format(id=tids[0]))
5fae5a
-
5fae5a
-                data = serialize_transaction(transactions[0])
5fae5a
+                tid = self._history_get_transaction(tids)
5fae5a
+                data = serialize_transaction(tid)
5fae5a
                 try:
5fae5a
                     filename = self.opts.output if self.opts.output is not None else "transaction.json"
5fae5a
 
5fae5a
@@ -371,14 +370,6 @@ class HistoryCommand(commands.Command):
5fae5a
                 except OSError as e:
5fae5a
                     raise dnf.cli.CliError(_('Error storing transaction: {}').format(str(e)))
5fae5a
 
5fae5a
-        if ret is None:
5fae5a
-            return
5fae5a
-        (code, strs) = ret
5fae5a
-        if code == 2:
5fae5a
-            self.cli.demands.resolving = True
5fae5a
-        elif code != 0:
5fae5a
-            raise dnf.exceptions.Error(strs[0])
5fae5a
-
5fae5a
     def run_resolved(self):
5fae5a
         if self.opts.transactions_action not in ("replay", "redo", "rollback", "undo"):
5fae5a
             return
5fae5a
@@ -393,7 +384,7 @@ class HistoryCommand(commands.Command):
5fae5a
         if warnings:
5fae5a
             logger.log(
5fae5a
                 dnf.logging.WARNING,
5fae5a
-                _("Warning, the following problems occurred while replaying the transaction:")
5fae5a
+                _("Warning, the following problems occurred while running a transaction:")
5fae5a
             )
5fae5a
             for w in warnings:
5fae5a
                 logger.log(dnf.logging.WARNING, "  " + w)
5fae5a
-- 
5fae5a
2.26.2
5fae5a
5fae5a
5fae5a
From 917f9f3b0fc418492293e08fa7db053b0c490d8f Mon Sep 17 00:00:00 2001
5fae5a
From: Daniel Mach <dmach@redhat.com>
5fae5a
Date: Thu, 10 Dec 2020 13:36:52 +0100
5fae5a
Subject: [PATCH 12/17] transaction_sr: Simplify error reporting, unify with
5fae5a
 history
5fae5a
5fae5a
---
5fae5a
 dnf/transaction_sr.py | 20 +++++++++-----------
5fae5a
 1 file changed, 9 insertions(+), 11 deletions(-)
5fae5a
5fae5a
diff --git a/dnf/transaction_sr.py b/dnf/transaction_sr.py
5fae5a
index 9926bebd..2122aba4 100644
5fae5a
--- a/dnf/transaction_sr.py
5fae5a
+++ b/dnf/transaction_sr.py
5fae5a
@@ -57,21 +57,19 @@ class TransactionFileError(dnf.exceptions.Error):
5fae5a
 
5fae5a
         # store args in case someone wants to read them from a caught exception
5fae5a
         self.filename = filename
5fae5a
-        self.errors = errors
5fae5a
-
5fae5a
         if isinstance(errors, (list, tuple)):
5fae5a
-            if len(errors) > 1:
5fae5a
-                msg = _('Errors in "{filename}":').format(filename=filename)
5fae5a
-                for error in errors:
5fae5a
-                    msg += "\n  " + str(error)
5fae5a
+            self.errors = errors
5fae5a
+        else:
5fae5a
+            self.errors = [errors]
5fae5a
 
5fae5a
-                super(TransactionFileError, self).__init__(msg)
5fae5a
-                return
5fae5a
+        if filename:
5fae5a
+            msg = _('The following problems occurred while replaying the transaction from file "{filename}":').format(filename=filename)
5fae5a
+        else:
5fae5a
+            msg = _('The following problems occurred while running a transaction:')
5fae5a
 
5fae5a
-            else:
5fae5a
-                errors = str(errors[0])
5fae5a
+        for error in self.errors:
5fae5a
+            msg += "\n  " + str(error)
5fae5a
 
5fae5a
-        msg = _('Error in "{filename}": {error}').format(filename=filename, error=errors)
5fae5a
         super(TransactionFileError, self).__init__(msg)
5fae5a
 
5fae5a
 
5fae5a
-- 
5fae5a
2.26.2
5fae5a
5fae5a
5fae5a
From d2fb741829445efee3187553cf7960f7bc2f643e Mon Sep 17 00:00:00 2001
5fae5a
From: Daniel Mach <dmach@redhat.com>
5fae5a
Date: Thu, 17 Dec 2020 16:37:01 +0100
5fae5a
Subject: [PATCH 13/17] transaction_sr: TransactionFileError exception to
5fae5a
 TransactionReplayError
5fae5a
5fae5a
---
5fae5a
 dnf/transaction_sr.py | 20 ++++++++++----------
5fae5a
 1 file changed, 10 insertions(+), 10 deletions(-)
5fae5a
5fae5a
diff --git a/dnf/transaction_sr.py b/dnf/transaction_sr.py
5fae5a
index 2122aba4..e4974eb9 100644
5fae5a
--- a/dnf/transaction_sr.py
5fae5a
+++ b/dnf/transaction_sr.py
5fae5a
@@ -48,7 +48,7 @@ class TransactionError(dnf.exceptions.Error):
5fae5a
         super(TransactionError, self).__init__(msg)
5fae5a
 
5fae5a
 
5fae5a
-class TransactionFileError(dnf.exceptions.Error):
5fae5a
+class TransactionReplayError(dnf.exceptions.Error):
5fae5a
     def __init__(self, filename, errors):
5fae5a
         """
5fae5a
         :param filename: The name of the transaction file being replayed
5fae5a
@@ -70,10 +70,10 @@ class TransactionFileError(dnf.exceptions.Error):
5fae5a
         for error in self.errors:
5fae5a
             msg += "\n  " + str(error)
5fae5a
 
5fae5a
-        super(TransactionFileError, self).__init__(msg)
5fae5a
+        super(TransactionReplayError, self).__init__(msg)
5fae5a
 
5fae5a
 
5fae5a
-class IncompatibleTransactionVersionError(TransactionFileError):
5fae5a
+class IncompatibleTransactionVersionError(TransactionReplayError):
5fae5a
     def __init__(self, filename, msg):
5fae5a
         super(IncompatibleTransactionVersionError, self).__init__(filename, msg)
5fae5a
 
5fae5a
@@ -84,7 +84,7 @@ def _check_version(version, filename):
5fae5a
     try:
5fae5a
         major = int(major)
5fae5a
     except ValueError as e:
5fae5a
-        raise TransactionFileError(
5fae5a
+        raise TransactionReplayError(
5fae5a
             filename,
5fae5a
             _('Invalid major version "{major}", number expected.').format(major=major)
5fae5a
         )
5fae5a
@@ -92,7 +92,7 @@ def _check_version(version, filename):
5fae5a
     try:
5fae5a
         int(minor)  # minor is unused, just check it's a number
5fae5a
     except ValueError as e:
5fae5a
-        raise TransactionFileError(
5fae5a
+        raise TransactionReplayError(
5fae5a
             filename,
5fae5a
             _('Invalid minor version "{minor}", number expected.').format(minor=minor)
5fae5a
         )
5fae5a
@@ -234,12 +234,12 @@ class TransactionReplay(object):
5fae5a
             try:
5fae5a
                 replay_data = json.load(f)
5fae5a
             except json.decoder.JSONDecodeError as e:
5fae5a
-                raise TransactionFileError(fn, str(e) + ".")
5fae5a
+                raise TransactionReplayError(fn, str(e) + ".")
5fae5a
 
5fae5a
         try:
5fae5a
             self._load_from_data(replay_data)
5fae5a
         except TransactionError as e:
5fae5a
-            raise TransactionFileError(fn, e)
5fae5a
+            raise TransactionReplayError(fn, e)
5fae5a
 
5fae5a
     def _load_from_data(self, data):
5fae5a
         self._replay_data = data
5fae5a
@@ -268,7 +268,7 @@ class TransactionReplay(object):
5fae5a
         fn = self._filename
5fae5a
 
5fae5a
         if "version" not in replay_data:
5fae5a
-            raise TransactionFileError(fn, _('Missing key "{key}".'.format(key="version")))
5fae5a
+            raise TransactionReplayError(fn, _('Missing key "{key}".'.format(key="version")))
5fae5a
 
5fae5a
         self._assert_type(replay_data["version"], str, "version", "string")
5fae5a
 
5fae5a
@@ -580,7 +580,7 @@ class TransactionReplay(object):
5fae5a
                 errors.append(e)
5fae5a
 
5fae5a
         if errors:
5fae5a
-            raise TransactionFileError(fn, errors)
5fae5a
+            raise TransactionReplayError(fn, errors)
5fae5a
 
5fae5a
     def post_transaction(self):
5fae5a
         """
5fae5a
@@ -635,4 +635,4 @@ class TransactionReplay(object):
5fae5a
                 pass
5fae5a
 
5fae5a
         if errors:
5fae5a
-            raise TransactionFileError(self._filename, errors)
5fae5a
+            raise TransactionReplayError(self._filename, errors)
5fae5a
-- 
5fae5a
2.26.2
5fae5a
5fae5a
5fae5a
From 1182143e58d4fda530d5dfd19f0d9c9406e8eff3 Mon Sep 17 00:00:00 2001
5fae5a
From: Daniel Mach <dmach@redhat.com>
5fae5a
Date: Thu, 17 Dec 2020 16:55:39 +0100
5fae5a
Subject: [PATCH 14/17] transaction_sr: Don't return if there's a mismatch in
5fae5a
 actions
5fae5a
5fae5a
When _ignore_installed == True, then an exception is raised anyway.
5fae5a
When _ignore_installed == False, get the requested package to the system
5fae5a
regardless the action.
5fae5a
---
5fae5a
 dnf/transaction_sr.py | 1 -
5fae5a
 1 file changed, 1 deletion(-)
5fae5a
5fae5a
diff --git a/dnf/transaction_sr.py b/dnf/transaction_sr.py
5fae5a
index e4974eb9..dae8d300 100644
5fae5a
--- a/dnf/transaction_sr.py
5fae5a
+++ b/dnf/transaction_sr.py
5fae5a
@@ -334,7 +334,6 @@ class TransactionReplay(object):
5fae5a
             if action == "Install" and query_na.installed() and not self._base._get_installonly_query(query_na):
5fae5a
                 self._raise_or_warn(self._ignore_installed,
5fae5a
                     _('Package "{na}" is already installed for action "{action}".').format(na=na, action=action))
5fae5a
-                return
5fae5a
 
5fae5a
             sltr = dnf.selector.Selector(self._base.sack).set(pkg=query)
5fae5a
             self._base.goal.install(select=sltr, optional=not self._base.conf.strict)
5fae5a
-- 
5fae5a
2.26.2
5fae5a
5fae5a
5fae5a
From ff32a3c68fa853b53084a1a4947f345062056f23 Mon Sep 17 00:00:00 2001
5fae5a
From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hr=C3=A1zk=C3=BD?= <lhrazky@redhat.com>
5fae5a
Date: Fri, 8 Jan 2021 13:37:45 +0100
5fae5a
Subject: [PATCH 15/17] cli/output: Return number of listed packages from
5fae5a
 listPkgs()
5fae5a
5fae5a
Instead of an error status and message.
5fae5a
---
5fae5a
 dnf/cli/cli.py              |  5 ++---
5fae5a
 dnf/cli/commands/history.py |  4 +++-
5fae5a
 dnf/cli/output.py           | 14 ++------------
5fae5a
 3 files changed, 7 insertions(+), 16 deletions(-)
5fae5a
5fae5a
diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py
5fae5a
index 3080ae64..be737ed3 100644
5fae5a
--- a/dnf/cli/cli.py
5fae5a
+++ b/dnf/cli/cli.py
5fae5a
@@ -505,7 +505,7 @@ class BaseCli(dnf.Base):
5fae5a
             # XXX put this into the ListCommand at some point
5fae5a
             if len(ypl.obsoletes) > 0 and basecmd == 'list':
5fae5a
             # if we've looked up obsolete lists and it's a list request
5fae5a
-                rop = [0, '']
5fae5a
+                rop = len(ypl.obsoletes)
5fae5a
                 print(_('Obsoleting Packages'))
5fae5a
                 for obtup in sorted(ypl.obsoletesTuples,
5fae5a
                                     key=operator.itemgetter(0)):
5fae5a
@@ -517,8 +517,7 @@ class BaseCli(dnf.Base):
5fae5a
             rrap = self.output.listPkgs(ypl.recent, _('Recently Added Packages'),
5fae5a
                                  basecmd, columns=columns)
5fae5a
             if len(patterns) and \
5fae5a
-                rrap[0] and rop[0] and rup[0] and rep[0] and rap[0] and \
5fae5a
-                raep[0] and rip[0]:
5fae5a
+                    rrap == 0 and rop == 0 and rup == 0 and rep == 0 and rap == 0 and raep == 0 and rip == 0:
5fae5a
                 raise dnf.exceptions.Error(_('No matching Packages to list'))
5fae5a
 
5fae5a
     def returnPkgLists(self, pkgnarrow='all', patterns=None,
5fae5a
diff --git a/dnf/cli/commands/history.py b/dnf/cli/commands/history.py
5fae5a
index dfd954ee..e9b91d0f 100644
5fae5a
--- a/dnf/cli/commands/history.py
5fae5a
+++ b/dnf/cli/commands/history.py
5fae5a
@@ -251,7 +251,9 @@ class HistoryCommand(commands.Command):
5fae5a
     def _hcmd_userinstalled(self):
5fae5a
         """Execute history userinstalled command."""
5fae5a
         pkgs = tuple(self.base.iter_userinstalled())
5fae5a
-        return self.output.listPkgs(pkgs, 'Packages installed by user', 'nevra')
5fae5a
+        n_listed = self.output.listPkgs(pkgs, 'Packages installed by user', 'nevra')
5fae5a
+        if n_listed == 0:
5fae5a
+            raise dnf.cli.CliError(_('No packages to list'))
5fae5a
 
5fae5a
     def _args2transaction_ids(self):
5fae5a
         """Convert commandline arguments to transaction ids"""
5fae5a
diff --git a/dnf/cli/output.py b/dnf/cli/output.py
5fae5a
index 6d729b63..6cfc9e22 100644
5fae5a
--- a/dnf/cli/output.py
5fae5a
+++ b/dnf/cli/output.py
5fae5a
@@ -597,18 +597,10 @@ class Output(object):
5fae5a
                        number
5fae5a
                  '>' - highlighting used when the package has a higher version
5fae5a
                        number
5fae5a
-        :return: (exit_code, [errors])
5fae5a
-
5fae5a
-        exit_code is::
5fae5a
-
5fae5a
-            0 = we're done, exit
5fae5a
-            1 = we've errored, exit with error string
5fae5a
-
5fae5a
+        :return: number of packages listed
5fae5a
         """
5fae5a
         if outputType in ['list', 'info', 'name', 'nevra']:
5fae5a
-            thingslisted = 0
5fae5a
             if len(lst) > 0:
5fae5a
-                thingslisted = 1
5fae5a
                 print('%s' % description)
5fae5a
                 info_set = set()
5fae5a
                 if outputType == 'list':
5fae5a
@@ -645,9 +637,7 @@ class Output(object):
5fae5a
                 if info_set:
5fae5a
                     print("\n".join(sorted(info_set)))
5fae5a
 
5fae5a
-            if thingslisted == 0:
5fae5a
-                return 1, [_('No packages to list')]
5fae5a
-            return 0, []
5fae5a
+            return len(lst)
5fae5a
 
5fae5a
     def userconfirm(self, msg=None, defaultyes_msg=None):
5fae5a
         """Get a yes or no from the user, and default to No
5fae5a
-- 
5fae5a
2.26.2
5fae5a
5fae5a
5fae5a
From 0226da7351eb97cd9c4c6739725b1f77d445764e Mon Sep 17 00:00:00 2001
5fae5a
From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hr=C3=A1zk=C3=BD?= <lhrazky@redhat.com>
5fae5a
Date: Fri, 8 Jan 2021 13:44:27 +0100
5fae5a
Subject: [PATCH 16/17] Clean up history command error handling
5fae5a
5fae5a
The removal of `ret` value error handling which was removed previously was not
5fae5a
complete. Most of it is was no-op as no errors were really propagated through
5fae5a
it, but the `history userinstalled` command was still relying on it.
5fae5a
5fae5a
The commit removes the last bit and replaces it with raising an exception.
5fae5a
---
5fae5a
 dnf/cli/commands/history.py | 17 ++++++++---------
5fae5a
 1 file changed, 8 insertions(+), 9 deletions(-)
5fae5a
5fae5a
diff --git a/dnf/cli/commands/history.py b/dnf/cli/commands/history.py
5fae5a
index e9b91d0f..7b38cb60 100644
5fae5a
--- a/dnf/cli/commands/history.py
5fae5a
+++ b/dnf/cli/commands/history.py
5fae5a
@@ -187,7 +187,7 @@ class HistoryCommand(commands.Command):
5fae5a
 
5fae5a
     def _hcmd_undo(self, extcmds):
5fae5a
         old = self._history_get_transaction(extcmds)
5fae5a
-        return self._revert_transaction(old)
5fae5a
+        self._revert_transaction(old)
5fae5a
 
5fae5a
     def _hcmd_rollback(self, extcmds):
5fae5a
         old = self._history_get_transaction(extcmds)
5fae5a
@@ -209,7 +209,7 @@ class HistoryCommand(commands.Command):
5fae5a
                 else:
5fae5a
                     merged_trans.merge(trans)
5fae5a
 
5fae5a
-        return self._revert_transaction(merged_trans)
5fae5a
+        self._revert_transaction(merged_trans)
5fae5a
 
5fae5a
     def _revert_transaction(self, trans):
5fae5a
         action_map = {
5fae5a
@@ -321,7 +321,6 @@ class HistoryCommand(commands.Command):
5fae5a
 
5fae5a
     def run(self):
5fae5a
         vcmd = self.opts.transactions_action
5fae5a
-        ret = None
5fae5a
 
5fae5a
         if vcmd == 'replay':
5fae5a
             self.base.read_comps(arch_filter=True)
5fae5a
@@ -338,17 +337,17 @@ class HistoryCommand(commands.Command):
5fae5a
             tids, merged_tids = self._args2transaction_ids()
5fae5a
 
5fae5a
             if vcmd == 'list' and (tids or not self.opts.transactions):
5fae5a
-                ret = self.output.historyListCmd(tids, reverse=self.opts.reverse)
5fae5a
+                self.output.historyListCmd(tids, reverse=self.opts.reverse)
5fae5a
             elif vcmd == 'info' and (tids or not self.opts.transactions):
5fae5a
-                ret = self.output.historyInfoCmd(tids, self.opts.transactions, merged_tids)
5fae5a
+                self.output.historyInfoCmd(tids, self.opts.transactions, merged_tids)
5fae5a
             elif vcmd == 'undo':
5fae5a
-                ret = self._hcmd_undo(tids)
5fae5a
+                self._hcmd_undo(tids)
5fae5a
             elif vcmd == 'redo':
5fae5a
-                ret = self._hcmd_redo(tids)
5fae5a
+                self._hcmd_redo(tids)
5fae5a
             elif vcmd == 'rollback':
5fae5a
-                ret = self._hcmd_rollback(tids)
5fae5a
+                self._hcmd_rollback(tids)
5fae5a
             elif vcmd == 'userinstalled':
5fae5a
-                ret = self._hcmd_userinstalled()
5fae5a
+                self._hcmd_userinstalled()
5fae5a
             elif vcmd == 'store':
5fae5a
                 tid = self._history_get_transaction(tids)
5fae5a
                 data = serialize_transaction(tid)
5fae5a
-- 
5fae5a
2.26.2
5fae5a
5fae5a
5fae5a
From 7e862711b3d7b9b444d966594630b49bf3761faf Mon Sep 17 00:00:00 2001
5fae5a
From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hr=C3=A1zk=C3=BD?= <lhrazky@redhat.com>
5fae5a
Date: Mon, 23 Nov 2020 16:32:16 +0100
5fae5a
Subject: [PATCH 17/17] Lazy-load base.comps instead of explicitly
5fae5a
5fae5a
Loading base.comps was done by calling a method at arbitrary places in
5fae5a
the code, this is hard to maintain and get right. The method could be
5fae5a
inadvertedly called multiple times per dnf run too.
5fae5a
5fae5a
Instead load the comps data lazily on first access. In case of the
5fae5a
shell, using "repo enable/disable" can cause the comps data to change
5fae5a
mid-run. Instead of explicitly reloading, clear the comps attribute and
5fae5a
let it be lazy-loaded again when needed.
5fae5a
5fae5a
Closes: #1690
5fae5a
Approved by: j-mracek
5fae5a
---
5fae5a
 dnf/base.py                   | 4 ++--
5fae5a
 dnf/cli/commands/group.py     | 5 -----
5fae5a
 dnf/cli/commands/history.py   | 2 --
5fae5a
 dnf/cli/commands/install.py   | 1 -
5fae5a
 dnf/cli/commands/remove.py    | 1 -
5fae5a
 dnf/cli/commands/repoquery.py | 1 -
5fae5a
 dnf/cli/commands/shell.py     | 3 +++
5fae5a
 dnf/cli/commands/upgrade.py   | 1 -
5fae5a
 tests/api/test_dnf_base.py    | 4 +---
5fae5a
 9 files changed, 6 insertions(+), 16 deletions(-)
5fae5a
5fae5a
diff --git a/dnf/base.py b/dnf/base.py
5fae5a
index a2955051..39c21c33 100644
5fae5a
--- a/dnf/base.py
5fae5a
+++ b/dnf/base.py
5fae5a
@@ -242,6 +242,8 @@ class Base(object):
5fae5a
     @property
5fae5a
     def comps(self):
5fae5a
         # :api
5fae5a
+        if self._comps is None:
5fae5a
+            self.read_comps(arch_filter=True)
5fae5a
         return self._comps
5fae5a
 
5fae5a
     @property
5fae5a
@@ -1881,7 +1883,6 @@ class Base(object):
5fae5a
             no_match_module_specs = install_specs.grp_specs
5fae5a
 
5fae5a
         if no_match_module_specs:
5fae5a
-            self.read_comps(arch_filter=True)
5fae5a
             exclude_specs.grp_specs = self._expand_groups(exclude_specs.grp_specs)
5fae5a
             self._install_groups(no_match_module_specs, exclude_specs, no_match_group_specs, strict)
5fae5a
 
5fae5a
@@ -2084,7 +2085,6 @@ class Base(object):
5fae5a
                     msg = _('Not a valid form: %s')
5fae5a
                     logger.warning(msg, grp_spec)
5fae5a
             elif grp_specs:
5fae5a
-                self.read_comps(arch_filter=True)
5fae5a
                 if self.env_group_remove(grp_specs):
5fae5a
                     done = True
5fae5a
 
5fae5a
diff --git a/dnf/cli/commands/group.py b/dnf/cli/commands/group.py
5fae5a
index bd17f80f..cf542799 100644
5fae5a
--- a/dnf/cli/commands/group.py
5fae5a
+++ b/dnf/cli/commands/group.py
5fae5a
@@ -110,9 +110,6 @@ class GroupCommand(commands.Command):
5fae5a
 
5fae5a
         return installed, available
5fae5a
 
5fae5a
-    def _grp_setup(self):
5fae5a
-        self.base.read_comps(arch_filter=True)
5fae5a
-
5fae5a
     def _info(self, userlist):
5fae5a
         for strng in userlist:
5fae5a
             group_matched = False
5fae5a
@@ -370,8 +367,6 @@ class GroupCommand(commands.Command):
5fae5a
         cmd = self.opts.subcmd
5fae5a
         extcmds = self.opts.args
5fae5a
 
5fae5a
-        self._grp_setup()
5fae5a
-
5fae5a
         if cmd == 'summary':
5fae5a
             return self._summary(extcmds)
5fae5a
         if cmd == 'list':
5fae5a
diff --git a/dnf/cli/commands/history.py b/dnf/cli/commands/history.py
5fae5a
index 7b38cb60..293d93fc 100644
5fae5a
--- a/dnf/cli/commands/history.py
5fae5a
+++ b/dnf/cli/commands/history.py
5fae5a
@@ -323,8 +323,6 @@ class HistoryCommand(commands.Command):
5fae5a
         vcmd = self.opts.transactions_action
5fae5a
 
5fae5a
         if vcmd == 'replay':
5fae5a
-            self.base.read_comps(arch_filter=True)
5fae5a
-
5fae5a
             self.replay = TransactionReplay(
5fae5a
                 self.base,
5fae5a
                 filename=self.opts.transaction_filename,
5fae5a
diff --git a/dnf/cli/commands/install.py b/dnf/cli/commands/install.py
5fae5a
index 38a90b61..b637af0b 100644
5fae5a
--- a/dnf/cli/commands/install.py
5fae5a
+++ b/dnf/cli/commands/install.py
5fae5a
@@ -151,7 +151,6 @@ class InstallCommand(commands.Command):
5fae5a
         return err_pkgs
5fae5a
 
5fae5a
     def _install_groups(self, grp_specs):
5fae5a
-        self.base.read_comps(arch_filter=True)
5fae5a
         try:
5fae5a
             self.base.env_group_install(grp_specs,
5fae5a
                                         tuple(self.base.conf.group_package_types),
5fae5a
diff --git a/dnf/cli/commands/remove.py b/dnf/cli/commands/remove.py
5fae5a
index f50dbd91..e455ba6e 100644
5fae5a
--- a/dnf/cli/commands/remove.py
5fae5a
+++ b/dnf/cli/commands/remove.py
5fae5a
@@ -142,7 +142,6 @@ class RemoveCommand(commands.Command):
5fae5a
                 skipped_grps = self.opts.grp_specs
5fae5a
 
5fae5a
             if skipped_grps:
5fae5a
-                self.base.read_comps(arch_filter=True)
5fae5a
                 for group in skipped_grps:
5fae5a
                     try:
5fae5a
                         if self.base.env_group_remove([group]):
5fae5a
diff --git a/dnf/cli/commands/repoquery.py b/dnf/cli/commands/repoquery.py
5fae5a
index 099a9312..b0d06a90 100644
5fae5a
--- a/dnf/cli/commands/repoquery.py
5fae5a
+++ b/dnf/cli/commands/repoquery.py
5fae5a
@@ -632,7 +632,6 @@ class RepoQueryCommand(commands.Command):
5fae5a
                 print("\n".join(sorted(pkgs)))
5fae5a
 
5fae5a
     def _group_member_report(self, query):
5fae5a
-        self.base.read_comps(arch_filter=True)
5fae5a
         package_conf_dict = {}
5fae5a
         for group in self.base.comps.groups:
5fae5a
             package_conf_dict[group.id] = set([pkg.name for pkg in group.packages_iter()])
5fae5a
diff --git a/dnf/cli/commands/shell.py b/dnf/cli/commands/shell.py
5fae5a
index 431fe502..18c886ff 100644
5fae5a
--- a/dnf/cli/commands/shell.py
5fae5a
+++ b/dnf/cli/commands/shell.py
5fae5a
@@ -239,6 +239,9 @@ exit (or quit)           exit the shell""")
5fae5a
             if fill_sack:
5fae5a
                 self.base.fill_sack()
5fae5a
 
5fae5a
+            # reset base._comps, as it has changed due to changing the repos
5fae5a
+            self.base._comps = None
5fae5a
+
5fae5a
         else:
5fae5a
             self._help('repo')
5fae5a
 
5fae5a
diff --git a/dnf/cli/commands/upgrade.py b/dnf/cli/commands/upgrade.py
5fae5a
index 44789c9a..f62cfcc1 100644
5fae5a
--- a/dnf/cli/commands/upgrade.py
5fae5a
+++ b/dnf/cli/commands/upgrade.py
5fae5a
@@ -124,7 +124,6 @@ class UpgradeCommand(commands.Command):
5fae5a
 
5fae5a
     def _update_groups(self):
5fae5a
         if self.skipped_grp_specs:
5fae5a
-            self.base.read_comps(arch_filter=True)
5fae5a
             self.base.env_group_upgrade(self.skipped_grp_specs)
5fae5a
             return True
5fae5a
         return False
5fae5a
diff --git a/tests/api/test_dnf_base.py b/tests/api/test_dnf_base.py
5fae5a
index ca71b75c..656bd225 100644
5fae5a
--- a/tests/api/test_dnf_base.py
5fae5a
+++ b/tests/api/test_dnf_base.py
5fae5a
@@ -34,9 +34,7 @@ class DnfBaseApiTest(TestCase):
5fae5a
     def test_comps(self):
5fae5a
         # Base.comps
5fae5a
         self.assertHasAttr(self.base, "comps")
5fae5a
-
5fae5a
-        # blank initially
5fae5a
-        self.assertEqual(self.base.comps, None)
5fae5a
+        self.assertHasType(self.base.comps, dnf.comps.Comps)
5fae5a
 
5fae5a
         self.base.read_comps()
5fae5a
         self.assertHasType(self.base.comps, dnf.comps.Comps)
5fae5a
-- 
5fae5a
2.26.2
5fae5a