Skip to content

Commit 9a47ccf

Browse files
committed
fix(pm): explain exact index route misses
1 parent 189c6d1 commit 9a47ccf

4 files changed

Lines changed: 64 additions & 0 deletions

File tree

src/pm/commands.cppm

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,10 @@ inline int cmd_add(const mcpplibs::cmdline::ParsedArgs& parsed) {
249249
hint += "\n hint: `mcpp index update` if it was published "
250250
"recently";
251251
}
252+
if (!selector.candidates.empty()) {
253+
hint += "\n route: " +
254+
route.describe(selector.candidates.front().namespace_);
255+
}
252256
mcpp::ui::error(std::format(
253257
"package '{}' not found in any configured index\n tried: {}{}",
254258
canonicalSelector, detail::format_tried(selector.candidates),

src/pm/index_route.cppm

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ struct IndexRoute {
5959
bool lazy_git(std::string_view ns) const;
6060
// Can a miss in this namespace be taken as proof of absence?
6161
bool authoritative_for(std::string_view ns) const;
62+
// Privacy-safe route state for a failed exact lookup. This deliberately
63+
// omits filesystem paths: CI and user diagnostics need to distinguish a
64+
// missing declaration from a missing local checkout without publishing
65+
// workstation-specific locations.
66+
std::string describe(std::string_view ns) const;
6267
// Read the descriptor for one coordinate through the right transport.
6368
std::optional<std::string> read(const DependencyCoordinate& coord) const;
6469
};
@@ -134,6 +139,35 @@ bool IndexRoute::authoritative_for(std::string_view ns) const {
134139
return ns.starts_with(std::string(kDefaultNamespace) + ".");
135140
}
136141

142+
std::string IndexRoute::describe(std::string_view ns) const {
143+
auto* idx = find_for_ns(ns);
144+
if (!idx) {
145+
return std::format(
146+
"builtin index for namespace '{}': registry {}",
147+
ns, cfg ? "available" : "unavailable");
148+
}
149+
if (idx->is_local()) {
150+
const auto root = mcpp::config::resolve_project_index_path(
151+
projectRoot, *idx);
152+
std::error_code ec;
153+
const bool rootPresent = std::filesystem::is_directory(root, ec);
154+
ec.clear();
155+
const bool pkgsPresent =
156+
std::filesystem::is_directory(root / "pkgs", ec);
157+
const auto label = idx->name.empty() ? std::string(ns) : idx->name;
158+
return std::format(
159+
"local index '{}': root {}, pkgs {}", label,
160+
rootPresent ? "present" : "absent",
161+
pkgsPresent ? "present" : "absent");
162+
}
163+
if (idx->is_builtin()) {
164+
return std::format(
165+
"builtin index '{}': registry {}", idx->name,
166+
cfg ? "available" : "unavailable");
167+
}
168+
return std::format("git index '{}': checkout pending", idx->name);
169+
}
170+
137171
std::optional<std::string>
138172
IndexRoute::read(const DependencyCoordinate& coord) const {
139173
auto* idx = find_for_ns(coord.namespace_);

tests/e2e/12_add_command.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,10 @@ diff -q "$TMP/before" mcpp.toml || { cat mcpp.toml; echo "mcpp.toml mutated for
193193
# (11) Same for an explicitly-namespaced miss in a readable index.
194194
err=$("$MCPP" add acme.nope@1.0.0 2>&1) && { echo "expected error for missing acme package"; exit 1; }
195195
[[ "$err" == *"(acme, nope)"* ]] || { echo "wrong error: $err"; exit 1; }
196+
[[ "$err" == *"route: local index 'acme': root present, pkgs present"* ]] || {
197+
echo "missing privacy-safe local-index route state: $err"
198+
exit 1
199+
}
196200
diff -q "$TMP/before" mcpp.toml || { cat mcpp.toml; echo "mcpp.toml mutated for missing package"; exit 1; }
197201

198202
# (12) A namespace no readable index covers cannot be refuted, so the add goes

tests/unit/test_pm_index_route.cpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,26 @@ TEST(PmIndexRoute, LocalPathIndexIsAuthoritative) {
6464
EXPECT_TRUE(route.authoritative_for("acme"));
6565
}
6666

67+
TEST(PmIndexRoute, RouteDescriptionDoesNotExposeItsLocalPath) {
68+
LocalIndex idx("diagnostic-privacy");
69+
auto indices = local_map(idx);
70+
mcpp::pm::IndexRoute route{ &indices, "/nowhere", nullptr };
71+
72+
const auto description = route.describe("acme");
73+
EXPECT_EQ(description, "local index 'acme': root present, pkgs present");
74+
EXPECT_EQ(description.find(idx.root.string()), std::string::npos);
75+
}
76+
77+
TEST(PmIndexRoute, RouteDescriptionDistinguishesAMissingLocalRoot) {
78+
mcpp::pm::IndexMap indices;
79+
indices["acme"] = mcpp::pm::IndexSpec{
80+
.name = "acme", .path = "missing-local-index" };
81+
mcpp::pm::IndexRoute route{ &indices, "/nowhere", nullptr };
82+
83+
EXPECT_EQ(route.describe("acme"),
84+
"local index 'acme': root absent, pkgs absent");
85+
}
86+
6787
// The regression behind #305/#307: a dotted selector is one exact NAMESPACE
6888
// PATH, so it resolves through the coordinate the manifest parser derives. Probing the
6989
// literal short name can never match — `package.name` is a single atomic
@@ -202,6 +222,8 @@ version = "0.1.0"
202222
(root / "index").lexically_normal());
203223

204224
mcpp::pm::IndexRoute route{ &indices, root / "member", nullptr };
225+
EXPECT_EQ(route.describe("acme"),
226+
"local index 'acme': root present, pkgs present");
205227
auto selector = mcpp::pm::resolve_dependency_selector("acme.util");
206228
auto found = mcpp::pm::lookup_descriptor(route, selector.candidates);
207229
ASSERT_TRUE(found.hit.has_value());

0 commit comments

Comments
 (0)