Handle whole workspace in `pub upgrade [--tighten|--major-versions]` (#4213)

diff --git a/lib/src/command/add.dart b/lib/src/command/add.dart
index ddf9933..5a78500 100644
--- a/lib/src/command/add.dart
+++ b/lib/src/command/add.dart
@@ -349,15 +349,10 @@
       dependencies.add(range);
     }
 
-    return Pubspec(
-      original.name,
-      version: original.version,
-      sdkConstraints: original.sdkConstraints,
+    return original.copyWith(
       dependencies: dependencies,
       devDependencies: devDependencies,
       dependencyOverrides: dependencyOverrides,
-      workspace: original.workspace,
-      resolution: original.resolution,
     );
   }
 
diff --git a/lib/src/command/dependency_services.dart b/lib/src/command/dependency_services.dart
index b1f17ac..9e1bcf7 100644
--- a/lib/src/command/dependency_services.dart
+++ b/lib/src/command/dependency_services.dart
@@ -105,14 +105,8 @@
           ?.firstWhereOrNull((element) => element.name == package.name);
       final multiBreakingVersion = breakingPackagesResult
           ?.firstWhereOrNull((element) => element.name == package.name);
-      final singleBreakingPubspec = Pubspec(
-        compatiblePubspec.name,
-        version: compatiblePubspec.version,
-        sdkConstraints: compatiblePubspec.sdkConstraints,
-        dependencies: compatiblePubspec.dependencies.values,
-        devDependencies: compatiblePubspec.devDependencies.values,
-        workspace: compatiblePubspec.workspace,
-      );
+      final singleBreakingPubspec = compatiblePubspec.copyWith();
+
       final dependencySet =
           _dependencySetOfPackage(singleBreakingPubspec, package);
       final kind = _kindString(compatiblePubspec, package.name);
@@ -745,13 +739,7 @@
   final pubspec = (upgradeType == _UpgradeType.multiBreaking ||
           upgradeType == _UpgradeType.smallestUpdate)
       ? stripVersionBounds(rootPubspec)
-      : Pubspec(
-          rootPubspec.name,
-          dependencies: rootPubspec.dependencies.values,
-          devDependencies: rootPubspec.devDependencies.values,
-          sdkConstraints: rootPubspec.sdkConstraints,
-          workspace: rootPubspec.workspace,
-        );
+      : rootPubspec.copyWith();
 
   final dependencySet = _dependencySetOfPackage(pubspec, package);
   if (dependencySet != null) {
diff --git a/lib/src/command/remove.dart b/lib/src/command/remove.dart
index c2e7e32..2dd1b79 100644
--- a/lib/src/command/remove.dart
+++ b/lib/src/command/remove.dart
@@ -123,15 +123,10 @@
         devDependencies.remove(package.name);
       }
     }
-    return Pubspec(
-      original.name,
-      version: original.version,
-      sdkConstraints: original.sdkConstraints,
+    return original.copyWith(
       dependencies: dependencies.values,
       devDependencies: devDependencies.values,
       dependencyOverrides: overrides.values,
-      workspace: original.workspace,
-      resolution: original.resolution,
     );
   }
 
diff --git a/lib/src/command/upgrade.dart b/lib/src/command/upgrade.dart
index 1832187..8d3c966 100644
--- a/lib/src/command/upgrade.dart
+++ b/lib/src/command/upgrade.dart
@@ -3,9 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'dart:async';
-import 'dart:io';
 
-import 'package:path/path.dart' as p;
 import 'package:pub_semver/pub_semver.dart';
 import 'package:yaml_edit/yaml_edit.dart';
 
@@ -21,7 +19,6 @@
 import '../sdk.dart';
 import '../solver.dart';
 import '../source/hosted.dart';
-import '../source/root.dart';
 import '../utils.dart';
 
 /// Handles the `upgrade` pub command.
