downgrade --tighten (#4245)
diff --git a/lib/src/command/downgrade.dart b/lib/src/command/downgrade.dart
index 6055d5f..acd40c8 100644
--- a/lib/src/command/downgrade.dart
+++ b/lib/src/command/downgrade.dart
@@ -5,10 +5,10 @@
import 'dart:async';
import '../command.dart';
+import '../command_runner.dart';
import '../log.dart' as log;
import '../solver.dart';
-/// Handles the `downgrade` pub command.
class DowngradeCommand extends PubCommand {
@override
String get name => 'downgrade';
@@ -23,6 +23,12 @@
@override
bool get isOffline => argResults.flag('offline');
+ bool get _dryRun => argResults.flag('dry-run');
+
+ bool get _tighten => argResults.flag('tighten');
+
+ bool get _example => argResults.flag('example');
+
DowngradeCommand() {
argParser.addFlag(
'offline',
@@ -51,6 +57,13 @@
help: 'Run this in the directory <dir>.',
valueHelp: 'dir',
);
+
+ argParser.addFlag(
+ 'tighten',
+ help:
+ 'Updates lower bounds in pubspec.yaml to match the resolved version.',
+ negatable: false,
+ );
}
@override
@@ -62,23 +75,32 @@
),
);
}
- var dryRun = argResults.flag('dry-run');
await entrypoint.acquireDependencies(
SolveType.downgrade,
unlock: argResults.rest,
- dryRun: dryRun,
+ dryRun: _dryRun,
);
var example = entrypoint.example;
if (argResults.flag('example') && example != null) {
await example.acquireDependencies(
SolveType.get,
unlock: argResults.rest,
- dryRun: dryRun,
+ dryRun: _dryRun,
summaryOnly: true,
);
}
+ if (_tighten) {
+ if (_example && entrypoint.example != null) {
+ log.warning(
+ 'Running `downgrade --tighten` only in `${entrypoint.workspaceRoot.dir}`. Run `$topLevelProgram pub upgrade --tighten --directory example/` separately.',
+ );
+ }
+ final changes = entrypoint.tighten();
+ entrypoint.applyChanges(changes, _dryRun);
+ }
+
if (isOffline) {
log.warning('Warning: Downgrading when offline may not update you to '
'the oldest versions of your dependencies.');
diff --git a/lib/src/command/upgrade.dart b/lib/src/command/upgrade.dart
index 3c40b67..1864aa2 100644
--- a/lib/src/command/upgrade.dart
+++ b/lib/src/command/upgrade.dart
@@ -5,7 +5,6 @@
import 'dart:async';
import 'package:pub_semver/pub_semver.dart';
-import 'package:yaml_edit/yaml_edit.dart';
import '../command.dart';
import '../command_runner.dart';
@@ -16,7 +15,6 @@
import '../package_name.dart';
import '../pubspec.dart';
import '../pubspec_utils.dart';
-import '../sdk.dart';
import '../solver.dart';
import '../source/hosted.dart';
import '../utils.dart';
@@ -137,25 +135,14 @@
} else {
await _runUpgrade(entrypoint);
if (_tighten) {
- final changes = tighten(
- entrypoint,
- entrypoint.lockFile.packages.values.toList(),
- );
- if (!_dryRun) {
- 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(package.pubspecPath, newPubspecText);
- }
- }
+ if (argResults.flag('example') && entrypoint.example != null) {
+ log.warning(
+ 'Running `upgrade --tighten` only in `${entrypoint.workspaceRoot.dir}`. Run `$topLevelProgram pub upgrade --tighten --directory example/` separately.',
+ );
}
- _outputChangeSummary(changes);
+ final changes =
+ entrypoint.tighten(packagesToUpgrade: _packagesToUpgrade);
+ entrypoint.applyChanges(changes, _dryRun);
}
}
if (argResults.flag('example') && entrypoint.example != null) {
@@ -178,75 +165,6 @@
_showOfflineWarning();
}
- /// Returns a list of changes to constraints in [pubspec] updated them to
- /// have their lower bound match the version in [packages].
- ///
- /// The return value is a mapping from the original package range to the updated.
- ///
- /// If packages to update where given in [_packagesToUpgrade], only those are
- /// tightened. Otherwise all packages are tightened.
- ///
- /// 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<Package, Map<PackageRange, PackageRange>> tighten(
- Entrypoint entrypoint,
- List<PackageId> packages, {
- Map<Package, Map<PackageRange, PackageRange>> existingChanges = const {},
- }) {
- final result = {...existingChanges};
- if (argResults.flag('example') && entrypoint.example != null) {
- log.warning(
- 'Running `upgrade --tighten` only in `${entrypoint.workspaceRoot.dir}`. Run `$topLevelProgram pub upgrade --tighten --directory example/` separately.',
- );
- }
-
- 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) {
- changesForPackage[range] = range
- .toRef()
- .withConstraint(VersionConstraint.compatibleWith(resolvedVersion));
- } else if (constraint is VersionRange) {
- final min = constraint.min;
- if (min != null && min < resolvedVersion) {
- changesForPackage[range] = range.toRef().withConstraint(
- VersionRange(
- min: resolvedVersion,
- max: constraint.max,
- includeMin: true,
- includeMax: constraint.includeMax,
- ).asCompatibleWithIfPossible(),
- );
- }
- }
- }
- return result;
- }
-
/// Return names of packages to be upgraded, and throws [UsageException] if
/// any package names not in the direct dependencies or dev_dependencies are given.
///
@@ -346,10 +264,10 @@
return applyChanges(package.pubspec, changes[package] ?? {});
}),
);
- changes = tighten(
- entrypoint,
- solveResult.packages,
+ changes = entrypoint.tighten(
+ packagesToUpgrade: _packagesToUpgrade,
existingChanges: changes,
+ packageVersions: solveResult.packages,
);
}
@@ -361,15 +279,7 @@
final solveType =
_packagesToUpgrade.isEmpty ? SolveType.upgrade : SolveType.get;
- if (!_dryRun) {
- for (final package in entrypoint.workspaceRoot.transitiveWorkspace) {
- final changesForPackage = changes[package] ?? {};
- if (changesForPackage.isNotEmpty) {
- final newPubspecText = _updatePubspecText(package, changesForPackage);
- writeTextFile(package.pubspecPath, newPubspecText);
- }
- }
- }
+ entrypoint.applyChanges(changes, _dryRun);
await entrypoint.withUpdatedRootPubspecs({
for (final MapEntry(key: package, value: changesForPackage)
in changes.entries)
@@ -380,8 +290,6 @@
precompile: !_dryRun && _precompile,
);
- _outputChangeSummary(changes);
-
// If any of the packages to upgrade are dependency overrides, then we
// show a warning.
final toUpgradeOverrides = toUpgrade
@@ -416,65 +324,6 @@
);
}
- /// 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(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, package),
- );
- }
- return yamlEditor.toString();
- }
-
- /// Outputs a summary of changes made to `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 {
- 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}');
- });
- }
- }
- }
-
void _showOfflineWarning() {
if (isOffline) {
log.warning('Warning: Upgrading when offline may not update you to the '
diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart
index 60515f4..557c2b0 100644
--- a/lib/src/entrypoint.dart
+++ b/lib/src/entrypoint.dart
@@ -13,6 +13,7 @@
import 'package:pub_semver/pub_semver.dart';
import 'package:source_span/source_span.dart';
import 'package:yaml/yaml.dart';
+import 'package:yaml_edit/yaml_edit.dart';
import 'command_runner.dart';
import 'dart.dart' as dart;
@@ -27,12 +28,14 @@
import 'package_graph.dart';
import 'package_name.dart';
import 'pubspec.dart';
+import 'pubspec_utils.dart';
import 'sdk.dart';
import 'sdk/flutter.dart';
import 'solver.dart';
import 'solver/report.dart';
import 'solver/solve_suggestions.dart';
import 'source/cached.dart';
+import 'source/hosted.dart';
import 'source/root.dart';
import 'source/unknown.dart';
import 'system_cache.dart';
@@ -1244,4 +1247,139 @@
}
}
}
+
+ /// Returns a list of changes to constraints of workspace pubspecs updated to
+ /// have their lower bound match the version in [packageVersions] (or
+ /// `this.lockFile`).
+ ///
+ /// The return value for each workspace package is a mapping from the original
+ /// package range to the updated.
+ ///
+ /// If packages to update where given in [packagesToUpgrade], only those are
+ /// tightened. Otherwise all packages are tightened.
+ ///
+ /// 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<Package, Map<PackageRange, PackageRange>> tighten({
+ List<String> packagesToUpgrade = const [],
+ Map<Package, Map<PackageRange, PackageRange>> existingChanges = const {},
+ List<PackageId>? packageVersions,
+ }) {
+ final result = {...existingChanges};
+
+ final toTighten = <(Package, PackageRange)>[];
+
+ for (final package in 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 =
+ (packageVersions?.firstWhere((p) => p.name == range.name) ??
+ lockFile.packages[range.name])!
+ .version;
+ if (range.source is HostedSource && constraint.isAny) {
+ changesForPackage[range] = range
+ .toRef()
+ .withConstraint(VersionConstraint.compatibleWith(resolvedVersion));
+ } else if (constraint is VersionRange) {
+ final min = constraint.min;
+ if (min != null && min < resolvedVersion) {
+ changesForPackage[range] = range.toRef().withConstraint(
+ VersionRange(
+ min: resolvedVersion,
+ max: constraint.max,
+ includeMin: true,
+ includeMax: constraint.includeMax,
+ ).asCompatibleWithIfPossible(),
+ );
+ }
+ }
+ }
+ return result;
+ }
+
+ /// Unless [dryRun], loads `pubspec.yaml` of each [package] in [changeSet] and applies the
+ /// changes to its (dev)-dependencies using yaml_edit to preserve textual structure.
+ ///
+ /// Outputs a summary of changes done or would have been done if not [dryRun].
+ void applyChanges(ChangeSet changeSet, bool dryRun) {
+ if (!dryRun) {
+ for (final package in workspaceRoot.transitiveWorkspace) {
+ final changesForPackage = changeSet[package];
+ if (changesForPackage == null || changesForPackage.isEmpty) {
+ continue;
+ }
+ final yamlEditor = YamlEditor(readTextFile(package.pubspecPath));
+ final deps = package.dependencies.keys;
+
+ for (final change in changesForPackage.values) {
+ final section =
+ deps.contains(change.name) ? 'dependencies' : 'dev_dependencies';
+ yamlEditor.update(
+ [section, change.name],
+ pubspecDescription(change, cache, package),
+ );
+ }
+ writeTextFile(package.pubspecPath, yamlEditor.toString());
+ }
+ }
+ _outputChangeSummary(changeSet, dryRun: dryRun);
+ }
+
+ /// Outputs a summary of [changeSet].
+ void _outputChangeSummary(
+ ChangeSet changeSet, {
+ required bool dryRun,
+ }) {
+ if (workspaceRoot.workspaceChildren.isEmpty) {
+ final changesToWorkspaceRoot = changeSet[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 {
+ if (changeSet.isEmpty) {
+ final wouldBe = dryRun ? 'would be made to' : 'to';
+ log.message('\nNo changes $wouldBe any pubspec.yaml!');
+ }
+ for (final package in workspaceRoot.transitiveWorkspace) {
+ final changesToPackage = changeSet[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}');
+ });
+ }
+ }
+ }
}
+
+/// For each package in a workspace, a set of changes to dependencies.
+typedef ChangeSet = Map<Package, Map<PackageRange, PackageRange>>;
diff --git a/test/downgrade/tighten_test.dart b/test/downgrade/tighten_test.dart
new file mode 100644
index 0000000..7505b83
--- /dev/null
+++ b/test/downgrade/tighten_test.dart
@@ -0,0 +1,33 @@
+// Copyright (c) 2024, 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() {
+ test('--tighten will set lower bounds to the actually achieved version',
+ () async {
+ await servePackages()
+ ..serve(
+ 'foo',
+ '1.0.0',
+ ) // Because of the bar constraint, this is not achievable.
+ ..serve('foo', '2.0.0')
+ ..serve('foo', '3.0.0')
+ ..serve('bar', '1.0.0', deps: {'foo': '>=2.0.0'});
+
+ await d.appDir(dependencies: {'foo': '>=1.0.0', 'bar': '^1.0.0'}).create();
+
+ await pubGet(output: contains('foo 3.0.0'));
+ await pubDowngrade(
+ args: ['--tighten'],
+ output: allOf(
+ contains('< foo 2.0.0 (was 3.0.0)'),
+ contains('foo: >=1.0.0 -> >=2.0.0'),
+ ),
+ );
+ });
+}
diff --git a/test/testdata/goldens/help_test/pub downgrade --help.txt b/test/testdata/goldens/help_test/pub downgrade --help.txt
index 483e764..a971395 100644
--- a/test/testdata/goldens/help_test/pub downgrade --help.txt
+++ b/test/testdata/goldens/help_test/pub downgrade --help.txt
@@ -11,6 +11,7 @@
--[no-]offline Use cached packages instead of accessing the network.
-n, --dry-run Report what dependencies would change but don't change any.
-C, --directory=<dir> Run this in the directory <dir>.
+ --tighten Updates lower bounds in pubspec.yaml to match the resolved version.
Run "pub help" to see global options.
See https://dart.dev/tools/pub/cmd/pub-downgrade for detailed documentation.
diff --git a/test/testdata/goldens/upgrade/example_warns_about_major_versions_test/pub upgrade --major-versions does not update major versions in example~.txt b/test/testdata/goldens/upgrade/example_warns_about_major_versions_test/pub upgrade --major-versions does not update major versions in example~.txt
index b5ea61a..0fb9eb7 100644
--- a/test/testdata/goldens/upgrade/example_warns_about_major_versions_test/pub upgrade --major-versions does not update major versions in example~.txt
+++ b/test/testdata/goldens/upgrade/example_warns_about_major_versions_test/pub upgrade --major-versions does not update major versions in example~.txt
@@ -2,13 +2,13 @@
## Section 0
$ pub upgrade --major-versions --example
+
+Changed 1 constraint in pubspec.yaml:
+ bar: ^1.0.0 -> ^2.0.0
Resolving dependencies...
Downloading packages...
+ bar 2.0.0
Changed 1 dependency!
-
-Changed 1 constraint in pubspec.yaml:
- bar: ^1.0.0 -> ^2.0.0
Resolving dependencies in `./example`...
Downloading packages...
Got dependencies in `./example`.
@@ -18,11 +18,11 @@
## Section 1
$ pub upgrade --major-versions --directory example
+
+Changed 1 constraint in pubspec.yaml:
+ foo: ^1.0.0 -> ^2.0.0
Resolving dependencies in `example`...
Downloading packages...
> foo 2.0.0 (was 1.0.0)
Changed 1 dependency in `example`!
-Changed 1 constraint in pubspec.yaml:
- foo: ^1.0.0 -> ^2.0.0
-