Create packages diff between the current state and a revision (#3911)

This commit is contained in:
Sandrine Pataut 2025-05-16 15:07:43 +02:00 committed by GitHub
parent 3c42ca9712
commit 39988503c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 408 additions and 52 deletions

View File

@ -14,6 +14,7 @@
#include <nlohmann/json.hpp>
#include <yaml-cpp/yaml.h>
#include "mamba/core/history.hpp"
#include "mamba/fs/filesystem.hpp"
#include "mamba/solver/request.hpp"

View File

@ -7,7 +7,6 @@
#ifndef MAMBA_CORE_HISTORY
#define MAMBA_CORE_HISTORY
#include <set>
#include <string>
#include <unordered_map>
#include <vector>
@ -15,6 +14,7 @@
#include "mamba/core/channel_context.hpp"
#include "mamba/fs/filesystem.hpp"
#include "mamba/specs/match_spec.hpp"
#include "mamba/specs/package_info.hpp"
namespace mamba
{
@ -29,7 +29,7 @@ namespace mamba
struct ParseResult
{
std::string head_line;
std::set<std::string> diff;
std::vector<std::string> diff;
std::vector<std::string> comments;
};
@ -38,7 +38,7 @@ namespace mamba
static UserRequest prefilled(const Context& context);
std::string date;
int revision_num = 0;
std::size_t revision_num = 0;
std::string cmd;
std::string conda_version;
@ -61,6 +61,37 @@ namespace mamba
ChannelContext& m_channel_context;
};
/** PackageDiff contains two maps of packages and their package info, one being for the
* installed packages, the other for the removed ones. This is used while looping on
* revisions to get the diff between the target revision and the current one.
*/
struct PackageDiff
{
std::unordered_map<std::string, specs::PackageInfo> removed_pkg_diff;
std::unordered_map<std::string, specs::PackageInfo> installed_pkg_diff;
[[nodiscard]] static PackageDiff
from_revision(const std::vector<History::UserRequest>& user_requests, std::size_t target_revision);
};
/** The following function parses the different formats that can be found in the history
* file.
*
* conda/mamba1 format:
*
* installed: +conda-forge/linux-64::xtl-0.8.0-h84d6215_0
* removed: -conda-forge/linux-64::xtl-0.8.0-h84d6215_0
*
* mamba2 broken format:
*
* installed: +conda-forge::xtl-0.8.0-h84d6215_0
* removed: -https://conda.anaconda.org/conda-forge/linux-64::xtl-0.8.0-h84d6215_0
*
* mamba2 new format:
* installed: +https://conda.anaconda.org/conda-forge/linux-64::xtl-0.8.0-h84d6215_0
* removed: -https://conda.anaconda.org/conda-forge/linux-64::xtl-0.8.0-h84d6215_0
*/
specs::PackageInfo read_history_url_entry(const std::string& s);
} // namespace mamba
#endif

View File

