Skip to content

Commit 6483c64

Browse files
committed
Re-vendor Pipenv locks to a newer patch
A Pipenv project vendored at one patch never moved to a newer patch for the same package: the re-vendor refused with pypi_pipenv_source_already_exists and the run exited 1, although the dry run previewed would_revendor. When the vendor ledger records the Pipfile.lock entry the older patch wrote, and that entry is unchanged, it is now rewired in place to the new wheel. The record carries the older entry's pre-vendor registry original forward, so vendor --revert still restores the user's pin. Without that record, or after an edit, it still refuses as before. Refs #769 Assisted-by: Claude Code:claude-opus-5-5
1 parent a3d9d22 commit 6483c64

2 files changed

Lines changed: 304 additions & 24 deletions

File tree

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

Lines changed: 184 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)