diff --git a/dnf5/commands/history/arguments.cpp b/dnf5/commands/history/arguments.cpp new file mode 100644 index 000000000..a54a33603 --- /dev/null +++ b/dnf5/commands/history/arguments.cpp @@ -0,0 +1,51 @@ +/* +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 Lesser General Public License as published by +the Free Software Foundation, either version 2.1 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 Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with libdnf. If not, see . +*/ + + +#include "arguments.hpp" + + +namespace dnf5 { + + +std::function(const char * arg)> create_history_id_autocomplete(Context & ctx) { + return [&ctx](const char * arg) { + const std::string_view to_complete{arg}; + libdnf5::transaction::TransactionHistory history(ctx.get_base()); + std::vector ids = history.list_transaction_ids(); + std::vector all_string_ids; + std::transform( + ids.begin(), ids.end(), std::back_inserter(all_string_ids), [](int num) { return std::to_string(num); }); + std::string last; + std::vector possible_ids; + for (const auto & id : all_string_ids) { + if (id.compare(0, to_complete.length(), to_complete) == 0) { + possible_ids.emplace_back(id); + last = id; + } + } + if (possible_ids.size() == 1) { + possible_ids[0] = last + " "; + } + return possible_ids; + }; +} + + +} // namespace dnf5 diff --git a/dnf5/commands/history/arguments.hpp b/dnf5/commands/history/arguments.hpp index e67f4f647..00abac49c 100644 --- a/dnf5/commands/history/arguments.hpp +++ b/dnf5/commands/history/arguments.hpp @@ -21,6 +21,8 @@ along with libdnf. If not, see . #ifndef DNF5_COMMANDS_HISTORY_ARGUMENTS_HPP #define DNF5_COMMANDS_HISTORY_ARGUMENTS_HPP +#include "dnf5/context.hpp" + #include #include @@ -30,8 +32,9 @@ namespace dnf5 { class TransactionSpecArguments : public libdnf5::cli::session::StringArgumentList { public: - explicit TransactionSpecArguments(libdnf5::cli::session::Command & command) - : StringArgumentList(command, "transaction-id", _("Transaction ID")) {} + explicit TransactionSpecArguments( + libdnf5::cli::session::Command & command, int nrepeats = libdnf5::cli::ArgumentParser::PositionalArg::OPTIONAL) + : StringArgumentList(command, "transaction-id", _("Transaction ID"), nrepeats) {} }; @@ -41,6 +44,32 @@ class ReverseOption : public libdnf5::cli::session::BoolOption { : BoolOption(command, "reverse", '\0', _("Reverse the order of transactions."), false) {} }; +class IgnoreInstalledOption : public libdnf5::cli::session::BoolOption { +public: + explicit IgnoreInstalledOption(libdnf5::cli::session::Command & command) + : BoolOption( + command, + "ignore-installed", + '\0', + _("Don't consider mismatches between installed and stored transaction packages as errors. This can " + "result in an empty transaction because among other things the option can ignore failing Remove " + "actions."), + false) {} +}; + +class IgnoreExtrasOption : public libdnf5::cli::session::BoolOption { +public: + explicit IgnoreExtrasOption(libdnf5::cli::session::Command & command) + : BoolOption( + command, + "ignore-extras", + '\0', + _("Don't consider extra packages pulled into the transaction as errors."), + false) {} +}; + +std::function(const char * arg)> create_history_id_autocomplete(Context & ctx); + } // namespace dnf5 diff --git a/dnf5/commands/history/history.cpp b/dnf5/commands/history/history.cpp index 80a6f49a2..ba4612bf6 100644 --- a/dnf5/commands/history/history.cpp +++ b/dnf5/commands/history/history.cpp @@ -57,7 +57,7 @@ void HistoryCommand::register_subcommands() { auto * software_management_commands_group = parser.add_new_group("history_software_management_commands"); 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_info.cpp b/dnf5/commands/history/history_info.cpp index 1c78df0d6..acc8bb333 100644 --- a/dnf5/commands/history/history_info.cpp +++ b/dnf5/commands/history/history_info.cpp @@ -33,6 +33,8 @@ void HistoryInfoCommand::set_argument_parser() { get_argument_parser_command()->set_description("Print details about transactions"); transaction_specs = std::make_unique(*this); + auto & ctx = get_context(); + transaction_specs->get_arg()->set_complete_hook_func(create_history_id_autocomplete(ctx)); reverse = std::make_unique(*this); } diff --git a/dnf5/commands/history/history_undo.cpp b/dnf5/commands/history/history_undo.cpp index 712c601ff..d15ef7cc7 100644 --- a/dnf5/commands/history/history_undo.cpp +++ b/dnf5/commands/history/history_undo.cpp @@ -19,14 +19,57 @@ along with libdnf. If not, see . #include "history_undo.hpp" +#include "arguments.hpp" +#include "commands/history/transaction_id.hpp" +#include "dnf5/shared_options.hpp" + +#include + namespace dnf5 { using namespace libdnf5::cli; void HistoryUndoCommand::set_argument_parser() { - get_argument_parser_command()->set_description("Revert all actions from the specified transactions"); + get_argument_parser_command()->set_description("Revert 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); + ignore_extras = std::make_unique(*this); + ignore_installed = std::make_unique(*this); } -void HistoryUndoCommand::run() {} +void HistoryUndoCommand::configure() { + auto & context = get_context(); + context.set_load_system_repo(true); + context.set_load_available_repos(Context::LoadAvailableRepos::ENABLED); +} + +void HistoryUndoCommand::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(ignore_extras->get_value()); + settings.set_ignore_installed(ignore_installed->get_value()); + goal->add_revert_transactions({target_trans}, settings); +} } // namespace dnf5 diff --git a/dnf5/commands/history/history_undo.hpp b/dnf5/commands/history/history_undo.hpp index 0b191b84d..e477d6edf 100644 --- a/dnf5/commands/history/history_undo.hpp +++ b/dnf5/commands/history/history_undo.hpp @@ -21,6 +21,8 @@ along with libdnf. If not, see . #ifndef DNF5_COMMANDS_HISTORY_HISTORY_UNDO_HPP #define DNF5_COMMANDS_HISTORY_HISTORY_UNDO_HPP +#include "commands/history/arguments.hpp" + #include @@ -31,7 +33,12 @@ class HistoryUndoCommand : public Command { public: explicit HistoryUndoCommand(Context & context) : Command(context, "undo") {} void set_argument_parser() override; + void configure() override; void run() override; + + std::unique_ptr transaction_specs{nullptr}; + std::unique_ptr ignore_extras{nullptr}; + std::unique_ptr ignore_installed{nullptr}; }; diff --git a/dnf5/main.cpp b/dnf5/main.cpp index 344e2f411..53d0634f1 100644 --- a/dnf5/main.cpp +++ b/dnf5/main.cpp @@ -974,6 +974,33 @@ static void print_resolve_hints(dnf5::Context & context) { } } + // hint --ignore-extras if there are unexpected extra packages in the transaction + if ((transaction_problems & libdnf5::GoalProblem::EXTRA) == libdnf5::GoalProblem::EXTRA) { + const std::string_view arg{"--ignore-extras"}; + if (has_named_arg(command, arg.substr(2))) { + hints.emplace_back(libdnf5::utils::sformat(_("{} to allow extra packages in the transaction"), arg)); + } + } + + // hint --ignore-installed if there are mismatches between installed and stored transaction packages in replay + if ((transaction_problems & libdnf5::GoalProblem::NOT_INSTALLED) == libdnf5::GoalProblem::NOT_INSTALLED || + (transaction_problems & libdnf5::GoalProblem::INSTALLED_IN_DIFFERENT_VERSION) == + libdnf5::GoalProblem::INSTALLED_IN_DIFFERENT_VERSION) { + for (const auto & resolve_log : context.get_transaction()->get_resolve_logs()) { + if (goal_action_is_replay(resolve_log.get_action())) { + const std::string_view arg{"--ignore-installed"}; + if (has_named_arg(command, arg.substr(2))) { + hints.emplace_back(libdnf5::utils::sformat( + _("{} to allow mismatches between installed and stored transaction packages. This can result " + "in an empty transaction because among other things the option can ignore failing Remove " + "actions."), + arg)); + break; + } + } + } + } + if ((transaction_problems & libdnf5::GoalProblem::SOLVER_ERROR) == libdnf5::GoalProblem::SOLVER_ERROR) { bool conflict = false; bool broken_file_dep = false; diff --git a/include/libdnf5/base/goal.hpp b/include/libdnf5/base/goal.hpp index 1590d0b23..ece360a1c 100644 --- a/include/libdnf5/base/goal.hpp +++ b/include/libdnf5/base/goal.hpp @@ -364,6 +364,16 @@ class Goal { const std::filesystem::path & transaction_path, 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 revert request of history transactions to the goal. + /// Can be called only once per Goal. + /// + /// @param transactions A vector of history transactions to be reverted. + /// @param settings A structure to override default goal settings. + void add_revert_transactions( + const std::vector & transactions, + 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 790ce18f2..cdb8a7a37 100644 --- a/include/libdnf5/base/goal_elements.hpp +++ b/include/libdnf5/base/goal_elements.hpp @@ -115,7 +115,10 @@ enum class GoalProblem : uint32_t { MODULE_SOLVER_ERROR_LATEST = (1 << 19), /// Error detected during resolvement of module dependencies MODULE_SOLVER_ERROR = (1 << 20), - MODULE_CANNOT_SWITH_STREAMS = (1 << 21) + MODULE_CANNOT_SWITH_STREAMS = (1 << 21), + /// Error when transaction contains additional unexpected elements. + /// Used when replaying transactions. + EXTRA = (1 << 22) }; /// Types of Goal actions @@ -137,12 +140,22 @@ enum class GoalAction { REASON_CHANGE, ENABLE, DISABLE, - RESET + RESET, + REPLAY_INSTALL, + REPLAY_REMOVE, + REPLAY_UPGRADE, + REPLAY_REINSTALL, + REPLAY_REASON_CHANGE, + REPLAY_REASON_OVERRIDE, + REVERT_COMPS_UPGRADE }; /// Convert GoalAction enum to user-readable string std::string goal_action_to_string(GoalAction action); +/// Check whether the action is a replay action +bool goal_action_is_replay(GoalAction action); + /// Settings for GoalJobSettings enum class GoalSetting { AUTO, SET_TRUE, SET_FALSE }; @@ -326,6 +339,20 @@ struct 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 + /// 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 + /// installed packages matching those in the transactions. + /// + /// Default: false + void set_ignore_installed(bool ignore_installed); + bool get_ignore_installed() const; + private: friend class Goal; diff --git a/include/libdnf5/transaction/transaction_history.hpp b/include/libdnf5/transaction/transaction_history.hpp index a7373f31e..8fe6d9b74 100644 --- a/include/libdnf5/transaction/transaction_history.hpp +++ b/include/libdnf5/transaction/transaction_history.hpp @@ -73,6 +73,18 @@ class TransactionHistory { /// @since 5.0 libdnf5::BaseWeakPtr get_base() const; + /// Get reason for package specified by name and arch at a point in history + /// specified by transaction id. + /// + /// @param name Name of rpm package + /// @param arch Arch of rpm package + /// @param transaction_id_point Id of a history transaction (can be obtained from + /// libdnf5::transaction::TransactionHistory) + /// @return Reason of the last transaction item before transaction_id_point that + /// has an rpm with matching name and arch. + TransactionItemReason transaction_item_reason_at( + const std::string & name, const std::string & arch, int64_t transaction_id_point); + private: /// Create a new Transaction object. libdnf5::transaction::Transaction new_transaction(); diff --git a/libdnf5/base/goal.cpp b/libdnf5/base/goal.cpp index 4a3de6ba2..342207fc6 100644 --- a/libdnf5/base/goal.cpp +++ b/libdnf5/base/goal.cpp @@ -107,9 +107,11 @@ class Goal::Impl { void add_resolved_group_specs_to_goal(base::Transaction & transaction); void add_resolved_environment_specs_to_goal(base::Transaction & transaction); GoalProblem add_module_specs_to_goal(base::Transaction & transaction); - GoalProblem add_transaction_replay_specs_to_goal(base::Transaction & transaction); + GoalProblem add_serialized_transaction_to_goal(base::Transaction & transaction); GoalProblem add_reason_change_specs_to_goal(base::Transaction & transaction); + GoalProblem resolve_reverted_transactions(base::Transaction & transaction); + std::pair add_install_to_goal( base::Transaction & transaction, GoalAction action, const std::string & spec, GoalJobSettings & settings); void add_provide_install_to_goal(const std::string & spec, GoalJobSettings & settings); @@ -170,6 +172,13 @@ class Goal::Impl { void set_exclude_from_weak(const std::vector & exclude_from_weak); void autodetect_unsatisfied_installed_weak_dependencies(); + // Paths to elements (packages/groups/envs) in replay are taken relative to replay_location. + GoalProblem add_replay_to_goal( + base::Transaction & transaction, + const transaction::TransactionReplay & replay, + GoalJobSettings settings, + std::filesystem::path replay_location = ""); + private: friend class Goal; BaseWeakPtr base; @@ -210,6 +219,8 @@ class Goal::Impl { // (path_to_serialized_transaction, settings) std::unique_ptr> serialized_transaction; + + std::unique_ptr, GoalJobSettings>> revert_transactions; }; Goal::Goal(const BaseWeakPtr & base) : p_impl(new Impl(base)) {} @@ -562,12 +573,32 @@ GoalProblem Goal::Impl::add_specs_to_goal(base::Transaction & transaction) { case GoalAction::RESET: { libdnf_throw_assertion("Unsupported action \"RESET\""); } + case GoalAction::REPLAY_INSTALL: { + libdnf_throw_assertion("Unsupported action \"REPLAY INSTALL\""); + } + case GoalAction::REPLAY_REMOVE: { + libdnf_throw_assertion("Unsupported action \"REPLAY REMOVE\""); + } + case GoalAction::REPLAY_UPGRADE: { + libdnf_throw_assertion("Unsupported action \"REPLAY UPGRADE\""); + } + case GoalAction::REPLAY_REINSTALL: { + libdnf_throw_assertion("Unsupported action \"REPLAY REINSTALL\""); + } + case GoalAction::REPLAY_REASON_CHANGE: { + libdnf_throw_assertion("Unsupported action \"REPLAY REASON CHANGE\""); + } + case GoalAction::REPLAY_REASON_OVERRIDE: { + libdnf_throw_assertion("Unsupported action \"REPLAY REASON OVERRIDE\""); + } + case GoalAction::REVERT_COMPS_UPGRADE: { + libdnf_throw_assertion("Unsupported action \"REVERT_COMPS_UPGRADE\""); + } } } return ret; } - GoalProblem Goal::Impl::add_module_specs_to_goal(base::Transaction & transaction) { auto ret = GoalProblem::NO_PROBLEM; module::ModuleSack & module_sack = *base->get_module_sack(); @@ -623,7 +654,7 @@ GoalProblem Goal::Impl::add_module_specs_to_goal(base::Transaction & transaction return ret; } -GoalProblem Goal::Impl::add_transaction_replay_specs_to_goal(base::Transaction & transaction) { +GoalProblem Goal::Impl::add_serialized_transaction_to_goal(base::Transaction & transaction) { if (!serialized_transaction) { return GoalProblem::NO_PROBLEM; } @@ -633,11 +664,52 @@ GoalProblem Goal::Impl::add_transaction_replay_specs_to_goal(base::Transaction & 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); +} + +static std::set query_to_vec_of_nevra_str(const libdnf5::rpm::PackageQuery & query) { + std::set query_set = {}; + std::transform(query.begin(), query.end(), std::inserter(query_set, query_set.begin()), [](const auto & pkg) { + return pkg.to_string(); + }); + + return query_set; +} + +GoalProblem Goal::Impl::add_replay_to_goal( + base::Transaction & transaction, + const transaction::TransactionReplay & replay, + GoalJobSettings settings, + std::filesystem::path replay_location) { + auto ret = GoalProblem::NO_PROBLEM; bool skip_unavailable = settings.resolve_skip_unavailable(base->get_config()); + std::unordered_set rpm_nevra_cache; + for (const auto & package_replay : replay.packages) { + rpm_nevra_cache.insert(package_replay.nevra); libdnf5::GoalJobSettings settings_per_package = settings; settings_per_package.set_clean_requirements_on_remove(libdnf5::GoalSetting::SET_FALSE); + + std::optional local_pkg; + if (!package_replay.package_path.empty()) { + // Package paths are relative to replay location + local_pkg = + base->get_repo_sack()->add_stored_transaction_package(replay_location / package_replay.package_path); + } + + const auto nevras = rpm::Nevra::parse(package_replay.nevra, {rpm::Nevra::Form::NEVRA}); + libdnf_assert( + nevras.size() == 1, + "Cannot parse rpm nevra or ambiguous \"{}\" while replaying transaction.", + package_replay.nevra); + + rpm::PackageQuery query_na(base); + query_na.filter_name(nevras[0].get_name()); + query_na.filter_arch(nevras[0].get_arch()); + auto query_nevra = query_na; + query_nevra.filter_nevra(nevras[0]); + if (!package_replay.repo_id.empty()) { repo::RepoQuery enabled_repos(base); enabled_repos.filter_enabled(true); @@ -647,15 +719,70 @@ GoalProblem Goal::Impl::add_transaction_replay_specs_to_goal(base::Transaction & } } - std::optional local_pkg; - - if (!package_replay.package_path.empty()) { - local_pkg = - base->get_repo_sack()->add_stored_transaction_package(replay_location / package_replay.package_path); - } if (package_replay.action == transaction::TransactionItemAction::UPGRADE || package_replay.action == transaction::TransactionItemAction::INSTALL || package_replay.action == transaction::TransactionItemAction::DOWNGRADE) { + if (query_nevra.empty()) { + auto problem = transaction.p_impl->report_not_found( + GoalAction::REPLAY_INSTALL, + package_replay.nevra, + settings, + skip_unavailable ? libdnf5::Logger::Level::WARNING : libdnf5::Logger::Level::ERROR); + if (!skip_unavailable) { + ret |= problem; + } + continue; + } + + // In order to properly report an error when another version of a package with action INSTALL is already + // installed we have to verify several conditions. + // - There is another versions installed for this package (name-arch). + // - The package isn't installonly. + // - The transaction doesn't contain an outbound action for this name-arch. + // This could happend during transaction reverting because upgrade/downgrade/reinstall (and obsoleting) actions are reverted as a REMOVE. + // For example upgrade transaction: [a-2 Upgrade, a-1 Replaced] is reverted to [a-2 Remove, a-1 Install]. + // This is because we don't store the "replaces" relationship in history DB (there is a table `item_replaced_by`, but it is not populated + // and it doesn't seem worth it to populate it because of this use case) so we don't know which action to pick. We could try to guess + // based on the transaction packages but check seems easier. + if (package_replay.action == transaction::TransactionItemAction::INSTALL) { + bool na_has_outbound_action = false; + query_na.filter_installed(); + for (const auto & installed_na : query_na) { + na_has_outbound_action |= + std::find_if(replay.packages.begin(), replay.packages.end(), [&installed_na](const auto & r) { + return r.nevra == installed_na.get_nevra() && transaction_item_action_is_outbound(r.action); + }) != replay.packages.end(); + if (na_has_outbound_action) { + break; + } + } + if (!na_has_outbound_action) { + auto is_installonly = query_na; + is_installonly.filter_installonly(); + + 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 log_level = libdnf5::Logger::Level::WARNING; + if (!settings.get_ignore_installed()) { + log_level = libdnf5::Logger::Level::ERROR; + ret = problem; + } + + transaction.p_impl->add_resolve_log( + GoalAction::REPLAY_INSTALL, + problem, + settings, + libdnf5::transaction::TransactionItemType::PACKAGE, + package_replay.nevra, + query_to_vec_of_nevra_str(query_na), + log_level); + continue; + } + } + } + if (local_pkg) { add_rpm_ids(GoalAction::INSTALL, *local_pkg, settings_per_package); } else { @@ -663,6 +790,18 @@ GoalProblem Goal::Impl::add_transaction_replay_specs_to_goal(base::Transaction & } transaction.p_impl->rpm_reason_overrides[package_replay.nevra] = package_replay.reason; } else if (package_replay.action == transaction::TransactionItemAction::REINSTALL) { + if (query_nevra.empty()) { + auto problem = transaction.p_impl->report_not_found( + GoalAction::REPLAY_REINSTALL, + package_replay.nevra, + settings, + skip_unavailable ? libdnf5::Logger::Level::WARNING : libdnf5::Logger::Level::ERROR); + if (!skip_unavailable) { + ret |= problem; + } + continue; + } + if (local_pkg) { add_rpm_ids(GoalAction::REINSTALL, *local_pkg, settings_per_package); } else { @@ -670,6 +809,39 @@ GoalProblem Goal::Impl::add_transaction_replay_specs_to_goal(base::Transaction & } transaction.p_impl->rpm_reason_overrides[package_replay.nevra] = package_replay.reason; } else if (package_replay.action == transaction::TransactionItemAction::REMOVE) { + if (query_nevra.empty()) { + auto problem = transaction.p_impl->report_not_found( + GoalAction::REPLAY_REMOVE, + package_replay.nevra, + settings, + skip_unavailable ? libdnf5::Logger::Level::WARNING : libdnf5::Logger::Level::ERROR); + if (!skip_unavailable) { + ret |= problem; + } + continue; + } + + query_nevra.filter_installed(); + if (query_nevra.empty()) { + auto log_level = libdnf5::Logger::Level::WARNING; + query_na.filter_installed(); + auto problem = + query_na.empty() ? GoalProblem::NOT_INSTALLED : GoalProblem::INSTALLED_IN_DIFFERENT_VERSION; + if (!settings.get_ignore_installed()) { + log_level = libdnf5::Logger::Level::ERROR; + ret |= problem; + } + transaction.p_impl->add_resolve_log( + GoalAction::REPLAY_REMOVE, + problem, + settings, + libdnf5::transaction::TransactionItemType::PACKAGE, + package_replay.nevra, + query_to_vec_of_nevra_str(query_na), + log_level); + continue; + } + if (local_pkg) { add_rpm_ids(GoalAction::REMOVE, *local_pkg, settings_per_package); } else { @@ -677,6 +849,41 @@ GoalProblem Goal::Impl::add_transaction_replay_specs_to_goal(base::Transaction & } transaction.p_impl->rpm_reason_overrides[package_replay.nevra] = package_replay.reason; } else if (package_replay.action == transaction::TransactionItemAction::REPLACED) { + if (query_nevra.empty()) { + auto problem = transaction.p_impl->report_not_found( + GoalAction::REPLAY_REMOVE, + package_replay.nevra, + settings, + skip_unavailable ? libdnf5::Logger::Level::WARNING : libdnf5::Logger::Level::ERROR); + if (!skip_unavailable) { + ret |= problem; + } + continue; + } + + query_nevra.filter_installed(); + if (query_nevra.empty()) { + auto log_level = libdnf5::Logger::Level::WARNING; + query_na.filter_installed(); + auto problem = + query_na.empty() ? GoalProblem::NOT_INSTALLED : GoalProblem::INSTALLED_IN_DIFFERENT_VERSION; + if (!settings.get_ignore_installed()) { + log_level = libdnf5::Logger::Level::ERROR; + ret |= problem; + } + transaction.p_impl->add_resolve_log( + GoalAction::REPLAY_REMOVE, + problem, + settings, + libdnf5::transaction::TransactionItemType::PACKAGE, + package_replay.nevra, + query_to_vec_of_nevra_str(query_na), + log_level); + continue; + } + // Removing the original versions (the reverse part of an action like e.g. Upgrade) is more robust, + // but we can't do it if skip_unavailable is set because if the inbound action is skipped we would + // simply remove the package. if (!skip_unavailable) { if (local_pkg) { add_rpm_ids(GoalAction::REMOVE, *local_pkg, settings_per_package); @@ -685,6 +892,18 @@ GoalProblem Goal::Impl::add_transaction_replay_specs_to_goal(base::Transaction & } } } else if (package_replay.action == transaction::TransactionItemAction::REASON_CHANGE) { + if (query_nevra.empty()) { + auto problem = transaction.p_impl->report_not_found( + GoalAction::REPLAY_REASON_CHANGE, + package_replay.nevra, + settings, + skip_unavailable ? libdnf5::Logger::Level::WARNING : libdnf5::Logger::Level::ERROR); + if (!skip_unavailable) { + ret |= problem; + } + continue; + } + rpm_reason_change_specs.emplace_back( package_replay.reason, package_replay.nevra, package_replay.group_id, settings_per_package); } else { @@ -693,6 +912,8 @@ GoalProblem Goal::Impl::add_transaction_replay_specs_to_goal(base::Transaction & } } + transaction.p_impl->rpm_replays_nevra_cache.emplace_back(rpm_nevra_cache, settings); + for (const auto & group_replay : replay.groups) { libdnf5::GoalJobSettings settings_per_group = settings; settings_per_group.set_group_no_packages(true); @@ -708,16 +929,51 @@ GoalProblem Goal::Impl::add_transaction_replay_specs_to_goal(base::Transaction & } } if (!group_replay.group_path.empty()) { + // Group paths are relative to replay location base->get_repo_sack()->add_stored_transaction_comps(replay_location / group_replay.group_path); } + comps::GroupQuery group_query_installed(base); + group_query_installed.filter_groupid(group_replay.group_id); + group_query_installed.filter_installed(true); + if (group_replay.action == transaction::TransactionItemAction::INSTALL) { group_specs.emplace_back( GoalAction::INSTALL, group_replay.reason, group_replay.group_id, settings_per_group); } else if (group_replay.action == transaction::TransactionItemAction::UPGRADE) { + if (group_query_installed.empty()) { + auto log_level = libdnf5::Logger::Level::WARNING; + if (!settings.get_ignore_installed()) { + log_level = libdnf5::Logger::Level::ERROR; + ret = GoalProblem::NOT_INSTALLED; + } + transaction.p_impl->add_resolve_log( + GoalAction::REPLAY_UPGRADE, + GoalProblem::NOT_INSTALLED, + settings, + libdnf5::transaction::TransactionItemType::GROUP, + group_replay.group_id, + {transaction_item_action_to_string(group_replay.action)}, + log_level); + } group_specs.emplace_back( GoalAction::UPGRADE, group_replay.reason, group_replay.group_id, settings_per_group); } else if (group_replay.action == transaction::TransactionItemAction::REMOVE) { + if (group_query_installed.empty()) { + auto log_level = libdnf5::Logger::Level::WARNING; + if (!settings.get_ignore_installed()) { + log_level = libdnf5::Logger::Level::ERROR; + ret = GoalProblem::NOT_INSTALLED; + } + transaction.p_impl->add_resolve_log( + GoalAction::REPLAY_REMOVE, + GoalProblem::NOT_INSTALLED, + settings, + libdnf5::transaction::TransactionItemType::GROUP, + group_replay.group_id, + {transaction_item_action_to_string(group_replay.action)}, + log_level); + } group_specs.emplace_back( GoalAction::REMOVE, group_replay.reason, group_replay.group_id, settings_per_group); } else { @@ -741,7 +997,12 @@ GoalProblem Goal::Impl::add_transaction_replay_specs_to_goal(base::Transaction & } } + comps::EnvironmentQuery env_query_installed(base); + env_query_installed.filter_environmentid(env_replay.environment_id); + env_query_installed.filter_installed(true); + if (!env_replay.environment_path.empty()) { + // Environment paths are relative to replay location base->get_repo_sack()->add_stored_transaction_comps(replay_location / env_replay.environment_path); } if (env_replay.action == transaction::TransactionItemAction::INSTALL) { @@ -751,12 +1012,42 @@ GoalProblem Goal::Impl::add_transaction_replay_specs_to_goal(base::Transaction & env_replay.environment_id, settings_per_environment); } else if (env_replay.action == transaction::TransactionItemAction::UPGRADE) { + if (env_query_installed.empty()) { + auto log_level = libdnf5::Logger::Level::WARNING; + if (!settings.get_ignore_installed()) { + log_level = libdnf5::Logger::Level::ERROR; + ret = GoalProblem::NOT_INSTALLED; + } + transaction.p_impl->add_resolve_log( + GoalAction::REPLAY_UPGRADE, + GoalProblem::NOT_INSTALLED, + settings, + libdnf5::transaction::TransactionItemType::ENVIRONMENT, + env_replay.environment_id, + {transaction_item_action_to_string(env_replay.action)}, + log_level); + } group_specs.emplace_back( GoalAction::UPGRADE, transaction::TransactionItemReason::USER, env_replay.environment_id, settings_per_environment); } else if (env_replay.action == transaction::TransactionItemAction::REMOVE) { + if (env_query_installed.empty()) { + auto log_level = libdnf5::Logger::Level::WARNING; + if (!settings.get_ignore_installed()) { + log_level = libdnf5::Logger::Level::ERROR; + ret = GoalProblem::NOT_INSTALLED; + } + transaction.p_impl->add_resolve_log( + GoalAction::REPLAY_REMOVE, + GoalProblem::NOT_INSTALLED, + settings, + libdnf5::transaction::TransactionItemType::ENVIRONMENT, + env_replay.environment_id, + {transaction_item_action_to_string(env_replay.action)}, + log_level); + } group_specs.emplace_back( GoalAction::REMOVE, transaction::TransactionItemReason::USER, @@ -768,10 +1059,9 @@ GoalProblem Goal::Impl::add_transaction_replay_specs_to_goal(base::Transaction & } } - return GoalProblem::NO_PROBLEM; + return ret; } - GoalProblem Goal::Impl::resolve_group_specs(std::vector & specs, base::Transaction & transaction) { auto ret = GoalProblem::NO_PROBLEM; auto & cfg_main = base->get_config(); @@ -1212,7 +1502,7 @@ GoalProblem Goal::Impl::add_reinstall_to_goal( settings, libdnf5::transaction::TransactionItemType::PACKAGE, spec, - {}, + query_to_vec_of_nevra_str(relevant_available_na), log_level); return skip_unavailable ? GoalProblem::NO_PROBLEM : GoalProblem::INSTALLED_IN_DIFFERENT_VERSION; } else { @@ -2206,6 +2496,133 @@ GoalProblem Goal::Impl::add_reason_change_to_goal( return GoalProblem::NO_PROBLEM; } +GoalProblem Goal::Impl::resolve_reverted_transactions(base::Transaction & transaction) { + if (!revert_transactions) { + return GoalProblem::NO_PROBLEM; + } + auto ret = GoalProblem::NO_PROBLEM; + + using Action = transaction::TransactionItemAction; + using Reason = transaction::TransactionItemReason; + const std::unordered_map REVERT_ACTION = { + {Action::INSTALL, Action::REMOVE}, + {Action::UPGRADE, Action::REPLACED}, + {Action::DOWNGRADE, Action::REPLACED}, + {Action::REINSTALL, Action::REINSTALL}, + {Action::REMOVE, Action::INSTALL}, + {Action::REPLACED, Action::INSTALL}, + {Action::REASON_CHANGE, Action::REASON_CHANGE}, + }; + transaction::TransactionReplay replay; + auto history = base->get_transaction_history(); + + auto & [reverting_transactions, settings] = *revert_transactions; + //TODO(amatej): Implement merging of transactions and merge the vector + // instead of taking the first one. + auto & reverting_transaction = reverting_transactions[0]; + + for (const auto & pkg : reverting_transaction.get_packages()) { + transaction::PackageReplay package_replay; + package_replay.nevra = libdnf5::rpm::to_nevra_string(pkg); + auto reverted_action = REVERT_ACTION.find(pkg.get_action()); + libdnf_assert( + reverted_action != REVERT_ACTION.end(), + "Cannot revert action: \"{}\"", + transaction_item_action_to_string(pkg.get_action())); + package_replay.action = reverted_action->second; + + // We cannot tell the previous reason if the action is REASON_CHANGE it could have been anything. + // For reverted action INSTALL and reason CLEAN the previous reason could have been either DEPENDENCY or WEAK DEPENDENCY + // to pick the right one we have to look into history. + if ((package_replay.action == Action::REASON_CHANGE) || + (package_replay.action == Action::INSTALL && pkg.get_reason() == Reason::CLEAN)) { + // We look up the reason based on only name and arch, this means we could find a different + // version of installonly package however we store only one reason for ALL versions of + // installonly packages so it doesn't matter. + package_replay.reason = + history->transaction_item_reason_at(pkg.get_name(), pkg.get_arch(), reverting_transaction.get_id() - 1); + } else if ( + package_replay.action == Action::REMOVE && + (pkg.get_reason() == Reason::DEPENDENCY || pkg.get_reason() == Reason::WEAK_DEPENDENCY)) { + package_replay.reason = Reason::CLEAN; + } else { + package_replay.reason = pkg.get_reason(); + } + + replay.packages.push_back(package_replay); + } + + for (const auto & group : reverting_transaction.get_comps_groups()) { + transaction::GroupReplay group_replay; + group_replay.group_id = group.to_string(); + // Do not revert UPGRADE for groups. Groups don't have an upgrade path so they cannot be + // upgraded or downgraded. The UPGRADE action is basically a synchronization with + // current group definition. Revert happens automatically by reverting the rpm actions. + if (group.get_action() != transaction::TransactionItemAction::UPGRADE) { + auto reverted_action = REVERT_ACTION.find(group.get_action()); + if (reverted_action == REVERT_ACTION.end()) { + libdnf_throw_assertion( + "Cannot revert action: \"{}\"", transaction_item_action_to_string(group.get_action())); + } + group_replay.action = reverted_action->second; + } else { + transaction.p_impl->add_resolve_log( + GoalAction::REVERT_COMPS_UPGRADE, + libdnf5::GoalProblem::UNSUPPORTED_ACTION, + settings, + libdnf5::transaction::TransactionItemType::GROUP, + group_replay.group_id, + {}, + libdnf5::Logger::Level::WARNING); + continue; + } + + if (group_replay.action == Action::INSTALL && group.get_reason() == Reason::CLEAN) { + group_replay.reason = Reason::DEPENDENCY; + } else if (group_replay.action == Action::REMOVE && group.get_reason() == Reason::DEPENDENCY) { + group_replay.reason = Reason::CLEAN; + } else { + group_replay.reason = group.get_reason(); + } + + replay.groups.push_back(group_replay); + } + + for (const auto & env : reverting_transaction.get_comps_environments()) { + transaction::EnvironmentReplay env_replay; + env_replay.environment_id = env.to_string(); + // Do not revert UPGRADE for environments. Environments don't have an upgrade path so they cannot be + // upgraded or downgraded. The UPGRADE action is basically a synchronization with + // current environment definition. Revert happens automatically by reverting the rpm + // actions. + if (env.get_action() != transaction::TransactionItemAction::UPGRADE) { + auto reverted_action = REVERT_ACTION.find(env.get_action()); + if (reverted_action == REVERT_ACTION.end()) { + libdnf_throw_assertion( + "Cannot revert action: \"{}\"", transaction_item_action_to_string(env.get_action())); + } + env_replay.action = reverted_action->second; + } else { + transaction.p_impl->add_resolve_log( + GoalAction::REVERT_COMPS_UPGRADE, + libdnf5::GoalProblem::UNSUPPORTED_ACTION, + settings, + libdnf5::transaction::TransactionItemType::ENVIRONMENT, + env_replay.environment_id, + {}, + libdnf5::Logger::Level::WARNING); + continue; + } + + replay.environments.push_back(env_replay); + } + + ret |= add_replay_to_goal(transaction, replay, settings); + + return ret; +} + + void Goal::Impl::add_paths_to_goal() { if (rpm_filepaths.empty()) { return; @@ -2345,7 +2762,9 @@ base::Transaction Goal::resolve() { // of specs, it doesn't resolve anything. Therefore it doesn't need any Sacks to be ready. // In fact given that it can add to rpm_filepaths it has to be added before `add_paths_to_goal()` // and thus before the provides are computed. - ret |= p_impl->add_transaction_replay_specs_to_goal(transaction); + // Both serialized and reverted transactions use TransactionReplay. + ret |= p_impl->add_serialized_transaction_to_goal(transaction); + ret |= p_impl->resolve_reverted_transactions(transaction); p_impl->add_paths_to_goal(); @@ -2488,8 +2907,6 @@ base::Transaction Goal::resolve() { libdnf5::Logger::Level::WARNING); } - //TODO(amatej): Add conditional check that no extra packages were added by the solver - transaction.p_impl->set_transaction(p_impl->rpm_goal, module_sack, ret); return transaction; @@ -2502,6 +2919,14 @@ void Goal::add_serialized_transaction( std::make_unique>(transaction_path, settings); } +void Goal::add_revert_transactions( + const std::vector & transactions, const libdnf5::GoalJobSettings & settings) { + libdnf_user_assert(!p_impl->revert_transactions, "Revert transactions cannot be set multiple times."); + p_impl->revert_transactions = + std::make_unique, GoalJobSettings>>(transactions, settings); +} + + void Goal::reset() { p_impl->module_specs.clear(); p_impl->rpm_specs.clear(); diff --git a/libdnf5/base/goal_elements.cpp b/libdnf5/base/goal_elements.cpp index 8ac6817b1..c7c66ce3b 100644 --- a/libdnf5/base/goal_elements.cpp +++ b/libdnf5/base/goal_elements.cpp @@ -168,6 +168,14 @@ class GoalJobSettings::Impl { ///// Reduce candidates for the operation according repository ids std::vector to_repo_ids; + /// For replaying transactions don't check for extra packages pulled into the transaction. + /// Used by history undo, system upgrade, ... + bool ignore_extras{false}; + + /// For replaying transactions don't check for installed packages matching those in transaction. + /// Used by history undo, system upgrade, ... + bool ignore_installed{false}; + GoalUsedSetting used_skip_broken{GoalUsedSetting::UNUSED}; GoalUsedSetting used_skip_unavailable{GoalUsedSetting::UNUSED}; GoalUsedSetting used_best{GoalUsedSetting::UNUSED}; @@ -371,10 +379,33 @@ std::string goal_action_to_string(GoalAction action) { return "Disable"; case GoalAction::RESET: return "Reset"; + case GoalAction::REPLAY_INSTALL: + return "Install action"; + case GoalAction::REPLAY_REMOVE: + return "Remove action"; + case GoalAction::REPLAY_UPGRADE: + return "Upgrade action"; + case GoalAction::REPLAY_REINSTALL: + return "Reinstall action"; + case GoalAction::REPLAY_REASON_CHANGE: + return "Reason change action"; + case GoalAction::REPLAY_REASON_OVERRIDE: + return "Reason override"; + case GoalAction::REVERT_COMPS_UPGRADE: + return "Revert comps upgrade"; } return ""; } +bool goal_action_is_replay(GoalAction action) { + if (action == GoalAction::REPLAY_INSTALL || action == GoalAction::REPLAY_REMOVE || + action == GoalAction::REPLAY_UPGRADE || action == GoalAction::REPLAY_REINSTALL || + action == GoalAction::REPLAY_REASON_CHANGE || action == GoalAction::REPLAY_REASON_OVERRIDE) { + return true; + } else { + return false; + } +} void GoalJobSettings::set_report_hint(bool report_hint) { p_impl->report_hint = report_hint; @@ -466,4 +497,18 @@ GoalUsedSetting GoalJobSettings::get_used_clean_requirements_on_remove() const { return p_impl->used_clean_requirements_on_remove; }; +void GoalJobSettings::set_ignore_extras(bool ignore_extras) { + p_impl->ignore_extras = ignore_extras; +} +bool GoalJobSettings::get_ignore_extras() const { + return p_impl->ignore_extras; +} + +void GoalJobSettings::set_ignore_installed(bool ignore_installed) { + p_impl->ignore_installed = ignore_installed; +} +bool GoalJobSettings::get_ignore_installed() const { + return p_impl->ignore_installed; +} + } // namespace libdnf5 diff --git a/libdnf5/base/log_event.cpp b/libdnf5/base/log_event.cpp index 62c0ddd2c..467aa94f3 100644 --- a/libdnf5/base/log_event.cpp +++ b/libdnf5/base/log_event.cpp @@ -153,6 +153,9 @@ std::string LogEvent::to_string( } else { return ret.append(utils::sformat(_("No match for group package: {}"), *spec)); } + } else if (goal_action_is_replay(action)) { + return ret.append( + utils::sformat(_("Cannot perform {0}, no match for: {1}."), goal_action_to_string(action), *spec)); } return ret.append(utils::sformat(_("No match for argument: {}"), *spec)); case GoalProblem::NOT_FOUND_IN_REPOSITORIES: @@ -160,16 +163,42 @@ std::string LogEvent::to_string( _("No match for argument '{0}' in repositories '{1}'"), *spec, utils::string::join(settings->get_to_repo_ids(), ", "))); - case GoalProblem::NOT_INSTALLED: + case GoalProblem::NOT_INSTALLED: { + if (goal_action_is_replay(action)) { + return ret.append(utils::sformat( + _("Cannot perform {0} for {1} '{2}' becasue it is not installed."), + goal_action_to_string(action), + transaction_item_type_to_string(*spec_type), + *spec)); + } return ret.append(utils::sformat(_("Packages for argument '{}' available, but not installed."), *spec)); + } case GoalProblem::NOT_INSTALLED_FOR_ARCHITECTURE: return ret.append(utils::sformat( _("Packages for argument '{}' available, but installed for a different architecture."), *spec)); case GoalProblem::ONLY_SRC: + if (goal_action_is_replay(action)) { + return ret.append(utils::sformat( + _("Cannot perform {0} because '{1}' matches only source packages."), + goal_action_to_string(action), + *spec)); + } return ret.append(utils::sformat(_("Argument '{}' matches only source packages."), *spec)); case GoalProblem::EXCLUDED: + if (goal_action_is_replay(action)) { + return ret.append(utils::sformat( + _("Cannot perform {0} because '{1}' matches only excluded packages."), + goal_action_to_string(action), + *spec)); + } return ret.append(utils::sformat(_("Argument '{}' matches only excluded packages."), *spec)); case GoalProblem::EXCLUDED_VERSIONLOCK: + if (goal_action_is_replay(action)) { + return ret.append(utils::sformat( + _("Cannot perform {0} because '{1}' matches only packages excluded by versionlock."), + goal_action_to_string(action), + *spec)); + } return ret.append(utils::sformat(_("Argument '{}' matches only packages excluded by versionlock."), *spec)); case GoalProblem::HINT_ICASE: if (additional_data.size() != 1) { @@ -191,9 +220,16 @@ std::string LogEvent::to_string( case GoalProblem::INSTALLED_IN_DIFFERENT_VERSION: if (action == GoalAction::REINSTALL) { return ret.append(utils::sformat( - _("Packages for argument '{}' installed and available, but in a different version => cannot " - "reinstall"), - *spec)); + _("Installed packages for argument '{0}' are not available in repositories in the same version, " + "available versions: {1}, cannot reinstall."), + *spec, + libdnf5::utils::string::join(additional_data, ","))); + } else if (goal_action_is_replay(action)) { + return ret.append(utils::sformat( + _("Cannot perform {0} because '{1}' is installed in a different version: '{2}'."), + goal_action_to_string(action), + *spec, + libdnf5::utils::string::join(additional_data, ","))); } return ret.append(utils::sformat( _("Packages for argument '{}' installed and available, but in a different version."), *spec)); @@ -220,6 +256,13 @@ std::string LogEvent::to_string( case GoalProblem::WRITE_DEBUG: return ret.append(utils::sformat(_("Debug data written to \"{}\""), *additional_data.begin())); case GoalProblem::UNSUPPORTED_ACTION: + if (action == GoalAction::REVERT_COMPS_UPGRADE) { + return ret.append(utils::sformat( + _("{0} upgrade cannot be reverted, however associated package actions will be. ({1} id: '{2}') ."), + transaction_item_type_to_string(*spec_type), + transaction_item_type_to_string(*spec_type), + *spec)); + } return ret.append(utils::sformat( _("{} action for argument \"{}\" is not supported."), goal_action_to_string(action), *spec)); case GoalProblem::MULTIPLE_STREAMS: { @@ -287,6 +330,13 @@ std::string LogEvent::to_string( _("Error: It is not possible to switch enabled streams of a module unless explicitly enabled via " "configuration option module_stream_switch.")); } + case GoalProblem::EXTRA: { + return ret.append(utils::sformat( + _("Extra package '{0}' (with action '{1}') which is not present in the stored transaction was pulled " + "into the transaction.\n"), + *spec, + *additional_data.begin())); + } } return ret; } diff --git a/libdnf5/base/transaction.cpp b/libdnf5/base/transaction.cpp index e03b016bc..2f6974799 100644 --- a/libdnf5/base/transaction.cpp +++ b/libdnf5/base/transaction.cpp @@ -243,7 +243,7 @@ GoalProblem Transaction::Impl::report_not_found( libdnf5::Logger::Level log_level) { auto sack = base->get_rpm_package_sack(); rpm::PackageQuery query(base, rpm::PackageQuery::ExcludeFlags::IGNORE_EXCLUDES); - if (action == GoalAction::REMOVE) { + if (action == GoalAction::REMOVE || action == GoalAction::REPLAY_REMOVE) { query.filter_installed(); } auto nevra_pair_reports = query.resolve_pkg_spec(pkg_spec, settings, true); @@ -259,7 +259,7 @@ GoalProblem Transaction::Impl::report_not_found( log_level); if (settings.get_report_hint()) { rpm::PackageQuery hints(base); - if (action == GoalAction::REMOVE) { + if (action == GoalAction::REMOVE || action == GoalAction::REPLAY_REMOVE) { hints.filter_installed(); } if (!settings.get_ignore_case() && settings.get_with_nevra()) { @@ -715,10 +715,36 @@ void Transaction::Impl::set_transaction( packages.emplace_back(std::move(tspkg)); } - // After all packages were added check rpm reason overrides - if (!rpm_reason_overrides.empty()) { + // After all packages were added check for extra packages and rpm reason overrides + if (!rpm_reason_overrides.empty() || !rpm_replays_nevra_cache.empty()) { for (auto & pkg : packages) { - const auto reason_override = rpm_reason_overrides.find(pkg.get_package().get_nevra()); + auto nevra = pkg.get_package().get_nevra(); + for (const auto & [rpm_cache, settings] : rpm_replays_nevra_cache) { + if (!rpm_cache.contains(nevra)) { + // If ignore_installed is true we don't want to check for REPLACED extras in the + // transaction. Some operation had to be done on an installed package but we are + // ignoring it. + if (!(settings.get_ignore_installed() && + pkg.get_action() == transaction::TransactionItemAction::REPLACED)) { + auto log_level = libdnf5::Logger::Level::WARNING; + if (!settings.get_ignore_extras()) { + this->problems |= GoalProblem::EXTRA; + log_level = libdnf5::Logger::Level::ERROR; + } + // report this as an error or a warning depending on the setting + this->add_resolve_log( + GoalAction::REPLAY_REASON_OVERRIDE, + GoalProblem::EXTRA, + GoalJobSettings(), + libdnf5::transaction::TransactionItemType::PACKAGE, + nevra, + {transaction_item_action_to_string(pkg.get_action())}, + log_level); + } + } + } + + const auto reason_override = rpm_reason_overrides.find(nevra); if (reason_override != rpm_reason_overrides.end()) { // For UPGRADE, DOWNGRADE and REINSTALL change the reason only if it stronger. // This is required because we don't want to for example mark some user installed diff --git a/libdnf5/base/transaction_impl.hpp b/libdnf5/base/transaction_impl.hpp index 06c196585..5cc2b656b 100644 --- a/libdnf5/base/transaction_impl.hpp +++ b/libdnf5/base/transaction_impl.hpp @@ -127,6 +127,8 @@ class Transaction::Impl { // Used during transaction replay to ensure stored reason are used std::unordered_map rpm_reason_overrides; + // Used during transaction replay to verify no extra packages were pulled into the transaction + std::vector, GoalJobSettings>> rpm_replays_nevra_cache; }; diff --git a/libdnf5/rpm/transaction.cpp b/libdnf5/rpm/transaction.cpp index c60012ca2..0719d7b4a 100644 --- a/libdnf5/rpm/transaction.cpp +++ b/libdnf5/rpm/transaction.cpp @@ -20,8 +20,6 @@ along with libdnf. If not, see . #include "transaction.hpp" -#include "package_set_impl.hpp" - #include "libdnf5/base/transaction.hpp" #include "libdnf5/common/exception.hpp" #include "libdnf5/rpm/package_query.hpp" @@ -39,9 +37,7 @@ along with libdnf. If not, see . #include #include -#include #include -#include namespace libdnf5::rpm { diff --git a/libdnf5/transaction/db/trans.cpp b/libdnf5/transaction/db/trans.cpp index 164813ae8..f1b0026b6 100644 --- a/libdnf5/transaction/db/trans.cpp +++ b/libdnf5/transaction/db/trans.cpp @@ -217,5 +217,43 @@ void TransactionDbUtils::trans_update(libdnf5::utils::SQLite3::Statement & query query.reset(); } +static constexpr const char * SQL_TRANS_ITEM_NAME_ARCH_REASON = R"**( + SELECT + "ti"."reason_id" + FROM + "trans_item" "ti" + JOIN + "trans" "t" ON ("ti"."trans_id" = "t"."id") + JOIN + "rpm" "i" USING ("item_id") + JOIN + "pkg_name" ON "i"."name_id" = "pkg_name"."id" + JOIN + "arch" ON "i"."arch_id" = "arch"."id" + WHERE + "t"."state_id" = 2 + AND "ti"."action_id" NOT IN (6) + AND "pkg_name"."name" = ? + AND "arch"."name" = ? + AND "ti"."trans_id" <= ? + ORDER BY + "ti"."trans_id" DESC + LIMIT 1 +)**"; + +TransactionItemReason TransactionDbUtils::transaction_item_reason_at( + const BaseWeakPtr & base, const std::string & name, const std::string & arch, int64_t transaction_id_point) { + auto conn = transaction_db_connect(*base); + + auto query = libdnf5::utils::SQLite3::Query(*conn, SQL_TRANS_ITEM_NAME_ARCH_REASON); + query.bindv(name, arch, transaction_id_point); + + if (query.step() == libdnf5::utils::SQLite3::Statement::StepResult::ROW) { + auto reason = static_cast(query.get("reason_id")); + return reason; + } + + return TransactionItemReason::NONE; +} } // namespace libdnf5::transaction diff --git a/libdnf5/transaction/db/trans.hpp b/libdnf5/transaction/db/trans.hpp index 479d9b97a..b21f089f3 100644 --- a/libdnf5/transaction/db/trans.hpp +++ b/libdnf5/transaction/db/trans.hpp @@ -25,6 +25,7 @@ along with libdnf. If not, see . #include "utils/sqlite3/sqlite3.hpp" #include "libdnf5/base/base_weak.hpp" +#include "libdnf5/transaction/transaction_item_reason.hpp" #include @@ -59,6 +60,10 @@ class TransactionDbUtils { /// Use a query to update a record in the 'trans' table static void trans_update(libdnf5::utils::SQLite3::Statement & query, Transaction & trans); + + /// Get reason for name-arch at a point in history specified by transaction id. + static TransactionItemReason transaction_item_reason_at( + const BaseWeakPtr & base, const std::string & name, const std::string & arch, int64_t transaction_id_point); }; } // namespace libdnf5::transaction diff --git a/libdnf5/transaction/transaction_history.cpp b/libdnf5/transaction/transaction_history.cpp index 5b6afd967..a4e6b7fee 100644 --- a/libdnf5/transaction/transaction_history.cpp +++ b/libdnf5/transaction/transaction_history.cpp @@ -75,4 +75,9 @@ TransactionHistoryWeakPtr TransactionHistory::get_weak_ptr() { return {this, &p_impl->guard}; } +TransactionItemReason TransactionHistory::transaction_item_reason_at( + const std::string & name, const std::string & arch, int64_t transaction_id_point) { + return TransactionDbUtils::transaction_item_reason_at(p_impl->base, name, arch, transaction_id_point); +} + } // namespace libdnf5::transaction diff --git a/libdnf5/transaction/transaction_sr.hpp b/libdnf5/transaction/transaction_sr.hpp index 6398f66c8..830cbb72b 100644 --- a/libdnf5/transaction/transaction_sr.hpp +++ b/libdnf5/transaction/transaction_sr.hpp @@ -34,6 +34,7 @@ struct GroupReplay { TransactionItemAction action; TransactionItemReason reason; std::string group_id; + // Path to serialized comps group relative to the transaction json file std::filesystem::path group_path; std::string repo_id; }; @@ -41,6 +42,7 @@ struct GroupReplay { struct EnvironmentReplay { TransactionItemAction action; std::string environment_id; + // Path to serialized comps environment relative to the transaction json file std::filesystem::path environment_path; std::string repo_id; }; @@ -49,7 +51,9 @@ struct PackageReplay { TransactionItemAction action; TransactionItemReason reason; std::string group_id; + // This nevra doesn't contain epoch if it is 0 std::string nevra; + // Path to rpm package relative to the transaction json file std::filesystem::path package_path; std::string repo_id; };