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 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=`` + 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=`` + 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=`` + 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=`` + The id to lookup/use for the group. If you don't specify an ````, 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=`` + 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=`` + The description to use for the group. + +``--display-order=`` + Change the integer which controls the order groups are presented in, for example in ``dnf grouplist``. + +``--translated-name=`` + A translation of the group name in the given language. The syntax is ``lang:text``. Eg. ``en:my-group-name-in-english`` + +``--translated-description=`` + 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))