pub upgrade foo, only unlocks foo from pubspec.lock (#2781)

* Fixed #2630 -- pub upgrade foo, just unlocks foo from `pubspec.lock`

* Fixed tests

* Update lib/src/solver.dart

Co-authored-by: Sigurd Meldgaard <sigurdm@google.com>

* Update lib/src/solver.dart

Co-authored-by: Sigurd Meldgaard <sigurdm@google.com>

* Added test for #2629

Co-authored-by: Sigurd Meldgaard <sigurdm@google.com>
diff --git a/lib/src/command/downgrade.dart b/lib/src/command/downgrade.dart
index 667ff61..1a116b9 100644
--- a/lib/src/command/downgrade.dart
+++ b/lib/src/command/downgrade.dart
@@ -43,8 +43,11 @@
           'The --packages-dir flag is no longer used and does nothing.'));
     }
     var dryRun = argResults['dry-run'];
-    await entrypoint.acquireDependencies(SolveType.DOWNGRADE,
-        useLatest: argResults.rest, dryRun: dryRun);
+    await entrypoint.acquireDependencies(
+      SolveType.DOWNGRADE,
+      unlock: argResults.rest,
+      dryRun: dryRun,
+    );
 
     if (isOffline) {
       log.warning('Warning: Downgrading when offline may not update you to '
diff --git a/lib/src/command/outdated.dart b/lib/src/command/outdated.dart
index b0b5f61..98248d1 100644
--- a/lib/src/command/outdated.dart
+++ b/lib/src/command/outdated.dart
@@ -374,7 +374,10 @@
 /// Try to solve [pubspec] return [PackageId]s in the resolution or `[]`.
 Future<List<PackageId>> _tryResolve(Pubspec pubspec, SystemCache cache) async {
   final solveResult = await tryResolveVersions(
-      SolveType.UPGRADE, cache, Package.inMemory(pubspec));
+    SolveType.UPGRADE,
+    cache,
+    Package.inMemory(pubspec),
+  );
   if (solveResult == null) {
     return [];
   }
diff --git a/lib/src/command/upgrade.dart b/lib/src/command/upgrade.dart
index 994cd7e..e51124f 100644
--- a/lib/src/command/upgrade.dart
+++ b/lib/src/command/upgrade.dart
@@ -75,10 +75,12 @@
   }
 
   Future<void> _runUpgrade() async {
-    await entrypoint.acquireDependencies(SolveType.UPGRADE,
-        useLatest: argResults.rest,
-        dryRun: _dryRun,
-        precompile: argResults['precompile']);
+    await entrypoint.acquireDependencies(
+      SolveType.UPGRADE,
+      unlock: argResults.rest,
+      dryRun: _dryRun,
+      precompile: argResults['precompile'],
+    );
 
     _showOfflineWarning();
   }
diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart
index a7b3fa2..4da50ac 100644
--- a/lib/src/entrypoint.dart
+++ b/lib/src/entrypoint.dart
@@ -224,7 +224,7 @@
   /// Updates [lockFile] and [packageRoot] accordingly.
   Future<void> acquireDependencies(
     SolveType type, {
-    List<String> useLatest,
+    Iterable<String> unlock,
     bool dryRun = false,
     bool precompile = false,
   }) async {
@@ -238,7 +238,7 @@
         cache,
         root,
         lockFile: lockFile,
-        useLatest: useLatest,
+        unlock: unlock,
       ),
     );
 
diff --git a/lib/src/solver.dart b/lib/src/solver.dart
index e5ddf55..18b4174 100644
--- a/lib/src/solver.dart
+++ b/lib/src/solver.dart
@@ -21,20 +21,26 @@
 /// that those dependencies place on each other and the requirements imposed by
 /// [lockFile].
 ///
-/// If [useLatest] is given, then only the latest versions of the referenced
-/// packages will be used. This is for forcing an upgrade to one or more
-/// packages.
+/// If [unlock] is given, then only packages listed in [unlock] will be unlocked
+/// from [lockFile]. This is useful for a upgrading specific packages only.
 ///
