diff --git a/dnf5daemon-server/dbus/interfaces/org.rpm.dnf.v0.History.xml b/dnf5daemon-server/dbus/interfaces/org.rpm.dnf.v0.History.xml index 387fce1131..1a74ac1a3b 100644 --- a/dnf5daemon-server/dbus/interfaces/org.rpm.dnf.v0.History.xml +++ b/dnf5daemon-server/dbus/interfaces/org.rpm.dnf.v0.History.xml @@ -81,6 +81,44 @@ along with libdnf. If not, see . + + + + + + diff --git a/dnf5daemon-server/services/history/history.cpp b/dnf5daemon-server/services/history/history.cpp index 3c05f37784..5ed8b4f19f 100644 --- a/dnf5daemon-server/services/history/history.cpp +++ b/dnf5daemon-server/services/history/history.cpp @@ -29,6 +29,8 @@ #include #include #include +#include +#include #include #include @@ -38,17 +40,28 @@ void History::dbus_register() { auto dbus_object = session.get_dbus_object(); #ifdef SDBUS_CPP_VERSION_2 dbus_object - ->addVTable(sdbus::MethodVTableItem{ - sdbus::MethodName{"recent_changes"}, - sdbus::Signature{"a{sv}"}, - {"options"}, - sdbus::Signature{"a{saa{sv}}"}, - {"changeset"}, - [this](sdbus::MethodCall call) -> void { - session.get_threads_manager().handle_method( - *this, &History::recent_changes, call, session.session_locale); - }, - {}}) + ->addVTable( + sdbus::MethodVTableItem{ + sdbus::MethodName{"recent_changes"}, + sdbus::Signature{"a{sv}"}, + {"options"}, + sdbus::Signature{"a{saa{sv}}"}, + {"changeset"}, + [this](sdbus::MethodCall call) -> void { + session.get_threads_manager().handle_method( + *this, &History::recent_changes, call, session.session_locale); + }, + {}}, + sdbus::MethodVTableItem{ + sdbus::MethodName{"list"}, + sdbus::Signature{"a{sv}"}, + {"options"}, + sdbus::Signature{"aa{sv}"}, + {"transactions"}, + [this](sdbus::MethodCall call) -> void { + session.get_threads_manager().handle_method(*this, &History::list, call, session.session_locale); + }, + {}}) .forInterface(dnfdaemon::INTERFACE_HISTORY); #else dbus_object->registerMethod( @@ -61,6 +74,16 @@ void History::dbus_register() { [this](sdbus::MethodCall call) -> void { session.get_threads_manager().handle_method(*this, &History::recent_changes, call, session.session_locale); }); + dbus_object->registerMethod( + dnfdaemon::INTERFACE_HISTORY, + "list", + sdbus::Signature{"a{sv}"}, + {"options"}, + sdbus::Signature{"aa{sv}"}, + {"transactions"}, + [this](sdbus::MethodCall call) -> void { + session.get_threads_manager().handle_method(*this, &History::list, call, session.session_locale); + }); #endif } @@ -105,19 +128,11 @@ sdbus::MethodReply History::recent_changes(sdbus::MethodCall & call) { if (options.contains("since")) { // only interested in transactions newer than the timestamp - // TODO(mblaha): Add a new method TransactionHistory::list_transactions_since() - // to retrieve transactions newer than a given point in time int64_t timestamp = dnfdaemon::key_value_map_get(options, "since"); - auto all_transactions = history.list_all_transactions(); - for (auto & trans : all_transactions) { - if (trans.get_dt_end() > timestamp) { - transactions.emplace_back(std::move(trans)); - } - } + transactions = history.list_transactions_since(timestamp); } else { // if timestamp is not present, use only the latest transaction - auto trans_ids = history.list_transaction_ids(); - transactions = history.list_transactions(std::vector{trans_ids.back()}); + transactions = std::vector{history.get_latest_transaction()}; } // the operator < for the Transaction class is kind of "reversed". // transA < transB means that transA.get_id() > transB.get_id() @@ -272,3 +287,193 @@ sdbus::MethodReply History::recent_changes(sdbus::MethodCall & call) { reply << output; return reply; } + +/// Convert a transaction::Package to a D-Bus compatible map with the requested attributes. +dnfdaemon::KeyValueMap trans_package_to_map( + const libdnf5::transaction::Package & pkg, const std::vector & attrs) { + dnfdaemon::KeyValueMap dbus_pkg; + for (const auto & attr : attrs) { + if (attr == "name") { + dbus_pkg.emplace("name", pkg.get_name()); + } else if (attr == "epoch") { + dbus_pkg.emplace("epoch", pkg.get_epoch()); + } else if (attr == "version") { + dbus_pkg.emplace("version", pkg.get_version()); + } else if (attr == "release") { + dbus_pkg.emplace("release", pkg.get_release()); + } else if (attr == "arch") { + dbus_pkg.emplace("arch", pkg.get_arch()); + } else if (attr == "evr") { + dbus_pkg.emplace("evr", get_evr(pkg)); + } else if (attr == "repo_id") { + dbus_pkg.emplace("repo_id", pkg.get_repoid()); + } else if (attr == "action") { + dbus_pkg.emplace("action", libdnf5::transaction::transaction_item_action_to_string(pkg.get_action())); + } else if (attr == "reason") { + dbus_pkg.emplace("reason", libdnf5::transaction::transaction_item_reason_to_string(pkg.get_reason())); + } + } + return dbus_pkg; +} + +sdbus::MethodReply History::list(sdbus::MethodCall & call) { + dnfdaemon::KeyValueMap options; + call >> options; + + // Parse options + auto limit = dnfdaemon::key_value_map_get(options, "limit", 0); + auto reverse = dnfdaemon::key_value_map_get(options, "reverse", false); + auto include_packages = dnfdaemon::key_value_map_get(options, "include_packages", true); + auto contains_pkgs = + dnfdaemon::key_value_map_get>(options, "contains_pkgs", std::vector{}); + auto transaction_attrs = dnfdaemon::key_value_map_get>( + options, + "transaction_attrs", + std::vector{"id", "start", "end", "user_id", "description", "status"}); + auto package_attrs = dnfdaemon::key_value_map_get>( + options, "package_attrs", std::vector{"name", "arch", "evr"}); + + auto & base = *session.get_base(); + libdnf5::transaction::TransactionHistory history(base); + + // Get transactions + std::vector transactions; + if (options.contains("since")) { + int64_t timestamp = dnfdaemon::key_value_map_get(options, "since"); + auto all_transactions = history.list_all_transactions(); + for (auto & trans : all_transactions) { + if (trans.get_dt_end() > timestamp) { + transactions.emplace_back(std::move(trans)); + } + } + } else { + transactions = history.list_all_transactions(); + } + + // Filter by package names if requested + if (!contains_pkgs.empty()) { + history.filter_transactions_by_pkg_names(transactions, contains_pkgs); + } + + // Sort transactions (default: newest first, i.e., descending by id) + // Transaction::operator< is "reversed" (a < b means a.id > b.id) + if (reverse) { + // oldest first = ascending by id = use std::greater with the reversed operator + std::sort(transactions.begin(), transactions.end(), std::greater{}); + } else { + // newest first = descending by id = use std::less with the reversed operator + std::sort(transactions.begin(), transactions.end()); + } + + // Apply limit + if (limit > 0 && transactions.size() > static_cast(limit)) { + transactions.erase(transactions.begin() + limit, transactions.end()); + } + + // Build output + dnfdaemon::KeyValueMapList output; + using Action = libdnf5::transaction::TransactionItemAction; + + for (auto & trans : transactions) { + dnfdaemon::KeyValueMap trans_map; + + // Add transaction attributes + for (const auto & attr : transaction_attrs) { + if (attr == "id") { + trans_map.emplace("id", trans.get_id()); + } else if (attr == "start") { + trans_map.emplace("start", trans.get_dt_start()); + } else if (attr == "end") { + trans_map.emplace("end", trans.get_dt_end()); + } else if (attr == "user_id") { + trans_map.emplace("user_id", static_cast(trans.get_user_id())); + } else if (attr == "description") { + trans_map.emplace("description", trans.get_description()); + } else if (attr == "status") { + trans_map.emplace("status", libdnf5::transaction::transaction_state_to_string(trans.get_state())); + } else if (attr == "releasever") { + trans_map.emplace("releasever", trans.get_releasever()); + } else if (attr == "comment") { + trans_map.emplace("comment", trans.get_comment()); + } + } + + // Add packages if requested + if (include_packages) { + dnfdaemon::KeyValueMapList installed; + dnfdaemon::KeyValueMapList removed; + dnfdaemon::KeyValueMapList upgraded; + dnfdaemon::KeyValueMapList downgraded; + dnfdaemon::KeyValueMapList reinstalled; + + // Track replaced packages to associate them with upgrades/downgrades + // Map from NA to the replaced package + std::unordered_map replaced_packages; + + // First pass: collect replaced packages + for (const auto & pkg : trans.get_packages()) { + if (pkg.get_action() == Action::REPLACED) { + std::string na = pkg.get_name() + "." + pkg.get_arch(); + replaced_packages.emplace(na, pkg); + } + } + + // Second pass: categorize packages + for (const auto & pkg : trans.get_packages()) { + const auto action = pkg.get_action(); + auto pkg_map = trans_package_to_map(pkg, package_attrs); + + switch (action) { + case Action::INSTALL: + installed.push_back(std::move(pkg_map)); + break; + case Action::REMOVE: + removed.push_back(std::move(pkg_map)); + break; + case Action::UPGRADE: { + std::string na = pkg.get_name() + "." + pkg.get_arch(); + auto it = replaced_packages.find(na); + if (it != replaced_packages.end()) { + pkg_map.emplace("original_evr", get_evr(it->second)); + } + upgraded.push_back(std::move(pkg_map)); + break; + } + case Action::DOWNGRADE: { + std::string na = pkg.get_name() + "." + pkg.get_arch(); + auto it = replaced_packages.find(na); + if (it != replaced_packages.end()) { + pkg_map.emplace("original_evr", get_evr(it->second)); + } + downgraded.push_back(std::move(pkg_map)); + break; + } + case Action::REINSTALL: + reinstalled.push_back(std::move(pkg_map)); + break; + case Action::REPLACED: + case Action::REASON_CHANGE: + case Action::ENABLE: + case Action::DISABLE: + case Action::RESET: + case Action::SWITCH: + // Skip these actions - REPLACED is handled above, + // others are module-related or reason changes + break; + } + } + + trans_map.emplace("installed", installed); + trans_map.emplace("removed", removed); + trans_map.emplace("upgraded", upgraded); + trans_map.emplace("downgraded", downgraded); + trans_map.emplace("reinstalled", reinstalled); + } + + output.push_back(std::move(trans_map)); + } + + auto reply = call.createReply(); + reply << output; + return reply; +} diff --git a/dnf5daemon-server/services/history/history.hpp b/dnf5daemon-server/services/history/history.hpp index 952f0e807b..fce514632d 100644 --- a/dnf5daemon-server/services/history/history.hpp +++ b/dnf5daemon-server/services/history/history.hpp @@ -33,6 +33,7 @@ class History : public IDbusSessionService { private: sdbus::MethodReply recent_changes(sdbus::MethodCall & call); + sdbus::MethodReply list(sdbus::MethodCall & call); }; #endif diff --git a/include/libdnf5/transaction/transaction_history.hpp b/include/libdnf5/transaction/transaction_history.hpp index 0ff0de947a..24dc8b5eca 100644 --- a/include/libdnf5/transaction/transaction_history.hpp +++ b/include/libdnf5/transaction/transaction_history.hpp @@ -65,11 +65,23 @@ class LIBDNF_API TransactionHistory { /// @return The listed transactions. std::vector list_transactions(int64_t start, int64_t end); + /// Lists all transactions that are newer than the provided timestamp. + /// + /// @param start The point of time against which is compared the timestamp + /// of the end of the transaction. + /// @return List of all transaction that ended after the timestamp. + std::vector list_transactions_since(int64_t start); + /// Lists all transactions from the transaction history. /// /// @return The listed transactions. std::vector list_all_transactions(); + /// Returns the latest transaction. + /// + /// @return The latest transaction. + Transaction get_latest_transaction(); + /// @return The `Base` object to which this object belongs. /// @since 5.0 libdnf5::BaseWeakPtr get_base() const; diff --git a/libdnf5/transaction/db/trans.cpp b/libdnf5/transaction/db/trans.cpp index 6f0e833919..fbf813da7b 100644 --- a/libdnf5/transaction/db/trans.cpp +++ b/libdnf5/transaction/db/trans.cpp @@ -109,6 +109,29 @@ std::vector TransactionDbUtils::select_transactions_by_ids( return TransactionDbUtils::load_from_select(base, query); } +std::vector TransactionDbUtils::select_transactions_since(const BaseWeakPtr & base, int64_t start) { + auto conn = transaction_db_connect(*base); + + std::string sql = select_sql; + sql += "WHERE \"dt_end\" > ?"; + + auto query = libdnf5::utils::SQLite3::Query(*conn, sql); + query.bindv(start); + + return TransactionDbUtils::load_from_select(base, query); +} + + +Transaction TransactionDbUtils::select_latest_transaction(const BaseWeakPtr & base) { + auto conn = transaction_db_connect(*base); + + std::string sql = select_sql; + sql += "ORDER BY \"trans\".\"id\" DESC LIMIT 1"; + + auto query = libdnf5::utils::SQLite3::Query(*conn, sql); + return TransactionDbUtils::load_from_select(base, query).front(); +} + std::vector TransactionDbUtils::select_transactions_by_range( const BaseWeakPtr & base, int64_t start, int64_t end) { diff --git a/libdnf5/transaction/db/trans.hpp b/libdnf5/transaction/db/trans.hpp index b23c4af36e..e164b27d59 100644 --- a/libdnf5/transaction/db/trans.hpp +++ b/libdnf5/transaction/db/trans.hpp @@ -49,6 +49,12 @@ class TransactionDbUtils { static std::vector select_transactions_by_ids( const BaseWeakPtr & base, const std::vector & ids); + // Selects all transactions that were finished after the ‹start› timestamp. + static std::vector select_transactions_since(const BaseWeakPtr & base, int64_t start); + + /// Selects latest transaction. + static Transaction select_latest_transaction(const BaseWeakPtr & base); + /// Selects transactions with ids within the [start, end] range (inclusive). static std::vector select_transactions_by_range(const BaseWeakPtr & base, int64_t start, int64_t end); diff --git a/libdnf5/transaction/transaction_history.cpp b/libdnf5/transaction/transaction_history.cpp index adb4ef3223..6b10d0faeb 100644 --- a/libdnf5/transaction/transaction_history.cpp +++ b/libdnf5/transaction/transaction_history.cpp @@ -63,10 +63,18 @@ std::vector TransactionHistory::list_transactions(int64_t start, in return TransactionDbUtils::select_transactions_by_range(p_impl->base, start, end); } +std::vector TransactionHistory::list_transactions_since(int64_t start) { + return TransactionDbUtils::select_transactions_since(p_impl->base, start); +} + std::vector TransactionHistory::list_all_transactions() { return TransactionDbUtils::select_transactions_by_ids(p_impl->base, {}); } +Transaction TransactionHistory::get_latest_transaction() { + return TransactionDbUtils::select_latest_transaction(p_impl->base); +} + BaseWeakPtr TransactionHistory::get_base() const { return p_impl->base; }