@ -4,6 +4,7 @@
//
// The full license is in the file LICENSE, distributed with this software.
#include <list>
#include <regex>
#include "mamba/core/channel_context.hpp"
@ -81,12 +82,12 @@ namespace mamba
{
if (res.size() > 0)
{
res[res.size() - 1].diff.insert(line);
res[res.size() - 1].diff.push_back(line);
}
else
{
res.push_back(ParseResult());
res[0].diff.insert(line);
res[0].diff.push_back(line);
}
}
}
@ -203,7 +204,7 @@ namespace mamba
std::vector<History::UserRequest> History::get_user_requests()
{
std::vector<UserRequest> res;
int revision_num = 0;
std::size_t revision_num = 0;
for (const auto& el : parse())
{
UserRequest r;
@ -325,4 +326,187 @@ namespace mamba
out << specs_output("neutered", entry.neutered);
}
}
specs::PackageInfo read_history_url_entry(const std::string& s)
{
std::string name_version_build_string;
std::string channel;
std::size_t pos_0 = s.rfind("/");
if (pos_0 != std::string::npos)
{
// s is of the form of
// `https://conda.anaconda.org/conda-forge/linux-64::xtl-0.8.0-h84d6215_0` or
// `conda-forge/linux-64::xtl-0.8.0-h84d6215_0`
std::string s_begin = s.substr(0, pos_0);
// s_begin is of the form of `https://conda.anaconda.org/conda-forge` or
// `conda-forge`
std::string s_end = s.substr(pos_0 + 1, s.size());
// s_end is of the form of `linux-64::xtl-0.8.0-h84d6215_0`
std::size_t pos = s_begin.rfind("/");
if (pos != std::string::npos)
{
channel = s_begin.substr(pos + 1, s_begin.size());
}
else
{
channel = s_begin;
}
name_version_build_string = std::get<1>(util::rsplit_once(s_end, "::"));
}
else
{
// s is of the form of `conda-forge::xtl-0.8.0-h84d6215_0`
auto double_colon_split = util::split_once(s, "::");
channel = std::get<0>(double_colon_split);
name_version_build_string = std::get<1>(double_colon_split).value_or("");
}
// `name_version_build_string` is of the form `xtl-0.8.0-h84d6215_0`
std::size_t pos_1 = name_version_build_string.rfind("-");
std::string name_version = name_version_build_string.substr(0, pos_1);
// `name_version` is of the form `xtl-0.8.0`
std::string build_string = name_version_build_string.substr(
pos_1 + 1,
name_version_build_string.size()
);
std::size_t pos_2 = name_version.rfind("-");
std::string name = name_version.substr(0, pos_2);
std::string version = name_version.substr(pos_2 + 1, name_version.size());
specs::PackageInfo pkg_info{ name, version, build_string, channel };
return pkg_info;
}
PackageDiff PackageDiff::from_revision(
const std::vector<History::UserRequest>& user_requests,
std::size_t target_revision
)
{
assert(!user_requests.empty());
struct revision
{
std::size_t key = 0;
std::unordered_map<std::string, specs::PackageInfo> removed_pkg = {};
std::unordered_map<std::string, specs::PackageInfo> installed_pkg = {};
};
std::list<revision> revisions;
for (auto r : user_requests)
{
if ((r.link_dists.size() > 0) || (r.unlink_dists.size() > 0))
{
if (r.revision_num > target_revision)
{
revision rev{ /*.key = */ r.revision_num };
for (auto ud : r.unlink_dists)
{
auto pkg_info = read_history_url_entry(ud);
const auto name = pkg_info.name;
rev.removed_pkg[name] = std::move(pkg_info);
}
for (auto ld : r.link_dists)
{
auto pkg_info = read_history_url_entry(ld);
const auto name = pkg_info.name;
rev.installed_pkg[name] = std::move(pkg_info);
}
revisions.push_back(rev);
}
}
}
PackageDiff pkg_diff{};
const auto handle_install = [&pkg_diff](revision& rev, const std::string& pkg_name)
{
bool res = false;
if (auto rev_iter = rev.installed_pkg.find(pkg_name); rev_iter != rev.installed_pkg.end())
{
const auto version = rev_iter->second.version;
auto iter = pkg_diff.removed_pkg_diff.find(pkg_name);
if (iter != pkg_diff.removed_pkg_diff.end() && iter->second.version == version)
{
pkg_diff.removed_pkg_diff.erase(iter);
}
else
{
pkg_diff.installed_pkg_diff[pkg_name] = rev_iter->second;
}
rev.installed_pkg.erase(rev_iter);
res = true;
}
return res;
};
auto handle_remove = [&pkg_diff](revision& rev, const std::string& pkg_name)
{
bool res = false;
if (auto rev_iter = rev.removed_pkg.find(pkg_name); rev_iter != rev.removed_pkg.end())
{
const auto version = rev_iter->second.version;
auto iter = pkg_diff.installed_pkg_diff.find(pkg_name);
if (iter != pkg_diff.installed_pkg_diff.end() && iter->second.version == version)
{
pkg_diff.installed_pkg_diff.erase(iter);
}
else
{
pkg_diff.removed_pkg_diff[pkg_name] = rev_iter->second;
}
rev.removed_pkg.erase(rev_iter);
res = true;
}
return res;
};
while (!revisions.empty())
{
auto& revision = *(revisions.begin());
while (!revision.removed_pkg.empty())
{
auto [pkg_name, pkg_info] = *(revision.removed_pkg.begin());
pkg_diff.removed_pkg_diff[pkg_name] = pkg_info;
revision.removed_pkg.erase(pkg_name);
bool lastly_removed = true; // whether last operation on package was a removal
lastly_removed = !handle_install(revision, pkg_name);
for (auto rev = std::next(revisions.begin()); rev != revisions.end(); ++rev)
{
if (lastly_removed)
{
lastly_removed = !handle_install(*rev, pkg_name);
}
else
{
lastly_removed = handle_remove(*rev, pkg_name);
if (lastly_removed)
{
lastly_removed = !handle_install(*rev, pkg_name);
}
}
}
}
while (!revision.installed_pkg.empty())
{
auto [pkg_name, pkg_info] = *(revision.installed_pkg.begin());
pkg_diff.installed_pkg_diff[pkg_name] = pkg_info;
revision.installed_pkg.erase(pkg_name);
bool lastly_removed = false;
for (auto rev = ++revisions.begin(); rev != revisions.end(); ++rev)
{
if (!lastly_removed)
{
lastly_removed = handle_remove(*rev, pkg_name);
if (lastly_removed)
{
lastly_removed = !handle_install(*rev, pkg_name);
}
}
}
}
revisions.pop_front();
}
return pkg_diff;
}
} // namespace mamba

