Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add history rollback command and transaction merging #1558

Merged
merged 4 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dnf5/commands/history/history.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ void HistoryCommand::register_subcommands() {
cmd.register_group(software_management_commands_group);
register_subcommand(std::make_unique<HistoryUndoCommand>(get_context()), software_management_commands_group);
// register_subcommand(std::make_unique<HistoryRedoCommand>(get_context()), software_management_commands_group);
// register_subcommand(std::make_unique<HistoryRollbackCommand>(get_context()), software_management_commands_group);
register_subcommand(std::make_unique<HistoryRollbackCommand>(get_context()), software_management_commands_group);
register_subcommand(std::make_unique<HistoryStoreCommand>(get_context()), software_management_commands_group);
}

Expand Down
53 changes: 52 additions & 1 deletion dnf5/commands/history/history_rollback.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,65 @@ along with libdnf. If not, see <https://www.gnu.org/licenses/>.

#include "history_rollback.hpp"

#include "arguments.hpp"
#include "dnf5/shared_options.hpp"
#include "transaction_id.hpp"

#include <libdnf5/utils/bgettext/bgettext-mark-domain.h>

namespace dnf5 {

using namespace libdnf5::cli;

void HistoryRollbackCommand::set_argument_parser() {
get_argument_parser_command()->set_description("Undo all transactions performed after the specified transaction");
transaction_specs = std::make_unique<TransactionSpecArguments>(*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<SkipUnavailableOption>(*this);
ignore_extras = std::make_unique<IgnoreExtrasOption>(*this);
ignore_installed = std::make_unique<IgnoreInstalledOption>(*this);
}

void HistoryRollbackCommand::configure() {
auto & context = get_context();
context.set_load_system_repo(true);
context.set_load_available_repos(Context::LoadAvailableRepos::ENABLED);
}

void HistoryRollbackCommand::run() {}
void HistoryRollbackCommand::run() {
auto ts_specs = transaction_specs->get_value();
libdnf5::transaction::TransactionHistory history(get_context().get_base());
std::vector<libdnf5::transaction::Transaction> transactions;
std::vector<libdnf5::transaction::Transaction> 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 max_id = history.list_transaction_ids().back();

int64_t target_id = target_trans[0].get_id() + 1;
if (target_id <= max_id) {
transactions = history.list_transactions(target_id, max_id);
}

auto goal = get_context().get_goal();
// To enable removal of dependency packages not present in the selected transactions
// 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(transactions, settings);
}

} // namespace dnf5
8 changes: 8 additions & 0 deletions dnf5/commands/history/history_rollback.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ along with libdnf. If not, see <https://www.gnu.org/licenses/>.
#ifndef DNF5_COMMANDS_HISTORY_HISTORY_ROLLBACK_HPP
#define DNF5_COMMANDS_HISTORY_HISTORY_ROLLBACK_HPP

#include "commands/history/arguments.hpp"

#include <dnf5/context.hpp>


Expand All @@ -31,7 +33,13 @@ class HistoryRollbackCommand : public Command {
public:
explicit HistoryRollbackCommand(Context & context) : Command(context, "rollback") {}
void set_argument_parser() override;
void configure() override;
void run() override;

private:
std::unique_ptr<TransactionSpecArguments> transaction_specs{nullptr};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider making these private

std::unique_ptr<libdnf5::cli::session::BoolOption> ignore_extras{nullptr};
std::unique_ptr<libdnf5::cli::session::BoolOption> ignore_installed{nullptr};
};


Expand Down
6 changes: 4 additions & 2 deletions include/libdnf5/base/goal_elements.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@ enum class GoalProblem : uint32_t {
EXTRA = (1 << 22),
MALFORMED = (1 << 23),
NOT_FOUND_DEBUGINFO = (1 << 24),
NOT_FOUND_DEBUGSOURCE = (1 << 25)
NOT_FOUND_DEBUGSOURCE = (1 << 25),
MERGE_ERROR = (1 << 26)
};

/// Types of Goal actions
Expand Down Expand Up @@ -155,7 +156,8 @@ enum class GoalAction {
REPLAY_REASON_CHANGE,
REPLAY_REASON_OVERRIDE,
REVERT_COMPS_UPGRADE,
INSTALL_DEBUG
INSTALL_DEBUG,
MERGE
};

/// Convert GoalAction enum to user-readable string
Expand Down
208 changes: 121 additions & 87 deletions libdnf5/base/goal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ along with libdnf. If not, see <https://www.gnu.org/licenses/>.
#include "solv/id_queue.hpp"
#include "solv/pool.hpp"
#include "solver_problems_internal.hpp"
#include "transaction/transaction_merge.hpp"
#include "transaction/transaction_sr.hpp"
#include "transaction_impl.hpp"
#include "utils/string.hpp"
Expand Down Expand Up @@ -670,6 +671,9 @@ GoalProblem Goal::Impl::add_specs_to_goal(base::Transaction & transaction) {
case GoalAction::REVERT_COMPS_UPGRADE: {
libdnf_throw_assertion("Unsupported action \"REVERT_COMPS_UPGRADE\"");
}
case GoalAction::MERGE: {
libdnf_throw_assertion("Unsupported action \"MERGE\"");
}
}
}
return ret;
Expand Down Expand Up @@ -2807,111 +2811,141 @@ GoalProblem Goal::Impl::resolve_reverted_transactions(base::Transaction & transa
{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();
std::vector<transaction::TransactionReplay> reverted_transactions;

for (auto & reverting_transaction : reverting_transactions) {
transaction::TransactionReplay replay;
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);
}

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;
}

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()));
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();
}
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;

