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