message: cli: add ability to analyze lockfile changes when listing changed packages
diff: @@ -18,7 +18,8 @@ import path from 'path';
  import { getPackages, Package } from '@manypkg/get-packages';
  import { paths } from '../paths';
  import { PackageRole } from '../role';
  -import { listChangedFiles } from '../git';
  +import { listChangedFiles, readFileAtRef } from '../git';
  +import { Lockfile } from '../versioning';
  type PackageJSON = Package['packageJson'];
  @@ -191,7 +192,10 @@ export class PackageGraph extends Map<string, PackageGraphNode> {
  return targets;
  }
  - async listChangedPackages(options: { ref: string }) {
  + async listChangedPackages(options: {
  + ref: string;
  + analyzeLockfile?: boolean;
  + }) {
  const changedFiles = await listChangedFiles(options.ref);
  const dirMap = new Map(
  @@ -234,6 +238,68 @@ export class PackageGraph extends Map<string, PackageGraphNode> {
  }
  }
  + if (changedFiles.includes('yarn.lock') && options.analyzeLockfile) {
  + // Load the lockfile in the working tree and the one at the ref and diff them
  + const thisLockfile = await Lockfile.load(
  + paths.resolveTargetRoot('yarn.lock'),
  + );
  + const otherLockfile = Lockfile.parse(
  + await readFileAtRef('yarn.lock', options.ref),
  + );
  + const diff = thisLockfile.diff(otherLockfile);
  +
  + // Create a simplified dependency graph only keeps track of package names
  + const graph = thisLockfile.createSimplifiedDependencyGraph();
  +
  + // Merge the dependency graph from the other lockfile into this one in
  + // order to be able to detect removals accurately.
  + {
  + const otherGraph = thisLockfile.createSimplifiedDependencyGraph();
  + for (const [name, dependencies] of otherGraph) {
  + const node = graph.get(name);
  + if (node) {
  + dependencies.forEach(d => node.add(d));
  + } else {
  + graph.set(name, dependencies);
  + }
  + }
  + }
  +
  + // The check is simplified by only considering the package names rather
  + // than the exact version range queries that were changed.
  + // TODO(Rugvip): Use a more exact check
  + const changedPackages = new Set(
  + [...diff.added, ...diff.changed, ...diff.removed].map(e => e.name),
  + );
  +
  + // Starting with our set of changed packages from the diff, we loop through
  + // the full graph and add any package that has a dependency on a changed package.
  + // We keep looping until all transitive dependencies have been detected.
  + let changed = false;
  + do {
  + changed = false;
  + for (const [name, dependencies] of graph) {
  + if (changedPackages.has(name)) {
  + continue;
  + }
  + for (const dep of dependencies) {
  + if (changedPackages.has(dep)) {
  + changed = true;
  + changedPackages.add(name);
  + break;
  + }
  + }
  + }
  + } while (changed);
  +
  + // Add all local packages that had a transitive dependency change to the result set
  + for (const node of this.values()) {
  + if (changedPackages.has(node.name) && !result.includes(node)) {
  + result.push(node);
  + }
  + }
  + }
  +
  return result;
  }
  }
  