@@ -426,7 +426,9 @@ enum WiringPlan {
426426 Hatch ( super :: pypi_hatch:: HatchProject ) ,
427427 Poetry ( Box < PoetryProject > ) ,
428428 Pdm ( Box < PdmProject > ) ,
429- Pipenv ( Box < PipenvProject > ) ,
429+ /// The ledger entry of an OLDER patch uuid whose Pipfile.lock wiring the
430+ /// guards admitted for an in-place re-wire (#769), if any.
431+ Pipenv ( Box < PipenvProject > , Option < Box < VendorEntry > > ) ,
430432 /// The lock already routes this package through THIS patch uuid's
431433 /// vendored wheel: no wiring — verify (or rebuild) the artifact only.
432434 InSync ,
@@ -862,12 +864,25 @@ async fn pypi_prelude<'p>(
862864 ) ,
863865 ) ) ;
864866 }
865- let target = match super :: pypi_pipenv:: check_target_guards (
867+ // A superseding patch (#769): the ledger entry that wired this
868+ // package at an older uuid holds the pre-vendor originals the
869+ // re-wire carries forward. An unreadable ledger leaves none, and
870+ // the guards then refuse the re-wire as before.
871+ let superseded = super :: state:: load_state_shared ( project_root)
872+ . await
873+ . ok ( )
874+ . and_then ( |state| {
875+ super :: state:: lookup_entry ( & state. entries , base)
876+ . filter ( |entry| entry. uuid != record. uuid )
877+ . cloned ( )
878+ } ) ;
879+ let target = match super :: pypi_pipenv:: check_target_guards_superseding (
866880 & project,
867881 & canon_name,
868882 & record. uuid ,
869883 version,
870884 hosted_origins,
885+ superseded. as_ref ( ) ,
871886 ) {
872887 Ok ( target) => target,
873888 // A refusal carries no warnings: probe nothing for it.
@@ -888,7 +903,9 @@ async fn pypi_prelude<'p>(
888903 wired_pin = pipenv_wired_pin ( & project. lock , & uuid_dir_rel) ;
889904 WiringPlan :: InSync
890905 }
891- PipenvTarget :: Fresh => WiringPlan :: Pipenv ( Box :: new ( project) ) ,
906+ PipenvTarget :: Fresh => {
907+ WiringPlan :: Pipenv ( Box :: new ( project) , superseded. map ( Box :: new) )
908+ }
892909 }
893910 }
894911 } ;
@@ -1285,7 +1302,7 @@ pub async fn vendor_pypi_with_pipenv_version<'a>(
12851302 . await
12861303 . map ( |( wiring, meta) | ( wiring, MetaSlot :: Pdm ( meta) ) )
12871304 }
1288- WiringPlan :: Pipenv ( project) => super :: pypi_pipenv:: wire_pipenv (
1305+ WiringPlan :: Pipenv ( project, superseded ) => super :: pypi_pipenv:: wire_pipenv_superseding (
12891306 & project,
12901307 project_root,
12911308 & canon_name,
@@ -1294,6 +1311,7 @@ pub async fn vendor_pypi_with_pipenv_version<'a>(
12941311 & artifact. sha256_hex ,
12951312 & record. uuid ,
12961313 & hosted_origins,
1314+ superseded. as_deref ( ) ,
12971315 )
12981316 . await
12991317 . map ( |( wiring, meta) | ( wiring, MetaSlot :: Pipenv ( meta) ) ) ,
@@ -3492,8 +3510,13 @@ wheels = [
34923510 tokio:: fs:: remove_dir_all ( & uuid_dir) . await . unwrap ( ) ;
34933511 let bytes = served_wheel ( b"service wheel at another filename" ) ;
34943512 let server = wiremock:: MockServer :: start ( ) . await ;
3495- mount_pypi_granted ( & server, "six-1.16.0-py3-none-any.whl" , & sri_sha512 ( & bytes) , & bytes)
3496- . await ;
3513+ mount_pypi_granted (
3514+ & server,
3515+ "six-1.16.0-py3-none-any.whl" ,
3516+ & sri_sha512 ( & bytes) ,
3517+ & bytes,
3518+ )
3519+ . await ;
34973520 let cfg = pypi_service_cfg ( & server. uri ( ) , VendorSource :: Service , false ) ;
34983521 let error = crate :: vendor:: test_support:: expect_failure ( vendor ( Some ( cfg) ) . await ) ;
34993522 assert ! (
@@ -4208,6 +4231,161 @@ wheels = [
42084231}
42094232"# ;
42104233
4234+ /// A Pipenv project (Pipfile.lock, no requirements.txt) over the
4235+ /// [`e2e_fixture`] install and blob store.
4236+ async fn pipenv_e2e_fixture ( ) -> E2eFixture {
4237+ let fx = e2e_fixture ( ) . await ;
4238+ tokio:: fs:: remove_file ( fx. root . join ( "requirements.txt" ) )
4239+ . await
4240+ . unwrap ( ) ;
4241+ touch ( & fx. root , "Pipfile.lock" , PIPENV_REGISTRY_LOCK ) . await ;
4242+ fx
4243+ }
4244+
4245+ /// Vendor `record` into the [`pipenv_e2e_fixture`] project.
4246+ async fn pipenv_vendor ( fx : & E2eFixture , record : & PatchRecord ) -> VendorOutcome {
4247+ let sources = PatchSources :: blobs_only ( & fx. blobs ) ;
4248+ crate :: vendor:: test_support:: vendor_pypi (
4249+ "pkg:pypi/six@1.16.0" ,
4250+ & fx. site_packages ,
4251+ & fx. root ,
4252+ record,
4253+ & sources,
4254+ "2026-06-09T00:00:00Z" ,
4255+ false ,
4256+ false ,
4257+ None ,
4258+ )
4259+ . await
4260+ }
4261+
4262+ async fn read_json ( root : & Path , name : & str ) -> serde_json:: Value {
4263+ serde_json:: from_str ( & tokio:: fs:: read_to_string ( root. join ( name) ) . await . unwrap ( ) ) . unwrap ( )
4264+ }
4265+
4266+ /// #769: a Pipfile.lock wired to an EARLIER patch uuid re-vendors in
4267+ /// place to the superseding uuid, as the `would_revendor` preview and
4268+ /// the CLI contract promise: the entry moves to the new wheel, its
4269+ /// record carries the pre-vendor registry original forward, and
4270+ /// `vendor --revert` of the NEW entry restores the registry pin.
4271+ #[ tokio:: test]
4272+ async fn pipenv_superseding_uuid_revendors_in_place ( ) {
4273+ const UUID2 : & str = "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d" ;
4274+ let fx = pipenv_e2e_fixture ( ) . await ;
4275+ let registry = read_json ( & fx. root , "Pipfile.lock" ) . await ;
4276+ let VendorOutcome :: Done { result, entry, .. } = pipenv_vendor ( & fx, & fx. record ) . await else {
4277+ panic ! ( "first vendor must be Done" ) ;
4278+ } ;
4279+ assert ! ( result. success, "{:?}" , result. error) ;
4280+ let first = entry. expect ( "entry on success" ) ;
4281+ save_ledger_entry ( & fx. root , & first) . await ;
4282+
4283+ let mut record2 = fx. record . clone ( ) ;
4284+ record2. uuid = UUID2 . to_string ( ) ;
4285+ let outcome = pipenv_vendor ( & fx, & record2) . await ;
4286+ let VendorOutcome :: Done { result, entry, .. } = outcome else {
4287+ panic ! ( "superseding uuid must re-vendor, got {outcome:?}" ) ;
4288+ } ;
4289+ assert ! ( result. success, "{:?}" , result. error) ;
4290+ let second = entry. expect ( "entry on success" ) ;
4291+ assert_eq ! ( second. uuid, UUID2 ) ;
4292+ assert_eq ! ( second. wiring. len( ) , 1 ) ;
4293+ assert_eq ! ( second. wiring[ 0 ] . key. as_deref( ) , Some ( "default:six" ) ) ;
4294+ assert_eq ! (
4295+ second. wiring[ 0 ] . original,
4296+ Some ( registry[ "default" ] [ "six" ] . clone( ) ) ,
4297+ "the pre-vendor registry original is carried forward"
4298+ ) ;
4299+ let lock = tokio:: fs:: read_to_string ( fx. root . join ( "Pipfile.lock" ) )
4300+ . await
4301+ . unwrap ( ) ;
4302+ assert ! ( !lock. contains( UUID ) , "Pipfile.lock kept uuid A:\n {lock}" ) ;
4303+ assert ! ( lock. contains( UUID2 ) , "Pipfile.lock not on uuid B:\n {lock}" ) ;
4304+ assert ! ( fx
4305+ . root
4306+ . join( format!( ".socket/vendor/pypi/{UUID2}/{WHEEL_NAME}" ) )
4307+ . is_file( ) ) ;
4308+
4309+ save_ledger_entry ( & fx. root , & second) . await ;
4310+ let reverted = revert_pypi ( & second, & fx. root , false ) . await ;
4311+ assert ! ( reverted. success, "{:?}" , reverted. error) ;
4312+ assert ! ( reverted. warnings. is_empty( ) , "{:?}" , reverted. warnings) ;
4313+ assert_eq ! ( read_json( & fx. root, "Pipfile.lock" ) . await , registry) ;
4314+ }
4315+
4316+ /// #769: without a ledger entry for the older uuid there is no recorded
4317+ /// pre-vendor original to carry forward, so a re-wire could never be
4318+ /// reverted. That case still refuses, before anything is written.
4319+ #[ tokio:: test]
4320+ async fn pipenv_superseding_uuid_without_ledger_refuses ( ) {
4321+ const UUID2 : & str = "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d" ;
4322+ let fx = pipenv_e2e_fixture ( ) . await ;
4323+ let VendorOutcome :: Done { result, .. } = pipenv_vendor ( & fx, & fx. record ) . await else {
4324+ panic ! ( "first vendor must be Done" ) ;
4325+ } ;
4326+ assert ! ( result. success, "{:?}" , result. error) ;
4327+ let wired = tokio:: fs:: read_to_string ( fx. root . join ( "Pipfile.lock" ) )
4328+ . await
4329+ . unwrap ( ) ;
4330+
4331+ let mut record2 = fx. record . clone ( ) ;
4332+ record2. uuid = UUID2 . to_string ( ) ;
4333+ let outcome = pipenv_vendor ( & fx, & record2) . await ;
4334+ let VendorOutcome :: Refused { code, detail } = outcome else {
4335+ panic ! ( "expected Refused, got {outcome:?}" ) ;
4336+ } ;
4337+ assert_eq ! ( code, "pypi_pipenv_source_already_exists" ) ;
4338+ assert ! ( detail. contains( UUID ) , "{detail}" ) ;
4339+ assert ! ( detail. contains( "records no wiring" ) , "{detail}" ) ;
4340+ assert_eq ! (
4341+ tokio:: fs:: read_to_string( fx. root. join( "Pipfile.lock" ) )
4342+ . await
4343+ . unwrap( ) ,
4344+ wired
4345+ ) ;
4346+ assert ! ( !fx
4347+ . root
4348+ . join( format!( ".socket/vendor/pypi/{UUID2}" ) )
4349+ . exists( ) ) ;
4350+ }
4351+
4352+ /// #769: a re-wire replays only what the older entry's ledger recorded.
4353+ /// A wired entry edited since vendoring refuses before anything is
4354+ /// written.
4355+ #[ tokio:: test]
4356+ async fn pipenv_superseding_uuid_drifted_entry_refuses ( ) {
4357+ const UUID2 : & str = "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d" ;
4358+ let fx = pipenv_e2e_fixture ( ) . await ;
4359+ let VendorOutcome :: Done { result, entry, .. } = pipenv_vendor ( & fx, & fx. record ) . await else {
4360+ panic ! ( "first vendor must be Done" ) ;
4361+ } ;
4362+ assert ! ( result. success, "{:?}" , result. error) ;
4363+ save_ledger_entry ( & fx. root , & entry. expect ( "entry on success" ) ) . await ;
4364+ let mut lock = read_json ( & fx. root , "Pipfile.lock" ) . await ;
4365+ lock[ "default" ] [ "six" ] [ "markers" ] = serde_json:: json!( "python_version >= '3.8'" ) ;
4366+ let drifted = serde_json:: to_string_pretty ( & lock) . unwrap ( ) + "\n " ;
4367+ touch ( & fx. root , "Pipfile.lock" , & drifted) . await ;
4368+
4369+ let mut record2 = fx. record . clone ( ) ;
4370+ record2. uuid = UUID2 . to_string ( ) ;
4371+ let outcome = pipenv_vendor ( & fx, & record2) . await ;
4372+ let VendorOutcome :: Refused { code, detail } = outcome else {
4373+ panic ! ( "expected Refused, got {outcome:?}" ) ;
4374+ } ;
4375+ assert_eq ! ( code, "pypi_pipenv_source_already_exists" ) ;
4376+ assert ! ( detail. contains( "changed since vendoring" ) , "{detail}" ) ;
4377+ assert_eq ! (
4378+ tokio:: fs:: read_to_string( fx. root. join( "Pipfile.lock" ) )
4379+ . await
4380+ . unwrap( ) ,
4381+ drifted
4382+ ) ;
4383+ assert ! ( !fx
4384+ . root
4385+ . join( format!( ".socket/vendor/pypi/{UUID2}" ) )
4386+ . exists( ) ) ;
4387+ }
4388+
42114389 /// A relock regenerated the wired entry to a registry reference whose
42124390 /// hash list differs from the recorded original (Pipenv 2022.12.19 does
42134391 /// exactly this; 2026.x reproduces the original and converges silently):
0 commit comments