Repository navigation
Fix NuGet patching the default cache over configured folders (#397, #398) - #1769
Mikola Lysenko (mikolalysenko) merged 5 commits into
Conversation
Assisted-by: Claude Code:claude-opus-5-5
Agent-mode apply, scan and vex picked the first package root holding a package, and the default ~/.nuget/packages came before the project's own restore folders. A project that relocates its packages folder (globalPackagesFolder in nuget.config, RestorePackagesPath, or repositoryPath for packages.config) had the user-wide copy patched instead, apply reported success, and vex attested not_affected while the build used the unpatched copy. With a cold default cache, a project two levels below the solution root was not found at all. Roots are now resolved the way NuGet does: - the packages.config folder honors repositoryPath (nearest nuget.config wins, relative to that config's directory); - every obj/project.assets.json under the root, at any depth, contributes the packageFolders its restore used, ahead of the global folder; - the global folder is NUGET_PACKAGES, else the nearest globalPackagesFolder (user config alone in -g mode), else the default; a relocated folder replaces the default one. The nuget.config reader now records the configuration/config settings. Fixes #397 Fixes #398 Assisted-by: Claude Code:claude-opus-5-5
Covers the global-mode leg of #397 end to end: a globalPackagesFolder in the user-level NuGet.Config must be the copy `scan -g --sync` patches, and the stale default ~/.nuget/packages copy must stay untouched. Assisted-by: Claude Code:claude-opus-5-5
- Read parent configs when --cwd is relative (the default `.`): the config chain starts from the absolute path. - Build the config chain once per crawl and query both settings. - Expand %VAR% like .NET: an unset variable stays literal instead of dropping the setting and falling back to the default folder. - Honor repositoryPath only when a packages.config lives under the root, so a shared store set in a parent or user config is not walked unscoped ahead of a PackageReference project's restore. - Skip the configured stores and already-found package folders in the restore walk, so a big in-tree store can't use up its budget. - Use DOTNET_CLI_HOME for the default folder off Windows, matching the user-config lookup. - Make the -g CLI test write the user config where Windows reads it. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Restore walk still enters package stores
- Fixed by stripping trailing slashes from package folder paths and checking if directories are package stores when popping from the pending queue.
Or push these changes by commenting:
@cursor push 9f55775994
Preview (9f55775994)
diff --git a/crates/socket-patch-core/src/crawlers/nuget_crawler.rs b/crates/socket-patch-core/src/crawlers/nuget_crawler.rs
--- a/crates/socket-patch-core/src/crawlers/nuget_crawler.rs
+++ b/crates/socket-patch-core/src/crawlers/nuget_crawler.rs
@@ -616,7 +616,15 @@
let package_folders = json
.get("packageFolders")
.and_then(|folders| folders.as_object())
- .map(|folders| folders.keys().map(PathBuf::from).collect())
+ .map(|folders| {
+ folders
+ .keys()
+ .map(|key| {
+ let path = key.strip_suffix('/').unwrap_or(key);
+ PathBuf::from(path)
+ })
+ .collect()
+ })
.unwrap_or_default();
let libraries = json
.get("libraries")
@@ -684,6 +692,9 @@
let mut pending = vec![root];
let mut listed = 0usize;
while let Some(dir) = pending.pop() {
+ if skip.contains(&dir) || walk.package_folders.contains(&dir) {
+ continue;
+ }
listed += 1;
if listed > SCOPE_DIR_BUDGET {
walk.libraries = None;You can send follow-ups to the cloud agent here.
The restore walk queued a directory's subdirectories before reading that directory's own project.assets.json, so a store it names (a solution-root RestorePackagesPath of pkgs/) was still walked. A large store could spend the 10,000-directory budget before deeper restores were seen, and a project-shaped folder inside it dropped the restore scope. The walk now queues children after reading the restore, and skips a queued directory that a later restore names. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit b90c942. Configure here.
Final review briefWhat it does. For NuGet, agent-mode Risk: low–medium. It only changes which NuGet copy gets picked, and the order now follows NuGet's own. The medium part: the restore walk used to run only when Look here
Verified
Changes I made: none. Open questions (not blocking):
Auto-merge is armed, so approving sends this straight to the merge queue. Generated by Claude Code |

LLM Description written by Claude Code:claude-opus-5-5
Fixes #397
Fixes #398
Summary
Agent-mode
apply,scanandvexon NuGet now use the package folder the project actually restores from. Before this change, a project that relocated its packages folder had the user-wide~/.nuget/packagescopy patched instead.applyexited 0 and VEX attestednot_affectedwhile the build linked the unpatched copy.Root cause
nuget_crawler::package_rootslisted roots in this order:<cwd>/packages, then the default global folder (NUGET_PACKAGESor~/.nuget/packages), then thepackageFoldersofobj/project.assets.json(root and one level down only). The first root that yields a purl wins (merge_first_wins). It never read the folders NuGet is configured to use:globalPackagesFolder(project nuget.config chain, or the user config under-g) andRestorePackagesPath. A warm default cache won. With a cold default cache, a project two levels below the solution root (src/App) wasn't found at all.repositoryPathfor packages.config projects.<cwd>/packageswas the only legacy root checked, so the global folder that nuget.exe also extracts into won.Fix
formats::nuget::parse_confignow recordsconfiguration/config/addsettings (and<clear />). Neweffective_settingresolves a setting from the nearest config.vendor::nuget_config::settings_chainreads the configs NuGet merges for a directory (that directory and every ancestor, nearest first, then the user config) once per crawl. A relative--cwdis made absolute first.SettingsChain::folderexpands%VAR%the way .NET does and resolves relative values against the directory of the config that set them.repositoryPath, honored only when apackages.configexists under the root; otherwise<cwd>/packages.packageFoldersof everyobj/project.assets.jsonunder the root, at any depth. These recordRestorePackagesPathand a configuredglobalPackagesFolderas the restore resolved them. This reuses the existing restore-scope walk (restore_scopebecamerestore_walk). The walk never enters the configured stores, or any store a restore names, so a big in-tree store can't use up its 10k-directory budget.NUGET_PACKAGES, then the nearestglobalPackagesFolder, then the default. A relocated folder replaces the default one.-g) uses the same global-folder rule over the user config.DOTNET_CLI_HOMEoff Windows, matching the existing user-config lookup.The npm/PyPI/gem wrappers only dispatch to the binary, so they need no parallel change.
Tests (red on
main→ green here)src/App+globalPackagesFoldernuget_crawler::tests::configured_global_packages_folder_beats_a_warm_default_cache…::deep_project_restore_is_found_with_a_cold_default_cacheRestorePackagesPath…::restore_packages_path_beats_a_warm_default_cache-gwith user-configglobalPackagesFolder…::global_mode_reads_the_user_config_global_packages_folder-gend to end (scan -g --syncpatches the configured copy, leaves the default copy alone)in_process_remote_ecosystems_apply::nuget_global_apply_patches_the_user_configured_global_packages_folderrepositoryPath=lib/pkgs, from the solution root and from the project dir…::repository_path_folder_beats_the_global_folder…::restore_walk_does_not_enter_a_store_its_restore_namesNUGET_PACKAGESstill overrides config…::nuget_packages_env_overrides_the_configured_folderrepositoryPathwithout packages.config is ignored…::repository_path_is_ignored_without_a_packages_configformats::nuget::tests::config_settings_resolve_nearest_first,vendor::nuget_config::settings_tests::*Commands run locally (all pass):
cargo fmt --all -- --checkcargo clippy --locked --workspace --all-features -- -D warningscargo test -p socket-patch-core --all-features --lib: 4 failures are permission-based tests in files this PR doesn't touch (copy_tree,vlt_heal,pypi_poetry,pypi_requirements) that can't fail under uid 0 in this container.cargo test -p socket-patch-core --all-features --test crawler_nuget_e2ecargo test -p socket-patch-cli --all-features --lib --test in_process_remote_ecosystems_apply --test e2e_nuget --test ecosystem_dispatch_e2e --test e2e_vex --test e2e_vex_vendor --test e2e_vex_redirect --test in_process_rollback_all_ecosystems --test in_process_get_hosted_ecosystems --test e2e_safety_internalsCI on b90c942 is green (
ci-oksuccess, including the full test matrix and the real-dotnetNuGet suites). Bugbot's latest review of b90c942 found no new issues, and its one earlier finding is fixed and resolved./code-review highfindings not fixedsrc/Appcase in Agent-mode NuGet apply patches ~/.nuget/packages instead of the project's configured globalPackagesFolder / RestorePackagesPath, reports success, and VEX attests not_affected #397. It's bounded by the existing 10k-directory budget and skips package stores.settings_chainduplicates the config discovery ininherited_source_keys_traced. That function walks in the opposite order and records every probed path for a read cache. Merging the two would change the vendored-feed code path, which is out of scope here, so I'd leave it as a follow-up refactor.🤖 Generated with Claude Code
https://claude.ai/code/session_011f9aeS2ity8CoowhYN5FpJ