View File

@ -1,24 +1,74 @@
==> 2020-05-03 11:18:43 <==
# cmd: /usr/bin/conda create -n mamba
# conda version: 4.8.3
==> 2020-05-03 11:19:42 <==
# cmd: /usr/bin/conda install pybind11 libsolv libarchive libcurl nlohmann_json pip cpp-tabulate -c conda-forge
# conda version: 4.8.3
+conda-forge/linux-64::_libgcc_mutex-0.1-conda_forge
# update specs: ["cpp-tabulate", "libsolv", "nlohmann_json", "pybind11", "pip", "libarchive", "libcurl"]
==> 2020-05-04 08:58:01 <==
# cmd: /home/mariana/.local/bin/mamba remove cpp-tabulate pybind11 pip
# conda version: 4.8.3
-conda-forge/noarch::wheel-0.34.2-py_1
# remove specs: ["cpp-tabulate", "pybind11", "pip"]
==> 2020-05-04 08:59:15 <==
# cmd: /home/mariana/.local/bin/mamba install -c conda-forge cpp-tabulate==1.0.0
# conda version: 4.8.3
+conda-forge/linux-64::cpp-tabulate-1.0-hc9558a2_0
# update specs: ["cpp-tabulate==1.0.0"]
==> 2020-05-04 09:00:02 <==
# cmd: /home/mariana/.local/bin/mamba upgrade -c conda-forge cpp-tabulate
# conda version: 4.8.3
-conda-forge/linux-64::cpp-tabulate-1.0-hc9558a2_0
+conda-forge/linux-64::cpp-tabulate-1.2-hc9558a2_0
==> 2025-04-14 09:23:10 <==
# cmd: /home/sandrinepataut/.local/bin/micromamba install nlohmann_json
# conda version: 3.8.0
+conda-forge::nlohmann_json-3.12.0-h3f2d84a_0
# update specs: ["nlohmann_json"]
==> 2025-04-14 09:23:50 <==
# cmd: /home/sandrinepataut/.local/bin/micromamba install xtl=0.7.2
# conda version: 3.8.0
+conda-forge::libgomp-14.2.0-h767d61c_2
+conda-forge::_libgcc_mutex-0.1-conda_forge
+conda-forge::_openmp_mutex-4.5-2_gnu
+conda-forge::libgcc-14.2.0-h767d61c_2
+conda-forge::libstdcxx-14.2.0-h8f9b012_2
+conda-forge::libgcc-ng-14.2.0-h69a702a_2
+conda-forge::libstdcxx-ng-14.2.0-h4852527_2
+conda-forge::xtl-0.7.2-h4bd325d_1
# update specs: ["xtl=0.7.2"]
==> 2025-04-14 09:24:22 <==
# cmd: /home/sandrinepataut/.local/bin/micromamba install cpp-tabulate=1.0
# conda version: 3.8.0
+conda-forge::cpp-tabulate-1.0-hc9558a2_0
# update specs: ["cpp-tabulate=1.0"]
==> 2025-04-14 09:24:49 <==
# cmd: /home/sandrinepataut/.local/bin/micromamba update cpp-tabulate
# conda version: 3.8.0
-https://conda.anaconda.org/conda-forge/linux-64::cpp-tabulate-1.0-hc9558a2_0
+conda-forge::cpp-tabulate-1.5-hf52228f_1
# update specs: ["cpp-tabulate"]
# remove specs: ["cpp-tabulate"]
==> 2025-04-14 09:25:49 <==
# cmd: /home/sandrinepataut/.local/bin/micromamba install wheel=0.34.2 openssl=3.5.0
# conda version: 3.8.0
+conda-forge::libexpat-2.7.0-h5888daf_0
+conda-forge::liblzma-5.8.1-hb9d3cd8_0
+conda-forge::libmpdec-4.0.0-h4bc722e_0
+conda-forge::libuuid-2.38.1-h0b41bf4_0
+conda-forge::bzip2-1.0.8-h4bc722e_7
+conda-forge::ld_impl_linux-64-2.43-h712a8e2_4
+conda-forge::libffi-3.4.6-h2dba641_1
+conda-forge::libzlib-1.3.1-hb9d3cd8_2
+conda-forge::ncurses-6.5-h2d0b736_3
+conda-forge::python_abi-3.13-6_cp313
+conda-forge::ca-certificates-2025.1.31-hbcca054_0
+conda-forge::tk-8.6.13-noxft_h4845f30_101
+conda-forge::libsqlite-3.49.1-hee588c1_2
+conda-forge::readline-8.2-h8c095d6_2
+conda-forge::openssl-3.5.0-h7b32b05_0
+conda-forge::tzdata-2025b-h78e105d_0
+conda-forge::python-3.13.3-hf636f53_100_cp313
+conda-forge::pip-25.0.1-pyh145f28c_0
+conda-forge::setuptools-78.1.0-pyhff2d567_0
+conda-forge::wheel-0.34.2-py_1
# update specs: ["wheel=0.34.2", "openssl=3.5.0"]
==> 2025-04-14 09:26:26 <==
# cmd: /home/sandrinepataut/.local/bin/micromamba update wheel
# conda version: 3.8.0
-https://conda.anaconda.org/conda-forge/noarch::setuptools-78.1.0-pyhff2d567_0
-https://conda.anaconda.org/conda-forge/noarch::wheel-0.34.2-py_1
+conda-forge::wheel-0.45.1-pyhd8ed1ab_1
# update specs: ["wheel"]
# remove specs: ["wheel"]
==> 2025-04-14 09:27:06 <==
# cmd: /home/sandrinepataut/.local/bin/micromamba remove wheel xtl nlohmann_json
# conda version: 3.8.0
-https://conda.anaconda.org/conda-forge/linux-64::nlohmann_json-3.12.0-h3f2d84a_0
-https://conda.anaconda.org/conda-forge/noarch::wheel-0.45.1-pyhd8ed1ab_1
-https://conda.anaconda.org/conda-forge/linux-64::xtl-0.7.2-h4bd325d_1
# remove specs: ["wheel", "xtl", "nlohmann_json"]
==> 2025-04-14 09:27:41 <==
# cmd: /home/sandrinepataut/.local/bin/micromamba install wheel=0.40.0 xtl=0.8.0
# conda version: 3.8.0
+conda-forge::xtl-0.8.0-h84d6215_0
+conda-forge::wheel-0.40.0-pyhd8ed1ab_1
# update specs: ["wheel=0.40.0", "xtl=0.8.0"]

