@@ -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