Skip to content
Open
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
38 changes: 38 additions & 0 deletions dnf5daemon-server/dbus/interfaces/org.rpm.dnf.v0.History.xml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,44 @@ along with libdnf. If not, see <https://www.gnu.org/licenses/>.
<arg name="options" type="a{sv}" direction="in" />
<arg name="changeset" type="a{saa{sv}}" direction="out" />
</method>

<!--
list:
@options: an array of key/value pairs
@transactions: array of transactions with their packages

Get a list of transactions from the history database. Each transaction
includes metadata and categorized package changes.

The packages in each category are represented as dictionaries containing
the requested set of attributes. For upgraded and downgraded packages,
a "from_evr" attribute is included with the original package EVR.

Following options are supported:

- limit: int64
Maximum number of transactions to return.
- since: int64, unix timestamp
Only return transactions completed after this time.
- reverse: boolean, default "false"
If true, return oldest transactions first. Default is newest first.
- contains_pkgs: list of strings
Filter to transactions containing packages with these names.
- transaction_attrs: list of strings, default ["id", "start", "end", "user_id", "description", "status"]
List of transaction attributes to include. Supported: id, start, end,
user_id, description, status, releasever, comment.
- package_attrs: list of strings, default ["name", "arch", "evr"]
List of package attributes to include. Supported: name, epoch,
version, release, arch, evr, repo_id, action, reason.
- include_packages: boolean, default "true"
Whether to include package lists (installed, removed, upgraded, downgraded).

Unknown options are ignored.
-->
<method name="list">
<arg name="options" type="a{sv}" direction="in" />
<arg name="transactions" type="aa{sv}" direction="out" />
</method>
</interface>

</node>
247 changes: 226 additions & 21 deletions dnf5daemon-server/services/history/history.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
#include <libdnf5/rpm/package_query.hpp>
#include <libdnf5/transaction/rpm_package.hpp>
#include <libdnf5/transaction/transaction_history.hpp>
#include <libdnf5/transaction/transaction_item_action.hpp>
#include <libdnf5/transaction/transaction_item_reason.hpp>
#include <libdnf5/utils/format.hpp>
#include <sdbus-c++/sdbus-c++.h>

Expand All @@ -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(
Expand All @@ -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
}

Expand Down Expand Up @@ -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<int64_t>(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<int64_t>{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()
Expand Down Expand Up @@ -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<std::string> & 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<int64_t>(options, "limit", 0);
auto reverse = dnfdaemon::key_value_map_get<bool>(options, "reverse", false);
auto include_packages = dnfdaemon::key_value_map_get<bool>(options, "include_packages", true);
auto contains_pkgs =
dnfdaemon::key_value_map_get<std::vector<std::string>>(options, "contains_pkgs", std::vector<std::string>{});
auto transaction_attrs = dnfdaemon::key_value_map_get<std::vector<std::string>>(
options,
"transaction_attrs",
std::vector<std::string>{"id", "start", "end", "user_id", "description", "status"});
auto package_attrs = dnfdaemon::key_value_map_get<std::vector<std::string>>(
options, "package_attrs", std::vector<std::string>{"name", "arch", "evr"});

auto & base = *session.get_base();
libdnf5::transaction::TransactionHistory history(base);

// Get transactions
std::vector<libdnf5::transaction::Transaction> transactions;
if (options.contains("since")) {
int64_t timestamp = dnfdaemon::key_value_map_get<int64_t>(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<size_t>(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<uint32_t>(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<std::string, libdnf5::transaction::Package> 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;
}
1 change: 1 addition & 0 deletions dnf5daemon-server/services/history/history.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class History : public IDbusSessionService {

private:
sdbus::MethodReply recent_changes(sdbus::MethodCall & call);
sdbus::MethodReply list(sdbus::MethodCall & call);
};

#endif
12 changes: 12 additions & 0 deletions include/libdnf5/transaction/transaction_history.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,23 @@ class LIBDNF_API TransactionHistory {
/// @return The listed transactions.
std::vector<Transaction> 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<Transaction> list_transactions_since(int64_t start);

/// Lists all transactions from the transaction history.
///
/// @return The listed transactions.
std::vector<Transaction> 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;
Expand Down
23 changes: 23 additions & 0 deletions libdnf5/transaction/db/trans.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,29 @@ std::vector<Transaction> TransactionDbUtils::select_transactions_by_ids(
return TransactionDbUtils::load_from_select(base, query);
}

std::vector<Transaction> 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<Transaction> TransactionDbUtils::select_transactions_by_range(
const BaseWeakPtr & base, int64_t start, int64_t end) {
Expand Down
Loading
Loading