@@ -640,9 +640,10 @@ async fn cargo_prelude(
640640}
641641
642642/// The wet run's refusals past the dry-run branch and before the first
643- /// service call: a live hosted redirect nothing can revert, and a lock entry
644- /// the vendored copy's version tag cannot be written into. `Ok` is the lock
645- /// probe the hot path routes on.
643+ /// service call: a live hosted redirect nothing can revert, a lock entry
644+ /// resolved outside crates.io, and a lock entry the vendored copy's version
645+ /// tag cannot be written into. `Ok` is the lock probe the hot path routes
646+ /// on.
646647async fn cargo_wet_preflight (
647648 project_root : & Path ,
648649 name : & str ,
@@ -670,19 +671,34 @@ async fn cargo_wet_preflight(
670671 ) ,
671672 ) ) ;
672673 }
674+ if let Some ( refusal) = lock_source_refusal ( project_root, name, version) . await {
675+ return Err ( refusal) ;
676+ }
677+ match lock_tag_preflight ( project_root, name, version, uuid) . await {
678+ ( _, Some ( refusal) ) => Err ( refusal) ,
679+ ( probe, None ) => Ok ( probe) ,
680+ }
681+ }
682+
683+ /// A lock entry resolved from anywhere but crates.io (a git fork, another
684+ /// registry): server patches require the crates.io package. The dry run
685+ /// raises it too, unless a live hosted redirect owns the entry (the wet run
686+ /// restores that first).
687+ async fn lock_source_refusal (
688+ project_root : & Path ,
689+ name : & str ,
690+ version : & str ,
691+ ) -> Option < VendorOutcome > {
673692 if let cargo_lock:: LockEntryProbe :: Source ( source) =
674693 cargo_lock:: probe_lock_entry ( project_root, name, version) . await
675694 {
676695 if source != "registry+https://github.com/rust-lang/crates.io-index"
677696 && source != "sparse+https://index.crates.io/"
678697 {
679- return Err ( refused ( "vendor_source_unsupported" , format ! ( "{name}@{version} resolves from {source}; server patches require the crates.io package" ) ) ) ;
698+ return Some ( refused ( "vendor_source_unsupported" , format ! ( "{name}@{version} resolves from {source}; server patches require the crates.io package" ) ) ) ;
680699 }
681700 }
682- match lock_tag_preflight ( project_root, name, version, uuid) . await {
683- ( _, Some ( refusal) ) => Err ( refusal) ,
684- ( probe, None ) => Ok ( probe) ,
685- }
701+ None
686702}
687703
688704/// The cargo backend's locked-version refusal of `record` — `Cargo.lock`
@@ -780,6 +796,14 @@ pub async fn vendor_cargo_crate<'a>(
780796 let ( name, version) = ( name. as_str ( ) , version. as_str ( ) ) ;
781797
782798 if dry_run {
799+ let hosted_live = hosted_redirect_residue ( project_root, name, version)
800+ . await
801+ . is_some ( ) ;
802+ if !hosted_live {
803+ if let Some ( refusal) = lock_source_refusal ( project_root, name, version) . await {
804+ return refusal;
805+ }
806+ }
783807 let mut dry_warnings = Vec :: new ( ) ;
784808 if !cargo_copy_matches ( & copy_dir, & record. files ) . await {
785809 if let Err ( outcome) =
@@ -798,10 +822,7 @@ pub async fn vendor_cargo_crate<'a>(
798822 // Preview the wet run's lock edit — unless a live hosted redirect
799823 // still owns the lock entry: the wet run reverts it from the
800824 // redirect ledger first, so its lock is not this one.
801- if hosted_redirect_residue ( project_root, name, version)
802- . await
803- . is_none ( )
804- {
825+ if !hosted_live {
805826 let ( lock_probe, refusal) =
806827 lock_tag_preflight ( project_root, name, version, & record. uuid ) . await ;
807828 if let Some ( refusal) = refusal {
@@ -2512,6 +2533,50 @@ mod tests {
25122533 assert ! ( lock_text( root) . await . contains( & tagged( UUID ) ) ) ;
25132534 }
25142535
2536+ /// A lock entry resolved from outside crates.io is refused by the dry
2537+ /// run exactly as by the wet run, before either asks the service.
2538+ #[ tokio:: test]
2539+ async fn dry_run_refuses_a_non_crates_io_source_like_the_wet_run ( ) {
2540+ for source in [
2541+ "git+https://example.com/fork/cfg-if#abc123" ,
2542+ "registry+https://my-registry.example/index" ,
2543+ ] {
2544+ let ( dir, blobs, pristine, record) = fixture ( ) . await ;
2545+ let root = dir. path ( ) ;
2546+ let lock = lock_body ( ) . replace ( SOURCE , source) ;
2547+ tokio:: fs:: write ( root. join ( "Cargo.lock" ) , & lock)
2548+ . await
2549+ . unwrap ( ) ;
2550+ let server = wiremock:: MockServer :: start ( ) . await ;
2551+ crate :: vendor:: test_support:: mount_no_results ( & server) . await ;
2552+ let cfg = crate :: vendor:: test_support:: service_cfg (
2553+ & server. uri ( ) ,
2554+ crate :: vendor:: VendorSource :: Service ,
2555+ false ,
2556+ ) ;
2557+ let sources = PatchSources :: blobs_only ( & blobs) ;
2558+ for dry_run in [ true , false ] {
2559+ let outcome = crate :: vendor:: test_support:: vendor_cargo_crate (
2560+ PURL ,
2561+ pristine. as_path ( ) ,
2562+ root,
2563+ & record,
2564+ & sources,
2565+ "2026-06-09T00:00:00Z" ,
2566+ dry_run,
2567+ false ,
2568+ Some ( & cfg) ,
2569+ )
2570+ . await ;
2571+ let detail = expect_refused ( outcome, "vendor_source_unsupported" ) ;
2572+ assert ! ( detail. contains( source) , "{dry_run}: {detail}" ) ;
2573+ }
2574+ assert_eq ! ( crate :: vendor:: test_support:: request_count( & server) . await , 0 ) ;
2575+ assert_eq ! ( lock_text( root) . await , lock) ;
2576+ assert ! ( !root. join( format!( ".socket/vendor/cargo/{UUID}" ) ) . exists( ) ) ;
2577+ }
2578+ }
2579+
25152580 #[ tokio:: test]
25162581 async fn test_refuses_locked_version_mismatch ( ) {
25172582 let ( dir, blobs, pristine, record) = fixture ( ) . await ;
0 commit comments