Skip to content

Commit f6b7fb9

Browse files
Keep NuGet patches installed with <clear /> and NuGet.config (#284)
* Keep hosted NuGet source after <clear /> Hosted mode inserted the Socket package source (and, when the config already had a packageSourceMapping, its mapping) directly after the section's open tag. A config that starts the section with <clear />, common in corporate setups, then discarded the Socket entry, so dotnet restore failed NU1100 / NU1101 for the patched package. The entries now land after the last <clear /> in their section. Assisted-by: claude-code:claude-opus-5-5 * Edit the NuGet config spelling NuGet reads NuGet reads the first of nuget.config, NuGet.config and NuGet.Config in a directory. Hosted mode only read nuget.config and vendored mode skipped NuGet.config, so on a case-sensitive filesystem a project whose config used another spelling got a new nuget.config that shadowed it. Its sources and mappings were ignored and packages from private feeds failed to restore. Both modes now edit the existing file in place and only create nuget.config when no spelling exists. Assisted-by: claude-code:claude-opus-5-5 * fix(core/redirect): match NuGet section tags tolerantly for <clear /> inserts The after-<clear /> insert found section bounds with literal `</packageSources>` / `</packageSourceMapping>` and the mapping open tag with a literal `<packageSourceMapping>`. A close tag with whitespace before `>` sent the insert back ahead of the `<clear />`, which drops it; a spaced mapping open tag authored a duplicate section. A commented-out `<clear />` also became the anchor, splicing the Socket source inside the comment. Match tags with the same whitespace/attribute tolerance as the rest of the rewriter and skip comments. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NPYQfCTLY7F4eQTa8hjinL * fix(core/redirect): slice bytes, not str, when masking comments Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NPYQfCTLY7F4eQTa8hjinL --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1a8608b commit f6b7fb9

7 files changed

Lines changed: 336 additions & 16 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -914,6 +914,22 @@ into the new version's section — see docs/releasing.md.
914914

915915
### Fixed
916916

917+
- **Hosted nuget redirects survive a `<clear />` in `nuget.config`.** The
918+
Socket source (and, in an existing `<packageSourceMapping>`, its
919+
mapping) was inserted ahead of the section's `<clear />`, which NuGet
920+
applies to everything read before it: `dotnet restore` then failed
921+
NU1100 / NU1101 for the patched package. Both now land after the last
922+
`<clear />`.
923+
924+
- **nuget redirects and vendoring edit the config NuGet actually reads.**
925+
NuGet reads the first of `nuget.config`, `NuGet.config` and
926+
`NuGet.Config` in a directory. Hosted mode only knew `nuget.config`
927+
and vendored mode missed `NuGet.config`, so on a case-sensitive
928+
filesystem they created a fresh `nuget.config` that shadowed the
929+
project's own file: its sources and mappings vanished and private
930+
packages failed restore. Both modes now edit the existing spelling in
931+
place.
932+
917933
- **`rollback` fetches a before-blob that only a store peer variant
918934
needs.** The before-blob gate now probes every pnpm and vlt store variant
919935
copy the rollback restores, so an online rollback no longer fails

‎crates/socket-patch-cli/src/commands/scan/hosted.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,11 @@ pub(crate) const REDIRECT_CANDIDATE_FILES: &[&str] = &[
7272
// otherwise the `[registries.…]` block lands in a file cargo ignores.
7373
".cargo/config",
7474
"composer.lock",
75+
// Every spelling NuGet probes: the rewriter edits the one NuGet reads
76+
// rather than shadowing it with a fresh `nuget.config`.
7577
"nuget.config",
78+
"NuGet.config",
79+
"NuGet.Config",
7680
"packages.lock.json",
7781
"Gemfile",
7882
"Gemfile.lock",

‎crates/socket-patch-cli/src/hosted_memory/redirect.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ fn file_ecosystem(rel: &str) -> Option<&'static str> {
178178
| "pyproject.toml" | "hatch.toml" => "pypi",
179179
"Cargo.toml" | "Cargo.lock" | "config.toml" | "config" => "cargo",
180180
"composer.lock" => "composer",
181-
"nuget.config" | "packages.lock.json" => "nuget",
181+
"nuget.config" | "NuGet.config" | "NuGet.Config" | "packages.lock.json" => "nuget",
182182
"Gemfile" | "Gemfile.lock" | "gems.rb" | "gems.locked" => "gem",
183183
"go.mod" | "go.sum" => "golang",
184184
"pom.xml" | "maven.config" | "checksums.sha256" => "maven",

‎crates/socket-patch-cli/src/hosted_memory/roots.rs‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,15 @@ pub(crate) const UNSUPPORTED_MARKERS: [(&str, &[&str]); 2] = [
4848
"settings.gradle.kts",
4949
],
5050
),
51-
("nuget", &["packages.lock.json", "nuget.config"]),
51+
(
52+
"nuget",
53+
&[
54+
"packages.lock.json",
55+
"nuget.config",
56+
"NuGet.config",
57+
"NuGet.Config",
58+
],
59+
),
5260
];
5361