-/// If [upgradeAll] is true, the contents of [lockFile] are ignored.
+/// If [unlock] is empty [SolveType.GET] interprets this as lock everything,
+/// while [SolveType.UPGRADE] and [SolveType.DOWNGRADE] interprets an empty
+/// [unlock] as unlock everything.
 Future<SolveResult> resolveVersions(
-    SolveType type, SystemCache cache, Package root,
-    {LockFile lockFile, Iterable<String> useLatest}) {
+  SolveType type,
+  SystemCache cache,
+  Package root, {
+  LockFile lockFile,
+  Iterable<String> unlock,
+}) {
+  lockFile ??= LockFile.empty();
   return VersionSolver(
     type,
     cache,
     root,
-    lockFile ?? LockFile.empty(),
-    useLatest ?? const [],
+    lockFile,
+    unlock ?? [],
   ).solve();
 }
 
@@ -46,17 +52,27 @@
 /// Like [resolveVersions] except that this function returns `null` where a
 /// similar call to [resolveVersions] would throw a [SolveFailure].
 ///
-/// If [useLatest] is given, then only the latest versions of the referenced
-/// packages will be used. This is for forcing an upgrade to one or more
-/// packages.
+/// If [unlock] is given, only packages listed in [unlock] will be unlocked
+/// from [lockFile]. This is useful for a upgrading specific packages only.
 ///
-/// If [upgradeAll] is true, the contents of [lockFile] are ignored.
+/// If [unlock] is empty [SolveType.GET] interprets this as lock everything,
+/// while [SolveType.UPGRADE] and [SolveType.DOWNGRADE] interprets an empty
+/// [unlock] as unlock everything.
 Future<SolveResult> tryResolveVersions(
-    SolveType type, SystemCache cache, Package root,
-    {LockFile lockFile, Iterable<String> useLatest}) async {
+  SolveType type,
+  SystemCache cache,
+  Package root, {
+  LockFile lockFile,
+  Iterable<String> unlock,
+}) async {
   try {
-    return await resolveVersions(type, cache, root,
-        lockFile: lockFile, useLatest: useLatest);
+    return await resolveVersions(
+      type,
+      cache,
+      root,
+      lockFile: lockFile,
+      unlock: unlock,
+    );
   } on SolveFailure {
     return null;
   }
diff --git a/lib/src/solver/version_solver.dart b/lib/src/solver/version_solver.dart
index fcc7bbe..67d4fe0 100644
--- a/lib/src/solver/version_solver.dart
+++ b/lib/src/solver/version_solver.dart
@@ -69,18 +69,13 @@
   /// which other packages' constraints should be ignored.
   final Set<String> _overriddenPackages;
 
-  /// The set of packages for which the lockfile should be ignored and only the
-  /// most recent versions should be used.
-  final Set<String> _useLatest;
-
-  /// The set of packages for which we've added an incompatibility that forces
-  /// the latest version to be used.
-  final _haveUsedLatest = <PackageRef>{};
+  /// The set of packages for which the lockfile should be ignored.
+  final Set<String> _unlock;
 
   VersionSolver(this._type, this._systemCache, this._root, this._lockFile,
-      Iterable<String> useLatest)
+      Iterable<String> unlock)
       : _overriddenPackages = MapKeySet(_root.pubspec.dependencyOverrides),
-        _useLatest = Set.from(useLatest);
+        _unlock = {...unlock};
 
   /// Finds a set of dependencies that match the root package's constraints, or
   /// throws an error if no such set is available.
