pub fn changed_packages_from_lockfile(
        &self,
        previous: &dyn Lockfile,
    ) -> Result<Vec<ExternalDependencyChange>, ChangedPackagesError> {
        let current = self.lockfile().ok_or(ChangedPackagesError::NoLockfile)?;

        let external_deps = self
            .packages()
            .filter_map(|(_name, info)| {
                info.unresolved_external_dependencies.as_ref().map(|dep| {
                    (
                        info.package_path().to_unix().to_string(),
                        dep.iter()
                            .map(|(name, version)| (name.to_owned(), version.to_owned()))
                            .collect(),
                    )
                })
            })
            .collect::<HashMap<_, HashMap<_, _>>>();

        // We're comparing to a previous lockfile, it's possible that a package was
        // added and thus won't exist in the previous lockfile. In that case,
        // we're fine to ignore it. Assuming there is not a commit with a stale
        // lockfile, the same commit should add the package, so it will get
        // picked up as changed.
        let closures = turborepo_lockfiles::all_transitive_closures(previous, external_deps, true)?;

        let global_change = current.global_change(previous);

        let changed = if global_change {
            None
        } else {
            self.packages
                .iter()
                .filter_map(|(name, info)| {
                    let previous_closure = closures.get(info.package_path().to_unix().as_str());
                    let not_equal = previous_closure != info.transitive_dependencies.as_ref();
                    if not_equal {
                        if let (Some(prev), Some(curr)) =
                            (previous_closure, info.transitive_dependencies.as_ref())
                        {
                            debug!(
                                "package {name} has differing closure: {:?}",
                                prev.symmetric_difference(curr)
                            );
                        }
                        let empty_set = HashSet::default();
                        let prev_deps = previous_closure.unwrap_or(&empty_set);
                        let curr_deps = info.transitive_dependencies.as_ref().unwrap_or(&empty_set);
                        // {a, b} -> {a, c}
                        // b was removed
                        // c was added
                        let added = curr_deps
                            .difference(prev_deps)
                            .cloned()
                            .sorted()
                            .collect::<Vec<_>>();
                        let removed = prev_deps
                            .difference(curr_deps)
                            .cloned()
                            .sorted()
                            .collect::<Vec<_>>();
                        Some((name, info, added, removed))
                    } else {
                        None
                    }
                })
                .map(|(name, info, added, removed)| match name {
                    PackageName::Other(n) => {
                        let w_name = PackageName::Other(n.to_owned());
                        let package = WorkspacePackage {
                            name: w_name.clone(),
                            path: info.package_path().to_owned(),
                        };
                        Some(ExternalDependencyChange {
                            package,
                            added,
                            removed,
                        })
                    }
                    // if the root package has changed, then we should report `None`
                    // since all packages need to be revalidated
                    PackageName::Root => None,
                })
                .collect::<Option<Vec<_>>>()
        };

        Ok(changed.unwrap_or_else(|| {
            self.packages
                .iter()
                .map(|(name, info)| {
                    let package = WorkspacePackage {
                        name: name.clone(),
                        path: info.package_path().to_owned(),
                    };
                    ExternalDependencyChange {
                        package,
                        added: Vec::new(),
                        removed: Vec::new(),
                    }
                })
                .collect()
        }))
    }
Returns a list of changed packages based on the contents of a previous
`Lockfile`. This assumes that none of the package.json in the package
change, it is the responsibility of the caller to verify this.
crates/turborepo-repository/src/package_graph/mod.rs