View File

@ -1,24 +1,74 @@
==> 2020-05-03 11:18:43 <==
# cmd: /usr/bin/conda create -n mamba
# conda version: 4.8.3
==> 2020-05-03 11:19:42 <==
# cmd: /usr/bin/conda install pybind11 libsolv libarchive libcurl nlohmann_json pip cpp-tabulate -c conda-forge
# conda version: 4.8.3
+conda-forge/linux-64::_libgcc_mutex-0.1-conda_forge
# update specs: ["cpp-tabulate", "libsolv", "nlohmann_json", "pybind11", "pip", "libarchive", "libcurl"]
==> 2020-05-04 08:58:01 <==
# cmd: /home/mariana/.local/bin/mamba remove cpp-tabulate pybind11 pip
# conda version: 4.8.3
-conda-forge/noarch::wheel-0.34.2-py_1
# remove specs: ["cpp-tabulate", "pybind11", "pip"]
==> 2020-05-04 08:59:15 <==
# cmd: /home/mariana/.local/bin/mamba install -c conda-forge cpp-tabulate==1.0.0
# conda version: 4.8.3
+conda-forge/linux-64::cpp-tabulate-1.0-hc9558a2_0
# update specs: ["cpp-tabulate==1.0.0"]
==> 2020-05-04 09:00:02 <==
# cmd: /home/mariana/.local/bin/mamba upgrade -c conda-forge cpp-tabulate
# conda version: 4.8.3
-conda-forge/linux-64::cpp-tabulate-1.0-hc9558a2_0
+conda-forge/linux-64::cpp-tabulate-1.2-hc9558a2_0
==> 2025-04-14 09:23:10 <==
# cmd: /home/sandrinepataut/.local/bin/micromamba install nlohmann_json
# conda version: 3.8.0
+conda-forge::nlohmann_json-3.12.0-h3f2d84a_0
# update specs: ["nlohmann_json"]
==> 2025-04-14 09:23:50 <==
# cmd: /home/sandrinepataut/.local/bin/micromamba install xtl=0.7.2
# conda version: 3.8.0
+conda-forge::libgomp-14.2.0-h767d61c_2
+conda-forge::_libgcc_mutex-0.1-conda_forge
+conda-forge::_openmp_mutex-4.5-2_gnu
+conda-forge::libgcc-14.2.0-h767d61c_2
+conda-forge::libstdcxx-14.2.0-h8f9b012_2
+conda-forge::libgcc-ng-14.2.0-h69a702a_2
+conda-forge::libstdcxx-ng-14.2.0-h4852527_2
+conda-forge::xtl-0.7.2-h4bd325d_1
# update specs: ["xtl=0.7.2"]
==> 2025-04-14 09:24:22 <==
# cmd: /home/sandrinepataut/.local/bin/micromamba install cpp-tabulate=1.0
# conda version: 3.8.0
+conda-forge::cpp-tabulate-1.0-hc9558a2_0
# update specs: ["cpp-tabulate=1.0"]
==> 2025-04-14 09:24:49 <==
# cmd: /home/sandrinepataut/.local/bin/micromamba update cpp-tabulate
# conda version: 3.8.0
-https://conda.anaconda.org/conda-forge/linux-64::cpp-tabulate-1.0-hc9558a2_0
+conda-forge::cpp-tabulate-1.5-hf52228f_1
# update specs: ["cpp-tabulate"]
# remove specs: ["cpp-tabulate"]
==> 2025-04-14 09:25:49 <==
# cmd: /home/sandrinepataut/.local/bin/micromamba install wheel=0.34.2 openssl=3.5.0
# conda version: 3.8.0
+conda-forge::libexpat-2.7.0-h5888daf_0
+conda-forge::liblzma-5.8.1-hb9d3cd8_0
+conda-forge::libmpdec-4.0.0-h4bc722e_0
+conda-forge::libuuid-2.38.1-h0b41bf4_0
+conda-forge::bzip2-1.0.8-h4bc722e_7
+conda-forge::ld_impl_linux-64-2.43-h712a8e2_4
+conda-forge::libffi-3.4.6-h2dba641_1
+conda-forge::libzlib-1.3.1-hb9d3cd8_2
+conda-forge::ncurses-6.5-h2d0b736_3
+conda-forge::python_abi-3.13-6_cp313
+conda-forge::ca-certificates-2025.1.31-hbcca054_0
+conda-forge::tk-8.6.13-noxft_h4845f30_101
+conda-forge::libsqlite-3.49.1-hee588c1_2
+conda-forge::readline-8.2-h8c095d6_2
+conda-forge::openssl-3.5.0-h7b32b05_0
+conda-forge::tzdata-2025b-h78e105d_0
+conda-forge::python-3.13.3-hf636f53_100_cp313
+conda-forge::pip-25.0.1-pyh145f28c_0
+conda-forge::setuptools-78.1.0-pyhff2d567_0
+conda-forge::wheel-0.34.2-py_1
# update specs: ["wheel=0.34.2", "openssl=3.5.0"]
==> 2025-04-14 09:26:26 <==
# cmd: /home/sandrinepataut/.local/bin/micromamba update wheel
# conda version: 3.8.0
-https://conda.anaconda.org/conda-forge/noarch::setuptools-78.1.0-pyhff2d567_0
-https://conda.anaconda.org/conda-forge/noarch::wheel-0.34.2-py_1
+conda-forge::wheel-0.45.1-pyhd8ed1ab_1
# update specs: ["wheel"]
# remove specs: ["wheel"]
==> 2025-04-14 09:27:06 <==
# cmd: /home/sandrinepataut/.local/bin/micromamba remove wheel xtl nlohmann_json
# conda version: 3.8.0
-https://conda.anaconda.org/conda-forge/linux-64::nlohmann_json-3.12.0-h3f2d84a_0
-https://conda.anaconda.org/conda-forge/noarch::wheel-0.45.1-pyhd8ed1ab_1
-https://conda.anaconda.org/conda-forge/linux-64::xtl-0.7.2-h4bd325d_1
# remove specs: ["wheel", "xtl", "nlohmann_json"]
==> 2025-04-14 09:27:41 <==
# cmd: /home/sandrinepataut/.local/bin/micromamba install wheel=0.40.0 xtl=0.8.0
# conda version: 3.8.0
+conda-forge::xtl-0.8.0-h84d6215_0
+conda-forge::wheel-0.40.0-pyhd8ed1ab_1
# update specs: ["wheel=0.40.0", "xtl=0.8.0"]

