Handle non-default hosts in `upgrade --major-versions` (#3947)

diff --git a/lib/src/command/add.dart b/lib/src/command/add.dart
index b8a23cd..6286f74 100644
--- a/lib/src/command/add.dart
+++ b/lib/src/command/add.dart
@@ -20,6 +20,7 @@
 import '../package.dart';
 import '../package_name.dart';
 import '../pubspec.dart';
+import '../pubspec_utils.dart';
 import '../sdk.dart';
 import '../solver.dart';
 import '../source/git.dart';
@@ -685,23 +686,17 @@
       final ref = update.ref;
       final name = ref.name;
       final resultId = resultPackages.firstWhere((id) => id.name == name);
-      var description = ref.description;
-      final versionConstraintString =
-          constraint == null ? '^${resultId.version}' : constraint.toString();
-      late Object? pubspecInformation;
-      if (description is HostedDescription &&
-          description.url == cache.hosted.defaultUrl) {
-        pubspecInformation = versionConstraintString;
-      } else {
-        pubspecInformation = {
-          ref.source.name: ref.description.serializeForPubspec(
-            containingDir: entrypoint.rootDir,
-            languageVersion: entrypoint.root.pubspec.languageVersion,
-          ),
-          if (description is HostedDescription || constraint != null)
-            'version': versionConstraintString
-        };
-      }
+
+      Object? description = pubspecDescription(
+        ref.withConstraint(
+          constraint ??
+              (ref.source is HostedSource
+                  ? VersionConstraint.compatibleWith(resultId.version)
+                  : VersionConstraint.any),
+        ),
+        cache,
+        entrypoint,
+      );
 
       if (yamlEditor.parseAt(
             [dependencyKey],
@@ -713,14 +708,14 @@
         yamlEditor.update(
           [dependencyKey],
           wrapAsYamlNode(
-            {name: pubspecInformation},
+            {name: description},
             collectionStyle: CollectionStyle.BLOCK,
           ),
         );
       } else {
         final packagePath = [dependencyKey, name];
 
-        yamlEditor.update(packagePath, pubspecInformation);
+        yamlEditor.update(packagePath, description);
       }
 
       /// Remove the package from dev_dependencies if we are adding it to
diff --git a/lib/src/command/upgrade.dart b/lib/src/command/upgrade.dart
index bbe28ef..8cf9821 100644
--- a/lib/src/command/upgrade.dart
+++ b/lib/src/command/upgrade.dart
@@ -211,7 +211,7 @@
     for (final dep in declaredHostedDependencies) {
       final resolvedPackage = resolvedPackages[dep.name]!;
       if (!toUpgrade.contains(dep.name)) {
-        // If we're not to upgrade this package, or it wasn't in the
+        // If we're not trying to upgrade this package, or it wasn't in the
         // resolution somehow, then we ignore it.
         continue;
       }
@@ -296,25 +296,16 @@
     Map<PackageRange, PackageRange> changes,
   ) {
     ArgumentError.checkNotNull(changes, 'changes');
-
     final yamlEditor = YamlEditor(readTextFile(entrypoint.pubspecPath));
     final deps = entrypoint.root.pubspec.dependencies.keys;
-    final devDeps = entrypoint.root.pubspec.devDependencies.keys;
 
     for (final change in changes.values) {
-      if (deps.contains(change.name)) {
-        yamlEditor.update(
-          ['dependencies', change.name],
-          // TODO(jonasfj): Fix support for third-party pub servers.
-          change.constraint.toString(),
-        );
-      } else if (devDeps.contains(change.name)) {
-        yamlEditor.update(
-          ['dev_dependencies', change.name],
-          // TODO: Fix support for third-party pub servers
-          change.constraint.toString(),
-        );
-      }
+      final section =
+          deps.contains(change.name) ? 'dependencies' : 'dev_dependencies';
+      yamlEditor.update(
+        [section, change.name],
+        pubspecDescription(change, cache, entrypoint),
+      );
     }
     return yamlEditor.toString();
   }
diff --git a/lib/src/pubspec_utils.dart b/lib/src/pubspec_utils.dart
index 5edf74a..f843337 100644
--- a/lib/src/pubspec_utils.dart
+++ b/lib/src/pubspec_utils.dart
@@ -5,8 +5,11 @@
 import 'package:collection/collection.dart';
 import 'package:pub_semver/pub_semver.dart';
 
+import 'entrypoint.dart';
 import 'package_name.dart';
 import 'pubspec.dart';
+import 'source/hosted.dart';
+import 'system_cache.dart';
 
 /// Returns a new [Pubspec] without [original]'s dev_dependencies.
 Pubspec stripDevDependencies(Pubspec original) {
@@ -145,3 +148,35 @@
   /// just return an empty version constraint.
   return VersionConstraint.empty;
 }
+
+/// Returns a somewhat normalized version the description of a dependency with a
+/// version constraint (what comes after the version name in a dependencies
+/// section) as a json-style object.
+///
+/// Will use just the constraint for dependencies hosted at the default host.
+///
+/// Relative paths will be relative to [relativeEntrypoint].
+///
+/// The syntax used for hosted will depend on the language version of
+/// [relativeEntrypoint]
+Object? pubspecDescription(
+  PackageRange range,
+  SystemCache cache,
+  Entrypoint relativeEntrypoint,
+) {
+  final description = range.description;
+
+  final constraint = range.constraint;
+  if (description is HostedDescription &&
+      description.url == cache.hosted.defaultUrl) {
+    return constraint.toString();
+  } else {
+    return {
+      range.source.name: description.serializeForPubspec(
+        containingDir: relativeEntrypoint.rootDir,
+        languageVersion: relativeEntrypoint.root.pubspec.languageVersion,
+      ),
+      if (!constraint.isAny) 'version': constraint.toString()
+    };
+  }
+}
diff --git a/test/add/hosted/non_default_pub_server_test.dart b/test/add/hosted/non_default_pub_server_test.dart
index 7be0095..c2a339e 100644
--- a/test/add/hosted/non_default_pub_server_test.dart
+++ b/test/add/hosted/non_default_pub_server_test.dart
@@ -219,7 +219,7 @@
     ]).validate();
     await d.appDir(
       dependencies: {
-        'foo': {'version': 'any', 'hosted': url}
+        'foo': {'hosted': url}
       },
     ).validate();
   });
diff --git a/test/upgrade/upgrade_major_versions_test.dart b/test/upgrade/upgrade_major_versions_test.dart
index 691b221..51d7263 100644
--- a/test/upgrade/upgrade_major_versions_test.dart
+++ b/test/upgrade/upgrade_major_versions_test.dart
@@ -307,5 +307,32 @@
         ]),
       );
     });
+
+    test('works with an explicit "hosted" description:', () async {
+      await servePackages();
+      final alternativeServer = await startPackageServer();
+      alternativeServer.serve('foo', '1.0.0');
+      alternativeServer.serve('foo', '2.0.0');
+      await d.appDir(
+        dependencies: {
+          'foo': {'hosted': alternativeServer.url, 'version': '^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 -> ^2.0.0'),
+        ]),
+      );
+      await d.appDir(
+        dependencies: {
+          'foo': {'hosted': alternativeServer.url, 'version': '^2.0.0'},
+        },
+      ).validate();
+    });
   });
 }