From 71656a83571ddccdae5b24b4fcc34a518c9ff952 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 12 May 2026 12:40:32 +0000 Subject: [PATCH] fix(source-checkout): include `precise` in CheckoutGit dedup key `CheckoutGit::new` keyed only on `RepositoryReference` (URL + reference) and `compute` rebuilt `GitUrl` from the reference alone, so `resolver.fetch` always received `precise = None` and re-resolved to the branch's current HEAD. Two callers with different pinned commits on the same branch collided on a single cache slot, and `checkout_pinned_source(commit_A)` could silently return a `SourceCheckout` whose `path` pointed at commit B's checkout while `pinned` reported commit A. That's how a freshly-fetched manifest's tightened `host-dependencies` leaked into a build whose lock-file still pinned the older commit's host_packages, producing the silent `setuptools <82.0.0 | 82.0.1` mismatch in prefix-dev/pixi#6073. Key on the full `GitUrl` and pass it through unchanged so the resolver honours `precise` when set and advances to HEAD only when not. --- .../snapshots/integration__simple_test.snap | 5 ++-- crates/pixi_compute_sources/src/git.rs | 28 ++++++++++--------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/crates/pixi_command_dispatcher/tests/integration/snapshots/integration__simple_test.snap b/crates/pixi_command_dispatcher/tests/integration/snapshots/integration__simple_test.snap index cde84dcca5..fd3d8ebd34 100644 --- a/crates/pixi_command_dispatcher/tests/integration/snapshots/integration__simple_test.snap +++ b/crates/pixi_command_dispatcher/tests/integration/snapshots/integration__simple_test.snap @@ -6,12 +6,13 @@ Pixi solve (test@[PLATFORM]) ├── Git Checkout (file://[LOCAL_GIT_REPO]#[GIT_HASH]) ├── Source metadata (foobar-desktop @ git+file://[LOCAL_GIT_REPO]?subdirectory=recipe&rev=[GIT_REF]#[GIT_HASH]) │ ├── Build backend metadata (git+file://[LOCAL_GIT_REPO]?subdirectory=recipe&rev=[GIT_REF]#[GIT_HASH]) +│ │ ├── Git Checkout (file://[LOCAL_GIT_REPO]#[GIT_HASH]) │ │ └── Instantiate backend (pixi-build-rattler-build) -│ │ └── Conda solve #5 +│ │ └── Conda solve #6 │ └── Source record (foobar-desktop @ git+file://[LOCAL_GIT_REPO]?subdirectory=recipe&rev=[GIT_REF]#[GIT_HASH]) ├── Source metadata (foobar @ git+file://[LOCAL_GIT_REPO]?subdirectory=recipe&rev=[GIT_REF]#[GIT_HASH]) │ └── Source record (foobar @ git+file://[LOCAL_GIT_REPO]?subdirectory=recipe&rev=[GIT_REF]#[GIT_HASH]) -└── Conda solve #9 +└── Conda solve #10 Pixi install (test-env) ├── Backend source build (foobar-desktop) └── Backend source build (foobar) diff --git a/crates/pixi_compute_sources/src/git.rs b/crates/pixi_compute_sources/src/git.rs index 7717b065b6..54ddf58d1e 100644 --- a/crates/pixi_compute_sources/src/git.rs +++ b/crates/pixi_compute_sources/src/git.rs @@ -95,16 +95,19 @@ impl LifecycleKind for GitReporterLifecycle { } } -/// Dedup key for a git checkout. Keyed on [`RepositoryReference`] -/// (normalized URL + reference, no `precise`) so callers that differ -/// only in whether they pre-resolved the commit still dedup. +/// Dedup key for a git checkout. Keyed on the full [`GitUrl`]: URL, +/// reference, AND `precise` commit, so a caller asking for a specific +/// commit doesn't collide with one asking only for the branch. +/// Stripping `precise` from the key let `checkout_pinned_source(A)` +/// silently return the branch's HEAD instead of commit `A` +/// (prefix-dev/pixi#6073). #[derive(Clone, Debug, Display, Hash, PartialEq, Eq)] -#[display("{}@{}", _0.url.as_url(), _0.reference)] -pub struct CheckoutGit(RepositoryReference); +#[display("{}@{}", _0.repository(), _0.reference())] +pub struct CheckoutGit(GitUrl); impl CheckoutGit { pub fn new(git_url: &GitUrl) -> Self { - Self(RepositoryReference::from(git_url)) + Self(git_url.clone()) } } @@ -119,8 +122,9 @@ impl Key for CheckoutGit { let semaphore = data.git_checkout_semaphore().cloned(); let reporter = data.git_checkout_reporter().cloned(); + let reporter_env = RepositoryReference::from(&self.0); let lifecycle = - ReporterLifecycle::::queued(reporter.as_deref(), &self.0); + ReporterLifecycle::::queued(reporter.as_deref(), &reporter_env); let _permit = match semaphore.as_ref() { Some(s) => Some( @@ -132,14 +136,12 @@ impl Key for CheckoutGit { }; let _lifecycle = lifecycle.start(); - // `from_reference` auto-fills `precise` from a full-commit - // reference, so the resolver skips ref-resolution when it can. - let git_url = - GitUrl::from_reference(self.0.url.clone().into_url(), self.0.reference.clone()); - + // Pass the original `GitUrl` straight through so the resolver + // honours `precise` when set (pinned checkout) and advances to + // the branch HEAD when not (fresh resolve). Arc::new( resolver - .fetch(git_url, client, cache_dir.into_std_path_buf(), None) + .fetch(self.0.clone(), client, cache_dir.into_std_path_buf(), None) .await, ) }