richardphibel / rpms / dnf

Forked from rpms/dnf 2 years ago
Clone

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

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