From 4b016cee3ff42c36349714c1b38b394b4317174f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ale=C5=A1=20Mat=C4=9Bj?= <amatej@redhat.com>
Date: Mon, 25 Sep 2023 08:24:40 +0200
Subject: [PATCH] Avoid reinstalling installonly packages marked for ERASE

Without this patch reinstalling installonly pkg marked for ERASE might
be a valid smallest solution to our job.

For example when user wants to install through a provide we select all
packages that provide it and put them inside a `job install oneof ...`
if one of the providers is also marked for ERASE due to installonly
limit libsolv might decide to reinstall it.

To make sure it doesn't happen mark the available package also as ERASE.

https://github.com/openSUSE/libsolv/issues/540

https://issues.redhat.com/browse/RHEL-1253
(https://bugzilla.redhat.com/show_bug.cgi?id=2163474)

Conflict:NA
Reference:https://github.com/rpm-software-management/libdnf/commit/4b016cee3ff42c36349714c1b38b394b4317174f
---
 libdnf/goal/Goal.cpp | 35 +++++++++++++++++++++++++++++++++--
 1 file changed, 33 insertions(+), 2 deletions(-)

diff --git a/libdnf/goal/Goal.cpp b/libdnf/goal/Goal.cpp
index 42c078286..269f6b398 100644
--- a/libdnf/goal/Goal.cpp
+++ b/libdnf/goal/Goal.cpp
@@ -643,6 +643,12 @@ erase_flags2libsolv(int flags)
     return ret;
 }
 
+static bool
+NameSolvableComparator(const Solvable * first, const Solvable * second)
+{
+    return first->name < second->name;
+}
+
 Goal::Goal(const Goal & goal_src) : pImpl(new Impl(*goal_src.pImpl)) {}
 
 Goal::Impl::Impl(const Goal::Impl & goal_src)
@@ -1436,10 +1442,24 @@ Goal::Impl::limitInstallonlyPackages(Solver *solv, Queue *job)
     for (int i = 0; i < onlies->count; ++i) {
         Id p, pp;
         IdQueue q, installing;
+        std::vector<Solvable *> available_unused_providers;
 
+        // Add all providers of installonly provides that are marked for install
+        // to `q` IdQueue those that are not marked for install and are not already
+        // installed are added to available_unused_providers.
         FOR_PKG_PROVIDES(p, pp, onlies->elements[i])
-            if (solver_get_decisionlevel(solv, p) > 0)
+            // According to libsolv-bindings the decision level is positive for installs
+            // and negative for conflicts (conflicts with another package or dependency
+            // conflicts = dependencies cannot be met).
+            if (solver_get_decisionlevel(solv, p) > 0) {
                 q.pushBack(p);
+            } else {
+                Solvable *s = pool_id2solvable(pool, p);
+                if (s->repo != pool->installed) {
+                    available_unused_providers.push_back(s);
+                }
+            }
+
         if (q.size() <= (int) dnf_sack_get_installonly_limit(sack)) {
             continue;
         }
@@ -1457,6 +1477,7 @@ Goal::Impl::limitInstallonlyPackages(Solver *solv, Queue *job)
 
         struct InstallonliesSortCallback s_cb = {pool, dnf_sack_running_kernel(sack)};
         solv_sort(q.data(), q.size(), sizeof(q[0]), sort_packages, &s_cb);
+        std::sort(available_unused_providers.begin(), available_unused_providers.end(), NameSolvableComparator);
         IdQueue same_names;
         while (q.size() > 0) {
             same_name_subqueue(pool, q.getQueue(), same_names.getQueue());
@@ -1466,8 +1487,18 @@ Goal::Impl::limitInstallonlyPackages(Solver *solv, Queue *job)
             for (int j = 0; j < same_names.size(); ++j) {
                 Id id  = same_names[j];
                 Id action = SOLVER_ERASE;
-                if (j < (int) dnf_sack_get_installonly_limit(sack))
+                if (j < (int) dnf_sack_get_installonly_limit(sack)) {
                     action = SOLVER_INSTALL;
+                } else  {
+                    // We want to avoid reinstalling packages marked for ERASE, therefore
+                    // if some unused provider is also available we need to mark it ERASE as well.
+                    Solvable *s = pool_id2solvable(pool, id);
+                    auto low = std::lower_bound(available_unused_providers.begin(), available_unused_providers.end(), s, NameSolvableComparator);
+                    while (low != available_unused_providers.end() && (*low)->name == s->name) {
+                        queue_push2(job, SOLVER_ERASE | SOLVER_SOLVABLE, pool_solvable2id(pool, *low));
+                        ++low;
+                    }
+                }
                 queue_push2(job, action | SOLVER_SOLVABLE, id);
             }
         }
