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

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