diff --git a/.dnf-plugins-core.metadata b/.dnf-plugins-core.metadata
index fc3009b..40ecba9 100644
--- a/.dnf-plugins-core.metadata
+++ b/.dnf-plugins-core.metadata
@@ -1 +1 @@
-3b8638dec2cb91a13241106b9a57114ed037d2ca SOURCES/dnf-plugins-core-4.0.18.tar.gz
+40f26a50a6605eacb1e9c4a443f01655fa461767 SOURCES/dnf-plugins-core-4.0.21.tar.gz
diff --git a/.gitignore b/.gitignore
index dae8243..ae4a8af 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1 @@
-SOURCES/dnf-plugins-core-4.0.18.tar.gz
+SOURCES/dnf-plugins-core-4.0.21.tar.gz
diff --git a/SOURCES/0001-groups-manager-Re-introduce-yum-groups-manager-funct.patch b/SOURCES/0001-groups-manager-Re-introduce-yum-groups-manager-funct.patch
deleted file mode 100644
index ddebf40..0000000
--- a/SOURCES/0001-groups-manager-Re-introduce-yum-groups-manager-funct.patch
+++ /dev/null
@@ -1,653 +0,0 @@
-From 40f08d7a22907e6292c314462c01de94584c0854 Mon Sep 17 00:00:00 2001
-From: Marek Blaha <mblaha@redhat.com>
-Date: Tue, 27 Oct 2020 15:46:03 +0100
-Subject: [PATCH 1/2] [groups-manager] Re-introduce yum-groups-manager
- functionality (RhBug:1826016)
-
-Implements 'dnf groups-manager' command with features:
-- read, merge, print and write groups metadata files
-- edit group attributes name (with translated variants),
-  description (with translated variants), uservisible, displayorder
-- add packgages to group
-- remove packages from group
-
-= changelog =
-msg:           Re-introduce yum-groups-manager functionality
-type:          enhancement
-resolves:      https://bugzilla.redhat.com/show_bug.cgi?id=1826016
----
- dnf-plugins-core.spec     |  22 ++-
- doc/CMakeLists.txt        |   2 +
- doc/conf.py               |   2 +
- doc/groups-manager.rst    |  94 ++++++++++++
- doc/index.rst             |   1 +
- libexec/dnf-utils.in      |   1 +
- plugins/CMakeLists.txt    |   1 +
- plugins/groups_manager.py | 314 ++++++++++++++++++++++++++++++++++++++
- 8 files changed, 432 insertions(+), 5 deletions(-)
- create mode 100644 doc/groups-manager.rst
- create mode 100644 plugins/groups_manager.py
-
-diff --git a/dnf-plugins-core.spec b/dnf-plugins-core.spec
-index d13a996..42d0884 100644
---- a/dnf-plugins-core.spec
-+++ b/dnf-plugins-core.spec
-@@ -58,6 +58,7 @@ Provides:       dnf-command(debug-dump)
- Provides:       dnf-command(debug-restore)
- Provides:       dnf-command(debuginfo-install)
- Provides:       dnf-command(download)
-+Provides:       dnf-command(groups-manager)
- Provides:       dnf-command(repoclosure)
- Provides:       dnf-command(repograph)
- Provides:       dnf-command(repomanage)
-@@ -73,6 +74,7 @@ Provides:       dnf-plugin-debuginfo-install = %{version}-%{release}
- Provides:       dnf-plugin-download = %{version}-%{release}
- Provides:       dnf-plugin-generate_completion_cache = %{version}-%{release}
- Provides:       dnf-plugin-needs_restarting = %{version}-%{release}
-+Provides:       dnf-plugin-groups-manager = %{version}-%{release}
- Provides:       dnf-plugin-repoclosure = %{version}-%{release}
- Provides:       dnf-plugin-repodiff = %{version}-%{release}
- Provides:       dnf-plugin-repograph = %{version}-%{release}
-@@ -87,7 +89,7 @@ Conflicts:      dnf-plugins-extras-common-data < %{dnf_plugins_extra}
- 
- %description
- Core Plugins for DNF. This package enhances DNF with builddep, config-manager,
--copr, debug, debuginfo-install, download, needs-restarting, repoclosure,
-+copr, debug, debuginfo-install, download, needs-restarting, groups-manager, repoclosure,
- repograph, repomanage, reposync, changelog and repodiff commands. Additionally
- provides generate_completion_cache passive plugin.
- 
-@@ -129,7 +131,8 @@ Conflicts:      python-%{name} < %{version}-%{release}
- %description -n python2-%{name}
- Core Plugins for DNF, Python 2 interface. This package enhances DNF with builddep,
- config-manager, copr, degug, debuginfo-install, download, needs-restarting,
--repoclosure, repograph, repomanage, reposync, changelog and repodiff commands.
-+groups-manager, repoclosure, repograph, repomanage, reposync, changelog
-+and repodiff commands.
- Additionally provides generate_completion_cache passive plugin.
- %endif
- 
-@@ -163,7 +166,8 @@ Conflicts:      python-%{name} < %{version}-%{release}
- %description -n python3-%{name}
- Core Plugins for DNF, Python 3 interface. This package enhances DNF with builddep,
- config-manager, copr, debug, debuginfo-install, download, needs-restarting,
--repoclosure, repograph, repomanage, reposync, changelog and repodiff commands.
-+groups-manager, repoclosure, repograph, repomanage, reposync, changelog
-+and repodiff commands.
- Additionally provides generate_completion_cache passive plugin.
- %endif
- 
-@@ -190,8 +194,8 @@ Summary:        Yum-utils CLI compatibility layer
- %description -n %{yum_utils_subpackage_name}
- As a Yum-utils CLI compatibility layer, supplies in CLI shims for
- debuginfo-install, repograph, package-cleanup, repoclosure, repomanage,
--repoquery, reposync, repotrack, repodiff, builddep, config-manager, debug
--and download that use new implementations using DNF.
-+repoquery, reposync, repotrack, repodiff, builddep, config-manager, debug,
-+download and yum-groups-manager that use new implementations using DNF.
- %endif
- 
- %if 0%{?rhel} == 0 && %{with python2}
-@@ -458,6 +462,7 @@ ln -sf %{_libexecdir}/dnf-utils %{buildroot}%{_bindir}/yum-builddep
- ln -sf %{_libexecdir}/dnf-utils %{buildroot}%{_bindir}/yum-config-manager
- ln -sf %{_libexecdir}/dnf-utils %{buildroot}%{_bindir}/yum-debug-dump
- ln -sf %{_libexecdir}/dnf-utils %{buildroot}%{_bindir}/yum-debug-restore
-+ln -sf %{_libexecdir}/dnf-utils %{buildroot}%{_bindir}/yum-groups-manager
- ln -sf %{_libexecdir}/dnf-utils %{buildroot}%{_bindir}/yumdownloader
- # These commands don't have a dedicated man page, so let's just point them
- # to the utils page which contains their descriptions.
-@@ -483,6 +488,7 @@ PYTHONPATH=./plugins nosetests-%{python3_version} -s tests/
- %{_mandir}/man8/dnf-debuginfo-install.*
- %{_mandir}/man8/dnf-download.*
- %{_mandir}/man8/dnf-generate_completion_cache.*
-+%{_mandir}/man8/dnf-groups-manager.*
- %{_mandir}/man8/dnf-needs-restarting.*
- %{_mandir}/man8/dnf-repoclosure.*
- %{_mandir}/man8/dnf-repodiff.*
-@@ -513,6 +519,7 @@ PYTHONPATH=./plugins nosetests-%{python3_version} -s tests/
- %{python2_sitelib}/dnf-plugins/debuginfo-install.*
- %{python2_sitelib}/dnf-plugins/download.*
- %{python2_sitelib}/dnf-plugins/generate_completion_cache.*
-+%{python2_sitelib}/dnf-plugins/groups_manager.*
- %{python2_sitelib}/dnf-plugins/needs_restarting.*
- %{python2_sitelib}/dnf-plugins/repoclosure.*
- %{python2_sitelib}/dnf-plugins/repodiff.*
-@@ -538,6 +545,7 @@ PYTHONPATH=./plugins nosetests-%{python3_version} -s tests/
- %{python3_sitelib}/dnf-plugins/debuginfo-install.py
- %{python3_sitelib}/dnf-plugins/download.py
- %{python3_sitelib}/dnf-plugins/generate_completion_cache.py
-+%{python3_sitelib}/dnf-plugins/groups_manager.py
- %{python3_sitelib}/dnf-plugins/needs_restarting.py
- %{python3_sitelib}/dnf-plugins/repoclosure.py
- %{python3_sitelib}/dnf-plugins/repodiff.py
-@@ -552,6 +560,7 @@ PYTHONPATH=./plugins nosetests-%{python3_version} -s tests/
- %{python3_sitelib}/dnf-plugins/__pycache__/debuginfo-install.*
- %{python3_sitelib}/dnf-plugins/__pycache__/download.*
- %{python3_sitelib}/dnf-plugins/__pycache__/generate_completion_cache.*
-+%{python3_sitelib}/dnf-plugins/__pycache__/groups_manager.*
- %{python3_sitelib}/dnf-plugins/__pycache__/needs_restarting.*
- %{python3_sitelib}/dnf-plugins/__pycache__/repoclosure.*
- %{python3_sitelib}/dnf-plugins/__pycache__/repodiff.*
-@@ -579,6 +588,7 @@ PYTHONPATH=./plugins nosetests-%{python3_version} -s tests/
- %{_bindir}/yum-config-manager
- %{_bindir}/yum-debug-dump
- %{_bindir}/yum-debug-restore
-+%{_bindir}/yum-groups-manager
- %{_bindir}/yumdownloader
- %{_mandir}/man1/debuginfo-install.*
- %{_mandir}/man1/needs-restarting.*
-@@ -591,6 +601,7 @@ PYTHONPATH=./plugins nosetests-%{python3_version} -s tests/
- %{_mandir}/man1/yum-config-manager.*
- %{_mandir}/man1/yum-debug-dump.*
- %{_mandir}/man1/yum-debug-restore.*
-+%{_mandir}/man1/yum-groups-manager.*
- %{_mandir}/man1/yumdownloader.*
- %{_mandir}/man1/package-cleanup.*
- %{_mandir}/man1/dnf-utils.*
-@@ -612,6 +623,7 @@ PYTHONPATH=./plugins nosetests-%{python3_version} -s tests/
- %exclude %{_mandir}/man1/yum-config-manager.*
- %exclude %{_mandir}/man1/yum-debug-dump.*
- %exclude %{_mandir}/man1/yum-debug-restore.*
-+%exclude %{_mandir}/man1/yum-groups-manager.*
- %exclude %{_mandir}/man1/yumdownloader.*
- %exclude %{_mandir}/man1/package-cleanup.*
- %exclude %{_mandir}/man1/dnf-utils.*
-diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt
-index dd97eb2..3fb665d 100644
---- a/doc/CMakeLists.txt
-+++ b/doc/CMakeLists.txt
-@@ -26,6 +26,7 @@ INSTALL(FILES ${CMAKE_CURRENT_BINARY_DIR}/dnf-builddep.8
-     ${CMAKE_CURRENT_BINARY_DIR}/dnf-debuginfo-install.8
-     ${CMAKE_CURRENT_BINARY_DIR}/dnf-download.8
-     ${CMAKE_CURRENT_BINARY_DIR}/dnf-generate_completion_cache.8
-+    ${CMAKE_CURRENT_BINARY_DIR}/dnf-groups-manager.8
-     ${CMAKE_CURRENT_BINARY_DIR}/dnf-leaves.8
-     ${CMAKE_CURRENT_BINARY_DIR}/dnf-needs-restarting.8
-     ${CMAKE_CURRENT_BINARY_DIR}/dnf-repoclosure.8
-@@ -61,6 +62,7 @@ INSTALL(FILES ${CMAKE_CURRENT_BINARY_DIR}/debuginfo-install.1
-     ${CMAKE_CURRENT_BINARY_DIR}/yum-config-manager.1
-     ${CMAKE_CURRENT_BINARY_DIR}/yum-debug-dump.1
-     ${CMAKE_CURRENT_BINARY_DIR}/yum-debug-restore.1
-+    ${CMAKE_CURRENT_BINARY_DIR}/yum-groups-manager.1
-     ${CMAKE_CURRENT_BINARY_DIR}/yumdownloader.1
-     ${CMAKE_CURRENT_BINARY_DIR}/package-cleanup.1
-     ${CMAKE_CURRENT_BINARY_DIR}/dnf-utils.1
-diff --git a/doc/conf.py b/doc/conf.py
-index d760ef3..645185a 100644
---- a/doc/conf.py
-+++ b/doc/conf.py
-@@ -251,6 +251,7 @@ man_pages = [
-     ('download', 'dnf-download', u'DNF download Plugin', AUTHORS, 8),
-     ('generate_completion_cache', 'dnf-generate_completion_cache',
-         u'DNF generate_completion_cache Plugin', AUTHORS, 8),
-+    ('groups-manager', 'dnf-groups-manager', u'DNF groups-manager Plugin', AUTHORS, 8),
-     ('leaves', 'dnf-leaves', u'DNF leaves Plugin', AUTHORS, 8),
-     ('local', 'dnf-local', u'DNF local Plugin', AUTHORS, 8),
-     ('needs_restarting', 'dnf-needs-restarting', u'DNF needs_restarting Plugin', AUTHORS, 8),
-@@ -268,6 +269,7 @@ man_pages = [
-     ('copr', 'yum-copr', u'redirecting to DNF copr Plugin', AUTHORS, 8),
-     ('debuginfo-install', 'debuginfo-install', u'redirecting to DNF debuginfo-install Plugin',
-      AUTHORS, 1),
-+    ('groups-manager', 'yum-groups-manager', u'redirecting to DNF groups-manager Plugin', AUTHORS, 1),
-     ('needs_restarting', 'needs-restarting', u'redirecting to DNF needs-restarting Plugin',
-      AUTHORS, 1),
-     ('repoclosure', 'repoclosure', u'redirecting to DNF repoclosure Plugin', AUTHORS, 1),
-diff --git a/doc/groups-manager.rst b/doc/groups-manager.rst
-new file mode 100644
-index 0000000..f8f76a1
---- /dev/null
-+++ b/doc/groups-manager.rst
-@@ -0,0 +1,94 @@
-+..
-+  Copyright (C) 2020  Red Hat, Inc.
-+
-+  This copyrighted material is made available to anyone wishing to use,
-+  modify, copy, or redistribute it subject to the terms and conditions of
-+  the GNU General Public License v.2, or (at your option) any later version.
-+  This program is distributed in the hope that it will be useful, but WITHOUT
-+  ANY WARRANTY expressed or implied, including the implied warranties of
-+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
-+  Public License for more details.  You should have received a copy of the
-+  GNU General Public License along with this program; if not, write to the
-+  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
-+  02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
-+  source code or documentation are not subject to the GNU General Public
-+  License and may only be used or replicated with the express permission of
-+  Red Hat, Inc.
-+
-+=========================
-+DNF groups-manager Plugin
-+=========================
-+
-+Create and edit groups repository metadata files.
-+
-+--------
-+Synopsis
-+--------
-+
-+``dnf groups-manager [options] [package-name-spec [package-name-spec ...]]``
-+
-+-----------
-+Description
-+-----------
-+groups-manager plugin is used to create or edit a group metadata file for a repository. This is often much easier than writing/editing the XML by hand. The groups-manager can load an entire file of groups metadata and either create a new group or edit an existing group and then write all of the groups metadata back out.
-+
-+---------
-+Arguments
-+---------
-+
-+``<package-name-spec>``
-+    Package to add to a group or remove from a group.
-+
-+-------
-+Options
-+-------
-+
-+All general DNF options are accepted, see `Options` in :manpage:`dnf(8)` for details.
-+
-+``--load=<path_to_comps.xml>``
-+    Load the groups metadata information from the specified file before performing any operations. Metadata from all files are merged together if the option is specified multiple times.
-+
-+``--save=<path_to_comps.xml>``
-+    Save the result to this file. You can specify the name of a file you are loading from as the data will only be saved when all the operations have been performed. This option can also be specified multiple times.
-+
-+``--merge=<path_to_comps.xml>``
-+    This is the same as loading and saving a file, however the "merge" file is loaded before any others and saved last.
-+
-+``--print``
-+    Also print the result to stdout.
-+
-+``--id=<id>``
-+    The id to lookup/use for the group. If you don't specify an ``<id>``, but do specify a name that doesn't refer to an existing group, then an id for the group is generated based on the name.
-+
-+``-n <name>, --name=<name>``
-+    The name to lookup/use for the group. If you specify an existing group id, then the group with that id will have it's name changed to this value.
-+
-+``--description=<description>``
-+    The description to use for the group.
-+
-+``--display-order=<display_order>``
-+    Change the integer which controls the order groups are presented in, for example in ``dnf grouplist``.
-+
-+``--translated-name=<lang:text>``
-+    A translation of the group name in the given language. The syntax is ``lang:text``. Eg. ``en:my-group-name-in-english``
-+
-+``--translated-description=<lang:text>``
-+    A translation of the group description in the given language. The syntax is ``lang:text``. Eg. ``en:my-group-description-in-english``.
-+
-+``--user-visible``
-+    Make the group visible in ``dnf grouplist`` (this is the default).
-+
-+``--not-user-visible``
-+    Make the group not visible in ``dnf grouplist``.
-+
-+``--mandatory``
-+    Store the package names specified within the mandatory section of the specified group, the default is to use the default section.
-+
-+``--optional``
-+    Store the package names specified within the optional section of the specified group, the default is to use the default section.
-+
-+``--remove``
-+    Instead of adding packages remove them. Note that the packages are removed from all sections (default, mandatory and optional).
-+
-+``--dependencies``
-+    Also include the names of the direct dependencies for each package specified.
-diff --git a/doc/index.rst b/doc/index.rst
-index 91bb36e..7213253 100644
---- a/doc/index.rst
-+++ b/doc/index.rst
-@@ -33,6 +33,7 @@ This documents core plugins of DNF:
-    debuginfo-install
-    download
-    generate_completion_cache
-+   groups-manager
-    leaves
-    local
-    migrate
-diff --git a/libexec/dnf-utils.in b/libexec/dnf-utils.in
-index 667ce13..af1e893 100644
---- a/libexec/dnf-utils.in
-+++ b/libexec/dnf-utils.in
-@@ -37,6 +37,7 @@ MAPPING = {'debuginfo-install': ['debuginfo-install'],
-            'yum-config-manager': ['config-manager'],
-            'yum-debug-dump': ['debug-dump'],
-            'yum-debug-restore': ['debug-restore'],
-+           'yum-groups-manager': ['groups-manager'],
-            'yumdownloader': ['download']
-            }
- 
-diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt
-index 7465e53..f66d3df 100644
---- a/plugins/CMakeLists.txt
-+++ b/plugins/CMakeLists.txt
-@@ -6,6 +6,7 @@ INSTALL (FILES config_manager.py DESTINATION ${PYTHON_INSTALL_DIR}/dnf-plugins)
- INSTALL (FILES copr.py DESTINATION ${PYTHON_INSTALL_DIR}/dnf-plugins)
- INSTALL (FILES download.py DESTINATION ${PYTHON_INSTALL_DIR}/dnf-plugins)
- INSTALL (FILES generate_completion_cache.py DESTINATION ${PYTHON_INSTALL_DIR}/dnf-plugins)
-+INSTALL (FILES groups_manager.py DESTINATION ${PYTHON_INSTALL_DIR}/dnf-plugins)
- INSTALL (FILES leaves.py DESTINATION ${PYTHON_INSTALL_DIR}/dnf-plugins)
- if (${WITHOUT_LOCAL} STREQUAL "0")
- INSTALL (FILES local.py DESTINATION ${PYTHON_INSTALL_DIR}/dnf-plugins)
-diff --git a/plugins/groups_manager.py b/plugins/groups_manager.py
-new file mode 100644
-index 0000000..382df37
---- /dev/null
-+++ b/plugins/groups_manager.py
-@@ -0,0 +1,314 @@
-+# groups_manager.py
-+# DNF plugin for managing comps groups metadata files
-+#
-+# Copyright (C) 2020 Red Hat, Inc.
-+#
-+# This copyrighted material is made available to anyone wishing to use,
-+# modify, copy, or redistribute it subject to the terms and conditions of
-+# the GNU General Public License v.2, or (at your option) any later version.
-+# This program is distributed in the hope that it will be useful, but WITHOUT
-+# ANY WARRANTY expressed or implied, including the implied warranties of
-+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
-+# Public License for more details.  You should have received a copy of the
-+# GNU General Public License along with this program; if not, write to the
-+# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
-+# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
-+# source code or documentation are not subject to the GNU General Public
-+# License and may only be used or replicated with the express permission of
-+# Red Hat, Inc.
-+#
-+
-+from __future__ import absolute_import
-+from __future__ import unicode_literals
-+
-+import argparse
-+import gzip
-+import libcomps
-+import os
-+import re
-+import shutil
-+import tempfile
-+
-+from dnfpluginscore import _, logger
-+import dnf
-+import dnf.cli
-+
-+
-+RE_GROUP_ID_VALID = '-a-z0-9_.:'
-+RE_GROUP_ID = re.compile(r'^[{}]+$'.format(RE_GROUP_ID_VALID))
-+RE_LANG = re.compile(r'^[-a-zA-Z0-9_.@]+$')
-+COMPS_XML_OPTIONS = {
-+    'default_explicit': True,
-+    'uservisible_explicit': True,
-+    'empty_groups': True}
-+
-+
-+def group_id_type(value):
-+    '''group id validator'''
-+    if not RE_GROUP_ID.match(value):
-+        raise argparse.ArgumentTypeError(_('Invalid group id'))
-+    return value
-+
-+
-+def translation_type(value):
-+    '''translated texts validator'''
-+    data = value.split(':', 2)
-+    if len(data) != 2:
-+        raise argparse.ArgumentTypeError(
-+            _("Invalid translated data, should be in form 'lang:text'"))
-+    lang, text = data
-+    if not RE_LANG.match(lang):
-+        raise argparse.ArgumentTypeError(_('Invalid/empty language for translated data'))
-+    return lang, text
-+
-+
-+def text_to_id(text):
-+    '''generate group id based on its name'''
-+    group_id = text.lower()
-+    group_id = re.sub('[^{}]'.format(RE_GROUP_ID_VALID), '', group_id)
-+    if not group_id:
-+        raise dnf.cli.CliError(
-+            _("Can't generate group id from '{}'. Please specify group id using --id.").format(
-+                text))
-+    return group_id
-+
-+
-+@dnf.plugin.register_command
-+class GroupsManagerCommand(dnf.cli.Command):
-+    aliases = ('groups-manager',)
-+    summary = _('create and edit groups metadata file')
-+
-+    def __init__(self, cli):
-+        super(GroupsManagerCommand, self).__init__(cli)
-+        self.comps = libcomps.Comps()
-+
-+    @staticmethod
-+    def set_argparser(parser):
-+        # input / output options
-+        parser.add_argument('--load', action='append', default=[],
-+                            metavar='COMPS.XML',
-+                            help=_('load groups metadata from file'))
-+        parser.add_argument('--save', action='append', default=[],
-+                            metavar='COMPS.XML',
-+                            help=_('save groups metadata to file'))
-+        parser.add_argument('--merge', metavar='COMPS.XML',
-+                            help=_('load and save groups metadata to file'))
-+        parser.add_argument('--print', action='store_true', default=False,
-+                            help=_('print the result metadata to stdout'))
-+        # group options
-+        parser.add_argument('--id', type=group_id_type,
-+                            help=_('group id'))
-+        parser.add_argument('-n', '--name', help=_('group name'))
-+        parser.add_argument('--description',
-+                            help=_('group description'))
-+        parser.add_argument('--display-order', type=int,
-+                            help=_('group display order'))
-+        parser.add_argument('--translated-name', action='append', default=[],
-+                            metavar='LANG:TEXT', type=translation_type,
-+                            help=_('translated name for the group'))
-+        parser.add_argument('--translated-description', action='append', default=[],
-+                            metavar='LANG:TEXT', type=translation_type,
-+                            help=_('translated description for the group'))
-+        visible = parser.add_mutually_exclusive_group()
-+        visible.add_argument('--user-visible', dest='user_visible', action='store_true',
-+                             default=None,
-+                             help=_('make the group user visible (default)'))
-+        visible.add_argument('--not-user-visible', dest='user_visible', action='store_false',
-+                             default=None,
-+                             help=_('make the group user invisible'))
-+
-+        # package list options
-+        section = parser.add_mutually_exclusive_group()
-+        section.add_argument('--mandatory', action='store_true',
-+                             help=_('add packages to the mandatory section'))
-+        section.add_argument('--optional', action='store_true',
-+                             help=_('add packages to the optional section'))
-+        section.add_argument('--remove', action='store_true', default=False,
-+                             help=_('remove packages from the group instead of adding them'))
-+        parser.add_argument('--dependencies', action='store_true',
-+                            help=_('include also direct dependencies for packages'))
-+
-+        parser.add_argument("packages", nargs='*', metavar='PACKAGE',
-+                            help=_('package specification'))
-+
-+    def configure(self):
-+        demands = self.cli.demands
-+
-+        if self.opts.packages:
-+            demands.sack_activation = True
-+            demands.available_repos = True
-+            demands.load_system_repo = False
-+
-+        # handle --merge option (shortcut to --load and --save the same file)
-+        if self.opts.merge:
-+            self.opts.load.insert(0, self.opts.merge)
-+            self.opts.save.append(self.opts.merge)
-+
-+        # check that group is specified when editing is attempted
-+        if (self.opts.description
-+                or self.opts.display_order
-+                or self.opts.translated_name
-+                or self.opts.translated_description
-+                or self.opts.user_visible is not None
-+                or self.opts.packages):
-+            if not self.opts.id and not self.opts.name:
-+                raise dnf.cli.CliError(
-+                    _("Can't edit group without specifying it (use --id or --name)"))
-+
-+    def load_input_files(self):
-+        """
-+        Loads all input xml files.
-+        Returns True if at least one file was successfuly loaded
-+        """
-+        for file_name in self.opts.load:
-+            file_comps = libcomps.Comps()
-+            try:
-+                if file_name.endswith('.gz'):
-+                    # libcomps does not support gzipped files - decompress to temporary
-+                    # location
-+                    with gzip.open(file_name) as gz_file:
-+                        temp_file = tempfile.NamedTemporaryFile(delete=False)
-+                        try:
-+                            shutil.copyfileobj(gz_file, temp_file)
-+                            # close temp_file to ensure the content is flushed to disk
-+                            temp_file.close()
-+                            file_comps.fromxml_f(temp_file.name)
-+                        finally:
-+                            os.unlink(temp_file.name)
-+                else:
-+                    file_comps.fromxml_f(file_name)
-+            except (IOError, OSError, libcomps.ParserError) as err:
-+                # gzip module raises OSError on reading from malformed gz file
-+                # get_last_errors() output often contains duplicit lines, remove them
-+                seen = set()
-+                for error in file_comps.get_last_errors():
-+                    if error in seen:
-+                        continue
-+                    logger.error(error.strip())
-+                    seen.add(error)
-+                raise dnf.exceptions.Error(
-+                    _("Can't load file \"{}\": {}").format(file_name, err))
-+            else:
-+                self.comps += file_comps
-+
-+    def save_output_files(self):
-+        for file_name in self.opts.save:
-+            try:
-+                # xml_f returns a list of errors / log entries
-+                errors = self.comps.xml_f(file_name, xml_options=COMPS_XML_OPTIONS)
-+            except libcomps.XMLGenError as err:
-+                errors = [err]
-+            if errors:
-+                # xml_f() method could return more than one error. In this case
-+                # raise the latest of them and log the others.
-+                for err in errors[:-1]:
-+                    logger.error(err.strip())
-+                raise dnf.exceptions.Error(_("Can't save file \"{}\": {}").format(
-+                    file_name, errors[-1].strip()))
-+
-+
-+    def find_group(self, group_id, name):
-+        '''
-+        Try to find group according to command line parameters - first by id
-+        then by name.
-+        '''
-+        group = None
-+        if group_id:
-+            for grp in self.comps.groups:
-+                if grp.id == group_id:
-+                    group = grp
-+                    break
-+        if group is None and name:
-+            for grp in self.comps.groups:
-+                if grp.name == name:
-+                    group = grp
-+                    break
-+        return group
-+
-+    def edit_group(self, group):
-+        '''
-+        Set attributes and package lists for selected group
-+        '''
-+        def langlist_to_strdict(lst):
-+            str_dict = libcomps.StrDict()
-+            for lang, text in lst:
-+                str_dict[lang] = text
-+            return str_dict
-+
-+        # set group attributes
-+        if self.opts.name:
-+            group.name = self.opts.name
-+        if self.opts.description:
-+            group.desc = self.opts.description
-+        if self.opts.display_order:
-+            group.display_order = self.opts.display_order
-+        if self.opts.user_visible is not None:
-+            group.uservisible = self.opts.user_visible
-+        if self.opts.translated_name:
-+            group.name_by_lang = langlist_to_strdict(self.opts.translated_name)
-+        if self.opts.translated_description:
-+            group.desc_by_lang = langlist_to_strdict(self.opts.translated_description)
-+
-+        # edit packages list
-+        if self.opts.packages:
-+            # find packages according to specifications from command line
-+            packages = set()
-+            for pkg_spec in self.opts.packages:
-+                q = self.base.sack.query().filterm(name__glob=pkg_spec).latest()
-+                if not q:
-+                    logger.warning(_("No match for argument: {}").format(pkg_spec))
-+                    continue
-+                packages.update(q)
-+            if self.opts.dependencies:
-+                # add packages that provide requirements
-+                requirements = set()
-+                for pkg in packages:
-+                    requirements.update(pkg.requires)
-+                packages.update(self.base.sack.query().filterm(provides=requirements))
-+
-+            pkg_names = {pkg.name for pkg in packages}
-+
-+            if self.opts.remove:
-+                for pkg_name in pkg_names:
-+                    for pkg in group.packages_match(name=pkg_name,
-+                                                    type=libcomps.PACKAGE_TYPE_UNKNOWN):
-+                        group.packages.remove(pkg)
-+            else:
-+                if self.opts.mandatory:
-+                    pkg_type = libcomps.PACKAGE_TYPE_MANDATORY
-+                elif self.opts.optional:
-+                    pkg_type = libcomps.PACKAGE_TYPE_OPTIONAL
-+                else:
-+                    pkg_type = libcomps.PACKAGE_TYPE_DEFAULT
-+                for pkg_name in sorted(pkg_names):
-+                    if not group.packages_match(name=pkg_name, type=pkg_type):
-+                        group.packages.append(libcomps.Package(name=pkg_name, type=pkg_type))
-+
-+    def run(self):
-+        self.load_input_files()
-+
-+        if self.opts.id or self.opts.name:
-+            # we are adding / editing a group
-+            group = self.find_group(group_id=self.opts.id, name=self.opts.name)
-+            if group is None:
-+                # create a new group
-+                if self.opts.remove:
-+                    raise dnf.exceptions.Error(_("Can't remove packages from non-existent group"))
-+                group = libcomps.Group()
-+                if self.opts.id:
-+                    group.id = self.opts.id
-+                    group.name = self.opts.id
-+                elif self.opts.name:
-+                    group_id = text_to_id(self.opts.name)
-+                    if self.find_group(group_id=group_id, name=None):
-+                        raise dnf.cli.CliError(
-+                            _("Group id '{}' generated from '{}' is duplicit. "
-+                              "Please specify group id using --id.").format(
-+                                  group_id, self.opts.name))
-+                    group.id = group_id
-+                self.comps.groups.append(group)
-+            self.edit_group(group)
-+
-+        self.save_output_files()
-+        if self.opts.print or (not self.opts.save):
-+            print(self.comps.xml_str(xml_options=COMPS_XML_OPTIONS))
--- 
-2.26.2
-
diff --git a/SOURCES/0001-versionlock-Do-not-exclude-locked-obsoleters-RhBug1957280.patch b/SOURCES/0001-versionlock-Do-not-exclude-locked-obsoleters-RhBug1957280.patch
new file mode 100644
index 0000000..133ea78
--- /dev/null
+++ b/SOURCES/0001-versionlock-Do-not-exclude-locked-obsoleters-RhBug1957280.patch
@@ -0,0 +1,38 @@
+From a3b9e17628994b43080b8c03b9f665a0e6514cd6 Mon Sep 17 00:00:00 2001
+From: Marek Blaha <mblaha@redhat.com>
+Date: Tue, 11 May 2021 08:29:31 +0200
+Subject: [PATCH] versionlock: Do not exclude locked obsoleters (RhBug:1957280)
+
+The versionlock plugin excludes all obsoleters of locked packages. If
+both versions (obsoleted package and its obsoleter) are locked, this
+leads to the inability to install the obsoleter package. The patch
+protects all locked packages from being excluded as obsoleters.
+
+= changelog =
+msg:           versionlock: Locking obsoleted package does not make the obsoleter unavailable
+type:          bugfix
+resolves:      https://bugzilla.redhat.com/show_bug.cgi?id=1957280
+---
+ plugins/versionlock.py | 6 ++++--
+ 1 file changed, 4 insertions(+), 2 deletions(-)
+
+diff --git a/plugins/versionlock.py b/plugins/versionlock.py
+index d997130..c89a75d 100644
+--- a/plugins/versionlock.py
++++ b/plugins/versionlock.py
+@@ -113,8 +113,10 @@ class VersionLock(dnf.Plugin):
+             other_versions = all_versions.difference(locked_query)
+             excludes_query = excludes_query.union(other_versions)
+             # exclude also anything that obsoletes the locked versions of packages
+-            excludes_query = excludes_query.union(
+-                self.base.sack.query().filterm(obsoletes=locked_query))
++            obsoletes_query = self.base.sack.query().filterm(obsoletes=locked_query)
++            # leave out obsoleters that are also part of locked versions (otherwise the obsoleter package
++            # would not be installable at all)
++            excludes_query = excludes_query.union(obsoletes_query.difference(locked_query))
+ 
+         excludes_query.filterm(reponame__neq=hawkey.SYSTEM_REPO_NAME)
+         if excludes_query:
+--
+libgit2 1.0.1
+
diff --git a/SOURCES/0002-needs-restarting-add-s-to-list-services-RhBug-177293.patch b/SOURCES/0002-needs-restarting-add-s-to-list-services-RhBug-177293.patch
deleted file mode 100644
index b832730..0000000
--- a/SOURCES/0002-needs-restarting-add-s-to-list-services-RhBug-177293.patch
+++ /dev/null
@@ -1,138 +0,0 @@
-From b2a912724d737ca7ac4350885b54117f5e043046 Mon Sep 17 00:00:00 2001
-From: Nicola Sella <nsella@redhat.com>
-Date: Thu, 5 Mar 2020 12:45:39 +0100
-Subject: [PATCH 2/2] [needs-restarting] add -s to list services
- (RhBug:1772939)
-
-= changelog =
-msg:           [needs-restarting] add -s to list services (RhBug:1772939)
-type:          bugfix
-resolves:      https://bugzilla.redhat.com/show_bug.cgi?id=1772939
-
-Closes: #395
-Approved by: kontura
----
- dnf-plugins-core.spec       |  6 ++++++
- doc/needs_restarting.rst    |  3 +++
- plugins/needs_restarting.py | 33 +++++++++++++++++++++++++++++++++
- 3 files changed, 42 insertions(+)
-
-diff --git a/dnf-plugins-core.spec b/dnf-plugins-core.spec
-index 42d0884..012dde8 100644
---- a/dnf-plugins-core.spec
-+++ b/dnf-plugins-core.spec
-@@ -99,8 +99,10 @@ Summary:        Core Plugins for DNF
- %{?python_provide:%python_provide python2-%{name}}
- BuildRequires:  python2-dnf >= %{dnf_lowest_compatible}
- %if 0%{?rhel} && 0%{?rhel} <= 7
-+BuildRequires:  dbus-python
- BuildRequires:  python-nose
- %else
-+BuildRequires:  python2-dbus
- BuildRequires:  python2-nose
- %endif
- BuildRequires:  python2-devel
-@@ -110,8 +112,10 @@ Requires:       python2-distro
- Requires:       python2-dnf >= %{dnf_lowest_compatible}
- Requires:       python2-hawkey >= %{hawkey_version}
- %if 0%{?rhel} && 0%{?rhel} <= 7
-+Requires:       dbus-python
- Requires:       python-dateutil
- %else
-+Requires:       python2-dbus
- Requires:       python2-dateutil
- %endif
- Provides:       python2-dnf-plugins-extras-debug = %{version}-%{release}
-@@ -140,12 +144,14 @@ Additionally provides generate_completion_cache passive plugin.
- %package -n python3-%{name}
- Summary:    Core Plugins for DNF
- %{?python_provide:%python_provide python3-%{name}}
-+BuildRequires:  python3-dbus
- BuildRequires:  python3-devel
- BuildRequires:  python3-dnf >= %{dnf_lowest_compatible}
- BuildRequires:  python3-nose
- %if 0%{?fedora}
- Requires:       python3-distro
- %endif
-+Requires:       python3-dbus
- Requires:       python3-dnf >= %{dnf_lowest_compatible}
- Requires:       python3-hawkey >= %{hawkey_version}
- Requires:       python3-dateutil
-diff --git a/doc/needs_restarting.rst b/doc/needs_restarting.rst
-index e79b43f..1a3fbbe 100644
---- a/doc/needs_restarting.rst
-+++ b/doc/needs_restarting.rst
-@@ -48,3 +48,6 @@ All general DNF options are accepted, see `Options` in :manpage:`dnf(8)` for det
- ``-r, --reboothint``
- 
-     Only report whether a reboot is required (exit code 1) or not (exit code 0).
-+
-+``-s, --services``
-+    Only list the affected systemd services.
-diff --git a/plugins/needs_restarting.py b/plugins/needs_restarting.py
-index 69203f4..f6bf525 100644
---- a/plugins/needs_restarting.py
-+++ b/plugins/needs_restarting.py
-@@ -29,6 +29,7 @@ from dnfpluginscore import logger, _
- 
- import dnf
- import dnf.cli
-+import dbus
- import functools
- import os
- import re
-@@ -126,6 +127,30 @@ def print_cmd(pid):
-     print('%d : %s' % (pid, command))
- 
- 
-+def get_service_dbus(pid):
-+    bus = dbus.SystemBus()
-+    systemd_manager_object = bus.get_object(
-+        'org.freedesktop.systemd1',
-+        '/org/freedesktop/systemd1'
-+    )
-+    systemd_manager_interface = dbus.Interface(
-+        systemd_manager_object,
-+        'org.freedesktop.systemd1.Manager'
-+    )
-+    service_proxy = bus.get_object(
-+        'org.freedesktop.systemd1',
-+        systemd_manager_interface.GetUnitByPID(pid)
-+    )
-+    service_properties = dbus.Interface(
-+        service_proxy, dbus_interface="org.freedesktop.DBus.Properties")
-+    name = service_properties.Get(
-+        "org.freedesktop.systemd1.Unit",
-+        'Id'
-+    )
-+    if name.endswith(".service"):
-+        return name
-+    return
-+
- def smap2opened_file(pid, line):
-     slash = line.find('/')
-     if slash < 0:
-@@ -205,6 +230,8 @@ class NeedsRestartingCommand(dnf.cli.Command):
-         parser.add_argument('-r', '--reboothint', action='store_true',
-                             help=_("only report whether a reboot is required "
-                                    "(exit code 1) or not (exit code 0)"))
-+        parser.add_argument('-s', '--services', action='store_true',
-+                            help=_("only report affected systemd services"))
- 
-     def configure(self):
-         demands = self.cli.demands
-@@ -251,5 +278,11 @@ class NeedsRestartingCommand(dnf.cli.Command):
-             if pkg.installtime > process_start(ofile.pid):
-                 stale_pids.add(ofile.pid)
- 
-+        if self.opts.services:
-+            names = set([get_service_dbus(pid) for pid in sorted(stale_pids)])
-+            for name in names:
-+                if name is not None:
-+                    print(name)
-+            return 0
-         for pid in sorted(stale_pids):
-             print_cmd(pid)
--- 
-2.26.2
-
diff --git a/SOURCES/0002-repomanage-Allow-running-only-with-metadata.patch b/SOURCES/0002-repomanage-Allow-running-only-with-metadata.patch
new file mode 100644
index 0000000..2067f00
--- /dev/null
+++ b/SOURCES/0002-repomanage-Allow-running-only-with-metadata.patch
@@ -0,0 +1,49 @@
+From 716c5978a8036df22d6f5b430ba38c35d034f3ea Mon Sep 17 00:00:00 2001
+From: Aleš Matěj <amatej@redhat.com>
+Date: Tue, 8 Jun 2021 10:25:55 +0200
+Subject: [PATCH] [repomanage] Allow running only with metadata
+
+Requiring some packages to be present even if there are repodata was
+arbitrary because they are never used.
+---
+ plugins/repomanage.py | 10 +++++-----
+ 1 file changed, 5 insertions(+), 5 deletions(-)
+
+diff --git a/plugins/repomanage.py b/plugins/repomanage.py
+index 445006d..989bd78 100644
+--- a/plugins/repomanage.py
++++ b/plugins/repomanage.py
+@@ -58,18 +58,13 @@ class RepoManageCommand(dnf.cli.Command):
+         if self.opts.new and self.opts.old:
+             raise dnf.exceptions.Error(_("Pass either --old or --new, not both!"))
+ 
+-        rpm_list = []
+-        rpm_list = self._get_file_list(self.opts.path, ".rpm")
+         verfile = {}
+         pkgdict = {}
+         module_dict = {}  # {NameStream: {Version: [modules]}}
+         all_modular_artifacts = set()
+ 
+         keepnum = int(self.opts.keep) # the number of items to keep
+ 
+-        if len(rpm_list) == 0:
+-            raise dnf.exceptions.Error(_("No files to process"))
+-
+         try:
+             repo_conf = self.base.repos.add_new_repo("repomanage_repo", self.base.conf, baseurl=[self.opts.path])
+             # Always expire the repo, otherwise repomanage could use cached metadata and give identical results
+@@ -88,6 +83,11 @@ class RepoManageCommand(dnf.cli.Command):
+                         module_package.getVersionNum(), []).append(module_package)
+ 
+         except dnf.exceptions.RepoError:
++            rpm_list = []
++            rpm_list = self._get_file_list(self.opts.path, ".rpm")
++            if len(rpm_list) == 0:
++                raise dnf.exceptions.Error(_("No files to process"))
++
+             self.base.reset(sack=True, repos=True)
+             self.base.fill_sack(load_system_repo=False, load_available_repos=False)
+             try:
+--
+libgit2 1.0.1
+
diff --git a/SOURCES/0003-repomanage-Enhance-repomanage-documentation-RhBug1898293.patch b/SOURCES/0003-repomanage-Enhance-repomanage-documentation-RhBug1898293.patch
new file mode 100644
index 0000000..7563d02
--- /dev/null
+++ b/SOURCES/0003-repomanage-Enhance-repomanage-documentation-RhBug1898293.patch
@@ -0,0 +1,42 @@
+From 1b432bada5a3627f729cb42b99b7a93f808e3a80 Mon Sep 17 00:00:00 2001
+From: Aleš Matěj <amatej@redhat.com>
+Date: Tue, 8 Jun 2021 11:48:07 +0200
+Subject: [PATCH] [repomanage] Enhance repomanage documentation (RhBug:1898293)
+
+= changelog =
+msg: Enhance repomanage documentation
+type: enhancement
+resolves: https://bugzilla.redhat.com/show_bug.cgi?id=1898293
+---
+ doc/repomanage.rst | 7 ++++---
+ 1 file changed, 4 insertions(+), 3 deletions(-)
+
+diff --git a/doc/repomanage.rst b/doc/repomanage.rst
+index e4da441..e3171ef 100644
+--- a/doc/repomanage.rst
++++ b/doc/repomanage.rst
+@@ -31,9 +31,10 @@ Synopsis
+ Description
+ -----------
+ 
+-`repomanage` prints newest or oldest packages in a repository specified by <path> for easy piping to xargs or similar programs. In case <path> doesn't contain a valid repository it is searched for rpm packages which are then used instead.
++`repomanage` prints newest or older packages in a repository specified by <path> for easy piping to xargs or similar programs. In case <path> doesn't contain a valid repodata, it is searched for rpm packages which are then used instead.
++If the repodata are present, `repomanage` uses them as the source of truth, it doesn't verify that they match the present rpm packages. In fact, `repomanage` can run with just the repodata, no rpm packages are needed.
+ 
+-In order to work correctly with modular packages <path> has to contain repodata with modular metadata. If modular content is present `repomanage` prints packages from newest or oldest versions of each stream in addition to newest or oldest non-modular packages.
++In order to work correctly with modular packages, <path> has to contain repodata with modular metadata. If modular content is present, `repomanage` prints packages from newest or older stream versions in addition to newest or older non-modular packages.
+ 
+ 
+ Options
+@@ -44,7 +45,7 @@ All general DNF options are accepted, see `Options` in :manpage:`dnf(8)` for det
+ The following options set what packages are displayed. These options are mutually exclusive, i.e. only one can be specified. If no option is specified, the newest packages are shown.
+ 
+ ``--old``
+-    Show older packages.
++    Show older packages (for a package or a stream show all versions except the newest one).
+ 
+ ``--new``
+     Show newest packages.
+--
+libgit2 1.0.1
+
diff --git a/SOURCES/0003-reposync-Check-GPG-signatures-of-downloaded-packages-RhBug-1856818.patch b/SOURCES/0003-reposync-Check-GPG-signatures-of-downloaded-packages-RhBug-1856818.patch
deleted file mode 100644
index 45cc285..0000000
--- a/SOURCES/0003-reposync-Check-GPG-signatures-of-downloaded-packages-RhBug-1856818.patch
+++ /dev/null
@@ -1,194 +0,0 @@
-From a4f21266a6dab9e77913d56c04aba1e579f0e0c1 Mon Sep 17 00:00:00 2001
-From: Marek Blaha <mblaha@redhat.com>
-Date: Fri, 23 Oct 2020 09:06:35 +0200
-Subject: [PATCH 1/2] [reposync] Reorder options alphabetically
-
----
- doc/reposync.rst    | 30 +++++++++++++++---------------
- plugins/reposync.py | 18 +++++++++---------
- 2 files changed, 24 insertions(+), 24 deletions(-)
-
-diff --git a/doc/reposync.rst b/doc/reposync.rst
-index 71a435dc..3b820f33 100644
---- a/doc/reposync.rst
-+++ b/doc/reposync.rst
-@@ -39,36 +39,36 @@ Options
- 
- All general DNF options are accepted. Namely, the ``--repoid`` option can be used to specify the repositories to synchronize. See `Options` in :manpage:`dnf(8)` for details.
- 
--``-p <download-path>, --download-path=<download-path>``
--    Root path under which the downloaded repositories are stored, relative to the current working directory. Defaults to the current working directory. Every downloaded repository has a subdirectory named after its ID under this path.
--    
--``--norepopath``
--    Don't add the reponame to the download path. Can only be used when syncing a single repository (default is to add the reponame).
--
--``--download-metadata``
--    Download all repository metadata. Downloaded copy is instantly usable as a repository, no need to run createrepo_c on it.
--
- ``-a <architecture>, --arch=<architecture>``
-     Download only packages of given architectures (default is all architectures). Can be used multiple times.
- 
--``--source``
--    Operate on source packages.
-+``--delete``
-+    Delete local packages no longer present in repository.
-+
-+``--download-metadata``
-+    Download all repository metadata. Downloaded copy is instantly usable as a repository, no need to run createrepo_c on it.
- 
- ``-m, --downloadcomps``
-     Also download and uncompress comps.xml. Consider using ``--download-metadata`` option which will download all available repository metadata.
- 
-+``--metadata-path``
-+    Root path under which the downloaded metadata are stored. It defaults to ``--download-path`` value if not given.
-+
- ``-n, --newest-only``
-     Download only newest packages per-repo.
- 
--``--delete``
--    Delete local packages no longer present in repository.
-+``--norepopath``
-+    Don't add the reponame to the download path. Can only be used when syncing a single repository (default is to add the reponame).
- 
--``--metadata-path``
--    Root path under which the downloaded metadata are stored. It defaults to ``--download-path`` value if not given.
-+``-p <download-path>, --download-path=<download-path>``
-+    Root path under which the downloaded repositories are stored, relative to the current working directory. Defaults to the current working directory. Every downloaded repository has a subdirectory named after its ID under this path.
- 
- ``--remote-time``
-     Try to set the timestamps of the downloaded files to those on the remote side.
- 
-+``--source``
-+    Operate on source packages.
-+
- ``-u, --urls``
-     Just print urls of what would be downloaded, don't download.
- 
-diff --git a/plugins/reposync.py b/plugins/reposync.py
-index 7556e7eb..6f572cac 100644
---- a/plugins/reposync.py
-+++ b/plugins/reposync.py
-@@ -63,24 +63,24 @@ def set_argparser(parser):
-                             help=_('download only packages for this ARCH'))
-         parser.add_argument('--delete', default=False, action='store_true',
-                             help=_('delete local packages no longer present in repository'))
--        parser.add_argument('-m', '--downloadcomps', default=False, action='store_true',
--                            help=_('also download and uncompress comps.xml'))
-         parser.add_argument('--download-metadata', default=False, action='store_true',
-                             help=_('download all the metadata.'))
-+        parser.add_argument('-m', '--downloadcomps', default=False, action='store_true',
-+                            help=_('also download and uncompress comps.xml'))
-+        parser.add_argument('--metadata-path',
-+                            help=_('where to store downloaded repository metadata. '
-+                                   'Defaults to the value of --download-path.'))
-         parser.add_argument('-n', '--newest-only', default=False, action='store_true',
-                             help=_('download only newest packages per-repo'))
--        parser.add_argument('-p', '--download-path', default='./',
--                            help=_('where to store downloaded repositories'))
-         parser.add_argument('--norepopath', default=False, action='store_true',
-                             help=_("Don't add the reponame to the download path."))
--        parser.add_argument('--metadata-path',
--                            help=_('where to store downloaded repository metadata. '
--                                   'Defaults to the value of --download-path.'))
--        parser.add_argument('--source', default=False, action='store_true',
--                            help=_('operate on source packages'))
-+        parser.add_argument('-p', '--download-path', default='./',
-+                            help=_('where to store downloaded repositories'))
-         parser.add_argument('--remote-time', default=False, action='store_true',
-                             help=_('try to set local timestamps of local files by '
-                                    'the one on the server'))
-+        parser.add_argument('--source', default=False, action='store_true',
-+                            help=_('operate on source packages'))
-         parser.add_argument('-u', '--urls', default=False, action='store_true',
-                             help=_("Just list urls of what would be downloaded, "
-                                    "don't download"))
-
-From 978b7f2b1c654fed7b1b4cf45cb607143226804c Mon Sep 17 00:00:00 2001
-From: Marek Blaha <mblaha@redhat.com>
-Date: Fri, 23 Oct 2020 09:14:02 +0200
-Subject: [PATCH 2/2] [reposync] Check GPG signatures of downloaded packages
- (RhBug:1856818)
-
-YUMv3 reposync used to have --gpgcheck option to remove packages that fail GPG
-signature checking after downloading.
-This patch implements the option for DNF.
-
-= changelog =
-msg:           Add --gpgcheck option to reposync (RhBug:1856818)
-type:          enhancement
-resolves:      https://bugzilla.redhat.com/show_bug.cgi?id=1856818
----
- doc/reposync.rst    |  4 ++++
- plugins/reposync.py | 21 +++++++++++++++++++++
- 2 files changed, 25 insertions(+)
-
-diff --git a/doc/reposync.rst b/doc/reposync.rst
-index 3b820f33..de40957f 100644
---- a/doc/reposync.rst
-+++ b/doc/reposync.rst
-@@ -48,6 +48,10 @@ All general DNF options are accepted. Namely, the ``--repoid`` option can be use
- ``--download-metadata``
-     Download all repository metadata. Downloaded copy is instantly usable as a repository, no need to run createrepo_c on it.
- 
-+``-g, --gpgcheck``
-+    Remove packages that fail GPG signature checking after downloading. Exit code is ``1`` if at least one package was removed.
-+    Note that for repositories with ``gpgcheck=0`` set in their configuration the GPG signature is not checked even with this option used.
-+
- ``-m, --downloadcomps``
-     Also download and uncompress comps.xml. Consider using ``--download-metadata`` option which will download all available repository metadata.
- 
-diff --git a/plugins/reposync.py b/plugins/reposync.py
-index 6f572cac..c891bfa2 100644
---- a/plugins/reposync.py
-+++ b/plugins/reposync.py
-@@ -24,6 +24,7 @@
- import hawkey
- import os
- import shutil
-+import types
- 
- from dnfpluginscore import _, logger
- from dnf.cli.option_parser import OptionParser
-@@ -65,6 +66,9 @@ def set_argparser(parser):
-                             help=_('delete local packages no longer present in repository'))
-         parser.add_argument('--download-metadata', default=False, action='store_true',
-                             help=_('download all the metadata.'))
-+        parser.add_argument('-g', '--gpgcheck', default=False, action='store_true',
-+                            help=_('Remove packages that fail GPG signature checking '
-+                                   'after downloading'))
-         parser.add_argument('-m', '--downloadcomps', default=False, action='store_true',
-                             help=_('also download and uncompress comps.xml'))
-         parser.add_argument('--metadata-path',
-@@ -114,6 +118,7 @@ def configure(self):
- 
-     def run(self):
-         self.base.conf.keepcache = True
-+        gpgcheck_ok = True
-         for repo in self.base.repos.iter_enabled():
-             if self.opts.remote_time:
-                 repo._repo.setPreserveRemoteTime(True)
-@@ -150,8 +155,24 @@ def run(self):
-                 self.print_urls(pkglist)
-             else:
-                 self.download_packages(pkglist)
-+                if self.opts.gpgcheck:
-+                    for pkg in pkglist:
-+                        local_path = self.pkg_download_path(pkg)
-+                        # base.package_signature_check uses pkg.localPkg() to determine
-+                        # the location of the package rpm file on the disk.
-+                        # Set it to the correct download path.
-+                        pkg.localPkg  = types.MethodType(
-+                            lambda s, local_path=local_path: local_path, pkg)
-+                        result, error = self.base.package_signature_check(pkg)
-+                        if result != 0:
-+                            logger.warning(_("Removing {}: {}").format(
-+                                os.path.basename(local_path), error))
-+                            os.unlink(local_path)
-+                            gpgcheck_ok = False
-             if self.opts.delete:
-                 self.delete_old_local_packages(repo, pkglist)
-+        if not gpgcheck_ok:
-+            raise dnf.exceptions.Error(_("GPG signature check failed."))
- 
-     def repo_target(self, repo):
-         return _pkgdir(self.opts.destdir or self.opts.download_path,
diff --git a/SPECS/dnf-plugins-core.spec b/SPECS/dnf-plugins-core.spec
index 09b3283..4fda121 100644
--- a/SPECS/dnf-plugins-core.spec
+++ b/SPECS/dnf-plugins-core.spec
@@ -6,6 +6,8 @@
 %global yum_utils_subpackage_name yum-utils
 %endif
 
+%define __cmake_in_source_build 1
+
 %if 0%{?rhel} && 0%{?rhel} <= 7
 %bcond_with python3
 %else
@@ -31,15 +33,16 @@
 %endif
 
 Name:           dnf-plugins-core
-Version:        4.0.18
-Release:        3%{?dist}
+Version:        4.0.21
+Release:        2%{?dist}
 Summary:        Core Plugins for DNF
 License:        GPLv2+
 URL:            https://github.com/rpm-software-management/dnf-plugins-core
 Source0:        %{url}/archive/%{version}/%{name}-%{version}.tar.gz
-Patch1:         0001-groups-manager-Re-introduce-yum-groups-manager-funct.patch
-Patch2:         0002-needs-restarting-add-s-to-list-services-RhBug-177293.patch
-Patch3:         0003-reposync-Check-GPG-signatures-of-downloaded-packages-RhBug-1856818.patch
+Patch1:         0001-versionlock-Do-not-exclude-locked-obsoleters-RhBug1957280.patch
+Patch2:         0002-repomanage-Allow-running-only-with-metadata.patch
+Patch3:         0003-repomanage-Enhance-repomanage-documentation-RhBug1898293.patch
+
 BuildArch:      noarch
 BuildRequires:  cmake
 BuildRequires:  gettext
@@ -101,10 +104,8 @@ Summary:        Core Plugins for DNF
 BuildRequires:  python2-dnf >= %{dnf_lowest_compatible}
 %if 0%{?rhel} && 0%{?rhel} <= 7
 BuildRequires:  dbus-python
-BuildRequires:  python-nose
 %else
 BuildRequires:  python2-dbus
-BuildRequires:  python2-nose
 %endif
 BuildRequires:  python2-devel
 %if 0%{?fedora}
@@ -147,7 +148,6 @@ Summary:    Core Plugins for DNF
 BuildRequires:  python3-dbus
 BuildRequires:  python3-devel
 BuildRequires:  python3-dnf >= %{dnf_lowest_compatible}
-BuildRequires:  python3-nose
 %if 0%{?fedora}
 Requires:       python3-distro
 %endif
@@ -478,10 +478,14 @@ ln -sf %{_mandir}/man1/%{yum_utils_subpackage_name}.1.gz %{buildroot}%{_mandir}/
 
 %check
 %if %{with python2}
-PYTHONPATH=./plugins nosetests-%{python2_version} -s tests/
+    pushd build-py2
+    ctest -VV
+    popd
 %endif
 %if %{with python3}
-PYTHONPATH=./plugins nosetests-%{python3_version} -s tests/
+    pushd build-py3
+    ctest -VV
+    popd
 %endif
 
 %files
@@ -761,6 +765,20 @@ PYTHONPATH=./plugins nosetests-%{python3_version} -s tests/
 %endif
 
 %changelog
+* Tue Jul 27 2021 Pavla Kratochvilova <pkratoch@redhat.com> - 4.0.21-2
+- [versionlock] Locking obsoleted package does not make the obsoleter unavailable (RhBug:1957280) 
+- [repomanage] Allow running with metadata only
+- [repomanage] Enhance repomanage documentation (RhBug:1898293)
+
+* Wed May 19 2021 Pavla Kratochvilova <pkratoch@redhat.com> - 4.0.21-1
+- Update to 4.0.21
+- [repomanage] Don't use cached metadata (RhBug:1899852)
+- [needs-restarting] fix -r in nspawn containers (RhBug:1913962,1914251)
+- doc: add packages to needs-restarting conf
+- Set blacklist subcommand as deprecated
+- Removed dependency on dnf.yum.misc.Checksum class (RhBug:1935465)
+- Bugs fixed (RhBug:1914827,1916782)
+
 * Fri Jan 15 2021 Nicola Sella <nsella@redhat.com> - 4.0.18-3
 - [reposync] Check GPG signatures of downloaded packages (RhBug:1856818)