From 965822885f51988665fd83a7c994f439b4e61736 Mon Sep 17 00:00:00 2001 From: Matej Focko Date: Mon, 30 Mar 2026 09:07:40 +0200 Subject: [PATCH 1/2] feat(daemon): add History.list method Fixes #2538 Assisted-by: Claude Opus 4.5 Signed-off-by: Matej Focko --- .../interfaces/org.rpm.dnf.v0.History.xml | 38 +++ .../services/history/history.cpp | 257 ++++++++++++++++-- .../services/history/history.hpp | 1 + 3 files changed, 276 insertions(+), 20 deletions(-) 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 17ea0ab75a..d091b1571d 100644 --- a/dnf5daemon-server/dbus/interfaces/org.rpm.dnf.v0.History.xml +++ b/dnf5daemon-server/dbus/interfaces/org.rpm.dnf.v0.History.xml @@ -83,6 +83,44 @@ along with libdnf. If not, see . + + + + + + diff --git a/dnf5daemon-server/services/history/history.cpp b/dnf5daemon-server/services/history/history.cpp index a68b9f2db8..0e34667de9 100644 --- a/dnf5daemon-server/services/history/history.cpp +++ b/dnf5daemon-server/services/history/history.cpp @@ -29,26 +29,54 @@ #include #include #include +#include +#include #include #include #include +namespace { + +/// Formats the EVR of the package from a transaction. Omits the epoch when set +/// to ‹0›. +std::string get_evr(const libdnf5::transaction::Package & trans_pkg) { + auto epoch = trans_pkg.get_epoch(); + if (epoch == "0") { + return libdnf5::utils::sformat("{}-{}", trans_pkg.get_version(), trans_pkg.get_release()); + } else { + return libdnf5::utils::sformat("{}:{}-{}", epoch, trans_pkg.get_version(), trans_pkg.get_release()); + } +} + +} // namespace + 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,18 +89,19 @@ 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 } -std::string get_evr(const libdnf5::transaction::Package & trans_pkg) { - auto epoch = trans_pkg.get_epoch(); - if (epoch == "0") { - return libdnf5::utils::sformat("{}-{}", trans_pkg.get_version(), trans_pkg.get_release()); - } else { - return libdnf5::utils::sformat("{}:{}-{}", epoch, trans_pkg.get_version(), trans_pkg.get_release()); - } -} - dnfdaemon::KeyValueMap history_package_to_map(const libdnf5::transaction::Package & trans_pkg) { dnfdaemon::KeyValueMap dbus_package; dbus_package.emplace("name", trans_pkg.get_name()); @@ -273,3 +302,191 @@ 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(attr, pkg.get_name()); + } else if (attr == "arch") { + dbus_pkg.emplace(attr, pkg.get_arch()); + } else if (attr == "evr") { + dbus_pkg.emplace(attr, get_evr(pkg)); + } else if (attr == "epoch") { + dbus_pkg.emplace(attr, pkg.get_epoch()); + } else if (attr == "release") { + dbus_pkg.emplace(attr, pkg.get_release()); + } else if (attr == "repo_id") { + dbus_pkg.emplace(attr, pkg.get_repoid()); + } else if (attr == "action") { + dbus_pkg.emplace(attr, libdnf5::transaction::transaction_item_action_to_string(pkg.get_action())); + } else if (attr == "reason") { + dbus_pkg.emplace(attr, 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 0ec83e854c..c6bc060678 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 From 32d331bc7135e5f3594b06b2b24df5463931c26a Mon Sep 17 00:00:00 2001 From: Matej Focko Date: Thu, 2 Apr 2026 10:35:58 +0200 Subject: [PATCH 2/2] fix(refactor): add specialized transaction listings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Allow getting the latest transaction. • Allow querying the transactions since given timestamp. Signed-off-by: Matej Focko --- .../services/history/history.cpp | 19 +++------------ .../transaction/transaction_history.hpp | 12 ++++++++++ libdnf5/transaction/db/trans.cpp | 23 +++++++++++++++++++ libdnf5/transaction/db/trans.hpp | 6 +++++ libdnf5/transaction/transaction_history.cpp | 8 +++++++ 5 files changed, 52 insertions(+), 16 deletions(-) diff --git a/dnf5daemon-server/services/history/history.cpp b/dnf5daemon-server/services/history/history.cpp index 0e34667de9..fa7a039d38 100644 --- a/dnf5daemon-server/services/history/history.cpp +++ b/dnf5daemon-server/services/history/history.cpp @@ -135,19 +135,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() @@ -353,12 +345,7 @@ sdbus::MethodReply History::list(sdbus::MethodCall & call) { 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)); - } - } + transactions = history.list_transactions_since(timestamp); } else { transactions = history.list_all_transactions(); } 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; }