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