View File

@ -21,6 +21,7 @@
#include "mamba/core/channel_context.hpp"
#include "mamba/core/history.hpp"
#include "mamba/core/prefix_data.hpp"
#include "mambatests.hpp"
@ -105,6 +106,45 @@ namespace mamba
std::vector<History::UserRequest> user_reqs = history_instance.get_user_requests();
}
TEST_CASE("parse_all_formats")
{
std::vector<std::string> test_list{
"conda-forge/linux-64::xtl-0.8.0-h84d6215_0",
"conda-forge::xtl-0.8.0-h84d6215_0",
"https://conda.anaconda.org/conda-forge/linux-64::xtl-0.8.0-h84d6215_0"
};
for (auto s : test_list)
{
specs::PackageInfo pkg_info = mamba::read_history_url_entry(s);
REQUIRE(pkg_info.name == "xtl");
REQUIRE(pkg_info.version == "0.8.0");
REQUIRE(pkg_info.channel == "conda-forge");
REQUIRE(pkg_info.build_string == "h84d6215_0");
}
}
TEST_CASE("revision_diff")
{
auto channel_context = ChannelContext::make_conda_compatible(mambatests::context());
// Gather history from current history file.
History history_instance(mambatests::test_data_dir / "history/parse", channel_context);
const std::vector<History::UserRequest> user_requests = history_instance.get_user_requests(
);
std::size_t target_revision = 1;
auto pkg_diff = PackageDiff::from_revision(user_requests, target_revision);
const auto& removed_pkg_diff = pkg_diff.removed_pkg_diff;
const auto& installed_pkg_diff = pkg_diff.installed_pkg_diff;
REQUIRE(removed_pkg_diff.find("nlohmann_json")->second.version == "3.12.0");
REQUIRE(removed_pkg_diff.find("xtl")->second.version == "0.7.2");
REQUIRE(installed_pkg_diff.find("cpp-tabulate")->second.version == "1.5");
REQUIRE(installed_pkg_diff.find("wheel")->second.version == "0.40.0");
REQUIRE(installed_pkg_diff.find("openssl")->second.version == "3.5.0");
REQUIRE(installed_pkg_diff.find("xtl")->second.version == "0.8.0");
}
#ifndef _WIN32
TEST_CASE("parse_segfault")
{