diff --git a/dnf5.spec b/dnf5.spec index 560d7c102..2794b4670 100644 --- a/dnf5.spec +++ b/dnf5.spec @@ -58,6 +58,7 @@ Provides: dnf5-command(module) Provides: dnf5-command(offline) Provides: dnf5-command(provides) Provides: dnf5-command(reinstall) +Provides: dnf5-command(replay) Provides: dnf5-command(remove) Provides: dnf5-command(repo) Provides: dnf5-command(repoquery) @@ -308,6 +309,7 @@ It supports RPM packages, modulemd modules, and comps groups & environments. %{_mandir}/man8/dnf*-provides.8.* %{_mandir}/man8/dnf*-reinstall.8.* %{_mandir}/man8/dnf*-remove.8.* +%{_mandir}/man8/dnf*-replay.8.* %{_mandir}/man8/dnf*-repo.8.* %{_mandir}/man8/dnf*-repoquery.8.* %{_mandir}/man8/dnf*-search.8.* diff --git a/dnf5/commands/history/history.cpp b/dnf5/commands/history/history.cpp index ba4612bf6..693485366 100644 --- a/dnf5/commands/history/history.cpp +++ b/dnf5/commands/history/history.cpp @@ -22,7 +22,6 @@ along with libdnf. If not, see . #include "history_info.hpp" #include "history_list.hpp" #include "history_redo.hpp" -#include "history_replay.hpp" #include "history_rollback.hpp" #include "history_store.hpp" #include "history_undo.hpp" @@ -61,7 +60,6 @@ void HistoryCommand::register_subcommands() { // register_subcommand(std::make_unique(get_context()), software_management_commands_group); // register_subcommand(std::make_unique(get_context()), software_management_commands_group); register_subcommand(std::make_unique(get_context()), software_management_commands_group); - // register_subcommand(std::make_unique(get_context()), software_management_commands_group); } void HistoryCommand::pre_configure() { diff --git a/dnf5/commands/history/history_replay.cpp b/dnf5/commands/history/history_replay.cpp deleted file mode 100644 index 41e0fd2bd..000000000 --- a/dnf5/commands/history/history_replay.cpp +++ /dev/null @@ -1,30 +0,0 @@ -/* -Copyright Contributors to the libdnf project. - -This file is part of libdnf: https://github.com/rpm-software-management/libdnf/ - -Libdnf is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -Libdnf is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty 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 libdnf. If not, see . -*/ - -#include "history_replay.hpp" - -namespace dnf5 { - -void HistoryReplayCommand::set_argument_parser() { - get_argument_parser_command()->set_description("Replay a transaction that was previously stored to a file"); -} - -void HistoryReplayCommand::run() {} - -} // namespace dnf5 diff --git a/dnf5/commands/history/history_store.cpp b/dnf5/commands/history/history_store.cpp index 9f3067720..04d0b7b36 100644 --- a/dnf5/commands/history/history_store.cpp +++ b/dnf5/commands/history/history_store.cpp @@ -36,11 +36,11 @@ void HistoryStoreCommand::set_argument_parser() { auto & parser = ctx.get_argument_parser(); output_option = dynamic_cast( - parser.add_init_value(std::make_unique("./transaction.json"))); + parser.add_init_value(std::make_unique("./transaction"))); auto query_format = parser.add_new_named_arg("output"); query_format->set_long_name("output"); query_format->set_short_name('o'); - query_format->set_description("File path for storing the transaction, default is \"./transaction.json\""); + query_format->set_description("Path to a directory for storing the transaction, default is \"./transaction\""); query_format->set_has_value(true); query_format->set_arg_value_help("PATH"); query_format->link_value(output_option); @@ -55,11 +55,14 @@ void HistoryStoreCommand::run() { std::vector transactions; auto logger = get_context().get_base().get_logger(); - std::filesystem::path tmp_path(output_option->get_value()); + std::filesystem::create_directories(output_option->get_value()); - if (std::filesystem::exists(tmp_path)) { + std::filesystem::path trans_file_path(output_option->get_value()); + trans_file_path /= TRANSACTION_JSON; + + if (std::filesystem::exists(trans_file_path)) { std::cout << libdnf5::utils::sformat( - _("File \"{}\" already exists, it will be overwritten.\n"), tmp_path.string()); + _("File \"{}\" already exists, it will be overwritten.\n"), trans_file_path.string()); // ask user for the file overwrite confirmation if (!libdnf5::cli::utils::userconfirm::userconfirm(get_context().get_base().get_config())) { throw libdnf5::cli::AbortedByUserError(); @@ -81,12 +84,12 @@ void HistoryStoreCommand::run() { const std::string json = transactions[0].serialize(); - auto tmp_file = libdnf5::utils::fs::TempFile(tmp_path.parent_path(), tmp_path.filename()); + auto tmp_file = libdnf5::utils::fs::TempFile(trans_file_path.parent_path(), trans_file_path.filename()); auto & file = tmp_file.open_as_file("w+"); file.write(json); file.close(); - std::filesystem::rename(tmp_file.get_path(), output_option->get_value()); + std::filesystem::rename(tmp_file.get_path(), trans_file_path); tmp_file.release(); } diff --git a/dnf5/commands/offline/offline.cpp b/dnf5/commands/offline/offline.cpp index 31c3bdef3..52f47e8cc 100644 --- a/dnf5/commands/offline/offline.cpp +++ b/dnf5/commands/offline/offline.cpp @@ -449,7 +449,7 @@ void OfflineExecuteCommand::run() { const auto & installroot = ctx.get_base().get_config().get_installroot_option().get_value(); const auto & datadir = installroot / libdnf5::offline::DEFAULT_DATADIR.relative_path(); std::filesystem::create_directories(datadir); - const auto & transaction_json_path = datadir / "transaction.json"; + const auto & transaction_json_path = datadir / TRANSACTION_JSON; const auto & goal = std::make_unique(ctx.get_base()); diff --git a/dnf5/commands/replay/replay.cpp b/dnf5/commands/replay/replay.cpp new file mode 100644 index 000000000..8b71984c3 --- /dev/null +++ b/dnf5/commands/replay/replay.cpp @@ -0,0 +1,77 @@ +/* +Copyright Contributors to the libdnf project. + +This file is part of libdnf: https://github.com/rpm-software-management/libdnf/ + +Libdnf is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +Libdnf is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty 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 libdnf. If not, see . +*/ + +#include "replay.hpp" + +#include "commands/history/arguments.hpp" + +#include +#include + +namespace dnf5 { + +void ReplayCommand::set_parent_command() { + auto * arg_parser_parent_cmd = get_session().get_argument_parser().get_root_command(); + auto * arg_parser_this_cmd = get_argument_parser_command(); + arg_parser_parent_cmd->register_command(arg_parser_this_cmd); + arg_parser_parent_cmd->get_group("software_management_commands").register_argument(arg_parser_this_cmd); +} + +void ReplayCommand::set_argument_parser() { + auto & cmd = *get_argument_parser_command(); + cmd.set_description("Replay a transaction that was previously stored to a directory"); + auto & ctx = get_context(); + auto & parser = ctx.get_argument_parser(); + + auto * transaction_path_arg = parser.add_new_positional_arg("transaction-path", 1, nullptr, nullptr); + transaction_path_arg->set_description( + "Path to a directory with stored transaction. Only a single path with one transaction is supported."); + transaction_path_arg->set_parse_hook_func([this]( + [[maybe_unused]] libdnf5::cli::ArgumentParser::PositionalArg * arg, + [[maybe_unused]] int argc, + const char * const argv[]) { + transaction_path = argv[0]; + return true; + }); + cmd.register_positional_arg(transaction_path_arg); + + auto skip_broken = std::make_unique(*this); + std::make_unique(*this); + ignore_extras = std::make_unique(*this); + ignore_installed = std::make_unique(*this); +} + +void ReplayCommand::configure() { + auto & context = get_context(); + context.set_load_system_repo(true); + context.set_load_available_repos(Context::LoadAvailableRepos::ENABLED); +} + +void ReplayCommand::run() { + auto & context = get_context(); + auto settings = libdnf5::GoalJobSettings(); + + settings.set_ignore_extras(ignore_extras->get_value()); + settings.set_ignore_installed(ignore_installed->get_value()); + + context.get_goal()->add_serialized_transaction( + std::filesystem::path(transaction_path) / TRANSACTION_JSON, settings); +} + +} // namespace dnf5 diff --git a/dnf5/commands/history/history_replay.hpp b/dnf5/commands/replay/replay.hpp similarity index 61% rename from dnf5/commands/history/history_replay.hpp rename to dnf5/commands/replay/replay.hpp index 9f379d930..3e5bbf2bf 100644 --- a/dnf5/commands/history/history_replay.hpp +++ b/dnf5/commands/replay/replay.hpp @@ -18,24 +18,32 @@ along with libdnf. If not, see . */ -#ifndef DNF5_COMMANDS_HISTORY_HISTORY_REPLAY_HPP -#define DNF5_COMMANDS_HISTORY_HISTORY_REPLAY_HPP +#ifndef DNF5_COMMANDS_REPLAY__REPLAY_HPP +#define DNF5_COMMANDS_REPLAY__REPLAY_HPP #include - +#include namespace dnf5 { -class HistoryReplayCommand : public Command { +class ReplayCommand : public Command { public: - explicit HistoryReplayCommand(Context & context) : Command(context, "replay") {} + explicit ReplayCommand(Context & context) : Command(context, "replay") {} + void set_parent_command() override; + void configure() override; void set_argument_parser() override; void run() override; + +private: + std::string transaction_path; + + std::unique_ptr ignore_extras{nullptr}; + std::unique_ptr ignore_installed{nullptr}; }; } // namespace dnf5 -#endif // DNF5_COMMANDS_HISTORY_HISTORY_REPLAY_HPP +#endif // DNF5_COMMANDS_REPLAY__REPLAY_HPP diff --git a/dnf5/context.cpp b/dnf5/context.cpp index 83e210ce8..94a71dca7 100644 --- a/dnf5/context.cpp +++ b/dnf5/context.cpp @@ -47,7 +47,6 @@ along with libdnf. If not, see . #include #include #include -#include #include #include #include @@ -344,7 +343,7 @@ void Context::Impl::store_offline(libdnf5::base::Transaction & transaction) { // First, serialize the transaction transaction.store_comps(comps_location); - const auto transaction_json_path = offline_datadir / "transaction.json"; + const auto transaction_json_path = offline_datadir / TRANSACTION_JSON; libdnf5::utils::fs::File transaction_json_file{transaction_json_path, "w"}; transaction_json_file.write(transaction.serialize(packages_in_trans_dir, comps_in_trans_dir)); transaction_json_file.close(); @@ -405,7 +404,7 @@ void Context::Impl::store_offline(libdnf5::base::Transaction & transaction) { void Context::Impl::download_and_run(libdnf5::base::Transaction & transaction) { if (!transaction_store_path.empty()) { - auto transaction_location = transaction_store_path / "transaction.json"; + auto transaction_location = transaction_store_path / TRANSACTION_JSON; constexpr const char * packages_in_trans_dir{"./packages"}; auto packages_location = transaction_store_path / packages_in_trans_dir; constexpr const char * comps_in_trans_dir{"./comps"}; diff --git a/dnf5/include/dnf5/context.hpp b/dnf5/include/dnf5/context.hpp index ab9148d97..505fa8401 100644 --- a/dnf5/include/dnf5/context.hpp +++ b/dnf5/include/dnf5/context.hpp @@ -32,10 +32,8 @@ along with libdnf. If not, see . #include #include -#include #include #include -#include #include #include #include @@ -43,6 +41,8 @@ along with libdnf. If not, see . namespace dnf5 { +constexpr const char * TRANSACTION_JSON = "transaction.json"; + class Plugins; class DNF_API Context : public libdnf5::cli::session::Session { diff --git a/dnf5/main.cpp b/dnf5/main.cpp index cc1df1cff..8c26b13a9 100644 --- a/dnf5/main.cpp +++ b/dnf5/main.cpp @@ -40,6 +40,7 @@ along with libdnf. If not, see . #include "commands/provides/provides.hpp" #include "commands/reinstall/reinstall.hpp" #include "commands/remove/remove.hpp" +#include "commands/replay/replay.hpp" #include "commands/repo/repo.hpp" #include "commands/repoquery/repoquery.hpp" #include "commands/search/search.hpp" @@ -674,6 +675,7 @@ static void add_commands(Context & context) { context.add_and_initialize_command(std::make_unique(context)); context.add_and_initialize_command(std::make_unique(context)); context.add_and_initialize_command(std::make_unique(context)); + context.add_and_initialize_command(std::make_unique(context)); context.add_and_initialize_command(std::make_unique(context)); context.add_and_initialize_command(std::make_unique(context)); diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt index ebb15e4f7..27052df06 100644 --- a/doc/CMakeLists.txt +++ b/doc/CMakeLists.txt @@ -78,6 +78,7 @@ if(WITH_MAN) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/man/dnf5-provides.8 DESTINATION share/man/man8) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/man/dnf5-reinstall.8 DESTINATION share/man/man8) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/man/dnf5-remove.8 DESTINATION share/man/man8) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/man/dnf5-replay.8 DESTINATION share/man/man8) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/man/dnf5-repo.8 DESTINATION share/man/man8) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/man/dnf5-repoquery.8 DESTINATION share/man/man8) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/man/dnf5-search.8 DESTINATION share/man/man8) diff --git a/doc/changes.rst b/doc/changes.rst index ce26ea2e5..0de9dcd62 100644 --- a/doc/changes.rst +++ b/doc/changes.rst @@ -203,6 +203,10 @@ Changes to individual commands ``history`` * ``undo`` subcommand now accepts ``--ignore-extras`` and ``--ignore-installed`` like original ``history replay`` command. + * ``store`` subcommand now creates a directory with transaction JSON file instead of a single transaction JSON file directly. + * ``store`` subcommand's ``--output`` option now accepts a directory path instead of a file. The default is ``./transaction``. + * ``replay`` subcommand was moved to a standalone ``replay`` command, that now accepts a path to a directory instead of a file path. + The directory can be created with ``--store`` option and in addition to the JSON transaction, it can contain packages, group and environments used in the transaction. ``info`` * Dropped ``--all`` option since this behavior is the default one. diff --git a/doc/commands/history.8.rst b/doc/commands/history.8.rst index c7a0bb9ba..9120b0fb9 100644 --- a/doc/commands/history.8.rst +++ b/doc/commands/history.8.rst @@ -59,10 +59,7 @@ Subcommands | Undo all transactions performed after the specified transaction. ``store`` - | Store the transaction into the file. - -``replay`` - | Replay the transaction that was previously stored into the file. + | Store the transaction into a directory. Options for ``list`` and ``info`` diff --git a/doc/commands/index.rst b/doc/commands/index.rst index a08dd737b..18154c957 100644 --- a/doc/commands/index.rst +++ b/doc/commands/index.rst @@ -27,6 +27,7 @@ DNF5 Commands provides.8 reinstall.8 remove.8 + replay.8 repo.8 repoquery.8 search.8 diff --git a/doc/commands/replay.8.rst b/doc/commands/replay.8.rst new file mode 100644 index 000000000..d2d7719c2 --- /dev/null +++ b/doc/commands/replay.8.rst @@ -0,0 +1,72 @@ +.. + Copyright Contributors to the libdnf project. + + This file is part of libdnf: https://github.com/rpm-software-management/libdnf/ + + Libdnf is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Libdnf is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty 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 libdnf. If not, see . + +.. _replay_command_ref-label: + +############### + Replay Command +############### + +Synopsis +======== + +``dnf5 replay [options] `` + + +Description +=========== + +Replay a transaction stored in a directory at ````. The transaction directory can be created either by +the ``--store`` option, available for all transaction commands, or by `History Store Command`. The replay will perform +the exact same operations on the packages as in the original transaction and will return with an error in case of any +differences in installed packages or their versions. + +To run the replay the transaction directory has to contain a file with the transaction in JSON format named ``transaction.json``. +The directory can also contain packages, comps groups or comps environments that will be used used in the replayed transaction. + + +Options +======= + +``--ignore-extras`` + | Don't consider extra packages pulled into the transaction as errors. + | They will still be reported as warnings. + +``--ignore-installed`` + | Don't consider mismatches between installed and stored transaction packages as errors. + | They will still be reported as warnings. + | For install actions skip already installed packages. + | For upgrade actions skip groups or environments that are not installed. + | For remove actions skip not installed packages/groups/environments. + | Using this option can result in an empty transaction. + +``--skip-broken`` + | Resolve any dependency problems by removing packages that are causing problems from the transaction. + +``--skip-unavailable`` + | In case some packages stored in the transaction are not available on the target system, + | skip them instead of erroring out. + +Examples +======== + +``dnf5 replay ./transaction`` + | Replay a transaction stored at ./transaction. + +``dnf5 replay ./transaction --skip-unavailable`` + | Replay a transaction stored at ./transaction skipping unavailable packages. diff --git a/doc/conf.py.in b/doc/conf.py.in index 8893b0838..f2d86d80b 100644 --- a/doc/conf.py.in +++ b/doc/conf.py.in @@ -134,6 +134,7 @@ man_pages = [ ('commands/provides.8', 'dnf5-provides', 'Provides Command', AUTHORS, 8), ('commands/reinstall.8', 'dnf5-reinstall', 'Reinstall Command', AUTHORS, 8), ('commands/remove.8', 'dnf5-remove', 'Remove Command', AUTHORS, 8), + ('commands/replay.8', 'dnf5-replay', 'Replay Command', AUTHORS, 8), ('commands/repo.8', 'dnf5-repo', 'Repo Command', AUTHORS, 8), ('commands/repoquery.8', 'dnf5-repoquery', 'Repoquery Command', AUTHORS, 8), ('commands/search.8', 'dnf5-search', 'Search Command', AUTHORS, 8), diff --git a/doc/dnf5.8.rst b/doc/dnf5.8.rst index 1c3f2a002..d49f38ecf 100644 --- a/doc/dnf5.8.rst +++ b/doc/dnf5.8.rst @@ -112,6 +112,9 @@ For more details see the separate man page for the specific command, f.e. ``man :ref:`remove ` | Remove packages. +:ref:`replay ` + | Replay stored transactions. + :ref:`repo ` | Manage repositories. diff --git a/include/libdnf5/base/goal_elements.hpp b/include/libdnf5/base/goal_elements.hpp index 54d17cf8f..a38a5802c 100644 --- a/include/libdnf5/base/goal_elements.hpp +++ b/include/libdnf5/base/goal_elements.hpp @@ -121,7 +121,8 @@ enum class GoalProblem : uint32_t { MODULE_CANNOT_SWITH_STREAMS = (1 << 21), /// Error when transaction contains additional unexpected elements. /// Used when replaying transactions. - EXTRA = (1 << 22) + EXTRA = (1 << 22), + MALFORMED = (1 << 23) }; /// Types of Goal actions @@ -144,6 +145,7 @@ enum class GoalAction { ENABLE, DISABLE, RESET, + REPLAY_PARSE, REPLAY_INSTALL, REPLAY_REMOVE, REPLAY_UPGRADE, diff --git a/libdnf5/base/goal.cpp b/libdnf5/base/goal.cpp index 342207fc6..6415d0bdd 100644 --- a/libdnf5/base/goal.cpp +++ b/libdnf5/base/goal.cpp @@ -573,6 +573,9 @@ GoalProblem Goal::Impl::add_specs_to_goal(base::Transaction & transaction) { case GoalAction::RESET: { libdnf_throw_assertion("Unsupported action \"RESET\""); } + case GoalAction::REPLAY_PARSE: { + libdnf_throw_assertion("Unsupported action \"REPLAY PARSE\""); + } case GoalAction::REPLAY_INSTALL: { libdnf_throw_assertion("Unsupported action \"REPLAY INSTALL\""); } @@ -661,10 +664,22 @@ GoalProblem Goal::Impl::add_serialized_transaction_to_goal(base::Transaction & t auto & [replay_path, settings] = *serialized_transaction; utils::fs::File replay_file(replay_path, "r"); - auto replay_location = replay_path.remove_filename(); - auto replay = transaction::parse_transaction_replay(replay_file.read()); - - return add_replay_to_goal(transaction, replay, settings, replay_location); + auto replay_location = replay_path; + replay_location.remove_filename(); + try { + auto replay = transaction::parse_transaction_replay(replay_file.read()); + return add_replay_to_goal(transaction, replay, settings, replay_location); + } catch (const libdnf5::transaction::TransactionReplayError & ex) { + transaction.p_impl->add_resolve_log( + GoalAction::REPLAY_PARSE, + libdnf5::GoalProblem::MALFORMED, + settings, + libdnf5::transaction::TransactionItemType::PACKAGE, + replay_path, + {ex.what()}, + libdnf5::Logger::Level::ERROR); + return libdnf5::GoalProblem::MALFORMED; + } } static std::set query_to_vec_of_nevra_str(const libdnf5::rpm::PackageQuery & query) { diff --git a/libdnf5/base/goal_elements.cpp b/libdnf5/base/goal_elements.cpp index dcc00b99d..46455801a 100644 --- a/libdnf5/base/goal_elements.cpp +++ b/libdnf5/base/goal_elements.cpp @@ -380,6 +380,8 @@ std::string goal_action_to_string(GoalAction action) { return _("Disable"); case GoalAction::RESET: return _("Reset"); + case GoalAction::REPLAY_PARSE: + return _("Parse serialized transaction"); case GoalAction::REPLAY_INSTALL: return _("Install action"); case GoalAction::REPLAY_REMOVE: diff --git a/libdnf5/base/log_event.cpp b/libdnf5/base/log_event.cpp index b3693263a..15a29cecd 100644 --- a/libdnf5/base/log_event.cpp +++ b/libdnf5/base/log_event.cpp @@ -337,6 +337,9 @@ std::string LogEvent::to_string( *spec, *additional_data.begin())); } + case GoalProblem::MALFORMED: { + return ret.append(utils::sformat(_("Cannot parse file: '{0}': {1}.\n"), *spec, *additional_data.begin())); + } } return ret; } diff --git a/libdnf5/transaction/transaction_sr.cpp b/libdnf5/transaction/transaction_sr.cpp index 88a0645cd..48562213f 100644 --- a/libdnf5/transaction/transaction_sr.cpp +++ b/libdnf5/transaction/transaction_sr.cpp @@ -44,13 +44,6 @@ namespace libdnf5::transaction { constexpr const char * VERSION_MAJOR = "1"; constexpr const char * VERSION_MINOR = "0"; -class TransactionReplayError : public Error { -public: - using Error::Error; - const char * get_domain_name() const noexcept override { return "libdnf5::transaction"; } - const char * get_name() const noexcept override { return "TransactionReplayError"; } -}; - TransactionReplay parse_transaction_replay(const std::string & json_serialized_transaction) { if (json_serialized_transaction.empty()) { throw TransactionReplayError(M_("Transaction replay JSON serialized transaction input is empty")); diff --git a/libdnf5/transaction/transaction_sr.hpp b/libdnf5/transaction/transaction_sr.hpp index 830cbb72b..e72ca076d 100644 --- a/libdnf5/transaction/transaction_sr.hpp +++ b/libdnf5/transaction/transaction_sr.hpp @@ -29,6 +29,13 @@ along with libdnf. If not, see . namespace libdnf5::transaction { +class TransactionReplayError : public Error { +public: + using Error::Error; + const char * get_domain_name() const noexcept override { return "libdnf5::transaction"; } + const char * get_name() const noexcept override { return "TransactionReplayError"; } +}; + struct GroupReplay { TransactionItemAction action;