@@ -324,22 +319,6 @@
     // If we require a package from an unknown source, add an incompatibility
     // that will force a conflict for that package.
     for (var candidate in unsatisfied) {
-      if (_useLatest.contains(candidate.name) &&
-          candidate.source.hasMultipleVersions) {
-        var ref = candidate.toRef();
-        if (_haveUsedLatest.add(ref)) {
-          // All versions of [ref] other than the latest are forbidden.
-          var latestVersion = (await _packageLister(ref).latest).version;
-          _addIncompatibility(Incompatibility([
-            Term(
-                ref.withConstraint(
-                    VersionConstraint.any.difference(latestVersion)),
-                true),
-          ], IncompatibilityCause.useLatest));
-          return candidate.name;
-        }
-      }
-
       if (candidate.source is! UnknownSource) continue;
       _addIncompatibility(Incompatibility(
           [Term(candidate.withConstraint(VersionConstraint.any), true)],
@@ -350,9 +329,6 @@
     /// Prefer packages with as few remaining versions as possible, so that if a
     /// conflict is necessary it's forced quickly.
     var package = await minByAsync(unsatisfied, (package) async {
-      // If we're forced to use the latest version of a package, it effectively
-      // only has one version to choose from.
-      if (_useLatest.contains(package.name)) return 1;
       return await _packageLister(package).countVersions(package.constraint);
     });
 
@@ -490,7 +466,12 @@
   ///
   /// Returns `null` if it isn't in the lockfile (or has been unlocked).
   PackageId _getLocked(String package) {
-    if (_type == SolveType.GET) return _lockFile.packages[package];
+    if (_type == SolveType.GET) {
+      if (_unlock.contains(package)) {
+        return null;
+      }
+      return _lockFile.packages[package];
+    }
 
     // When downgrading, we don't want to force the latest versions of
     // non-hosted packages, since they don't support multiple versions and thus
@@ -500,7 +481,7 @@
       if (locked != null && !locked.source.hasMultipleVersions) return locked;
     }
 
-    if (_useLatest.isEmpty || _useLatest.contains(package)) return null;
+    if (_unlock.isEmpty || _unlock.contains(package)) return null;
     return _lockFile.packages[package];
   }
 
diff --git a/test/downgrade/unlock_dependers_test.dart b/test/downgrade/unlock_dependers_test.dart
deleted file mode 100644
index 8f785a6..0000000
--- a/test/downgrade/unlock_dependers_test.dart
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright (c) 2012, 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(
-      "downgrades a locked package's dependers in order to get it to "
-      'min version', () async {
-    await servePackages((builder) {
-      builder.serve('foo', '2.0.0', deps: {'bar': '>1.0.0'});
-      builder.serve('bar', '2.0.0');
-    });
-
-    await d.appDir({'foo': 'any', 'bar': 'any'}).create();
-
-    await pubGet();
-
-    await d.appPackagesFile({'foo': '2.0.0', 'bar': '2.0.0'}).validate();
-
-    globalPackageServer.add((builder) {
-      builder.serve('foo', '1.0.0', deps: {'bar': 'any'});
-      builder.serve('bar', '1.0.0');
-    });
-
-    await pubDowngrade(args: ['bar']);
-
-    await d.appPackagesFile({'foo': '1.0.0', 'bar': '1.0.0'}).validate();
-  });
-}
diff --git a/test/downgrade/unlock_single_package_test.dart b/test/downgrade/unlock_single_package_test.dart
new file mode 100644
index 0000000..e381f37
--- /dev/null
+++ b/test/downgrade/unlock_single_package_test.dart
@@ -0,0 +1,59 @@
+// 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() {
+  test('can unlock a single package only in downgrade', () async {
+    await servePackages((builder) {
+      builder.serve('foo', '2.1.0', deps: {'bar': '>1.0.0'});
+      builder.serve('bar', '2.1.0');
+    });
+
+    await d.appDir({'foo': 'any', 'bar': 'any'}).create();
+
+    await pubGet();
+    await d.appPackagesFile({'foo': '2.1.0', 'bar': '2.1.0'}).validate();
+
+    globalPackageServer.add((builder) {
+      builder.serve('foo', '1.0.0', deps: {'bar': 'any'});
+      builder.serve('bar', '1.0.0');
+    });
+
+    await pubDowngrade(args: ['bar']);
+    await d.appPackagesFile({'foo': '2.1.0', 'bar': '2.1.0'}).validate();
+
+    globalPackageServer.add((builder) {
+      builder.serve('foo', '2.0.0', deps: {'bar': 'any'});
+      builder.serve('bar', '2.0.0');
+    });
+
+    await pubDowngrade(args: ['bar']);
+    await d.appPackagesFile({'foo': '2.1.0', 'bar': '2.0.0'}).validate();
+
+    await pubDowngrade();
+    await d.appPackagesFile({'foo': '1.0.0', 'bar': '1.0.0'}).validate();
+  });
+
+  test('will not downgrade below constraint #2629', () async {
+    await servePackages((builder) {
+      builder.serve('foo', '1.0.0');
+      builder.serve('foo', '2.0.0');
+      builder.serve('foo', '2.1.0');
+    });
+
+    await d.appDir({'foo': '^2.0.0'}).create();
+
+    await pubGet();
+
+    await d.appPackagesFile({'foo': '2.1.0'}).validate();
+
+    await pubDowngrade(args: ['foo']);
+
+    await d.appPackagesFile({'foo': '2.0.0'}).validate();
+  });
+}
diff --git a/test/upgrade/hosted/unlock_dependers_test.dart b/test/upgrade/hosted/unlock_dependers_test.dart
deleted file mode 100644
index 2674505..0000000
--- a/test/upgrade/hosted/unlock_dependers_test.dart
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright (c) 2012, 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(
-      "upgrades a locked package's dependers in order to get it to max "
-      'version', () async {
-    await servePackages((builder) {
-      builder.serve('foo', '1.0.0', deps: {'bar': '<2.0.0'});
-      builder.serve('bar', '1.0.0');
-    });
-
-    await d.appDir({'foo': 'any', 'bar': 'any'}).create();
-
-    await pubGet();
-
-    await d.appPackagesFile({'foo': '1.0.0', 'bar': '1.0.0'}).validate();
-
-    globalPackageServer.add((builder) {
-      builder.serve('foo', '2.0.0', deps: {'bar': '<3.0.0'});
-      builder.serve('bar', '2.0.0');
-    });
-
-    await pubUpgrade(args: ['bar']);
-
-    await d.appPackagesFile({'foo': '2.0.0', 'bar': '2.0.0'}).validate();
-  });
-}
diff --git a/test/upgrade/hosted/unlock_single_package_test.dart b/test/upgrade/hosted/unlock_single_package_test.dart
new file mode 100644
index 0000000..ed2b678
--- /dev/null
+++ b/test/upgrade/hosted/unlock_single_package_test.dart
@@ -0,0 +1,45 @@
+// 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() {
+  test('can unlock a single package only in upgrade', () async {
+    await servePackages((builder) {
+      builder.serve('foo', '1.0.0', deps: {'bar': '<2.0.0'});
+      builder.serve('bar', '1.0.0');
+    });
+
+    await d.appDir({'foo': 'any', 'bar': 'any'}).create();
+
+    await pubGet();
+
+    await d.appPackagesFile({'foo': '1.0.0', 'bar': '1.0.0'}).validate();
+
+    globalPackageServer.add((builder) {
+      builder.serve('foo', '2.0.0', deps: {'bar': '<3.0.0'});
+      builder.serve('bar', '2.0.0');
+    });
+
+    // This can't upgrade 'bar'
+    await pubUpgrade(args: ['bar']);
+
+    await d.appPackagesFile({'foo': '1.0.0', 'bar': '1.0.0'}).validate();
+
+    // Introducing foo and bar 1.1.0, to show that only 'bar' will be upgraded
+    globalPackageServer.add((builder) {
+      builder.serve('foo', '1.1.0', deps: {'bar': '<2.0.0'});
+      builder.serve('bar', '1.1.0');
+    });
+
+    await pubUpgrade(args: ['bar']);
+    await d.appPackagesFile({'foo': '1.0.0', 'bar': '1.1.0'}).validate();
+
+    await pubUpgrade();
+    await d.appPackagesFile({'foo': '2.0.0', 'bar': '2.0.0'}).validate();
+  });
+}