Repository: astral-sh/uv
File: crates/uv/src/commands/project/lock.rs (Rust)

Code before:
    #[must_use]
    pub(super) fn with_refresh(mut self, refresh: &'env Refresh) -> Self {
        self.refresh = Some(refresh);
        self
    }

    /// Perform a [`LockOperation`].
    pub(super) async fn execute(self, target: LockTarget<'_>) -> Result<LockResult, ProjectError> {
        match self.mode {
            LockMode::Frozen => {
                // Read the existing lockfile, but don't attempt to lock the project.
                let existing = target
                    .read()
                    .await?
                    .ok_or_else(|| ProjectError::MissingLockfile)?;
                Ok(LockResult::Unchanged(existing))
            }
            LockMode::Locked(interpreter, lock_source) => {
                // Read the existing lockfile.
                let existing = target
                    .read()
                    .await?
                    .ok_or_else(|| ProjectError::MissingLockfile)?;

                // Perform the lock operation, but don't write the lockfile to disk.
                let result = do_lock(
                    target,
                    interpreter,
                    Some(existing),
                    self.constraints,
                    self.refresh,
                    self.settings,
                    self.client_builder,
                    self.state,
                    self.logger,
                    self.concurrency,
                    self.cache,
                    self.workspace_cache,
                    self.printer,
                    self.preview,
                )
                .await?;

                // If the lockfile changed, return an error.
                if let LockResult::Changed(prev, cur) = result {
                    return Err(ProjectError::LockMismatch(
                        prev.map(Box::new),
                        Box::new(cur),
                        lock_source,
                    ));
                }

Review comment (bug):
This is technically more strict than the previous check, but it will only fail in cases where the lockfile doesn't match the on-disk workspace structure, while afaik we require the workspace structure to be intact through `VirtualProject::discover` (`uv export --frozen --no-emit-workspace --no-emit-project` panics too)?

Code after:
    #[must_use]
    pub(super) fn with_refresh(mut self, refresh: &'env Refresh) -> Self {
        self.refresh = Some(refresh);
        self
    }

    /// Perform a [`LockOperation`].
    pub(super) async fn execute(self, target: LockTarget<'_>) -> Result<LockResult, ProjectError> {
        match self.mode {
            LockMode::Frozen => {
                // Read the existing lockfile, but don't attempt to lock the project.
                let existing = target
                    .read()
                    .await?
                    .ok_or_else(|| ProjectError::MissingLockfile)?;
                // Check if the discovered workspace members match the locked workspace members.
                if let LockTarget::Workspace(workspace) = target {
                    for package_name in workspace.packages().keys() {
                        existing
                            .find_by_name(package_name)
                            .map_err(|_| ProjectError::LockWorkspaceMismatch(package_name.clone()))?
                            .ok_or_else(|| {
                                ProjectError::LockWorkspaceMismatch(package_name.clone())
                            })?;
                    }
                }
                Ok(LockResult::Unchanged(existing))
            }
            LockMode::Locked(interpreter, lock_source) => {
                // Read the existing lockfile.
                let existing = target
                    .read()
                    .await?
                    .ok_or_else(|| ProjectError::MissingLockfile)?;

                // Perform the lock operation, but don't write the lockfile to disk.
                let result = do_lock(
                    target,
                    interpreter,
                    Some(existing),
                    self.constraints,
                    self.refresh,
                    self.settings,
                    self.client_builder,
                    self.state,
                    self.logger,
                    self.concurrency,
                    self.cache,
                    self.workspace_cache,
                    self.printer,
                    self.preview,