@@ -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) ) ) ,
@@ -4208,6 +4226,161 @@ wheels = [
42084226}
42094227"# ;
42104228
4229+ /// A Pipenv project (Pipfile.lock, no requirements.txt) over the
4230+ /// [`e2e_fixture`] install and blob store.
4231+ async fn pipenv_e2e_fixture ( ) -> E2eFixture {
4232+ let fx = e2e_fixture ( ) . await ;
4233+ tokio:: fs:: remove_file ( fx. root . join ( "requirements.txt" ) )
4234+ . await
4235+ . unwrap ( ) ;
4236+ touch ( & fx. root , "Pipfile.lock" , PIPENV_REGISTRY_LOCK ) . await ;
4237+ fx
4238+ }
4239+
4240+ /// Vendor `record` into the [`pipenv_e2e_fixture`] project.
4241+ async fn pipenv_vendor ( fx : & E2eFixture , record : & PatchRecord ) -> VendorOutcome {
4242+ let sources = PatchSources :: blobs_only ( & fx. blobs ) ;
4243+ crate :: vendor:: test_support:: vendor_pypi (
4244+ "pkg:pypi/six@1.16.0" ,
4245+ & fx. site_packages ,
4246+ & fx. root ,
4247+ record,
4248+ & sources,
4249+ "2026-06-09T00:00:00Z" ,
4250+ false ,
4251+ false ,
4252+ None ,
4253+ )
4254+ . await
4255+ }
4256+
4257+ async fn read_json ( root : & Path , name : & str ) -> serde_json:: Value {
4258+ serde_json:: from_str ( & tokio:: fs:: read_to_string ( root. join ( name) ) . await . unwrap ( ) ) . unwrap ( )
4259+ }
4260+
4261+ /// #769: a Pipfile.lock wired to an EARLIER patch uuid re-vendors in
4262+ /// place to the superseding uuid, as the `would_revendor` preview and
4263+ /// the CLI contract promise: the entry moves to the new wheel, its
4264+ /// record carries the pre-vendor registry original forward, and
4265+ /// `vendor --revert` of the NEW entry restores the registry pin.
4266+ #[ tokio:: test]
4267+ async fn pipenv_superseding_uuid_revendors_in_place ( ) {
4268+ const UUID2 : & str = "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d" ;
4269+ let fx = pipenv_e2e_fixture ( ) . await ;
4270+ let registry = read_json ( & fx. root , "Pipfile.lock" ) . await ;
4271+ let VendorOutcome :: Done { result, entry, .. } = pipenv_vendor ( & fx, & fx. record ) . await else {
4272+ panic ! ( "first vendor must be Done" ) ;
4273+ } ;
4274+ assert ! ( result. success, "{:?}" , result. error) ;
4275+ let first = entry. expect ( "entry on success" ) ;
4276+ save_ledger_entry ( & fx. root , & first) . await ;
4277+
4278+ let mut record2 = fx. record . clone ( ) ;
4279+ record2. uuid = UUID2 . to_string ( ) ;
4280+ let outcome = pipenv_vendor ( & fx, & record2) . await ;
4281+ let VendorOutcome :: Done { result, entry, .. } = outcome else {
4282+ panic ! ( "superseding uuid must re-vendor, got {outcome:?}" ) ;
4283+ } ;
4284+ assert ! ( result. success, "{:?}" , result. error) ;
4285+ let second = entry. expect ( "entry on success" ) ;
4286+ assert_eq ! ( second. uuid, UUID2 ) ;
4287+ assert_eq ! ( second. wiring. len( ) , 1 ) ;
4288+ assert_eq ! ( second. wiring[ 0 ] . key. as_deref( ) , Some ( "default:six" ) ) ;
4289+ assert_eq ! (
4290+ second. wiring[ 0 ] . original,
4291+ Some ( registry[ "default" ] [ "six" ] . clone( ) ) ,
4292+ "the pre-vendor registry original is carried forward"
4293+ ) ;
4294+ let lock = tokio:: fs:: read_to_string ( fx. root . join ( "Pipfile.lock" ) )
4295+ . await
4296+ . unwrap ( ) ;
4297+ assert ! ( !lock. contains( UUID ) , "Pipfile.lock kept uuid A:\n {lock}" ) ;
4298+ assert ! ( lock. contains( UUID2 ) , "Pipfile.lock not on uuid B:\n {lock}" ) ;
4299+ assert ! ( fx
4300+ . root
4301+ . join( format!( ".socket/vendor/pypi/{UUID2}/{WHEEL_NAME}" ) )
4302+ . is_file( ) ) ;
4303+
4304+ save_ledger_entry ( & fx. root , & second) . await ;
4305+ let reverted = revert_pypi ( & second, & fx. root , false ) . await ;
4306+ assert ! ( reverted. success, "{:?}" , reverted. error) ;
4307+ assert ! ( reverted. warnings. is_empty( ) , "{:?}" , reverted. warnings) ;
4308+ assert_eq ! ( read_json( & fx. root, "Pipfile.lock" ) . await , registry) ;
4309+ }
4310+
4311+ /// #769: without a ledger entry for the older uuid there is no recorded
4312+ /// pre-vendor original to carry forward, so a re-wire could never be
4313+ /// reverted. That case still refuses, before anything is written.
4314+ #[ tokio:: test]
4315+ async fn pipenv_superseding_uuid_without_ledger_refuses ( ) {
4316+ const UUID2 : & str = "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d" ;
4317+ let fx = pipenv_e2e_fixture ( ) . await ;
4318+ let VendorOutcome :: Done { result, .. } = pipenv_vendor ( & fx, & fx. record ) . await else {
4319+ panic ! ( "first vendor must be Done" ) ;
4320+ } ;
4321+ assert ! ( result. success, "{:?}" , result. error) ;
4322+ let wired = tokio:: fs:: read_to_string ( fx. root . join ( "Pipfile.lock" ) )
4323+ . await
4324+ . unwrap ( ) ;
4325+
4326+ let mut record2 = fx. record . clone ( ) ;
4327+ record2. uuid = UUID2 . to_string ( ) ;
4328+ let outcome = pipenv_vendor ( & fx, & record2) . await ;
4329+ let VendorOutcome :: Refused { code, detail } = outcome else {
4330+ panic ! ( "expected Refused, got {outcome:?}" ) ;
4331+ } ;
4332+ assert_eq ! ( code, "pypi_pipenv_source_already_exists" ) ;
4333+ assert ! ( detail. contains( UUID ) , "{detail}" ) ;
4334+ assert ! ( detail. contains( "records no wiring" ) , "{detail}" ) ;
4335+ assert_eq ! (
4336+ tokio:: fs:: read_to_string( fx. root. join( "Pipfile.lock" ) )
4337+ . await
4338+ . unwrap( ) ,
4339+ wired
4340+ ) ;
4341+ assert ! ( !fx
4342+ . root
4343+ . join( format!( ".socket/vendor/pypi/{UUID2}" ) )
4344+ . exists( ) ) ;
4345+ }
4346+
4347+ /// #769: a re-wire replays only what the older entry's ledger recorded.
4348+ /// A wired entry edited since vendoring refuses before anything is
4349+ /// written.
4350+ #[ tokio:: test]
4351+ async fn pipenv_superseding_uuid_drifted_entry_refuses ( ) {
4352+ const UUID2 : & str = "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d" ;
4353+ let fx = pipenv_e2e_fixture ( ) . await ;
4354+ let VendorOutcome :: Done { result, entry, .. } = pipenv_vendor ( & fx, & fx. record ) . await else {
4355+ panic ! ( "first vendor must be Done" ) ;
4356+ } ;
4357+ assert ! ( result. success, "{:?}" , result. error) ;
4358+ save_ledger_entry ( & fx. root , & entry. expect ( "entry on success" ) ) . await ;
4359+ let mut lock = read_json ( & fx. root , "Pipfile.lock" ) . await ;
4360+ lock[ "default" ] [ "six" ] [ "markers" ] = serde_json:: json!( "python_version >= '3.8'" ) ;
4361+ let drifted = serde_json:: to_string_pretty ( & lock) . unwrap ( ) + "\n " ;
4362+ touch ( & fx. root , "Pipfile.lock" , & drifted) . await ;
4363+
4364+ let mut record2 = fx. record . clone ( ) ;
4365+ record2. uuid = UUID2 . to_string ( ) ;
4366+ let outcome = pipenv_vendor ( & fx, & record2) . await ;
4367+ let VendorOutcome :: Refused { code, detail } = outcome else {
4368+ panic ! ( "expected Refused, got {outcome:?}" ) ;
4369+ } ;
4370+ assert_eq ! ( code, "pypi_pipenv_source_already_exists" ) ;
4371+ assert ! ( detail. contains( "changed since vendoring" ) , "{detail}" ) ;
4372+ assert_eq ! (
4373+ tokio:: fs:: read_to_string( fx. root. join( "Pipfile.lock" ) )
4374+ . await
4375+ . unwrap( ) ,
4376+ drifted
4377+ ) ;
4378+ assert ! ( !fx
4379+ . root
4380+ . join( format!( ".socket/vendor/pypi/{UUID2}" ) )
4381+ . exists( ) ) ;
4382+ }
4383+
42114384 /// A relock regenerated the wired entry to a registry reference whose
42124385 /// hash list differs from the recorded original (Pipenv 2022.12.19 does
42134386 /// exactly this; 2026.x reproduces the original and converges silently):
0 commit comments