@@ -141,14 +138,21 @@
       await _runUpgrade(entrypoint);
       if (_tighten) {
         final changes = tighten(
-          entrypoint.workspaceRoot.pubspec,
+          entrypoint,
           entrypoint.lockFile.packages.values.toList(),
         );
         if (!_dryRun) {
-          final newPubspecText = _updatePubspec(changes);
+          for (final package in entrypoint.workspaceRoot.transitiveWorkspace) {
+            final changesForPackage = changes[package];
+            if (changesForPackage == null || changesForPackage.isEmpty) {
+              continue;
+            }
+            final newPubspecText =
+                _updatePubspecText(package, changesForPackage);
 
-          if (changes.isNotEmpty) {
-            writeTextFile(entrypoint.workspaceRoot.pubspecPath, newPubspecText);
+            if (changes.isNotEmpty) {
+              writeTextFile(package.pubspecPath, newPubspecText);
+            }
           }
         }
         _outputChangeSummary(changes);
@@ -184,10 +188,10 @@
   ///
   /// If a dependency has already been updated in [existingChanges], the update
   /// will apply on top of that change (eg. preserving the new upper bound).
-  Map<PackageRange, PackageRange> tighten(
-    Pubspec pubspec,
+  Map<Package, Map<PackageRange, PackageRange>> tighten(
+    Entrypoint entrypoint,
     List<PackageId> packages, {
-    Map<PackageRange, PackageRange> existingChanges = const {},
+    Map<Package, Map<PackageRange, PackageRange>> existingChanges = const {},
   }) {
     final result = {...existingChanges};
     if (argResults.flag('example') && entrypoint.example != null) {
@@ -195,27 +199,41 @@
         'Running `upgrade --tighten` only in `${entrypoint.workspaceRoot.dir}`. Run `$topLevelProgram pub upgrade --tighten --directory example/` separately.',
       );
     }
-    final toTighten = _packagesToUpgrade.isEmpty
-        ? [
-            ...pubspec.dependencies.values,
-            ...pubspec.devDependencies.values,
-          ]
-        : [
-            for (final name in _packagesToUpgrade)
-              pubspec.dependencies[name] ?? pubspec.devDependencies[name],
-          ].nonNulls;
-    for (final range in toTighten) {
-      final constraint = (result[range] ?? range).constraint;
+
+    final toTighten = <(Package, PackageRange)>[];
+
+    for (final package in entrypoint.workspaceRoot.transitiveWorkspace) {
+      if (_packagesToUpgrade.isEmpty) {
+        for (final range in [
+          ...package.dependencies.values,
+          ...package.devDependencies.values,
+        ]) {
+          toTighten.add((package, range));
+        }
+      } else {
+        for (final packageToUpgrade in _packagesToUpgrade) {
+          final range = package.dependencies[packageToUpgrade] ??
+              package.devDependencies[packageToUpgrade];
+          if (range != null) {
+            toTighten.add((package, range));
+          }
+        }
+      }
+    }
+
+    for (final (package, range) in toTighten) {
+      final changesForPackage = result[package] ??= {};
+      final constraint = (changesForPackage[range] ?? range).constraint;
       final resolvedVersion =
           packages.firstWhere((p) => p.name == range.name).version;
       if (range.source is HostedSource && constraint.isAny) {
-        result[range] = range
+        changesForPackage[range] = range
             .toRef()
             .withConstraint(VersionConstraint.compatibleWith(resolvedVersion));
       } else if (constraint is VersionRange) {
         final min = constraint.min;
         if (min != null && min < resolvedVersion) {
-          result[range] = range.toRef().withConstraint(
+          changesForPackage[range] = range.toRef().withConstraint(
                 VersionRange(
                   min: resolvedVersion,
                   max: constraint.max,
@@ -232,27 +250,24 @@
   /// Return names of packages to be upgraded, and throws [UsageException] if
   /// any package names not in the direct dependencies or dev_dependencies are given.
   ///
-  /// This assumes that either `--major-versions` or `--null-safety` was passed.
+  /// This assumes that `--major-versions` was passed.
   List<String> _directDependenciesToUpgrade() {
     assert(_upgradeMajorVersions);
 
-    final directDeps = [
-      ...entrypoint.workspaceRoot.pubspec.dependencies.keys,
-      ...entrypoint.workspaceRoot.pubspec.devDependencies.keys,
-    ];
+    final directDeps = {
+      for (final package in entrypoint.workspaceRoot.transitiveWorkspace) ...[
+        ...package.dependencies.keys,
+        ...package.devDependencies.keys,
+      ],
+    }.toList();
     final toUpgrade =
         _packagesToUpgrade.isEmpty ? directDeps : _packagesToUpgrade;
 
     // Check that all package names in upgradeOnly are direct-dependencies
     final notInDeps = toUpgrade.where((n) => !directDeps.contains(n));
     if (toUpgrade.any(notInDeps.contains)) {
-      var modeFlag = '';
-      if (_upgradeMajorVersions) {
-        modeFlag = '--major-versions';
-      }
-
       usageException('''
-Dependencies specified in `$topLevelProgram pub upgrade $modeFlag <dependencies>` must
+Dependencies specified in `$topLevelProgram pub upgrade --major-versions <dependencies>` must
 be direct 'dependencies' or 'dev_dependencies', following packages are not:
  - ${notInDeps.join('\n - ')}
 
@@ -263,15 +278,11 @@
   }
 
   Future<void> _runUpgradeMajorVersions() async {
-    // TODO(https://github.com/dart-lang/pub/issues/4127): This should operate
-    // on all pubspecs in the workspace.
     final toUpgrade = _directDependenciesToUpgrade();
-
-    final resolvablePubspec = stripVersionBounds(
-      entrypoint.workspaceRoot.pubspec,
-      stripOnly: toUpgrade,
-    );
-
+    final workspace = {
+      for (final package in entrypoint.workspaceRoot.transitiveWorkspace)
+        package.dir: package,
+    };
     // Solve [resolvablePubspec] in-memory and consolidate the resolved
     // versions of the packages into a map for quick searching.
     final resolvedPackages = <String, PackageId>{};
@@ -281,10 +292,16 @@
         return await resolveVersions(
           SolveType.upgrade,
           cache,
-          Package(
-            resolvablePubspec,
+          Package.load(
             entrypoint.workspaceRoot.dir,
-            entrypoint.workspaceRoot.workspaceChildren,
+            entrypoint.cache.sources,
+            withPubspecOverrides: true,
+            loadPubspec: (
+              path, {
+              expectedName,
+              required withPubspecOverrides,
+            }) =>
+                stripVersionBounds(workspace[path]!.pubspec),
           ),
         );
       },
@@ -293,40 +310,42 @@
     for (final resolvedPackage in solveResult.packages) {
       resolvedPackages[resolvedPackage.name] = resolvedPackage;
     }
-
-    // Changes to be made to `pubspec.yaml`.
+    final dependencyOverriddenDeps = <String>[];
+    // Changes to be made to `pubspec.yaml` of each package.
     // Mapping from original to changed value.
-    var changes = <PackageRange, PackageRange>{};
-    final declaredHostedDependencies = [
-      ...entrypoint.workspaceRoot.pubspec.dependencies.values,
-      ...entrypoint.workspaceRoot.pubspec.devDependencies.values,
-    ].where((dep) => dep.source is HostedSource);
-    for (final dep in declaredHostedDependencies) {
-      final resolvedPackage = resolvedPackages[dep.name]!;
-      if (!toUpgrade.contains(dep.name)) {
-        // If we're not trying to upgrade this package, or it wasn't in the
-        // resolution somehow, then we ignore it.
-        continue;
-      }
+    var changes = <Package, Map<PackageRange, PackageRange>>{};
+    for (final package in entrypoint.workspaceRoot.transitiveWorkspace) {
+      final declaredHostedDependencies = [
+        ...package.dependencies.values,
+        ...package.devDependencies.values,
+      ].where((dep) => dep.source is HostedSource);
+      for (final dep in declaredHostedDependencies) {
+        final resolvedPackage = resolvedPackages[dep.name]!;
+        if (!toUpgrade.contains(dep.name)) {
+          // If we're not trying to upgrade this package, or it wasn't in the
+          // resolution somehow, then we ignore it.
+          continue;
+        }
 
-      // Skip [dep] if it has a dependency_override.
-      if (entrypoint.workspaceRoot.dependencyOverrides.containsKey(dep.name)) {
-        continue;
-      }
+        // Skip [dep] if it has a dependency_override.
+        if (entrypoint.workspaceRoot.dependencyOverrides
+            .containsKey(dep.name)) {
+          dependencyOverriddenDeps.add(dep.name);
+          continue;
+        }
 
-      if (dep.constraint.allowsAll(resolvedPackage.version)) {
-        // If constraint allows the resolvable version we found, then there is
-        // no need to update the `pubspec.yaml`
-        continue;
-      }
+        if (dep.constraint.allowsAll(resolvedPackage.version)) {
+          // If constraint allows the resolvable version we found, then there is
+          // no need to update the `pubspec.yaml`
+          continue;
+        }
 
-      changes[dep] = dep.toRef().withConstraint(
-            VersionConstraint.compatibleWith(
-              resolvedPackage.version,
-            ),
-          );
+        (changes[package] ??= {})[dep] = dep.toRef().withConstraint(
+              VersionConstraint.compatibleWith(resolvedPackage.version),
+            );
+      }
     }
-    var newPubspecText = _updatePubspec(changes);
+
     if (_tighten) {
       // Do another solve with the updated constraints to obtain the correct
       // versions to tighten to. This should be fast (everything is cached, and
@@ -335,18 +354,21 @@
       final solveResult = await resolveVersions(
         SolveType.upgrade,
         cache,
-        Package(
-          _updatedPubspec(newPubspecText, entrypoint),
+        Package.load(
           entrypoint.workspaceRoot.dir,
-          entrypoint.workspaceRoot.workspaceChildren,
+          entrypoint.cache.sources,
+          loadPubspec: (path, {expectedName, required withPubspecOverrides}) {
+            final package = workspace[path]!;
+            final changesForPackage = changes[package] ?? {};
+            return applyChanges(package.pubspec, changesForPackage);
+          },
         ),
       );
       changes = tighten(
-        entrypoint.workspaceRoot.pubspec,
+        entrypoint,
         solveResult.packages,
         existingChanges: changes,
       );
-      newPubspecText = _updatePubspec(changes);
     }
 
     // When doing '--majorVersions' for specific packages we try to update other
@@ -358,18 +380,23 @@
         _packagesToUpgrade.isEmpty ? SolveType.upgrade : SolveType.get;
 
     if (!_dryRun) {
-      if (changes.isNotEmpty) {
-        writeTextFile(entrypoint.workspaceRoot.pubspecPath, newPubspecText);
+      for (final package in entrypoint.workspaceRoot.transitiveWorkspace) {
+        final changesForPackage = changes[package] ?? {};
+        if (changesForPackage.isNotEmpty) {
+          final newPubspecText = _updatePubspecText(package, changesForPackage);
+          writeTextFile(package.pubspecPath, newPubspecText);
+        }
       }
     }
-
-    await entrypoint
-        .withWorkPubspec(_updatedPubspec(newPubspecText, entrypoint))
-        .acquireDependencies(
-          solveType,
-          dryRun: _dryRun,
-          precompile: !_dryRun && _precompile,
-        );
+    await entrypoint.withUpdatedPubspecs({
+      for (final MapEntry(key: package, value: changesForPackage)
+          in changes.entries)
+        package: applyChanges(package.pubspec, changesForPackage),
+    }).acquireDependencies(
+      solveType,
+      dryRun: _dryRun,
+      precompile: !_dryRun && _precompile,
+    );
 
     _outputChangeSummary(changes);
 
@@ -387,59 +414,82 @@
     _showOfflineWarning();
   }
 
-  Pubspec _updatedPubspec(String contents, Entrypoint entrypoint) {
-    String? overridesFileContents;
-    final overridesPath =
-        p.join(entrypoint.workspaceRoot.dir, Pubspec.pubspecOverridesFilename);
-    try {
-      overridesFileContents = readTextFile(overridesPath);
-    } on IOException {
-      overridesFileContents = null;
+  Pubspec applyChanges(
+    Pubspec original,
+    Map<PackageRange, PackageRange> changes,
+  ) {
+    final dependencies = {...original.dependencies};
+    final devDependencies = {...original.devDependencies};
+
+    for (final change in changes.values) {
+      if (dependencies[change.name] != null) {
+        dependencies[change.name] = change;
+      } else {
+        devDependencies[change.name] = change;
+      }
     }
-    return Pubspec.parse(
-      contents,
-      cache.sources,
-      location: Uri.parse(entrypoint.workspaceRoot.pubspecPath),
-      overridesFileContents: overridesFileContents,
-      overridesLocation: Uri.file(overridesPath),
-      containingDescription: RootDescription(entrypoint.workspaceRoot.dir),
+    return original.copyWith(
+      dependencies: dependencies.values,
+      devDependencies: devDependencies.values,
     );
   }
 
-  /// Updates `pubspec.yaml` with given [changes].
-  String _updatePubspec(
+  /// Loads `pubspec.yaml` of [package] and applies [changes] to its
+  /// (dev)-dependencies.
+  ///
+  /// Returns the updated textual representation using yaml-edit to preserve
+  /// structure.
+  String _updatePubspecText(
+    Package package,
     Map<PackageRange, PackageRange> changes,
   ) {
     ArgumentError.checkNotNull(changes, 'changes');
-    final yamlEditor =
-        YamlEditor(readTextFile(entrypoint.workspaceRoot.pubspecPath));
-    final deps = entrypoint.workspaceRoot.pubspec.dependencies.keys;
+    final yamlEditor = YamlEditor(readTextFile(package.pubspecPath));
+    final deps = package.dependencies.keys;
 
     for (final change in changes.values) {
       final section =
           deps.contains(change.name) ? 'dependencies' : 'dev_dependencies';
       yamlEditor.update(
         [section, change.name],
-        pubspecDescription(change, cache, entrypoint.workspaceRoot),
+        pubspecDescription(change, cache, package),
       );
     }
     return yamlEditor.toString();
   }
 
   /// Outputs a summary of changes made to `pubspec.yaml`.
-  void _outputChangeSummary(Map<PackageRange, PackageRange> changes) {
-    ArgumentError.checkNotNull(changes, 'changes');
-
-    if (changes.isEmpty) {
-      final wouldBe = _dryRun ? 'would be made to' : 'to';
-      log.message('\nNo changes $wouldBe pubspec.yaml!');
+  void _outputChangeSummary(
+    Map<Package, Map<PackageRange, PackageRange>> changes,
+  ) {
+    if (entrypoint.workspaceRoot.workspaceChildren.isEmpty) {
+      final changesToWorkspaceRoot = changes[entrypoint.workspaceRoot] ?? {};
+      if (changesToWorkspaceRoot.isEmpty) {
+        final wouldBe = _dryRun ? 'would be made to' : 'to';
+        log.message('\nNo changes $wouldBe pubspec.yaml!');
+      } else {
+        final changed = _dryRun ? 'Would change' : 'Changed';
+        log.message('\n$changed ${changesToWorkspaceRoot.length} '
+            '${pluralize('constraint', changesToWorkspaceRoot.length)} in pubspec.yaml:');
+        changesToWorkspaceRoot.forEach((from, to) {
+          log.message('  ${from.name}: ${from.constraint} -> ${to.constraint}');
+        });
+      }
     } else {
-      final changed = _dryRun ? 'Would change' : 'Changed';
-      log.message('\n$changed ${changes.length} '
-          '${pluralize('constraint', changes.length)} in pubspec.yaml:');
-      changes.forEach((from, to) {
-        log.message('  ${from.name}: ${from.constraint} -> ${to.constraint}');
-      });
+      if (changes.isEmpty) {
+        final wouldBe = _dryRun ? 'would be made to' : 'to';
+        log.message('\nNo changes $wouldBe any pubspec.yaml!');
+      }
+      for (final package in entrypoint.workspaceRoot.transitiveWorkspace) {
+        final changesToPackage = changes[package] ?? {};
+        if (changesToPackage.isEmpty) continue;
+        final changed = _dryRun ? 'Would change' : 'Changed';
+        log.message('\n$changed ${changesToPackage.length} '
+            '${pluralize('constraint', changesToPackage.length)} in ${package.pubspecPath}:');
+        changesToPackage.forEach((from, to) {
+          log.message('  ${from.name}: ${from.constraint} -> ${to.constraint}');
+        });
+      }
     }
   }
 
diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart
index bc809a8..861c9df 100644
--- a/lib/src/entrypoint.dart
+++ b/lib/src/entrypoint.dart
@@ -334,16 +334,15 @@
     }
   }
 
-  /// Creates an entrypoint at the same location, that will use [pubspec] for
-  /// resolution of the [workPackage].
-  Entrypoint withWorkPubspec(Pubspec pubspec) {
+  /// Creates an entrypoint at the same location, but with each pubspec in
+  /// [updatedPubspec] replacing the with one for the corresponding package.
+  Entrypoint withUpdatedPubspecs(Map<Package, Pubspec> updatedPubspecs) {
     final existingPubspecs = <String, Pubspec>{};
     // First extract all pubspecs from the workspace.
     for (final package in workspaceRoot.transitiveWorkspace) {
-      existingPubspecs[package.dir] = package.pubspec;
+      existingPubspecs[package.dir] =
+          updatedPubspecs[package] ?? package.pubspec;
     }
-    // Then override the one of the workPackage.
-    existingPubspecs[p.canonicalize(workPackage.dir)] = pubspec;
     final newWorkspaceRoot = Package.load(
       workspaceRoot.dir,
       cache.sources,
@@ -352,7 +351,7 @@
         expectedName,
         required withPubspecOverrides,
       }) =>
-          existingPubspecs[p.canonicalize(dir)] ??
+          existingPubspecs[dir] ??
           Pubspec.load(
             dir,
             cache.sources,
@@ -372,6 +371,12 @@
     );
   }
 
+  /// Creates an entrypoint at the same location, that will use [pubspec] for
+  /// resolution of the [workPackage].
+  Entrypoint withWorkPubspec(Pubspec pubspec) {
+    return withUpdatedPubspecs({workPackage: pubspec});
+  }
+
   /// Creates an entrypoint given package and lockfile objects.
   /// If a SolveResult is already created it can be passed as an optimization.
   Entrypoint.global(
diff --git a/lib/src/package.dart b/lib/src/package.dart
index d7f8642..ca6b3f6 100644
--- a/lib/src/package.dart
+++ b/lib/src/package.dart
@@ -64,7 +64,10 @@
     while (stack.isNotEmpty) {
       final current = stack.removeLast();
       yield current;
-      stack.addAll(current.workspaceChildren);
+      // Because we pick from the end of the stack, elements are added in
+      // reverse, such that they will be visited in the order they appear in the
+      // list.
+      stack.addAll(current.workspaceChildren.reversed);
     }
   }
 
diff --git a/lib/src/pubspec.dart b/lib/src/pubspec.dart
index a67a894..2a951ad 100644
--- a/lib/src/pubspec.dart
+++ b/lib/src/pubspec.dart
@@ -407,6 +407,31 @@
     );
   }
 
+  Pubspec copyWith({
+    String? name,
+    Version? version,
+    Iterable<PackageRange>? dependencies,
+    Iterable<PackageRange>? devDependencies,
+    Iterable<PackageRange>? dependencyOverrides,
+    Map? fields,
+    Map<String, SdkConstraint>? sdkConstraints,
+    List<String>? workspace,
+    //this.dependencyOverridesFromOverridesFile = false,
+    Resolution? resolution,
+  }) {
+    return Pubspec(
+      name ?? this.name,
+      version: version ?? this.version,
+      dependencies: dependencies ?? this.dependencies.values,
+      devDependencies: devDependencies ?? this.devDependencies.values,
+      dependencyOverrides:
+          dependencyOverrides ?? this.dependencyOverrides.values,
+      sdkConstraints: sdkConstraints ?? this.sdkConstraints,
+      workspace: workspace ?? this.workspace,
+      resolution: resolution ?? this.resolution,
+    );
+  }
+
   /// Ensures that [node] is a mapping.
   ///
   /// If [node] is already a map it is returned.
diff --git a/lib/src/pubspec_utils.dart b/lib/src/pubspec_utils.dart
index 74b2ce8..646114c 100644
--- a/lib/src/pubspec_utils.dart
+++ b/lib/src/pubspec_utils.dart
@@ -13,32 +13,12 @@
 
 /// Returns a new [Pubspec] without [original]'s dev_dependencies.
 Pubspec stripDevDependencies(Pubspec original) {
-  ArgumentError.checkNotNull(original, 'original');
-
-  return Pubspec(
-    original.name,
-    version: original.version,
-    sdkConstraints: original.sdkConstraints,
-    dependencies: original.dependencies.values,
-    devDependencies: [], // explicitly give empty list, to prevent lazy parsing
-    dependencyOverrides: original.dependencyOverrides.values,
-    workspace: original.workspace,
-  );
+  return original.copyWith(devDependencies: []);
 }
 
 /// Returns a new [Pubspec] without [original]'s dependency_overrides.
 Pubspec stripDependencyOverrides(Pubspec original) {
-  ArgumentError.checkNotNull(original, 'original');
-
-  return Pubspec(
-    original.name,
-    version: original.version,
-    sdkConstraints: original.sdkConstraints,
-    dependencies: original.dependencies.values,
-    devDependencies: original.devDependencies.values,
-    dependencyOverrides: [],
-    workspace: original.workspace,
-  );
+  return original.copyWith(dependencyOverrides: []);
 }
 
 /// Returns new pubspec with the same dependencies as [original] but with the
@@ -54,7 +34,6 @@
   Iterable<String>? stripOnly,
   bool stripLowerBound = false,
 }) {
-  ArgumentError.checkNotNull(original, 'original');
   stripOnly ??= [];
 
   List<PackageRange> stripBounds(
@@ -80,14 +59,9 @@
     return result;
   }
 
-  return Pubspec(
-    original.name,
-    version: original.version,
-    sdkConstraints: original.sdkConstraints,
+  return original.copyWith(
     dependencies: stripBounds(original.dependencies),
     devDependencies: stripBounds(original.devDependencies),
-    dependencyOverrides: original.dependencyOverrides.values,
-    workspace: original.workspace,
   );
 }
 
@@ -117,14 +91,9 @@
     return result;
   }
 
-  return Pubspec(
-    original.name,
-    version: original.version,
-    sdkConstraints: original.sdkConstraints,
+  return original.copyWith(
     dependencies: fixBounds(original.dependencies),
     devDependencies: fixBounds(original.devDependencies),
-    dependencyOverrides: original.dependencyOverrides.values,
-    workspace: original.workspace,
   );
 }
 
diff --git a/test/workspace_test.dart b/test/workspace_test.dart
index 7bf8638..967c737 100644
--- a/test/workspace_test.dart
+++ b/test/workspace_test.dart
@@ -478,14 +478,6 @@
   - myapp any
   - both ^1.0.0
 
-b 1.1.1
-
-dependencies:
-- myapp 1.2.3
-  - both ^1.0.0
-  - b any
-- both 1.0.0
-
 a 1.1.1
 
 dependencies:
@@ -498,6 +490,14 @@
 dev dependencies:
 - both 1.0.0
 
+b 1.1.1
+
+dependencies:
+- myapp 1.2.3
+  - both ^1.0.0
+  - b any
+- both 1.0.0
+
 transitive dependencies:
 - transitive 1.0.0''',
     );
@@ -515,14 +515,6 @@
   - myapp any
   - both ^1.0.0
 
-b 1.1.1
-
-dependencies:
-- myapp 1.2.3
-  - both ^1.0.0
-  - b any
-- both 1.0.0
-
 a 1.1.1
 
 dependencies:
@@ -532,6 +524,14 @@
 - foo 1.0.0
   - transitive ^1.0.0
 
+b 1.1.1
+
+dependencies:
+- myapp 1.2.3
+  - both ^1.0.0
+  - b any
+- both 1.0.0
+
 transitive dependencies:
 - transitive 1.0.0''',
     );
@@ -546,12 +546,6 @@
 - b 1.1.1 [myapp both]
 - both 1.0.0
 
-b 1.1.1
-
-dependencies:
-- both 1.0.0
-- myapp 1.2.3 [both b]
-
 a 1.1.1
 
 dependencies:
@@ -561,6 +555,12 @@
 dev dependencies:
 - both 1.0.0
 
+b 1.1.1
+
+dependencies:
+- both 1.0.0
+- myapp 1.2.3 [both b]
+
 transitive dependencies:
 - transitive 1.0.0''',
     );
@@ -775,4 +775,322 @@
       exitCode: DATA,
     );
   });
+
+  test('`upgrade` upgrades all workspace', () async {
+    final server = await servePackages();
+    server.serve('foo', '1.0.0');
+    server.serve('bar', '1.0.0');
+    await dir(appPath, [
+      libPubspec(
+        'myapp',
+        '1.2.3',
+        deps: {'foo': '^1.0.0'},
+        sdk: '^3.7.0',
+        extras: {
+          'workspace': ['a'],
+        },
+      ),
+      dir('a', [
+        libPubspec(
+          'a',
+          '1.0.0',
+          deps: {'bar': '^1.0.0'},
+          resolutionWorkspace: true,
+        ),
+      ]),
+    ]).create();
+    await pubGet(environment: {'_PUB_TEST_SDK_VERSION': '3.7.0'});
+    server.serve('foo', '1.5.0');
+    server.serve('bar', '1.5.0');
+    await pubUpgrade(
+      environment: {'_PUB_TEST_SDK_VERSION': '3.7.0'},
+      output: contains(
+        '''
+> bar 1.5.0 (was 1.0.0)
+> foo 1.5.0 (was 1.0.0)''',
+      ),
+    );
+  });
+
+  test('`upgrade --major-versions` upgrades all workspace', () async {
+    final server = await servePackages();
+    server.serve('foo', '1.5.0');
+    server.serve('foo', '2.0.0');
+    server.serve('bar', '1.0.0');
+    server.serve('bar', '2.0.0');
+    await dir(appPath, [
+      libPubspec(
+        'myapp',
+        '1.2.3',
+        deps: {'foo': '^1.0.0', 'bar': '1.0.0'},
+        sdk: '^3.7.0',
+        extras: {
+          'workspace': ['a'],
+        },
+      ),
+      dir('a', [
+        libPubspec(
+          'a',
+          '1.0.0',
+          deps: {'foo': '1.5.0'},
+          resolutionWorkspace: true,
+        ),
+      ]),
+    ]).create();
+
+    await pubGet(
+      environment: {'_PUB_TEST_SDK_VERSION': '3.7.0'},
+      output: contains('+ foo 1.5.0'),
+    );
+    await pubUpgrade(
+      args: ['--major-versions'],
+      environment: {'_PUB_TEST_SDK_VERSION': '3.7.0'},
+      output: contains(
+        '''
+Changed 2 constraints in pubspec.yaml:
+  foo: ^1.0.0 -> ^2.0.0
+  bar: 1.0.0 -> ^2.0.0
+
+Changed 1 constraint in a${s}pubspec.yaml:
+  foo: 1.5.0 -> ^2.0.0''',
+      ),
+    );
+
+    await dir(appPath, [
+      libPubspec(
+        'myapp',
+        '1.2.3',
+        deps: {'foo': '^2.0.0', 'bar': '^2.0.0'},
+        sdk: '^3.7.0',
+        extras: {
+          'workspace': ['a'],
+        },
+      ),
+      dir('a', [
+        libPubspec(
+          'a',
+          '1.0.0',
+          deps: {'foo': '^2.0.0'},
+          resolutionWorkspace: true,
+        ),
+      ]),
+    ]).validate();
+  });
+  test('`upgrade --major-versions foo` upgrades foo in all workspace',
+      () async {
+    final server = await servePackages();
+    server.serve('foo', '1.5.0');
+    server.serve('foo', '2.0.0');
+    server.serve('bar', '1.0.0');
+    server.serve('bar', '2.0.0');
+    await dir(appPath, [
+      libPubspec(
+        'myapp',
+        '1.2.3',
+        deps: {'foo': '^1.0.0', 'bar': '1.0.0'},
+        sdk: '^3.7.0',
+        extras: {
+          'workspace': ['a'],
+        },
+      ),
+      dir('a', [
+        libPubspec(
+          'a',
+          '1.0.0',
+          deps: {'foo': '1.5.0'},
+          resolutionWorkspace: true,
+        ),
+      ]),
+    ]).create();
+
+    await pubGet(
+      environment: {'_PUB_TEST_SDK_VERSION': '3.7.0'},
+      output: contains('+ foo 1.5.0'),
+    );
+    await pubUpgrade(
+      args: ['--major-versions', 'foo'],
+      environment: {'_PUB_TEST_SDK_VERSION': '3.7.0'},
+      output: contains(
+        '''
+Changed 1 constraint in pubspec.yaml:
+  foo: ^1.0.0 -> ^2.0.0
+
+Changed 1 constraint in a${s}pubspec.yaml:
+  foo: 1.5.0 -> ^2.0.0''',
+      ),
+    );
+    // Second run should mention "any pubspec.yaml".
+    await pubUpgrade(
+      args: ['--major-versions', 'foo'],
+      environment: {'_PUB_TEST_SDK_VERSION': '3.7.0'},
+      output: contains(
+        '''
+No changes to any pubspec.yaml!''',
+      ),
+    );
+    await pubUpgrade(
+      args: ['--major-versions', 'foo', '--dry-run'],
+      environment: {'_PUB_TEST_SDK_VERSION': '3.7.0'},
+      output: contains(
+        '''
+No changes would be made to any pubspec.yaml!''',
+      ),
+    );
+
+    await dir(appPath, [
+      libPubspec(
+        'myapp',
+        '1.2.3',
+        deps: {'foo': '^2.0.0', 'bar': '1.0.0'},
+        sdk: '^3.7.0',
+        extras: {
+          'workspace': ['a'],
+        },
+      ),
+      dir('a', [
+        libPubspec(
+          'a',
+          '1.0.0',
+          deps: {'foo': '^2.0.0'},
+          resolutionWorkspace: true,
+        ),
+      ]),
+    ]).validate();
+  });
+
+  test('`upgrade --tighten` updates all workspace', () async {
+    final server = await servePackages();
+    server.serve('foo', '1.5.0');
+    server.serve('bar', '1.5.0');
+    await dir(appPath, [
+      libPubspec(
+        'myapp',
+        '1.2.3',
+        deps: {'foo': '^1.0.0', 'bar': '^1.0.0'},
+        sdk: '^3.7.0',
+        extras: {
+          'workspace': ['a', 'b'],
+        },
+      ),
+      dir('a', [
+        libPubspec(
+          'a',
+          '1.0.0',
+          deps: {'foo': '^1.0.0'},
+          resolutionWorkspace: true,
+        ),
+      ]),
+      dir('b', [
+        libPubspec(
+          'b',
+          '1.0.0',
+          deps: {'bar': '^1.5.0'},
+          resolutionWorkspace: true,
+        ),
+      ]),
+    ]).create();
+
+    await pubGet(
+      environment: {'_PUB_TEST_SDK_VERSION': '3.7.0'},
+      output: contains('+ foo 1.5.0'),
+    );
+    await pubUpgrade(
+      args: ['--tighten'],
+      environment: {'_PUB_TEST_SDK_VERSION': '3.7.0'},
+      output: contains(
+        '''
+Changed 2 constraints in pubspec.yaml:
+  foo: ^1.0.0 -> ^1.5.0
+  bar: ^1.0.0 -> ^1.5.0
+
+Changed 1 constraint in a${s}pubspec.yaml:
+  foo: ^1.0.0 -> ^1.5.0''',
+      ),
+    );
+
+    await dir(appPath, [
+      libPubspec(
+        'myapp',
+        '1.2.3',
+        deps: {'foo': '^1.5.0', 'bar': '^1.5.0'},
+        sdk: '^3.7.0',
+        extras: {
+          'workspace': ['a', 'b'],
+        },
+      ),
+      dir('a', [
+        libPubspec(
+          'a',
+          '1.0.0',
+          deps: {'foo': '^1.5.0'},
+          resolutionWorkspace: true,
+        ),
+      ]),
+      dir('b', [
+        libPubspec(
+          'b',
+          '1.0.0',
+          deps: {'bar': '^1.5.0'},
+          resolutionWorkspace: true,
+        ),
+      ]),
+    ]).validate();
+  });
+
+  test('`upgrade --major-versions --tighten` updates all workspace', () async {
+    final server = await servePackages();
+    server.serve('foo', '1.5.0');
+    server.serve('bar', '1.5.0');
+    server.serve('foo', '2.0.0');
+    await dir(appPath, [
+      libPubspec(
+        'myapp',
+        '1.2.3',
+        deps: {'foo': '^1.0.0', 'bar': '^1.0.0'},
+        sdk: '^3.7.0',
+        extras: {
+          'workspace': ['a', 'b'],
+        },
+      ),
+      dir('a', [
+        libPubspec(
+          'a',
+          '1.0.0',
+          deps: {'foo': '^1.0.0'},
+          resolutionWorkspace: true,
+        ),
+      ]),
+      dir('b', [
+        libPubspec(
+          'b',
+          '1.0.0',
+          deps: {'bar': '^1.0.0'},
+          resolutionWorkspace: true,
+        ),
+      ]),
+    ]).create();
+
+    await pubGet(
+      environment: {'_PUB_TEST_SDK_VERSION': '3.7.0'},
+      output: contains('+ foo 1.5.0'),
+    );
+    await pubUpgrade(
+      args: ['--tighten', '--major-versions'],
+      environment: {'_PUB_TEST_SDK_VERSION': '3.7.0'},
+      output: contains(
+        '''
+Changed 2 constraints in pubspec.yaml:
+  foo: ^1.0.0 -> ^2.0.0
+  bar: ^1.0.0 -> ^1.5.0
+
+Changed 1 constraint in a${s}pubspec.yaml:
+  foo: ^1.0.0 -> ^2.0.0
+
+Changed 1 constraint in b${s}pubspec.yaml:
+  bar: ^1.0.0 -> ^1.5.0''',
+      ),
+    );
+  });
 }
+
+final s = p.separator;