5462
/// Directory names whose subtrees never hold a project root: installed

‎crates/socket-patch-cli/tests/in_process_get_hosted_ecosystems.rs‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,72 @@ async fn nuget_hosted_wires_source_mapping_and_lock_hash() {
651651
.unwrap();
652652
}
653653

654+
/// NuGet reads `NuGet.config` when no `nuget.config` exists: the grant must
655+
/// wire that file in place, not author a `nuget.config` that shadows it.
656+
#[tokio::test]
657+
#[serial]
658+
async fn nuget_hosted_wires_mixed_case_config_in_place() {
659+
const UUID: &str = "c4c4c4c4-c4c4-4c4c-8c4c-c4c4c4c4c4c4";
660+
const PURL: &str = "pkg:nuget/Newtonsoft.Json@13.0.3";
661+
let index_url = format!("http://patch.test/patch-registry/nuget/{TOKEN}/{UUID}/index.json");
662+
let url = format!(
663+
"http://patch.test/patch-registry/nuget/{TOKEN}/{UUID}/flat/newtonsoft.json/13.0.3/newtonsoft.json.13.0.3.nupkg"
664+
);
665+
666+
let server = MockServer::start().await;
667+
mock_view(&server, UUID, PURL).await;
668+
mock_reference(
669+
&server,
670+
UUID,
671+
PURL,
672+
&url,
673+
serde_json::json!({ "sha512": "sha512-NUGETPATCHED==" }),
674+
serde_json::json!({
675+
"kind": "nuget-v3",
676+
"indexUrl": index_url,
677+
"identifiers": {
678+
"name": "Newtonsoft.Json",
679+
"version": "13.0.3",
680+
"nugetIdLower": "newtonsoft.json",
681+
"nugetVersionNorm": "13.0.3",
682+
}
683+
}),
684+
)
685+
.await;
686+
687+
let tmp = tempfile::tempdir().unwrap();
688+
std::fs::write(
689+
tmp.path().join("NuGet.config"),
690+
r#"<?xml version="1.0" encoding="utf-8"?>
691+
<configuration>
692+
<packageSources>
693+
<add key="corp" value="https://nuget.corp.example/v3/index.json" />
694+
</packageSources>
695+
</configuration>
696+
"#,
697+
)
698+
.unwrap();
699+
let case_sensitive = !tmp.path().join("nuget.config").exists();
700+
701+
let code =
702+
socket_patch_cli::commands::get::run(get_hosted_args(UUID, tmp.path(), server.uri())).await;
703+
assert_eq!(code, 0, "get <uuid> --mode hosted (nuget) should succeed");
704+
705+
let config = std::fs::read_to_string(tmp.path().join("NuGet.config")).unwrap();
706+
assert!(
707+
config.contains(&format!(
708+
r#"<add key="socket-patch-{UUID}" value="{index_url}" />"#
709+
)) && config.contains(r#"<add key="corp""#),
710+
"NuGet.config wired in place, its own source kept; got:\n{config}"
711+
);
712+
if case_sensitive {
713+
assert!(
714+
!tmp.path().join("nuget.config").exists(),
715+
"no shadowing nuget.config authored"
716+
);
717+
}
718+
}
719+
654720
/// The manifest-less VEX steps for a nuget hosted checkout `get` wired
655721
/// (`http://patch.test` is the configured patch-server origin; nothing is
656722
/// installed, so the lock's re-pinned `contentHash` is the evidence):

0 commit comments

Comments
 (0)