Skip to content

Commit d583874

Browse files
mikolalysenkoclaude
andcommitted
Merge fix/gc-report-json-1257 (ff9704a) into v5/json-envelope-unify
Keep the envelope wording for the Cargo shared-cache GC keep (no event and no warning in the preview) and take #1273's vendor_artifact_gitignored NuGet/JVM contract updates with the envelope's top-level warnings[]. e2e_cargo keeps the events-based prune read with #1273's message. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2 parents 515c0bc + ff9704a commit d583874

15 files changed

Lines changed: 407 additions & 24 deletions

File tree

‎crates/socket-patch-cli/CLI_CONTRACT.md‎

Lines changed: 5 additions & 5 deletions
Large diffs are not rendered by default.

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,8 @@ async fn sync_keeps_entry_whose_shared_cache_copy_is_still_patched() {
373373
.unwrap()
374374
};
375375

376-
// The preview prunes only the pristine one.
376+
// The preview prunes only the pristine one (v5.0 one GC shape: a dry
377+
// run reports would-be prunes under the wet pass's key).
377378
let out = run(
378379
&[
379380
"scan",
@@ -394,7 +395,7 @@ async fn sync_keeps_entry_whose_shared_cache_copy_is_still_patched() {
394395
assert_eq!(
395396
manifest_pruned(&json),
396397
vec![serde_json::json!(ryu)],
397-
"{json:#}"
398+
"the preview must keep the still-patched cargo entry: {json:#}"
398399
);
399400

400401
let out = run(

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,7 @@ fn maven_reactor_vendor_fresh_checkout_offline_build_and_byte_exact_revert() {
425425
let mut want_added = vec![
426426
".mvn/maven.config".to_string(),
427427
".socket/vendor/maven2/.gitattributes".to_string(),
428+
".socket/vendor/maven2/.gitignore".to_string(),
428429
format!("{tree}/{ARTIFACT}-{SV}.jar"),
429430
format!("{tree}/{ARTIFACT}-{SV}.jar.sha1"),
430431
format!("{tree}/{ARTIFACT}-{SV}.pom"),
@@ -756,6 +757,7 @@ fn gradle_multi_project_vendor_locked_offline_tamper_and_byte_exact_revert() {
756757
socket_patch_core::vendor::jvm::gradle::SCRIPT_REL.to_string(),
757758
socket_patch_core::vendor::jvm::gradle::INDEX_REL.to_string(),
758759
".socket/vendor/gradle/.gitattributes".to_string(),
760+
".socket/vendor/gradle/.gitignore".to_string(),
759761
".socket/gradle/.gitattributes".to_string(),
760762
".socket/vendor/.gitattributes".to_string(),
761763
socket_patch_core::vendor::jvm::gradle::derived_metadata_rel(GROUP, ARTIFACT),

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -949,6 +949,7 @@ fn gradle_vendor_429_crlf_checkout_checks_and_reverts_clean() {
949949
"proj/.socket/vendor/.gitattributes",
950950
"proj/.socket/vendor/gradle-index.tsv",
951951
"proj/.socket/vendor/gradle/.gitattributes",
952+
"proj/.socket/vendor/gradle/.gitignore",
952953
FOO_METADATA,
953954
];
954955
for rel in text_files {

‎crates/socket-patch-core/src/vendor/jvm/apply.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ fn is_owned_file(rel: &str) -> bool {
9595
[
9696
maven_reactor::GITATTRIBUTES_REL,
9797
gradle::GITATTRIBUTES_REL,
98+
gradle::GITIGNORE_REL,
9899
gradle::SCRIPT_GITATTRIBUTES_REL,
99100
gradle::VENDOR_GITATTRIBUTES_REL,
100101
gradle::SCRIPT_REL,

‎crates/socket-patch-core/src/vendor/jvm/gradle.rs‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ pub const SCRIPT_REL: &str = ".socket/gradle/socket-patch.settings.gradle";
4646
use super::layout::GRADLE_TREE as TREE_ROOT;
4747
/// The tree root's `.gitattributes`, shared by every Gradle patch.
4848
pub const GITATTRIBUTES_REL: &str = ".socket/vendor/gradle/.gitattributes";
49+
/// The tree root's `.gitignore` (`!*`): re-includes the vendored jars
50+
/// against a user's `*.jar` rule (Java.gitignore), #620 / #1061.
51+
pub const GITIGNORE_REL: &str = ".socket/vendor/gradle/.gitignore";
4952
/// `.socket/gradle/`'s `.gitattributes` (`* -text`): the settings scripts
5053
/// there stay byte-exact on a `core.autocrlf` checkout (#429). Shared with
5154
/// the hosted script.
@@ -511,6 +514,12 @@ pub fn plan(
511514
records.push(created_or_adopted(SCRIPT_REL, read(SCRIPT_REL).is_some()));
512515
writes.push(text_write(SCRIPT_REL, SCRIPT.as_bytes().to_vec()));
513516
records.push(owned_file(read, GITATTRIBUTES_REL, &mut writes));
517+
records.push(super::owned_file_with(
518+
read,
519+
GITIGNORE_REL,
520+
super::coursier_tree::GITIGNORE.as_bytes(),
521+
&mut writes,
522+
));
514523
records.push(owned_file(read, SCRIPT_GITATTRIBUTES_REL, &mut writes));
515524
records.push(vendor_gitattributes(read, &mut writes));
516525

@@ -1071,6 +1080,7 @@ pub fn unplan(read: ReadFn<'_>, c: &Coords<'_>, records: &[WiringRecord]) -> Jvm
10711080
for (rel, expected) in [
10721081
(SCRIPT_REL, SCRIPT),
10731082
(GITATTRIBUTES_REL, super::TREE_GITATTRIBUTES),
1083+
(GITIGNORE_REL, super::coursier_tree::GITIGNORE),
10741084
(SCRIPT_GITATTRIBUTES_REL, super::TREE_GITATTRIBUTES),
10751085
] {
10761086
if rel == SCRIPT_GITATTRIBUTES_REL && hosted_left {
@@ -2585,6 +2595,7 @@ mod tests {
25852595
(".socket/vendor/.gitattributes", false),
25862596
(".socket/vendor/gradle-index.tsv", false),
25872597
(".socket/vendor/gradle/.gitattributes", false),
2598+
(".socket/vendor/gradle/.gitignore", false),
25882599
(".socket/vendor/gradle/com/google/code/gson/gson/2.10.1/gson-2.10.1.jar", true),
25892600
(".socket/vendor/gradle/com/google/code/gson/gson/2.10.1/gson-2.10.1.pom", true),
25902601
(".socket/vendor/gradle/com/google/code/gson/gson/2.10.1/socket-patch.vendor.json", true),
@@ -2614,6 +2625,7 @@ mod tests {
26142625
text_of(&plan, ".socket/vendor/gradle/.gitattributes"),
26152626
"* -text\n"
26162627
);
2628+
assert_eq!(text_of(&plan, GITIGNORE_REL), "!*\n");
26172629
assert!(plan.warnings.is_empty(), "{:?}", plan.warnings);
26182630
}
26192631

‎crates/socket-patch-core/src/vendor/jvm/layout.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ pub const CAPTURED_FILES: &[&str] = &[
163163
super::gradle::SCRIPT_REL,
164164
super::maven_reactor::GITATTRIBUTES_REL,
165165
super::gradle::GITATTRIBUTES_REL,
166+
super::gradle::GITIGNORE_REL,
166167
super::gradle::SCRIPT_GITATTRIBUTES_REL,
167168
super::gradle::VENDOR_GITATTRIBUTES_REL,
168169
super::coursier_tree::INDEX_REL,
@@ -543,6 +544,7 @@ mod tests {
543544
under(maven_reactor::GITATTRIBUTES_REL, MAVEN2_TREE);
544545
under(sbt::TREE_GITIGNORE_REL, MAVEN2_TREE);
545546
under(gradle::GITATTRIBUTES_REL, GRADLE_TREE);
547+
under(gradle::GITIGNORE_REL, GRADLE_TREE);
546548
under(coursier_tree::GITIGNORE_REL, COURSIER_TREE);
547549
under(coursier_tree::GITATTRIBUTES_REL, COURSIER_TREE);
548550
under(scala_cli::GUARD_REL, COURSIER_TREE);

‎crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs‎

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ use super::layout::MAVEN2_TREE as TREE_ROOT;
2525
pub const MAVEN_CONFIG: &str = ".mvn/maven.config";
2626
/// The tree root's `.gitattributes`, shared by every Maven patch.
2727
pub const GITATTRIBUTES_REL: &str = ".socket/vendor/maven2/.gitattributes";
28+
/// The tree root's `.gitignore` (`!*`), shared with sbt: re-includes the
29+
/// vendored jars against a user's `*.jar` rule (Java.gitignore), #1061.
30+
pub use super::sbt::TREE_GITIGNORE_REL as GITIGNORE_REL;
2831
const OFFLINE_LINE: &str = "-Daether.offline.protocols=file";
2932
const OFFLINE_KEY: &str = "-Daether.offline.protocols=";
3033
const TAIL_KEY: &str = "-Dmaven.repo.local.tail=";
@@ -313,6 +316,12 @@ pub fn plan_with_external(
313316
));
314317
}
315318
records.push(owned_file(read, GITATTRIBUTES_REL, &mut writes));
319+
records.push(super::owned_file_with(
320+
read,
321+
GITIGNORE_REL,
322+
super::coursier_tree::GITIGNORE.as_bytes(),
323+
&mut writes,
324+
));
316325
let (tree_dir, jar_rel, tree) = tree_writes(patch, &sv, suffixed_pom);
317326
writes.extend(tree);
318327

@@ -414,15 +423,17 @@ pub fn unplan(read: ReadFn<'_>, c: &Coords<'_>, records: &[WiringRecord]) -> Jvm
414423
before.insert(MAVEN_CONFIG.to_string(), Some(text));
415424
}
416425
}
417-
let created = records.iter().any(|w| {
418-
w.kind == OWNED_FILE_KIND && w.file == GITATTRIBUTES_REL && op_of(w) == "create"
419-
});
420-
if created && read(GITATTRIBUTES_REL).as_deref() == Some(TREE_GITATTRIBUTES.as_bytes()) {
421-
before.insert(
422-
GITATTRIBUTES_REL.to_string(),
423-
Some(TREE_GITATTRIBUTES.to_string()),
424-
);
425-
after.insert(GITATTRIBUTES_REL.to_string(), None);
426+
for (rel, body) in [
427+
(GITATTRIBUTES_REL, TREE_GITATTRIBUTES),
428+
(GITIGNORE_REL, super::coursier_tree::GITIGNORE),
429+
] {
430+
let created = records
431+
.iter()
432+
.any(|w| w.kind == OWNED_FILE_KIND && w.file == rel && op_of(w) == "create");
433+
if created && read(rel).as_deref() == Some(body.as_bytes()) {
434+
before.insert(rel.to_string(), Some(body.to_string()));
435+
after.insert(rel.to_string(), None);
436+
}
426437
}
427438
}
428439
JvmUnplan {

‎crates/socket-patch-core/src/vendor/jvm/mod.rs‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,17 @@ pub enum Shape {
230230
Other,
231231
}
232232

233+
/// The committed vendor trees a plan for `shape` writes into.
234+
pub(crate) fn shape_trees(shape: Shape) -> &'static [&'static str] {
235+
match shape {
236+
Shape::MavenReactor | Shape::Sbt => &[layout::MAVEN2_TREE],
237+
Shape::Gradle => &[layout::GRADLE_TREE],
238+
Shape::Mixed => &[layout::MAVEN2_TREE, layout::GRADLE_TREE],
239+
Shape::ScalaCli => &[layout::COURSIER_TREE],
240+
Shape::Other => &[],
241+
}
242+
}
243+
233244
/// A planned file: project-relative forward-slash path and its full new
234245
/// bytes.
235246
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -589,6 +600,7 @@ pub(crate) fn eol_blind(rel: &str) -> bool {
589600
gradle::SCRIPT_REL,
590601
gradle::INDEX_REL,
591602
gradle::GITATTRIBUTES_REL,
603+
gradle::GITIGNORE_REL,
592604
gradle::SCRIPT_GITATTRIBUTES_REL,
593605
gradle::VENDOR_GITATTRIBUTES_REL,
594606
maven_reactor::GITATTRIBUTES_REL,

‎crates/socket-patch-core/src/vendor/maven_repo.rs‎

Lines changed: 130 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -552,8 +552,29 @@ pub async fn jvm_gate_preflight(
552552
}
553553
let shape = detect_shape(project_root);
554554
super::jvm::sbt_gate::for_shape(shape, project_root, &g, &a, &v)
555-
.map(|_| ())
556-
.map_err(|stop| stop.code_and_detail(purl))
555+
.map_err(|stop| stop.code_and_detail(purl))?;
556+
// Checked here too, so a hosted->vendored takeover keeps its pin
557+
// instead of restoring upstream and then refusing (#1061).
558+
match ignored_tree_root(shape, project_root).await {
559+
Some(refusal) => Err(refusal),
560+
None => Ok(()),
561+
}
562+
}
563+
564+
/// The `vendor_artifact_gitignored` refusal when git ignores a tree root
565+
/// `shape` writes into. Each tree root owns a `!*` `.gitignore` that
566+
/// re-includes file rules such as Java.gitignore's `*.jar` (#1061), but a
567+
/// rule ignoring the root itself (`.socket/`) can't be undone from inside.
568+
async fn ignored_tree_root(
569+
shape: super::jvm::Shape,
570+
project_root: &Path,
571+
) -> Option<(&'static str, String)> {
572+
for tree in super::jvm::shape_trees(shape) {
573+
if let Some(refusal) = super::npm_dir::ignored_root_refusal(project_root, tree).await {
574+
return Some(refusal);
575+
}
576+
}
577+
None
557578
}
558579

559580
/// The committed tree bytes for `record` (jar, upstream pom, module, and
@@ -686,6 +707,10 @@ async fn jvm_prelude(
686707
let gate_pass =
687708
super::jvm::sbt_gate::for_shape(shape, project_root, &group_id, &artifact_id, &version)
688709
.map_err(|stop| stop.into_outcome(purl))?;
710+
// The tree must survive the commit the vendored workflow ends with.
711+
if let Some((code, detail)) = ignored_tree_root(shape, project_root).await {
712+
return Err(refused(code, detail));
713+
}
689714
Ok(JvmPrelude {
690715
group_id,
691716
artifact_id,
@@ -1965,6 +1990,109 @@ mod tests {
19651990
assert!(root.join(".socket/vendor/gradle-index.tsv").is_file());
19661991
}
19671992

1993+
/// The JVM shapes #1061 names, each as a fresh project: a single-module
1994+
/// pom, a multi-module reactor and a Gradle-only build.
1995+
async fn jvm_shape_fixture(shape: &str) -> (tempfile::TempDir, PathBuf, PathBuf, PatchRecord) {
1996+
match shape {
1997+
"pom" => fixture(Some(project_pom()), true, true).await,
1998+
"reactor" => reactor_fixture(true).await,
1999+
_ => {
2000+
let fx = fixture(None, true, true).await;
2001+
std::fs::write(fx.0.path().join("build.gradle"), "plugins { id 'java' }\n")
2002+
.unwrap();
2003+
fx
2004+
}
2005+
}
2006+
}
2007+
2008+
/// Every file under `.socket/` (relative, `/`-separated).
2009+
fn socket_files(root: &Path) -> Vec<String> {
2010+
crate::vendor::test_support::tree_snapshot(root)
2011+
.into_keys()
2012+
.filter(|rel| rel.starts_with(".socket/"))
2013+
.collect()
2014+
}
2015+
2016+
/// #1061 (and #620): GitHub's stock Java.gitignore ignores `*.jar`.
2017+
/// Vendoring a Maven, reactor or Gradle project must still leave every
2018+
/// written tree file committable (the tree roots re-include them), and
2019+
/// revert removes the re-include it created.
2020+
#[tokio::test]
2021+
#[serial_test::serial]
2022+
async fn a_jar_ignore_rule_is_overridden_by_the_tree_gitignore() {
2023+
use crate::vendor::test_support::{git_project, JAVA_GITIGNORE};
2024+
for shape in ["pom", "reactor", "gradle"] {
2025+
let (dir, blobs, installed, record) = jvm_shape_fixture(shape).await;
2026+
let root = dir.path();
2027+
if git_project(root, JAVA_GITIGNORE).is_none() {
2028+
return;
2029+
}
2030+
let (result, entry, _) =
2031+
unwrap_done(run_vendor(root, &blobs, &installed, &record, false).await);
2032+
assert!(result.success, "{shape}: {:?}", result.error);
2033+
let entry = entry.expect("ledger entry");
2034+
assert!(entry.artifact.path.ends_with(".jar"), "{shape}");
2035+
let written = socket_files(root);
2036+
assert!(
2037+
written.contains(&entry.artifact.path),
2038+
"{shape}: {written:?}"
2039+
);
2040+
assert_eq!(
2041+
crate::vendor::npm_dir::gitignored(root, &written).await,
2042+
None,
2043+
"{shape}: git commits every vendored file"
2044+
);
2045+
2046+
let reverted = revert_maven(&entry, root, false).await;
2047+
assert!(reverted.success, "{shape}: {reverted:?}");
2048+
let left = socket_files(root)
2049+
.into_iter()
2050+
.filter(|rel| rel.ends_with(".gitignore"))
2051+
.collect::<Vec<_>>();
2052+
assert!(left.is_empty(), "{shape}: revert leaves {left:?}");
2053+
}
2054+
}
2055+
2056+
/// #1061: a rule that ignores the vendor tree itself (`.socket/`) can't
2057+
/// be overridden from inside it, so every JVM shape refuses
2058+
/// `vendor_artifact_gitignored` before writing, dry run included.
2059+
#[tokio::test]
2060+
#[serial_test::serial]
2061+
async fn a_jvm_tree_directory_ignore_rule_refuses_before_any_write() {
2062+
use crate::vendor::test_support::git_project;
2063+
for shape in ["pom", "reactor", "gradle"] {
2064+
for rule in [".socket/", ".socket/vendor/"] {
2065+
for dry_run in [false, true] {
2066+
let (dir, blobs, installed, record) = jvm_shape_fixture(shape).await;
2067+
let root = dir.path();
2068+
if git_project(root, &format!("{rule}\n")).is_none() {
2069+
return;
2070+
}
2071+
let before = crate::vendor::test_support::tree_snapshot(root);
2072+
let (code, detail) = unwrap_refused(
2073+
run_vendor(root, &blobs, &installed, &record, dry_run).await,
2074+
);
2075+
assert_eq!(
2076+
code, "vendor_artifact_gitignored",
2077+
"{shape} {rule}: {detail}"
2078+
);
2079+
assert!(detail.contains(rule), "{shape} {rule}: {detail}");
2080+
assert_eq!(
2081+
crate::vendor::test_support::tree_snapshot(root),
2082+
before,
2083+
"{shape} {rule}: nothing written"
2084+
);
2085+
// The takeover gate refuses too, before any restore.
2086+
assert_eq!(
2087+
jvm_gate_preflight(root, PURL).await.map_err(|(c, _)| c),
2088+
Err("vendor_artifact_gitignored"),
2089+
"{shape} {rule}"
2090+
);
2091+
}
2092+
}
2093+
}
2094+
}
2095+
19682096
#[tokio::test]
19692097
#[serial_test::serial]
19702098
async fn refuses_unsafe_coordinates() {

0 commit comments

Comments
 (0)