chantra / rpms / dnf

Forked from rpms/dnf 2 years ago
Clone

Blame SOURCES/0003-Post-transaction-summary-is-logged-for-API-users-RhBug-1855158.patch

4f4af9
From 9ed390d08a9f2b66f4e352532fa526fc64e329d4 Mon Sep 17 00:00:00 2001
4f4af9
From: Marek Blaha <mblaha@redhat.com>
4f4af9
Date: Tue, 28 Jul 2020 09:50:10 +0200
4f4af9
Subject: [PATCH 1/3] Remove unused loops attribute from
4f4af9
 DepSolveProgressCallBack
4f4af9
4f4af9
---
4f4af9
 dnf/cli/output.py | 5 -----
4f4af9
 1 file changed, 5 deletions(-)
4f4af9
4f4af9
diff --git a/dnf/cli/output.py b/dnf/cli/output.py
4f4af9
index de188ffbd1..44d5f8b89f 100644
4f4af9
--- a/dnf/cli/output.py
4f4af9
+++ b/dnf/cli/output.py
4f4af9
@@ -1987,10 +1987,6 @@ def historyInfoCmdPkgsAltered(self, old, pats=[]):
4f4af9
 class DepSolveProgressCallBack(dnf.callback.Depsolve):
4f4af9
     """Provides text output callback functions for Dependency Solver callback."""
4f4af9
 
4f4af9
-    def __init__(self):
4f4af9
-        """requires yum-cli log and errorlog functions as arguments"""
4f4af9
-        self.loops = 0
4f4af9
-
4f4af9
     def pkg_added(self, pkg, mode):
4f4af9
         """Print information about a package being added to the
4f4af9
         transaction set.
4f4af9
@@ -2037,7 +2033,6 @@ def start(self):
4f4af9
         process.
4f4af9
         """
4f4af9
         logger.debug(_('--> Starting dependency resolution'))
4f4af9
-        self.loops += 1
4f4af9
 
4f4af9
     def end(self):
4f4af9
         """Output a message stating that dependency resolution has finished."""
4f4af9
4f4af9
From 0ee646f4965c597f2832ed3df9d9f0e6546dcc54 Mon Sep 17 00:00:00 2001
4f4af9
From: Marek Blaha <mblaha@redhat.com>
4f4af9
Date: Wed, 21 Oct 2020 11:47:43 +0200
4f4af9
Subject: [PATCH 2/3] Remove unused parameter of _make_lists()
4f4af9
4f4af9
---
4f4af9
 dnf/cli/output.py | 7 ++++---
4f4af9
 1 file changed, 4 insertions(+), 3 deletions(-)
4f4af9
4f4af9
diff --git a/dnf/cli/output.py b/dnf/cli/output.py
4f4af9
index 44d5f8b89f..af8a968770 100644
4f4af9
--- a/dnf/cli/output.py
4f4af9
+++ b/dnf/cli/output.py
4f4af9
@@ -52,7 +52,8 @@
4f4af9
 
4f4af9
 logger = logging.getLogger('dnf')
4f4af9
 