replay.groups.push_back(group_replay);
}

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();
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);
}

replay.groups.push_back(group_replay);
reverted_transactions.push_back(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;
std::reverse(reverted_transactions.begin(), reverted_transactions.end());

// Prepare installed map which is needed for merging
libdnf5::rpm::PackageQuery installed_query(base, libdnf5::rpm::PackageQuery::ExcludeFlags::IGNORE_EXCLUDES);
installed_query.filter_installed();
std::unordered_map<std::string, std::vector<std::string>> installed;
for (const auto & pkg : installed_query) {
const auto name_arch = pkg.get_name() + "." + pkg.get_arch();
if (installed.contains(name_arch)) {
installed[name_arch].push_back(pkg.get_nevra());
} 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;
installed[name_arch] = {pkg.get_nevra()};
}
}
auto [merged_transactions, problems] = merge_transactions(
reverted_transactions, installed, base->get_config().get_installonlypkgs_option().get_value());

replay.environments.push_back(env_replay);
for (const auto & problem : problems) {
transaction.p_impl->add_resolve_log(
GoalAction::MERGE,
libdnf5::GoalProblem::MERGE_ERROR,
settings,
libdnf5::transaction::TransactionItemType::PACKAGE,
{},
{problem},
libdnf5::Logger::Level::WARNING);
}

ret |= add_replay_to_goal(transaction, replay, settings);
ret |= add_replay_to_goal(transaction, merged_transactions, settings);

return ret;
}
Expand Down
2 changes: 2 additions & 0 deletions libdnf5/base/goal_elements.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,8 @@ std::string goal_action_to_string(GoalAction action) {
return _("Revert comps upgrade");
case GoalAction::INSTALL_DEBUG:
return _("Install debug RPMs");
case GoalAction::MERGE:
return _("Transaction merge");
}
return "";
}
Expand Down
3 changes: 3 additions & 0 deletions libdnf5/base/log_event.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,9 @@ std::string LogEvent::to_string(
utils::string::join(
additional_data, C_("String for joining NEVRAs - e.g. `foo-4-4.noarch, bar-5-5.noarch`", ", "))));
}
case GoalProblem::MERGE_ERROR: {
return ret.append(utils::sformat(_("Transaction merge error: '{0}'"), *additional_data.begin()));
}
}
return ret;
}
Expand Down
Loading
Loading