pub upgrade --major-versions (#2784)
* Naive implementation
* Added test, works for dev_dependencies
* Changes per review comments
* Only remove upper bounds
* Function renaming
* Documentation
* Updated documentation
* Fix upgrade --breaking tests
* clean up argument parsing
* Improved dry-run output
* Fixed tests
* Removed unnecessary alias
* Update lib/src/command/upgrade.dart
Co-authored-by: Sigurd Meldgaard <sigurdm@google.com>
* Update lib/src/command/upgrade.dart
Co-authored-by: Sigurd Meldgaard <sigurdm@google.com>
* Update lib/src/command/upgrade.dart
Co-authored-by: Sigurd Meldgaard <sigurdm@google.com>
* Update lib/src/command/upgrade.dart
Co-authored-by: Sigurd Meldgaard <sigurdm@google.com>
* Update lib/src/command/upgrade.dart
Co-authored-by: Sigurd Meldgaard <sigurdm@google.com>
* Update lib/src/command/upgrade.dart
Co-authored-by: Sigurd Meldgaard <sigurdm@google.com>
* Update lib/src/command/upgrade.dart
Co-authored-by: Sigurd Meldgaard <sigurdm@google.com>
* Added warning for dependency overrides
* nit: fix comment
* Nits and tests for warning
Co-authored-by: Garett Tok <garettok@hotmail.com>
Co-authored-by: Sigurd Meldgaard <sigurdm@google.com>
diff --git a/lib/src/command/upgrade.dart b/lib/src/command/upgrade.dart
index e51124f..36713e1 100644
--- a/lib/src/command/upgrade.dart
+++ b/lib/src/command/upgrade.dart
@@ -16,6 +16,7 @@
import '../package.dart';
import '../package_name.dart';
import '../pubspec.dart';
+import '../pubspec_utils.dart';
import '../solver.dart';
import '../source/hosted.dart';
import '../yaml_edit/editor.dart';
@@ -53,6 +54,13 @@
argParser.addFlag('nullsafety', negatable: false, hide: true);
argParser.addFlag('packages-dir', hide: true);
+
+ argParser.addFlag(
+ 'major-versions',
+ help: 'Upgrades packages to their latest resolvable versions, '
+ 'and updates pubspec.yaml.',
+ negatable: false,
+ );
}
/// Avoid showing spinning progress messages when not in a terminal.
@@ -60,6 +68,13 @@
bool get _dryRun => argResults['dry-run'];
+ bool get _precompile => argResults['precompile'];
+
+ bool get _upgradeNullSafety =>
+ argResults['nullsafety'] || argResults['null-safety'];
+
+ bool get _upgradeMajorVersions => argResults['major-versions'];
+
@override
Future<void> runProtected() async {
if (argResults.wasParsed('packages-dir')) {
@@ -67,10 +82,18 @@
'The --packages-dir flag is no longer used and does nothing.'));
}
- if (argResults['nullsafety'] || argResults['null-safety']) {
+ if (_upgradeNullSafety && _upgradeMajorVersions) {
+ usageException('--major-versions and --null-safety cannot be combined');
+ }
+
+ if (_upgradeNullSafety) {
return await _runUpgradeNullSafety();
}
+ if (_upgradeMajorVersions) {
+ return await _runUpgradeMajorVersions();
+ }
+
return await _runUpgrade();
}
@@ -79,49 +102,164 @@
SolveType.UPGRADE,
unlock: argResults.rest,
dryRun: _dryRun,
- precompile: argResults['precompile'],
+ precompile: _precompile,
);
_showOfflineWarning();
}
- Future<void> _runUpgradeNullSafety() async {
+ /// 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.
+ List<String> _directDependenciesToUpgrade() {
+ assert(_upgradeNullSafety || _upgradeMajorVersions);
+
final directDeps = [
...entrypoint.root.pubspec.dependencies.keys,
...entrypoint.root.pubspec.devDependencies.keys
];
- final upgradeOnly = argResults.rest.isEmpty ? directDeps : argResults.rest;
+ final toUpgrade = argResults.rest.isEmpty ? directDeps : argResults.rest;
// Check that all package names in upgradeOnly are direct-dependencies
- if (upgradeOnly.any((name) => !directDeps.contains(name))) {
- final notDirectDeps =
- upgradeOnly.where((name) => !directDeps.contains(name)).toList();
+ final notInDeps = toUpgrade.where((n) => !directDeps.contains(n));
+ if (toUpgrade.any(notInDeps.contains)) {
+ var modeFlag = '';
+ if (_upgradeNullSafety) {
+ modeFlag = '--null-safety';
+ }
+ if (_upgradeMajorVersions) {
+ modeFlag = '--major-versions';
+ }
+
usageException('''
-Dependencies specified in `dart pub upgrade --nullsafety <dependencies>` must
+Dependencies specified in `dart pub upgrade $modeFlag <dependencies>` must
be direct 'dependencies' or 'dev_dependencies', following packages are not:
- - ${notDirectDeps.join('\n - ')}
+ - ${notInDeps.join('\n - ')}
''');
}
+ return toUpgrade;
+ }
+
+ Future<void> _runUpgradeMajorVersions() async {
+ final toUpgrade = _directDependenciesToUpgrade();
+
+ final resolvablePubspec = stripVersionUpperBounds(
+ entrypoint.root.pubspec,
+ stripOnly: toUpgrade,
+ );
+
+ // Solve [resolvablePubspec] in-memory and consolidate the resolved
+ // versions of the packages into a map for quick searching.
+ final resolvedPackages = <String, PackageId>{};
+ final solveResult = await log.spinner('Resolving dependencies', () async {
+ return await resolveVersions(
+ SolveType.UPGRADE,
+ cache,
+ Package.inMemory(resolvablePubspec),
+ );
+ }, condition: _shouldShowSpinner);
+ for (final resolvedPackage in solveResult?.packages ?? []) {
+ resolvedPackages[resolvedPackage.name] = resolvedPackage;
+ }
+
+ // Changes to be made to `pubspec.yaml`.
+ // Mapping from original to changed value.
+ final changes = <PackageRange, PackageRange>{};
+ final declaredHostedDependencies = [
+ ...entrypoint.root.pubspec.dependencies.values,
+ ...entrypoint.root.pubspec.devDependencies.values,
+ ].where((dep) => dep.source is HostedSource);
+ for (final dep in declaredHostedDependencies) {
+ final resolvedPackage = resolvedPackages[dep.name];
+ assert(resolvedPackage != null);
+ if (resolvedPackage == null || !toUpgrade.contains(dep.name)) {
+ // If we're not 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.root.dependencyOverrides.containsKey(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;
+ }
+
+ changes[dep] = dep.withConstraint(VersionConstraint.compatibleWith(
+ resolvedPackage.version,
+ ));
+ }
+
+ if (_dryRun) {
+ // Even if it is a dry run, run `acquireDependencies` so that the user
+ // gets a report on changes.
+ // TODO(jonasfj): Stop abusing Entrypoint.global for dry-run output
+ await Entrypoint.global(
+ Package.inMemory(resolvablePubspec),
+ entrypoint.lockFile,
+ cache,
+ solveResult: solveResult,
+ ).acquireDependencies(
+ SolveType.UPGRADE,
+ dryRun: true,
+ precompile: _precompile,
+ );
+ } else {
+ await _updatePubspec(changes);
+
+ // TODO: Allow Entrypoint to be created with in-memory pubspec, so that
+ // we can show the changes when not in --dry-run mode. For now we only show
+ // the changes made to pubspec.yaml in dry-run mode.
+ await Entrypoint.current(cache).acquireDependencies(
+ SolveType.UPGRADE,
+ precompile: _precompile,
+ );
+ }
+
+ _outputChangeSummary(changes);
+
+ // If any of the packages to upgrade are dependency overrides, then we
+ // show a warning.
+ final toUpgradeOverrides =
+ toUpgrade.where(entrypoint.root.dependencyOverrides.containsKey);
+ if (toUpgradeOverrides.isNotEmpty) {
+ log.warning(
+ 'Warning: dependency_overrides prevents upgrades for: '
+ '${toUpgradeOverrides.join(', ')}',
+ );
+ }
+
+ _showOfflineWarning();
+ }
+
+ Future<void> _runUpgradeNullSafety() async {
+ final toUpgrade = _directDependenciesToUpgrade();
+
final nullsafetyPubspec = await _upgradeToNullSafetyConstraints(
entrypoint.root.pubspec,
- upgradeOnly,
+ toUpgrade,
);
/// Solve [nullsafetyPubspec] in-memory and consolidate the resolved
/// versions of the packages into a map for quick searching.
final resolvedPackages = <String, PackageId>{};
- await log.spinner('Resolving dependencies', () async {
- final solveResult = await resolveVersions(
+ final solveResult = await log.spinner('Resolving dependencies', () async {
+ return await resolveVersions(
SolveType.UPGRADE,
cache,
Package.inMemory(nullsafetyPubspec),
);
- for (final resolvedPackage in solveResult?.packages ?? []) {
- resolvedPackages[resolvedPackage.name] = resolvedPackage;
- }
}, condition: _shouldShowSpinner);
+ for (final resolvedPackage in solveResult?.packages ?? []) {
+ resolvedPackages[resolvedPackage.name] = resolvedPackage;
+ }
/// Changes to be made to `pubspec.yaml`.
/// Mapping from original to changed value.
@@ -133,7 +271,7 @@
for (final dep in declaredHostedDependencies) {
final resolvedPackage = resolvedPackages[dep.name];
assert(resolvedPackage != null);
- if (resolvedPackage == null || !upgradeOnly.contains(dep.name)) {
+ if (resolvedPackage == null || !toUpgrade.contains(dep.name)) {
// If we're not to upgrade this package, or it wasn't in the
// resolution somehow, then we ignore it.
continue;
@@ -152,7 +290,21 @@
changes[dep] = dep.withConstraint(constraint);
}
- if (!_dryRun) {
+ if (_dryRun) {
+ // Even if it is a dry run, run `acquireDependencies` so that the user
+ // gets a report on changes.
+ // TODO(jonasfj): Stop abusing Entrypoint.global for dry-run output
+ await Entrypoint.global(
+ Package.inMemory(nullsafetyPubspec),
+ entrypoint.lockFile,
+ cache,
+ solveResult: solveResult,
+ ).acquireDependencies(
+ SolveType.UPGRADE,
+ dryRun: true,
+ precompile: _precompile,
+ );
+ } else {
await _updatePubspec(changes);
// TODO: Allow Entrypoint to be created with in-memory pubspec, so that
@@ -160,7 +312,7 @@
// the changes made to pubspec.yaml in dry-run mode.
await Entrypoint.current(cache).acquireDependencies(
SolveType.UPGRADE,
- precompile: argResults['precompile'],
+ precompile: _precompile,
);
}
@@ -173,6 +325,10 @@
// - root has git or path dependencies,
// - root has dependency_overrides
final nonMigratedDirectDeps = <String>[];
+ final directDeps = [
+ ...entrypoint.root.pubspec.dependencies.keys,
+ ...entrypoint.root.pubspec.devDependencies.keys
+ ];
await Future.wait(directDeps.map((name) async {
final resolvedPackage = resolvedPackages[name];
assert(resolvedPackage != null);
@@ -196,6 +352,8 @@
* Find other packages to use.
''');
}
+
+ _showOfflineWarning();
}
/// Updates `pubspec.yaml` with given [changes].
diff --git a/test/upgrade/dry_run_does_not_apply_changes_test.dart b/test/upgrade/dry_run_does_not_apply_changes_test.dart
index 5698b39..5304e5e 100644
--- a/test/upgrade/dry_run_does_not_apply_changes_test.dart
+++ b/test/upgrade/dry_run_does_not_apply_changes_test.dart
@@ -11,7 +11,7 @@
import '../test_pub.dart';
void main() {
- test('--dry-run shows report but does not apply changes', () async {
+ test('--dry-run: shows report, changes nothing', () async {
await servePackages((builder) {
builder.serve('foo', '1.0.0');
builder.serve('foo', '2.0.0');
@@ -22,25 +22,90 @@
await pubGet();
+ await d.dir(appPath, [
+ d.file('pubspec.lock', contains('1.0.0')),
+ d.dir('.dart_tool'),
+ ]).validate();
+
// Change the pubspec.
await d.appDir({'foo': 'any'}).create();
- // Also delete the "packages" directory.
- deleteEntry(path.join(d.sandbox, appPath, 'packages'));
+ // Also delete the ".dart_tool" directory.
+ deleteEntry(path.join(d.sandbox, appPath, '.dart_tool'));
// Do the dry run.
await pubUpgrade(
- args: ['--dry-run'],
- output: allOf([
- contains('> foo 2.0.0 (was 1.0.0)'),
- contains('Would change 1 dependency.')
- ]));
+ args: ['--dry-run'],
+ output: allOf([
+ contains('> foo 2.0.0 (was 1.0.0)'),
+ contains('Would change 1 dependency.'),
+ ]),
+ );
await d.dir(appPath, [
// The lockfile should be unmodified.
d.file('pubspec.lock', contains('1.0.0')),
- // The "packages" directory should not have been regenerated.
- d.nothing('packages')
+ // The ".dart_tool" directory should not have been regenerated.
+ d.nothing('.dart_tool')
+ ]).validate();
+ });
+
+ test('--dry-run --major-versions: shows report, changes nothing', () async {
+ await servePackages((builder) {
+ builder.serve('foo', '1.0.0');
+ builder.serve('foo', '2.0.0');
+ });
+
+ await d.appDir({'foo': '^1.0.0'}).create();
+
+ await pubGet();
+
+ await d.dir(appPath, [
+ d.file('pubspec.lock', contains('1.0.0')),
+ d.dir('.dart_tool'),
+ ]).validate();
+
+ // Also delete the ".dart_tool" directory.
+ deleteEntry(path.join(d.sandbox, appPath, '.dart_tool'));
+
+ // Do the dry run.
+ await pubUpgrade(
+ args: ['--dry-run', '--major-versions'],
+ output: allOf([
+ contains('Resolving dependencies...'),
+ contains('> foo 2.0.0 (was 1.0.0)'),
+ contains('Would change 1 dependency.'),
+ contains('Would change 1 constraint in pubspec.yaml:'),
+ contains('foo: ^1.0.0 -> ^2.0.0'),
+ ]),
+ );
+
+ await d.dir(appPath, [
+ // The pubspec should not be modified.
+ d.appPubspec({'foo': '^1.0.0'}),
+ // The lockfile should not be modified.
+ d.file('pubspec.lock', contains('1.0.0')),
+ // The ".dart_tool" directory should not have been regenerated.
+ d.nothing('.dart_tool')
+ ]).validate();
+
+ // Try without --dry-run
+ await pubUpgrade(
+ args: ['--major-versions'],
+ output: allOf([
+ contains('Resolving dependencies...'),
+ contains('> foo 2.0.0 (was 1.0.0)'),
+ contains('Downloading foo 2.0.0...'),
+ contains('Changed 1 dependency!'),
+ contains('Changed 1 constraint in pubspec.yaml:'),
+ contains('foo: ^1.0.0 -> ^2.0.0'),
+ ]),
+ );
+
+ await d.dir(appPath, [
+ d.appPubspec({'foo': '^2.0.0'}),
+ d.file('pubspec.lock', contains('2.0.0')),
+ d.dir('.dart_tool')
]).validate();
});
}
diff --git a/test/upgrade/upgrade_major_versions_test.dart b/test/upgrade/upgrade_major_versions_test.dart
new file mode 100644
index 0000000..c312717
--- /dev/null
+++ b/test/upgrade/upgrade_major_versions_test.dart
@@ -0,0 +1,264 @@
+// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:test/test.dart';
+
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+
+void main() {
+ group('pub upgrade --major-versions', () {
+ test('bumps dependency constraints and shows summary report', () async {
+ await servePackages((builder) {
+ builder.serve('foo', '1.0.0');
+ builder.serve('foo', '2.0.0');
+ builder.serve('bar', '0.1.0');
+ builder.serve('bar', '0.2.0');
+ builder.serve('baz', '1.0.0');
+ builder.serve('baz', '1.0.1');
+ });
+
+ await d.appDir({
+ 'foo': '^1.0.0',
+ 'bar': '^0.1.0',
+ 'baz': '^1.0.0',
+ }).create();
+
+ await pubGet();
+
+ // 2 constraints should be updated
+ await pubUpgrade(
+ args: ['--major-versions'],
+ output: allOf([
+ contains('Changed 2 constraints in pubspec.yaml:'),
+ contains('foo: ^1.0.0 -> ^2.0.0'),
+ contains('bar: ^0.1.0 -> ^0.2.0'),
+ ]),
+ );
+
+ await d.appDir({
+ 'foo': '^2.0.0',
+ 'bar': '^0.2.0',
+ 'baz': '^1.0.0',
+ }).validate();
+
+ await d.appPackagesFile({
+ 'foo': '2.0.0',
+ 'bar': '0.2.0',
+ 'baz': '1.0.1',
+ }).validate();
+ });
+
+ test('bumps dev_dependency constraints and shows summary report', () async {
+ await servePackages((builder) {
+ builder.serve('foo', '1.0.0');
+ builder.serve('foo', '2.0.0');
+ builder.serve('bar', '0.1.0');
+ builder.serve('bar', '0.2.0');
+ builder.serve('baz', '1.0.0');
+ builder.serve('baz', '1.0.1');
+ });
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dev_dependencies': {
+ 'foo': '^1.0.0',
+ 'bar': '^0.1.0',
+ 'baz': '^1.0.0',
+ }
+ }),
+ ]).create();
+
+ await pubGet();
+
+ // 2 constraints should be updated
+ await pubUpgrade(
+ args: ['--major-versions'],
+ output: allOf([
+ contains('Changed 2 constraints in pubspec.yaml:'),
+ contains('foo: ^1.0.0 -> ^2.0.0'),
+ contains('bar: ^0.1.0 -> ^0.2.0'),
+ ]),
+ );
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dev_dependencies': {
+ 'foo': '^2.0.0', // bumped
+ 'bar': '^0.2.0', // bumped
+ 'baz': '^1.0.0',
+ }
+ }),
+ ]).validate();
+
+ await d.appPackagesFile({
+ 'foo': '2.0.0',
+ 'bar': '0.2.0',
+ 'baz': '1.0.1',
+ }).validate();
+ });
+
+ test('upgrades only the selected package', () async {
+ await servePackages((builder) {
+ builder.serve('foo', '1.0.0');
+ builder.serve('foo', '2.0.0');
+ builder.serve('bar', '0.1.0');
+ builder.serve('bar', '0.2.0');
+ });
+
+ await d.appDir({
+ 'foo': '^1.0.0',
+ 'bar': '^0.1.0',
+ }).create();
+
+ await pubGet();
+
+ // 1 constraint should be updated
+ await pubUpgrade(
+ args: ['--major-versions', 'foo'],
+ output: allOf([
+ contains('Changed 1 constraint in pubspec.yaml:'),
+ contains('foo: ^1.0.0 -> ^2.0.0'),
+ ]),
+ );
+
+ await d.appDir({
+ 'foo': '^2.0.0', // bumped
+ 'bar': '^0.1.0',
+ }).validate();
+
+ await d.appPackagesFile({'foo': '2.0.0', 'bar': '0.1.0'}).validate();
+ });
+
+ test('chooses the latest version where possible', () async {
+ await servePackages((builder) {
+ builder.serve('foo', '1.0.0');
+ builder.serve('foo', '2.0.0');
+ builder.serve('foo', '3.0.0');
+ });
+
+ await d.appDir({'foo': '^1.0.0'}).create();
+
+ await pubGet();
+
+ await pubUpgrade(
+ args: ['--major-versions'],
+ output: allOf([
+ contains('Changed 1 constraint in pubspec.yaml:'),
+ contains('foo: ^1.0.0 -> ^3.0.0'),
+ ]),
+ );
+
+ await d.dir(appPath, [
+ // The pubspec file should be modified.
+ d.pubspec({
+ 'name': 'myapp',
+ 'dependencies': {'foo': '^3.0.0'}
+ }),
+ // The lockfile should be modified.
+ d.file('pubspec.lock', contains('3.0.0'))
+ ]).validate();
+
+ await d.appPackagesFile({'foo': '3.0.0'}).validate();
+ });
+
+ test('overridden dependencies - no resolution', () async {
+ await servePackages(
+ (builder) => builder
+ ..serve('foo', '1.0.0', deps: {'bar': '^2.0.0'})
+ ..serve('foo', '2.0.0', deps: {'bar': '^1.0.0'})
+ ..serve('bar', '1.0.0', deps: {'foo': '^1.0.0'})
+ ..serve('bar', '2.0.0', deps: {'foo': '^2.0.0'}),
+ );
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'version': '1.0.1',
+ 'dependencies': {
+ 'foo': 'any',
+ 'bar': 'any',
+ },
+ 'dependency_overrides': {
+ 'foo': '1.0.0',
+ 'bar': '1.0.0',
+ },
+ })
+ ]).create();
+
+ await pubGet();
+
+ await pubUpgrade(
+ args: ['--major-versions'],
+ output: contains('No changes to pubspec.yaml!'),
+ warning: allOf([
+ contains('Warning: dependency_overrides prevents upgrades for: '),
+ contains('foo'), // ordering not ensured
+ contains('bar'),
+ ]),
+ );
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'version': '1.0.1',
+ 'dependencies': {
+ 'foo': 'any',
+ 'bar': 'any',
+ },
+ 'dependency_overrides': {
+ 'foo': '1.0.0',
+ 'bar': '1.0.0',
+ },
+ })
+ ]).validate();
+
+ await d.appPackagesFile({'foo': '1.0.0', 'bar': '1.0.0'}).validate();
+ });
+
+ test('upgrade should not downgrade any versions', () async {
+ /// The version solver solves the packages with the least number of
+ /// versions remaining, so we add more 'bar' packages to force 'foo' to be
+ /// resolved first
+ await servePackages((builder) {
+ builder.serve('foo', '1.0.0');
+ builder.serve('foo', '2.0.0', pubspec: {
+ 'dependencies': {'bar': '1.0.0'}
+ });
+ builder.serve('bar', '1.0.0');
+ builder.serve('bar', '2.0.0');
+ builder.serve('bar', '3.0.0');
+ builder.serve('bar', '4.0.0');
+ });
+
+ await d.appDir({
+ 'foo': '^1.0.0',
+ 'bar': '^2.0.0',
+ }).create();
+
+ await pubGet();
+
+ // 1 constraint should be updated
+ await pubUpgrade(
+ args: ['--major-versions'],
+ output: allOf([
+ contains('Changed 1 constraint in pubspec.yaml:'),
+ contains('bar: ^2.0.0 -> ^4.0.0'),
+ ]),
+ );
+
+ await d.appDir({
+ 'foo': '^1.0.0',
+ 'bar': '^4.0.0',
+ }).validate();
+
+ await d.appPackagesFile({
+ 'foo': '1.0.0',
+ 'bar': '4.0.0',
+ }).validate();
+ });
+ });
+}