diff --git a/dnf5.spec b/dnf5.spec index 3d9eae939..5b4d73e43 100644 --- a/dnf5.spec +++ b/dnf5.spec @@ -296,8 +296,7 @@ It supports RPM packages, modulemd modules, and comps groups & environments. %{_mandir}/man8/dnf*-download.8.* %{_mandir}/man8/dnf*-environment.8.* %{_mandir}/man8/dnf*-group.8.* -# TODO(jkolarik): history is not ready yet -# %%{_mandir}/man8/dnf*-history.8.* +%{_mandir}/man8/dnf*-history.8.* %{_mandir}/man8/dnf*-info.8.* %{_mandir}/man8/dnf*-install.8.* %{_mandir}/man8/dnf*-leaves.8.* diff --git a/dnf5/commands/history/history.cpp b/dnf5/commands/history/history.cpp index 791c516ce..3d86d0367 100644 --- a/dnf5/commands/history/history.cpp +++ b/dnf5/commands/history/history.cpp @@ -57,7 +57,7 @@ void HistoryCommand::register_subcommands() { software_management_commands_group->set_header("Software Management Commands:"); cmd.register_group(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); register_subcommand(std::make_unique(get_context()), software_management_commands_group); register_subcommand(std::make_unique(get_context()), software_management_commands_group); } diff --git a/dnf5/commands/history/history_redo.cpp b/dnf5/commands/history/history_redo.cpp index a329e0b40..e46ea10f1 100644 --- a/dnf5/commands/history/history_redo.cpp +++ b/dnf5/commands/history/history_redo.cpp @@ -19,14 +19,60 @@ along with libdnf. If not, see . #include "history_redo.hpp" +#include "commands/history/transaction_id.hpp" +#include "dnf5/shared_options.hpp" + +#include + namespace dnf5 { using namespace libdnf5::cli; void HistoryRedoCommand::set_argument_parser() { - get_argument_parser_command()->set_description("Repeat transactions"); + get_argument_parser_command()->set_description("Repeat all actions from the specified transaction"); + transaction_specs = std::make_unique(*this, 1); + auto & ctx = get_context(); + transaction_specs->get_arg()->set_complete_hook_func(create_history_id_autocomplete(ctx)); + auto skip_unavailable = std::make_unique(*this); } -void HistoryRedoCommand::run() {} +void HistoryRedoCommand::configure() { + auto & context = get_context(); + context.set_load_system_repo(true); + context.set_load_available_repos(Context::LoadAvailableRepos::ENABLED); +} + +void HistoryRedoCommand::run() { + auto ts_specs = transaction_specs->get_value(); + libdnf5::transaction::TransactionHistory history(get_context().get_base()); + std::vector target_trans; + + target_trans = list_transactions_from_specs(history, ts_specs); + + if (target_trans.size() < 1) { + throw libdnf5::cli::CommandExitError(1, M_("No matching transaction ID found, exactly one required.")); + } + + if (target_trans.size() > 1) { + throw libdnf5::cli::CommandExitError(1, M_("Matched more than one transaction ID, exactly one required.")); + } + + auto goal = get_context().get_goal(); + // To enable removal of dependency packages not present in the selected transaction + // it requires allow_erasing. This will inform the user that additional removes + // are required and the transaction won't proceed without --ignore-extras. + goal->set_allow_erasing(true); + + auto settings = libdnf5::GoalJobSettings(); + settings.set_ignore_extras(true); + settings.set_ignore_installed(true); + // If a package is already installed in the requested version but with a different reason override the reason. + // This is useful when redoing an interrupted transaction. + // The longest part of a transaction is running rpm changes, saving reasons is done after rpm finishes. If a transaction + // is interrupted in the middle of the rpm part some rpms are changed but reasons are not updated at all. To handle this + // enforce the reasons in the selected transaction. + settings.set_override_reasons(true); + goal->add_redo_transaction(target_trans[0], settings); +} } // namespace dnf5 diff --git a/dnf5/commands/history/history_redo.hpp b/dnf5/commands/history/history_redo.hpp index 3b54a0c36..42569ca94 100644 --- a/dnf5/commands/history/history_redo.hpp +++ b/dnf5/commands/history/history_redo.hpp @@ -21,6 +21,8 @@ along with libdnf. If not, see . #ifndef DNF5_COMMANDS_HISTORY_HISTORY_REDO_HPP #define DNF5_COMMANDS_HISTORY_HISTORY_REDO_HPP +#include "commands/history/arguments.hpp" + #include @@ -31,7 +33,10 @@ class HistoryRedoCommand : public Command { public: explicit HistoryRedoCommand(Context & context) : Command(context, "redo") {} void set_argument_parser() override; + void configure() override; void run() override; + + std::unique_ptr transaction_specs{nullptr}; }; diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt index 27052df06..de64b81aa 100644 --- a/doc/CMakeLists.txt +++ b/doc/CMakeLists.txt @@ -65,8 +65,7 @@ if(WITH_MAN) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/man/dnf5-download.8 DESTINATION share/man/man8) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/man/dnf5-environment.8 DESTINATION share/man/man8) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/man/dnf5-group.8 DESTINATION share/man/man8) - # TODO(jkolarik): history is not ready yet - # install(FILES ${CMAKE_CURRENT_BINARY_DIR}/man/dnf5-history.8 DESTINATION share/man/man8) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/man/dnf5-history.8 DESTINATION share/man/man8) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/man/dnf5-info.8 DESTINATION share/man/man8) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/man/dnf5-install.8 DESTINATION share/man/man8) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/man/dnf5-leaves.8 DESTINATION share/man/man8) diff --git a/doc/commands/history.8.rst b/doc/commands/history.8.rst index 9120b0fb9..15782c71b 100644 --- a/doc/commands/history.8.rst +++ b/doc/commands/history.8.rst @@ -16,8 +16,6 @@ You should have received a copy of the GNU General Public License along with libdnf. If not, see . -.. - # TODO(jkolarik): Command not ready yet in upstream ... .. _history_command_ref-label: @@ -54,6 +52,9 @@ Subcommands ``redo`` | Repeat the specified transaction. + | Automatically uses ``--ignore-extras`` and ``--ignore-installed``. + | Unlike the rest of history commands it overrides reasons for transaction packages that are already installed. + | This command is useful to finish interrupted transactons. ``rollback`` | Undo all transactions performed after the specified transaction. @@ -69,12 +70,16 @@ Options for ``list`` and ``info`` | Reverse the order of transactions in the output. -Options for ``undo`` -==================== +Options for ``undo``, ``rollback`` and ``redo`` +=============================================== ``--skip-unavailable`` | Allow skipping packages actions that are not possible perform. + +Options for ``undo`` and ``rollback`` +===================================== + ``--ignore-extras`` | Don't consider extra packages pulled into the transaction as errors. | They will still be reported as warnings. diff --git a/doc/commands/index.rst b/doc/commands/index.rst index 18154c957..38f548228 100644 --- a/doc/commands/index.rst +++ b/doc/commands/index.rst @@ -16,6 +16,7 @@ DNF5 Commands download.8 environment.8 group.8 + history.8 info.8 install.8 leaves.8 @@ -37,5 +38,3 @@ DNF5 Commands versionlock.8 .. - # TODO(jkolarik): history not ready yet - history.8 diff --git a/doc/conf.py.in b/doc/conf.py.in index f2d86d80b..593923fc9 100644 --- a/doc/conf.py.in +++ b/doc/conf.py.in @@ -122,7 +122,7 @@ man_pages = [ ('commands/download.8', 'dnf5-download', 'Download Command', AUTHORS, 8), ('commands/environment.8', 'dnf5-environment', 'Environment Command', AUTHORS, 8), ('commands/group.8', 'dnf5-group', 'Group Command', AUTHORS, 8), - # TODO(jkolarik): ('commands/history.8', 'dnf5-history', 'History Command', AUTHORS, 8), + ('commands/history.8', 'dnf5-history', 'History Command', AUTHORS, 8), ('commands/info.8', 'dnf5-info', 'Info Command', AUTHORS, 8), ('commands/install.8', 'dnf5-install', 'Install Command', AUTHORS, 8), ('commands/leaves.8', 'dnf5-leaves', 'Leaves Command', AUTHORS, 8), diff --git a/doc/dnf5.8.rst b/doc/dnf5.8.rst index d49f38ecf..19edb7f66 100644 --- a/doc/dnf5.8.rst +++ b/doc/dnf5.8.rst @@ -79,6 +79,9 @@ For more details see the separate man page for the specific command, f.e. ``man :ref:`group ` | Manage comps groups. +:ref:`history ` + | Manage transaction history. + :ref:`info ` | Provide detailed information about installed or available packages. @@ -136,11 +139,6 @@ For more details see the separate man page for the specific command, f.e. ``man :ref:`versionlock ` | Protect packages from updates to newer versions. -.. - # TODO(jkolarik): History command is not ready yet - :ref:`history ` - | Manage transaction history. - Plugin commands --------------- @@ -417,6 +415,7 @@ Commands: | :manpage:`dnf5-download(8)`, :ref:`Download command ` | :manpage:`dnf5-environment(8)`, :ref:`Environment command ` | :manpage:`dnf5-group(8)`, :ref:`Group command ` + | :manpage:`dnf5-history(8)`, :ref:`History command, ` | :manpage:`dnf5-info(8)`, :ref:`Info command ` | :manpage:`dnf5-install(8)`, :ref:`Install command ` | :manpage:`dnf5-leaves(8)`, :ref:`Leaves command ` @@ -436,9 +435,6 @@ Commands: | :manpage:`dnf5-upgrade(8)`, :ref:`Upgrade command ` | :manpage:`dnf5-versionlock(8)`, :ref:`Versionlock command ` -.. - # TODO(jkolarik): History command is not ready yet - | :manpage:`dnf5-history(8)`, :ref:`History command, ` Application Plugins: | :manpage:`dnf5-automatic(8)`, :ref:`Automatic command ` diff --git a/include/libdnf5/base/goal.hpp b/include/libdnf5/base/goal.hpp index b625b174c..560cbb3c0 100644 --- a/include/libdnf5/base/goal.hpp +++ b/include/libdnf5/base/goal.hpp @@ -384,6 +384,16 @@ class LIBDNF_API Goal { const std::vector & transactions, const libdnf5::GoalJobSettings & settings = libdnf5::GoalJobSettings()); + /// @warning This method is experimental/unstable and should not be relied on. It may be removed without warning + /// Add redo request of history transaction to the goal. + /// Can be called only once per Goal. + /// + /// @param transaction A history transaction to be repeated. + /// @param settings A structure to override default goal settings. + void add_redo_transaction( + const libdnf5::transaction::Transaction & transaction, + const libdnf5::GoalJobSettings & settings = libdnf5::GoalJobSettings()); + /// When true it allows to remove installed packages to resolve dependency problems void set_allow_erasing(bool value); diff --git a/include/libdnf5/base/goal_elements.hpp b/include/libdnf5/base/goal_elements.hpp index 36c4e04fb..096fee615 100644 --- a/include/libdnf5/base/goal_elements.hpp +++ b/include/libdnf5/base/goal_elements.hpp @@ -349,20 +349,29 @@ struct LIBDNF_API GoalJobSettings : public ResolveSpecSettings { void set_to_repo_ids(std::vector to_repo_ids); std::vector get_to_repo_ids() const; - /// If set to true, after resolving serialized or reverted transactions don't check for + /// If set to true, after resolving serialized, reverted or redo transactions don't check for /// extra packages pulled into the transaction. /// /// Default: false void set_ignore_extras(bool ignore_extras); bool get_ignore_extras() const; - /// If set to true, after resolving serialized or reverted transactions don't check for + /// If set to true, after resolving serialized, reverted or redo transactions don't check for /// installed packages matching those in the transactions. /// /// Default: false void set_ignore_installed(bool ignore_installed); bool get_ignore_installed() const; + /// If set to true, after resolving serialized, reverted or redo transactions override reasons + /// of already installed packages. + /// This option only has an effect if ignore_installed is set otherwise the transaction fails + /// when it contains already installed packages. + /// + /// Default: false + void set_override_reasons(bool override_reasons); + bool get_override_reasons() const; + private: friend class Goal; diff --git a/include/libdnf5/transaction/comps_group.hpp b/include/libdnf5/transaction/comps_group.hpp index 4824ad451..c2e08c174 100644 --- a/include/libdnf5/transaction/comps_group.hpp +++ b/include/libdnf5/transaction/comps_group.hpp @@ -48,6 +48,11 @@ class LIBDNF_API CompsGroup : public TransactionItem { // @replaces libdnf:transaction/CompsGroupItem.hpp:method:CompsGroupItem.toStr() std::string to_string() const; + /// Get types of the packages to be installed with the group (related xml elements: ``) + /// + // @replaces libdnf:transaction/CompsGroupItem.hpp:method:CompsGroupItem.getPackageTypes() + libdnf5::comps::PackageType get_package_types() const noexcept; + CompsGroup(const CompsGroup & src); CompsGroup & operator=(const CompsGroup & src); CompsGroup(CompsGroup && src) noexcept; @@ -92,11 +97,6 @@ class LIBDNF_API CompsGroup : public TransactionItem { // @replaces libdnf:transaction/CompsGroupItem.hpp:method:CompsGroupItem.setTranslatedName(const std::string & value) LIBDNF_LOCAL void set_translated_name(const std::string & value); - /// Get types of the packages to be installed with the group (related xml elements: ``) - /// - // @replaces libdnf:transaction/CompsGroupItem.hpp:method:CompsGroupItem.getPackageTypes() - LIBDNF_LOCAL libdnf5::comps::PackageType get_package_types() const noexcept; - /// Set types of the packages to be installed with the group (related xml elements: ``) /// // @replaces libdnf:transaction/CompsGroupItem.hpp:method:CompsGroupItem.setPackageTypes(libdnf::CompsPackageType value) diff --git a/libdnf5/base/goal.cpp b/libdnf5/base/goal.cpp index 6d2afb189..5cda9b8d1 100644 --- a/libdnf5/base/goal.cpp +++ b/libdnf5/base/goal.cpp @@ -159,6 +159,7 @@ class Goal::Impl { GoalProblem add_reason_change_specs_to_goal(base::Transaction & transaction); GoalProblem resolve_reverted_transactions(base::Transaction & transaction); + GoalProblem resolve_redo_transaction(base::Transaction & transaction); std::pair add_install_to_goal( base::Transaction & transaction, GoalAction action, const std::string & spec, GoalJobSettings & settings); @@ -271,6 +272,7 @@ class Goal::Impl { std::unique_ptr> serialized_transaction; std::unique_ptr, GoalJobSettings>> revert_transactions; + std::unique_ptr> redo_transaction; }; Goal::Goal(const BaseWeakPtr & base) : p_impl(new Impl(base)) {} @@ -857,8 +859,21 @@ GoalProblem Goal::Impl::add_replay_to_goal( if (!query_na.empty() && is_installonly.empty()) { query_nevra.filter_installed(); - auto problem = query_nevra.empty() ? GoalProblem::INSTALLED_IN_DIFFERENT_VERSION - : GoalProblem::ALREADY_INSTALLED; + auto problem = GoalProblem::INSTALLED_IN_DIFFERENT_VERSION; + + if (!query_nevra.empty()) { + problem = GoalProblem::ALREADY_INSTALLED; + if (settings.get_override_reasons()) { + if ((*query_nevra.begin()).get_reason() != package_replay.reason) { + rpm_reason_change_specs.push_back(std::make_tuple( + package_replay.reason, + package_replay.nevra, + package_replay.group_id, + settings_per_package)); + } + } + } + auto log_level = libdnf5::Logger::Level::WARNING; if (!settings.get_ignore_installed()) { log_level = libdnf5::Logger::Level::ERROR; @@ -873,7 +888,9 @@ GoalProblem Goal::Impl::add_replay_to_goal( package_replay.nevra, query_to_vec_of_nevra_str(query_na), log_level); - continue; + if (problem == GoalProblem::ALREADY_INSTALLED) { + continue; + } } } } @@ -2950,6 +2967,13 @@ GoalProblem Goal::Impl::resolve_reverted_transactions(base::Transaction & transa return ret; } +GoalProblem Goal::Impl::resolve_redo_transaction(base::Transaction & transaction) { + if (!redo_transaction) { + return GoalProblem::NO_PROBLEM; + } + auto & [trans, settings] = *redo_transaction; + return add_replay_to_goal(transaction, transaction::to_replay(trans), settings); +} void Goal::Impl::add_paths_to_goal() { if (rpm_filepaths.empty()) { @@ -3093,6 +3117,7 @@ base::Transaction Goal::resolve() { // Both serialized and reverted transactions use TransactionReplay. ret |= p_impl->add_serialized_transaction_to_goal(transaction); ret |= p_impl->resolve_reverted_transactions(transaction); + ret |= p_impl->resolve_redo_transaction(transaction); p_impl->add_paths_to_goal(); @@ -3261,6 +3286,12 @@ void Goal::add_revert_transactions( std::make_unique, GoalJobSettings>>(transactions, settings); } +void Goal::add_redo_transaction( + const libdnf5::transaction::Transaction & transaction, const libdnf5::GoalJobSettings & settings) { + libdnf_user_assert(!p_impl->redo_transaction, "Redo transactions cannot be set multiple times."); + p_impl->redo_transaction = + std::make_unique>(transaction, settings); +} void Goal::reset() { p_impl->module_specs.clear(); diff --git a/libdnf5/base/goal_elements.cpp b/libdnf5/base/goal_elements.cpp index d8e0be8b8..c59c5257b 100644 --- a/libdnf5/base/goal_elements.cpp +++ b/libdnf5/base/goal_elements.cpp @@ -177,6 +177,10 @@ class GoalJobSettings::Impl { /// Used by history undo, system upgrade, ... bool ignore_installed{false}; + /// For replaying transactions override reasons for already installed packages. + /// Used by history redo. + bool override_reasons{false}; + GoalUsedSetting used_skip_broken{GoalUsedSetting::UNUSED}; GoalUsedSetting used_skip_unavailable{GoalUsedSetting::UNUSED}; GoalUsedSetting used_best{GoalUsedSetting::UNUSED}; @@ -518,4 +522,11 @@ bool GoalJobSettings::get_ignore_installed() const { return p_impl->ignore_installed; } +void GoalJobSettings::set_override_reasons(bool override_reasons) { + p_impl->override_reasons = override_reasons; +} +bool GoalJobSettings::get_override_reasons() const { + return p_impl->override_reasons; +} + } // namespace libdnf5 diff --git a/libdnf5/transaction/transaction.cpp b/libdnf5/transaction/transaction.cpp index 3a0f6f427..43917cbd1 100644 --- a/libdnf5/transaction/transaction.cpp +++ b/libdnf5/transaction/transaction.cpp @@ -326,46 +326,7 @@ void Transaction::finish(TransactionState state) { } std::string Transaction::serialize() { - TransactionReplay transaction_replay; - - for (const auto & pkg : get_packages()) { - PackageReplay package_replay; - - // Use to_nevra_string in order to have nevra wihtout epoch if it is 0 - package_replay.nevra = rpm::to_nevra_string(pkg); - package_replay.action = pkg.get_action(); - package_replay.reason = pkg.get_reason(); - package_replay.repo_id = pkg.get_repoid(); - //TODO(amatej): Add the group_id for reason change? - - transaction_replay.packages.push_back(package_replay); - } - - for (const auto & group : get_comps_groups()) { - GroupReplay group_replay; - - group_replay.group_id = group.to_string(); - group_replay.action = group.get_action(); - group_replay.reason = group.get_reason(); - group_replay.repo_id = group.get_repoid(); - group_replay.package_types = group.get_package_types(); - - transaction_replay.groups.push_back(group_replay); - } - - for (const auto & environment : get_comps_environments()) { - EnvironmentReplay environment_replay; - - environment_replay.environment_id = environment.to_string(); - environment_replay.action = environment.get_action(); - environment_replay.repo_id = environment.get_repoid(); - - transaction_replay.environments.push_back(environment_replay); - } - - ////TODO(amatej): potentially add modules - - return json_serialize(transaction_replay); + return json_serialize(to_replay(*this)); } diff --git a/libdnf5/transaction/transaction_sr.cpp b/libdnf5/transaction/transaction_sr.cpp index bba285c11..d809344bd 100644 --- a/libdnf5/transaction/transaction_sr.cpp +++ b/libdnf5/transaction/transaction_sr.cpp @@ -340,4 +340,47 @@ std::string json_serialize(const TransactionReplay & transaction_replay) { return json; } +TransactionReplay to_replay(libdnf5::transaction::Transaction & trans) { + TransactionReplay transaction_replay; + + for (const auto & pkg : trans.get_packages()) { + PackageReplay package_replay; + + // Use to_nevra_string in order to have nevra wihtout epoch if it is 0 + package_replay.nevra = rpm::to_nevra_string(pkg); + package_replay.action = pkg.get_action(); + package_replay.reason = pkg.get_reason(); + package_replay.repo_id = pkg.get_repoid(); + //TODO(amatej): Add the group_id for reason change? + + transaction_replay.packages.push_back(package_replay); + } + + for (const auto & group : trans.get_comps_groups()) { + GroupReplay group_replay; + + group_replay.group_id = group.to_string(); + group_replay.action = group.get_action(); + group_replay.reason = group.get_reason(); + group_replay.repo_id = group.get_repoid(); + group_replay.package_types = group.get_package_types(); + + transaction_replay.groups.push_back(group_replay); + } + + for (const auto & environment : trans.get_comps_environments()) { + EnvironmentReplay environment_replay; + + environment_replay.environment_id = environment.to_string(); + environment_replay.action = environment.get_action(); + environment_replay.repo_id = environment.get_repoid(); + + transaction_replay.environments.push_back(environment_replay); + } + + ////TODO(amatej): potentially add modules + + return transaction_replay; +} + } // namespace libdnf5::transaction diff --git a/libdnf5/transaction/transaction_sr.hpp b/libdnf5/transaction/transaction_sr.hpp index 3807a4ed9..8acf2341a 100644 --- a/libdnf5/transaction/transaction_sr.hpp +++ b/libdnf5/transaction/transaction_sr.hpp @@ -20,6 +20,8 @@ along with libdnf. If not, see . #ifndef LIBDNF5_TRANSACTION_TRANSACTION_REPLAY_HPP #define LIBDNF5_TRANSACTION_TRANSACTION_REPLAY_HPP +#include "libdnf5/transaction/transaction.hpp" + #include #include #include @@ -75,6 +77,7 @@ struct TransactionReplay { TransactionReplay parse_transaction_replay(const std::string & json_serialized_transaction); std::string json_serialize(const TransactionReplay & transaction_replay); +TransactionReplay to_replay(libdnf5::transaction::Transaction & trans); } // namespace libdnf5::transaction