4f4af9
-def _make_lists(transaction, goal):
4f4af9
+
4f4af9
+def _make_lists(transaction):
4f4af9
     b = dnf.util.Bunch({
4f4af9
         'downgraded': [],
4f4af9
         'erased': [],
4f4af9
@@ -1101,7 +1102,7 @@ def list_transaction(self, transaction, total_width=None):
4f4af9
             # in order to display module changes when RPM transaction is empty
4f4af9
             transaction = []
4f4af9
 
4f4af9
-        list_bunch = _make_lists(transaction, self.base._goal)
4f4af9
+        list_bunch = _make_lists(transaction)
4f4af9
         pkglist_lines = []
4f4af9
         data = {'n' : {}, 'v' : {}, 'r' : {}}
4f4af9
         a_wid = 0 # Arch can't get "that big" ... so always use the max.
4f4af9
@@ -1488,7 +1489,7 @@ def _tsi_or_pkg_nevra_cmp(item1, item2):
4f4af9
             return (item1.arch > item2.arch) - (item1.arch < item2.arch)
4f4af9
 
4f4af9
         out = ''
4f4af9
-        list_bunch = _make_lists(transaction, self.base._goal)
4f4af9
+        list_bunch = _make_lists(transaction)
4f4af9
 
4f4af9
         skipped_conflicts, skipped_broken = self._skipped_packages(
4f4af9
             report_problems=False, transaction=transaction)
4f4af9
4f4af9
From 865b7183453684de6a25e77fddf5a2d11fbffba8 Mon Sep 17 00:00:00 2001
4f4af9
From: Marek Blaha <mblaha@redhat.com>
4f4af9
Date: Wed, 21 Oct 2020 17:59:46 +0200
4f4af9
Subject: [PATCH 3/3] Post transaction summary is logged for API users
4f4af9
 (RhBug:1855158)
4f4af9
4f4af9
Post transaction summary is always logged into /var/log/dnf.log.
4f4af9
When transaction is called from cli, the summary is also printed to
4f4af9
stdout in columns (as previously).
4f4af9
4f4af9
= changelog =
4f4af9
msg:           Packages installed/removed via DNF API are logged into dnf.log
4f4af9
type:          enhancement
4f4af9
resolves:      https://bugzilla.redhat.com/show_bug.cgi?id=1855158
4f4af9
---
4f4af9
 dnf/base.py       |  46 ++++++++++++-
4f4af9
 dnf/cli/cli.py    |   8 ++-
4f4af9
 dnf/cli/output.py | 167 ++++++++--------------------------------------
4f4af9
 dnf/util.py       | 102 +++++++++++++++++++++++++++-
4f4af9
 4 files changed, 177 insertions(+), 146 deletions(-)
4f4af9
4f4af9
diff --git a/dnf/base.py b/dnf/base.py
4f4af9
index 075e74265a..c0d7712605 100644
4f4af9
--- a/dnf/base.py
4f4af9
+++ b/dnf/base.py
4f4af9
@@ -28,12 +28,12 @@
4f4af9
 import dnf
4f4af9
 import libdnf.transaction
4f4af9
 
4f4af9
+from copy import deepcopy
4f4af9
 from dnf.comps import CompsQuery
4f4af9
 from dnf.i18n import _, P_, ucd
4f4af9
 from dnf.util import _parse_specs
4f4af9
 from dnf.db.history import SwdbInterface
4f4af9
 from dnf.yum import misc
4f4af9
-from functools import reduce
4f4af9
 try:
4f4af9
     from collections.abc import Sequence
4f4af9
 except ImportError:
4f4af9
@@ -549,7 +549,7 @@ def _ts(self):
4f4af9
         if self.conf.ignorearch:
4f4af9
             self._rpm_probfilter.add(rpm.RPMPROB_FILTER_IGNOREARCH)
4f4af9
 
4f4af9
-        probfilter = reduce(operator.or_, self._rpm_probfilter, 0)
4f4af9
+        probfilter = functools.reduce(operator.or_, self._rpm_probfilter, 0)
4f4af9
         self._priv_ts.setProbFilter(probfilter)
4f4af9
         return self._priv_ts
4f4af9
 
4f4af9
@@ -890,6 +890,15 @@ def do_transaction(self, display=()):
4f4af9
         self._plugins.unload_removed_plugins(self.transaction)
4f4af9
         self._plugins.run_transaction()
4f4af9
 
4f4af9
+        # log post transaction summary
4f4af9
+        def _pto_callback(action, tsis):
4f4af9
+            msgs = []
4f4af9
+            for tsi in tsis:
4f4af9
+                msgs.append('{}: {}'.format(action, str(tsi)))
4f4af9
+            return msgs
4f4af9
+        for msg in dnf.util._post_transaction_output(self, self.transaction, _pto_callback):
4f4af9
+            logger.debug(msg)
4f4af9
+
4f4af9
         return tid
4f4af9
 
4f4af9
     def _trans_error_summary(self, errstring):
4f4af9
@@ -1311,7 +1320,7 @@ def _do_package_lists(self, pkgnarrow='all', patterns=None, showdups=None,
4f4af9
         if patterns is None or len(patterns) == 0:
4f4af9
             return list_fn(None)
4f4af9
         yghs = map(list_fn, patterns)
4f4af9
-        return reduce(lambda a, b: a.merge_lists(b), yghs)
4f4af9
+        return functools.reduce(lambda a, b: a.merge_lists(b), yghs)
4f4af9
 
4f4af9
     def _list_pattern(self, pkgnarrow, pattern, showdups, ignore_case,
4f4af9
                       reponame=None):
4f4af9
@@ -2579,6 +2588,37 @@ def setup_loggers(self):
4f4af9
         """
4f4af9
         self._logging._setup_from_dnf_conf(self.conf, file_loggers_only=True)
4f4af9
 
4f4af9
+    def _skipped_packages(self, report_problems, transaction):
4f4af9
+        """returns set of conflicting packages and set of packages with broken dependency that would
4f4af9
+        be additionally installed when --best and --allowerasing"""
4f4af9
+        if self._goal.actions & (hawkey.INSTALL | hawkey.UPGRADE | hawkey.UPGRADE_ALL):
4f4af9
+            best = True
4f4af9
+        else:
4f4af9
+            best = False
4f4af9
+        ng = deepcopy(self._goal)
4f4af9
+        params = {"allow_uninstall": self._allow_erasing,
4f4af9
+                  "force_best": best,
4f4af9
+                  "ignore_weak": True}
4f4af9
+        ret = ng.run(**params)
4f4af9
+        if not ret and report_problems:
4f4af9
+            msg = dnf.util._format_resolve_problems(ng.problem_rules())
4f4af9
+            logger.warning(msg)
4f4af9
+        problem_conflicts = set(ng.problem_conflicts(available=True))
4f4af9
+        problem_dependency = set(ng.problem_broken_dependency(available=True)) - problem_conflicts
4f4af9
+
4f4af9
+        def _nevra(item):
4f4af9
+            return hawkey.NEVRA(name=item.name, epoch=item.epoch, version=item.version,
4f4af9
+                                release=item.release, arch=item.arch)
4f4af9
+
4f4af9
+        # Sometimes, pkg is not in transaction item, therefore, comparing by nevra
4f4af9
+        transaction_nevras = [_nevra(tsi) for tsi in transaction]
4f4af9
+        skipped_conflicts = set(
4f4af9
+            [pkg for pkg in problem_conflicts if _nevra(pkg) not in transaction_nevras])
4f4af9
+        skipped_dependency = set(
4f4af9
+            [pkg for pkg in problem_dependency if _nevra(pkg) not in transaction_nevras])
4f4af9
+
4f4af9
+        return skipped_conflicts, skipped_dependency
4f4af9
+
4f4af9
 
4f4af9
 def _msg_installed(pkg):
4f4af9
     name = ucd(pkg)
4f4af9
diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py
4f4af9
index 0bc2c119d0..334000362c 100644
4f4af9
--- a/dnf/cli/cli.py
4f4af9
+++ b/dnf/cli/cli.py
4f4af9
@@ -252,8 +252,12 @@ def do_transaction(self, display=()):
4f4af9
             trans = None
4f4af9
 
4f4af9
         if trans:
4f4af9
-            msg = self.output.post_transaction_output(trans)
4f4af9
-            logger.info(msg)
4f4af9
+            # the post transaction summary is already written to log during
4f4af9
+            # Base.do_transaction() so here only print the messages to the
4f4af9
+            # user arranged in columns
4f4af9
+            print()
4f4af9
+            print('\n'.join(self.output.post_transaction_output(trans)))
4f4af9
+            print()
4f4af9
             for tsi in trans:
4f4af9
                 if tsi.state == libdnf.transaction.TransactionItemState_ERROR:
4f4af9
                     raise dnf.exceptions.Error(_('Transaction failed'))
4f4af9
diff --git a/dnf/cli/output.py b/dnf/cli/output.py
4f4af9
index af8a968770..6d729b63ba 100644
4f4af9
--- a/dnf/cli/output.py
4f4af9
+++ b/dnf/cli/output.py
4f4af9
@@ -21,9 +21,7 @@
4f4af9
 from __future__ import print_function
4f4af9
 from __future__ import unicode_literals
4f4af9
 
4f4af9
-from copy import deepcopy
4f4af9
 import fnmatch
4f4af9
-import functools
4f4af9
 import hawkey
4f4af9
 import itertools
4f4af9
 import libdnf.transaction
4f4af9
@@ -53,51 +51,6 @@
4f4af9
 logger = logging.getLogger('dnf')
4f4af9
 
4f4af9
 
4f4af9
-def _make_lists(transaction):
4f4af9
-    b = dnf.util.Bunch({
4f4af9
-        'downgraded': [],
4f4af9
-        'erased': [],
4f4af9
-        'erased_clean': [],
4f4af9
-        'erased_dep': [],
4f4af9
-        'installed': [],
4f4af9
-        'installed_group': [],
4f4af9
-        'installed_dep': [],
4f4af9
-        'installed_weak': [],
4f4af9
-        'reinstalled': [],
4f4af9
-        'upgraded': [],
4f4af9
-        'failed': [],
4f4af9
-    })
4f4af9
-
4f4af9
-    for tsi in transaction:
4f4af9
-        if tsi.state == libdnf.transaction.TransactionItemState_ERROR:
4f4af9
-            b.failed.append(tsi)
4f4af9
-        elif tsi.action == libdnf.transaction.TransactionItemAction_DOWNGRADE:
4f4af9
-            b.downgraded.append(tsi)
4f4af9
-        elif tsi.action == libdnf.transaction.TransactionItemAction_INSTALL:
4f4af9
-            if tsi.reason == libdnf.transaction.TransactionItemReason_GROUP:
4f4af9
-                b.installed_group.append(tsi)
4f4af9
-            elif tsi.reason == libdnf.transaction.TransactionItemReason_DEPENDENCY:
4f4af9
-                b.installed_dep.append(tsi)
4f4af9
-            elif tsi.reason == libdnf.transaction.TransactionItemReason_WEAK_DEPENDENCY:
4f4af9
-                b.installed_weak.append(tsi)
4f4af9
-            else:
4f4af9
-                # TransactionItemReason_USER
4f4af9
-                b.installed.append(tsi)
4f4af9
-        elif tsi.action == libdnf.transaction.TransactionItemAction_REINSTALL:
4f4af9
-            b.reinstalled.append(tsi)
4f4af9
-        elif tsi.action == libdnf.transaction.TransactionItemAction_REMOVE:
4f4af9
-            if tsi.reason == libdnf.transaction.TransactionItemReason_CLEAN:
4f4af9
-                b.erased_clean.append(tsi)
4f4af9
-            elif tsi.reason == libdnf.transaction.TransactionItemReason_DEPENDENCY:
4f4af9
-                b.erased_dep.append(tsi)
4f4af9
-            else:
4f4af9
-                b.erased.append(tsi)
4f4af9
-        elif tsi.action == libdnf.transaction.TransactionItemAction_UPGRADE:
4f4af9
-            b.upgraded.append(tsi)
4f4af9
-
4f4af9
-    return b
4f4af9
-
4f4af9
-
4f4af9
 def _spread_in_columns(cols_count, label, lst):
4f4af9
     left = itertools.chain((label,), itertools.repeat(''))
4f4af9
     lst_length = len(lst)
4f4af9
@@ -1057,37 +1010,6 @@ def list_group_transaction(self, comps, history, diff):
4f4af9
             out[0:0] = self._banner(col_data, (_('Group'), _('Packages'), '', ''))
4f4af9
         return '\n'.join(out)
4f4af9
 
4f4af9
-    def _skipped_packages(self, report_problems, transaction):
4f4af9
-        """returns set of conflicting packages and set of packages with broken dependency that would
4f4af9
-        be additionally installed when --best and --allowerasing"""
4f4af9
-        if self.base._goal.actions & (hawkey.INSTALL | hawkey.UPGRADE | hawkey.UPGRADE_ALL):
4f4af9
-            best = True
4f4af9
-        else:
4f4af9
-            best = False
4f4af9
-        ng = deepcopy(self.base._goal)
4f4af9
-        params = {"allow_uninstall": self.base._allow_erasing,
4f4af9
-                  "force_best": best,
4f4af9
-                  "ignore_weak": True}
4f4af9
-        ret = ng.run(**params)
4f4af9
-        if not ret and report_problems:
4f4af9
-            msg = dnf.util._format_resolve_problems(ng.problem_rules())
4f4af9
-            logger.warning(msg)
4f4af9
-        problem_conflicts = set(ng.problem_conflicts(available=True))
4f4af9
-        problem_dependency = set(ng.problem_broken_dependency(available=True)) - problem_conflicts
4f4af9
-
4f4af9
-        def _nevra(item):
4f4af9
-            return hawkey.NEVRA(name=item.name, epoch=item.epoch, version=item.version,
4f4af9
-                                release=item.release, arch=item.arch)
4f4af9
-
4f4af9
-        # Sometimes, pkg is not in transaction item, therefore, comparing by nevra
4f4af9
-        transaction_nevras = [_nevra(tsi) for tsi in transaction]
4f4af9
-        skipped_conflicts = set(
4f4af9
-            [pkg for pkg in problem_conflicts if _nevra(pkg) not in transaction_nevras])
4f4af9
-        skipped_dependency = set(
4f4af9
-            [pkg for pkg in problem_dependency if _nevra(pkg) not in transaction_nevras])
4f4af9
-
4f4af9
-        return skipped_conflicts, skipped_dependency
4f4af9
-
4f4af9
     def list_transaction(self, transaction, total_width=None):
4f4af9
         """Return a string representation of the transaction in an
4f4af9
         easy-to-read format.
4f4af9
@@ -1102,7 +1024,7 @@ def list_transaction(self, transaction, total_width=None):
4f4af9
             # in order to display module changes when RPM transaction is empty
4f4af9
             transaction = []
4f4af9
 
4f4af9
-        list_bunch = _make_lists(transaction)
4f4af9
+        list_bunch = dnf.util._make_lists(transaction)
4f4af9
         pkglist_lines = []
4f4af9
         data = {'n' : {}, 'v' : {}, 'r' : {}}
4f4af9
         a_wid = 0 # Arch can't get "that big" ... so always use the max.
4f4af9
@@ -1271,7 +1193,7 @@ def format_line(group):
4f4af9
         # show skipped conflicting packages
4f4af9
         if not self.conf.best and self.base._goal.actions & forward_actions:
4f4af9
             lines = []
4f4af9
-            skipped_conflicts, skipped_broken = self._skipped_packages(
4f4af9
+            skipped_conflicts, skipped_broken = self.base._skipped_packages(
4f4af9
                 report_problems=True, transaction=transaction)
4f4af9
             skipped_broken = dict((str(pkg), pkg) for pkg in skipped_broken)
4f4af9
             for pkg in sorted(skipped_conflicts):
4f4af9
@@ -1436,13 +1358,8 @@ def format_line(group):
4f4af9
                                   max_msg_count, count, msg_pkgs))
4f4af9
         return ''.join(out)
4f4af9
 
4f4af9
-    def post_transaction_output(self, transaction):
4f4af9
-        """Returns a human-readable summary of the results of the
4f4af9
-        transaction.
4f4af9
 
4f4af9
-        :return: a string containing a human-readable summary of the
4f4af9
-           results of the transaction
4f4af9
-        """
4f4af9
+    def _pto_callback(self, action, tsis):
4f4af9
         #  Works a bit like calcColumns, but we never overflow a column we just
4f4af9
         # have a dynamic number of columns.
4f4af9
         def _fits_in_cols(msgs, num):
4f4af9
@@ -1472,61 +1389,33 @@ def _fits_in_cols(msgs, num):
4f4af9
                 col_lens[col] *= -1
4f4af9
             return col_lens
4f4af9
 
4f4af9
-        def _tsi_or_pkg_nevra_cmp(item1, item2):
4f4af9
-            """Compares two transaction items or packages by nevra.
4f4af9
-               Used as a fallback when tsi does not contain package object.
4f4af9
-            """
4f4af9
-            ret = (item1.name > item2.name) - (item1.name < item2.name)
4f4af9
-            if ret != 0:
4f4af9
-                return ret
4f4af9
-            nevra1 = hawkey.NEVRA(name=item1.name, epoch=item1.epoch, version=item1.version,
4f4af9
-                                  release=item1.release, arch=item1.arch)
4f4af9
-            nevra2 = hawkey.NEVRA(name=item2.name, epoch=item2.epoch, version=item2.version,
4f4af9
-                                  release=item2.release, arch=item2.arch)
4f4af9
-            ret = nevra1.evr_cmp(nevra2, self.sack)
4f4af9
-            if ret != 0:
4f4af9
-                return ret
4f4af9
-            return (item1.arch > item2.arch) - (item1.arch < item2.arch)
4f4af9
-
4f4af9
-        out = ''
4f4af9
-        list_bunch = _make_lists(transaction)
4f4af9
-
4f4af9
-        skipped_conflicts, skipped_broken = self._skipped_packages(
4f4af9
-            report_problems=False, transaction=transaction)
4f4af9
-        skipped = skipped_conflicts.union(skipped_broken)
4f4af9
-
4f4af9
-        for (action, tsis) in [(_('Upgraded'), list_bunch.upgraded),
4f4af9
-                               (_('Downgraded'), list_bunch.downgraded),
4f4af9
-                               (_('Installed'), list_bunch.installed +
4f4af9
-                                list_bunch.installed_group +
4f4af9
-                                list_bunch.installed_weak +
4f4af9
-                                list_bunch.installed_dep),
4f4af9
-                               (_('Reinstalled'), list_bunch.reinstalled),
4f4af9
-                               (_('Skipped'), skipped),
4f4af9
-                               (_('Removed'), list_bunch.erased +
4f4af9
-                                   list_bunch.erased_dep +
4f4af9
-                                   list_bunch.erased_clean),
4f4af9
-                               (_('Failed'), list_bunch.failed)]:
4f4af9
-            if not tsis:
4f4af9
-                continue
4f4af9
-            msgs = []
4f4af9
-            out += '\n%s:\n' % action
4f4af9
-            for tsi in sorted(tsis, key=functools.cmp_to_key(_tsi_or_pkg_nevra_cmp)):
4f4af9
-                msgs.append(str(tsi))
4f4af9
-            for num in (8, 7, 6, 5, 4, 3, 2):
4f4af9
-                cols = _fits_in_cols(msgs, num)
4f4af9
-                if cols:
4f4af9
-                    break
4f4af9
-            if not cols:
4f4af9
-                cols = [-(self.term.columns - 2)]
4f4af9
-            while msgs:
4f4af9
-                current_msgs = msgs[:len(cols)]
4f4af9
-                out += '  '
4f4af9
-                out += self.fmtColumns(zip(current_msgs, cols), end=u'\n')
4f4af9
-                msgs = msgs[len(cols):]
4f4af9
-
4f4af9
+        if not tsis:
4f4af9
+            return ''
4f4af9
+        out = []
4f4af9
+        msgs = []
4f4af9
+        out.append('{}:'.format(action))
4f4af9
+        for tsi in tsis:
4f4af9
+            msgs.append(str(tsi))
4f4af9
+        for num in (8, 7, 6, 5, 4, 3, 2):
4f4af9
+            cols = _fits_in_cols(msgs, num)
4f4af9
+            if cols:
4f4af9
+                break
4f4af9
+        if not cols:
4f4af9
+            cols = [-(self.term.columns - 2)]
4f4af9
+        while msgs:
4f4af9
+            current_msgs = msgs[:len(cols)]
4f4af9
+            out.append('  {}'.format(self.fmtColumns(zip(current_msgs, cols))))
4f4af9
+            msgs = msgs[len(cols):]
4f4af9
         return out
4f4af9
 
4f4af9
+
4f4af9
+    def post_transaction_output(self, transaction):
4f4af9
+        """
4f4af9
+        Return a human-readable summary of the transaction. Packages in sections
4f4af9
+        are arranged to columns.
4f4af9
+        """
4f4af9
+        return dnf.util._post_transaction_output(self.base, transaction, self._pto_callback)
4f4af9
+
4f4af9
     def setup_progress_callbacks(self):
4f4af9
         """Set up the progress callbacks and various
4f4af9
            output bars based on debug level.
4f4af9
diff --git a/dnf/util.py b/dnf/util.py
4f4af9
index 8cf362706d..0beb04424d 100644
4f4af9
--- a/dnf/util.py
4f4af9
+++ b/dnf/util.py
4f4af9
@@ -24,13 +24,14 @@
4f4af9
 
4f4af9
 from .pycomp import PY3, basestring
4f4af9
 from dnf.i18n import _, ucd
4f4af9
-from functools import reduce
4f4af9
 import argparse
4f4af9
 import dnf
4f4af9
 import dnf.callback
4f4af9
 import dnf.const
4f4af9
 import dnf.pycomp
4f4af9
 import errno
4f4af9
+import functools
4f4af9
+import hawkey
4f4af9
 import itertools
4f4af9
 import locale
4f4af9
 import logging
4f4af9
@@ -41,6 +42,7 @@
4f4af9
 import tempfile
4f4af9
 import time
4f4af9
 import libdnf.repo
4f4af9
+import libdnf.transaction
4f4af9
 
4f4af9
 logger = logging.getLogger('dnf')
4f4af9
 
4f4af9
@@ -195,7 +197,7 @@ def group_by_filter(fn, iterable):
4f4af9
     def splitter(acc, item):
4f4af9
         acc[not bool(fn(item))].append(item)
4f4af9
         return acc
4f4af9
-    return reduce(splitter, iterable, ([], []))
4f4af9
+    return functools.reduce(splitter, iterable, ([], []))
4f4af9
 
4f4af9
 def insert_if(item, iterable, condition):
4f4af9
     """Insert an item into an iterable by a condition."""
4f4af9
@@ -504,3 +506,99 @@ def __setattr__(self, what, val):
4f4af9
         def setter(item):
4f4af9
             setattr(item, what, val)
4f4af9
         return list(map(setter, self))
4f4af9
+
4f4af9
+
4f4af9
+def _make_lists(transaction):
4f4af9
+    b = Bunch({
4f4af9
+        'downgraded': [],
4f4af9
+        'erased': [],
4f4af9
+        'erased_clean': [],
4f4af9
+        'erased_dep': [],
4f4af9
+        'installed': [],
4f4af9
+        'installed_group': [],
4f4af9
+        'installed_dep': [],
4f4af9
+        'installed_weak': [],
4f4af9
+        'reinstalled': [],
4f4af9
+        'upgraded': [],
4f4af9
+        'failed': [],
4f4af9
+    })
4f4af9
+
4f4af9
+    for tsi in transaction:
4f4af9
+        if tsi.state == libdnf.transaction.TransactionItemState_ERROR:
4f4af9
+            b.failed.append(tsi)
4f4af9
+        elif tsi.action == libdnf.transaction.TransactionItemAction_DOWNGRADE:
4f4af9
+            b.downgraded.append(tsi)
4f4af9
+        elif tsi.action == libdnf.transaction.TransactionItemAction_INSTALL:
4f4af9
+            if tsi.reason == libdnf.transaction.TransactionItemReason_GROUP:
4f4af9
+                b.installed_group.append(tsi)
4f4af9
+            elif tsi.reason == libdnf.transaction.TransactionItemReason_DEPENDENCY:
4f4af9
+                b.installed_dep.append(tsi)
4f4af9
+            elif tsi.reason == libdnf.transaction.TransactionItemReason_WEAK_DEPENDENCY:
4f4af9
+                b.installed_weak.append(tsi)
4f4af9
+            else:
4f4af9
+                # TransactionItemReason_USER
4f4af9
+                b.installed.append(tsi)
4f4af9
+        elif tsi.action == libdnf.transaction.TransactionItemAction_REINSTALL:
4f4af9
+            b.reinstalled.append(tsi)
4f4af9
+        elif tsi.action == libdnf.transaction.TransactionItemAction_REMOVE:
4f4af9
+            if tsi.reason == libdnf.transaction.TransactionItemReason_CLEAN:
4f4af9
+                b.erased_clean.append(tsi)
4f4af9
+            elif tsi.reason == libdnf.transaction.TransactionItemReason_DEPENDENCY:
4f4af9
+                b.erased_dep.append(tsi)
4f4af9
+            else:
4f4af9
+                b.erased.append(tsi)
4f4af9
+        elif tsi.action == libdnf.transaction.TransactionItemAction_UPGRADE:
4f4af9
+            b.upgraded.append(tsi)
4f4af9
+
4f4af9
+    return b
4f4af9
+
4f4af9
+
4f4af9
+def _post_transaction_output(base, transaction, action_callback):
4f4af9
+    """Returns a human-readable summary of the results of the
4f4af9
+    transaction.
4f4af9
+
4f4af9
+    :param action_callback: function generating output for specific action. It
4f4af9
+       takes two parameters - action as a string and list of affected packages for
4f4af9
+       this action
4f4af9
+    :return: a list of lines containing a human-readable summary of the
4f4af9
+       results of the transaction
4f4af9
+    """
4f4af9
+    def _tsi_or_pkg_nevra_cmp(item1, item2):
4f4af9
+        """Compares two transaction items or packages by nevra.
4f4af9
+           Used as a fallback when tsi does not contain package object.
4f4af9
+        """
4f4af9
+        ret = (item1.name > item2.name) - (item1.name < item2.name)
4f4af9
+        if ret != 0:
4f4af9
+            return ret
4f4af9
+        nevra1 = hawkey.NEVRA(name=item1.name, epoch=item1.epoch, version=item1.version,
4f4af9
+                              release=item1.release, arch=item1.arch)
4f4af9
+        nevra2 = hawkey.NEVRA(name=item2.name, epoch=item2.epoch, version=item2.version,
4f4af9
+                              release=item2.release, arch=item2.arch)
4f4af9
+        ret = nevra1.evr_cmp(nevra2, base.sack)
4f4af9
+        if ret != 0:
4f4af9
+            return ret
4f4af9
+        return (item1.arch > item2.arch) - (item1.arch < item2.arch)
4f4af9
+
4f4af9
+    list_bunch = dnf.util._make_lists(transaction)
4f4af9
+
4f4af9
+    skipped_conflicts, skipped_broken = base._skipped_packages(
4f4af9
+        report_problems=False, transaction=transaction)
4f4af9
+    skipped = skipped_conflicts.union(skipped_broken)
4f4af9
+
4f4af9
+    out = []
4f4af9
+    for (action, tsis) in [(_('Upgraded'), list_bunch.upgraded),
4f4af9
+                           (_('Downgraded'), list_bunch.downgraded),
4f4af9
+                           (_('Installed'), list_bunch.installed +
4f4af9
+                            list_bunch.installed_group +
4f4af9
+                            list_bunch.installed_weak +
4f4af9
+                            list_bunch.installed_dep),
4f4af9
+                           (_('Reinstalled'), list_bunch.reinstalled),
4f4af9
+                           (_('Skipped'), skipped),
4f4af9
+                           (_('Removed'), list_bunch.erased +
4f4af9
+                               list_bunch.erased_dep +
4f4af9
+                               list_bunch.erased_clean),
4f4af9
+                           (_('Failed'), list_bunch.failed)]:
4f4af9
+        out.extend(action_callback(
4f4af9
+            action, sorted(tsis, key=functools.cmp_to_key(_tsi_or_pkg_nevra_cmp))))
4f4af9
+
4f4af9
+    return out