Merge remote-tracking branch 'origin/cp_example' into merge-cp_example
diff --git a/.github/workflows/tag_from_sdk.yml b/.github/workflows/tag_from_sdk.yml
new file mode 100644
index 0000000..6b684b8
--- /dev/null
+++ b/.github/workflows/tag_from_sdk.yml
@@ -0,0 +1,22 @@
+name: Sync version tags from SDK
+on:
+  schedule:
+    # Run every day at 00:00
+    - cron: "0 0 * * *"
+
+jobs:
+  tag_from_sdk:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
+      with:
+        fetch-depth: 0 # Check out everything to be able to tag.
+    - uses: dart-lang/setup-dart@e51d8e571e22473a2ddebf0ef8a2123f0ab2c02c
+      with:
+        sdk: stable
+    - id: install
+      name: Install dependencies
+      run: dart pub get
+    - id: tags
+      name: push_missing_tags_to_origin
+      run: dart tool/create_version_tags_from_sdk.dart --create --push
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 3c302f3..1548ed9 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -24,7 +24,7 @@
       matrix:
         sdk: [dev]
     steps:
-      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
       - uses: dart-lang/setup-dart@e51d8e571e22473a2ddebf0ef8a2123f0ab2c02c
         with:
           sdk: ${{ matrix.sdk }}
@@ -52,7 +52,7 @@
         sdk: [dev]
         shard: [0, 1, 2, 3, 4, 5, 6]
     steps:
-      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
       - uses: dart-lang/setup-dart@e51d8e571e22473a2ddebf0ef8a2123f0ab2c02c
         with:
           sdk: ${{ matrix.sdk }}
diff --git a/lib/pub.dart b/lib/pub.dart
index d7b5653..12f364c 100644
--- a/lib/pub.dart
+++ b/lib/pub.dart
@@ -8,6 +8,7 @@
 import 'src/exceptions.dart';
 import 'src/http.dart';
 import 'src/pub_embeddable_command.dart';
+import 'src/source/git.dart';
 import 'src/system_cache.dart';
 
 export 'src/executable.dart'
@@ -67,3 +68,39 @@
   String message;
   ResolutionFailedException._(this.message);
 }
+
+/// Given a Git repo that contains a pub package, gets the name of the pub
+/// package.
+///
+/// Will download the repo to the system cache under the assumption that the
+/// package will be downloaded afterwards.
+///
+/// [url] points to the git repository. If it is a relative url, it is resolved
+/// as a file url relative to the path [relativeTo].
+///
+/// [ref] is the commit, tag, or branch name where the package should be looked
+/// up when fetching the name. If omitted, 'HEAD' is used.
+///
+/// [tagPattern] is a string containing `'{{version}}'` as a substring, the
+/// latest tag matching the pattern will be used for fetching the name.
+///
+/// Only one of [ref] and [tagPattern] can be used.
+///
+/// If [isOffline], only the already cached versions of the repo is used.
+Future<String> getPackageNameFromGitRepo(
+  String url, {
+  String? ref,
+  String? path,
+  String? tagPattern,
+  String? relativeTo,
+  bool isOffline = false,
+}) async {
+  return await GitSource.instance.getPackageNameFromRepo(
+    url,
+    ref,
+    path,
+    SystemCache(isOffline: isOffline),
+    relativeTo: relativeTo,
+    tagPattern: tagPattern,
+  );
+}
diff --git a/lib/src/command/add.dart b/lib/src/command/add.dart
index e6d6e54..23ba02b 100644
--- a/lib/src/command/add.dart
+++ b/lib/src/command/add.dart
@@ -167,7 +167,6 @@
       defaultsTo: true,
       help:
           'Also update dependencies in `example/` after modifying pubspec.yaml in the root package (if it exists).',
-      hide: true,
     );
   }
 
diff --git a/lib/src/command/check_resolution_up_to_date.dart b/lib/src/command/check_resolution_up_to_date.dart
new file mode 100644
index 0000000..66fff46
--- /dev/null
+++ b/lib/src/command/check_resolution_up_to_date.dart
@@ -0,0 +1,44 @@
+// Copyright (c) 2025, 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 'dart:async';
+
+import '../command.dart';
+import '../command_runner.dart';
+import '../entrypoint.dart';
+import '../log.dart' as log;
+import '../utils.dart';
+
+class CheckResolutionUpToDateCommand extends PubCommand {
+  @override
+  String get name => 'check-resolution-up-to-date';
+
+  @override
+  bool get hidden => true;
+
+  @override
+  String get description => '''
+Do a fast timestamp-based check to see resolution is up-to-date and internally
+consistent.
+
+If timestamps are correctly ordered, exit 0, and do not check the external sources for
+newer versions.
+Otherwise exit non-zero.
+''';
+
+  @override
+  String get argumentsDescription => '';
+
+  CheckResolutionUpToDateCommand();
+
+  @override
+  Future<void> runProtected() async {
+    final result = Entrypoint.isResolutionUpToDate(directory, cache);
+    if (result == null) {
+      fail('Resolution needs updating. Run `$topLevelProgram pub get`');
+    } else {
+      log.message('Resolution is up-to-date');
+    }
+  }
+}
diff --git a/lib/src/command/downgrade.dart b/lib/src/command/downgrade.dart
index cd90376..1dccee6 100644
--- a/lib/src/command/downgrade.dart
+++ b/lib/src/command/downgrade.dart
@@ -48,7 +48,6 @@
       'example',
       defaultsTo: true,
       help: 'Also run in `example/` (if it exists).',
-      hide: true,
     );
 
     argParser.addOption(
diff --git a/lib/src/command/get.dart b/lib/src/command/get.dart
index 978d6f6..9c08444 100644
--- a/lib/src/command/get.dart
+++ b/lib/src/command/get.dart
@@ -56,7 +56,6 @@
       'example',
       defaultsTo: true,
       help: 'Also run in `example/` (if it exists).',
-      hide: true,
     );
 
     argParser.addOption(
diff --git a/lib/src/command/login.dart b/lib/src/command/login.dart
index 04e765b..0a505b6 100644
--- a/lib/src/command/login.dart
+++ b/lib/src/command/login.dart
@@ -64,7 +64,9 @@
       try {
         switch (json.decode(userInfoRequest.body)) {
           case {'name': final String? name, 'email': final String email}:
-            return _UserInfo(name, email);
+            return _UserInfo(name: name, email: email);
+          case {'email': final String email}:
+            return _UserInfo(name: null, email: email);
           default:
             log.fine(
               'Bad response from $userInfoEndpoint: ${userInfoRequest.body}',
@@ -84,7 +86,7 @@
 class _UserInfo {
   final String? name;
   final String email;
-  _UserInfo(this.name, this.email);
+  _UserInfo({required this.name, required this.email});
   @override
   String toString() => ['<$email>', name ?? ''].join(' ');
 }
diff --git a/lib/src/command/remove.dart b/lib/src/command/remove.dart
index df648e9..5a68ca3 100644
--- a/lib/src/command/remove.dart
+++ b/lib/src/command/remove.dart
@@ -59,7 +59,6 @@
       'example',
       defaultsTo: true,
       help: 'Also update dependencies in `example/` (if it exists).',
-      hide: true,
     );
 
     argParser.addOption(
diff --git a/lib/src/command/upgrade.dart b/lib/src/command/upgrade.dart
index 0c25686..7078ac0 100644
--- a/lib/src/command/upgrade.dart
+++ b/lib/src/command/upgrade.dart
@@ -89,7 +89,6 @@
       'example',
       defaultsTo: true,
       help: 'Also run in `example/` (if it exists).',
-      hide: true,
     );
 
     argParser.addOption(
diff --git a/lib/src/command_runner.dart b/lib/src/command_runner.dart
index 9ced67e..9a372f1 100644
--- a/lib/src/command_runner.dart
+++ b/lib/src/command_runner.dart
@@ -13,6 +13,7 @@
 import 'command/add.dart';
 import 'command/bump.dart';
 import 'command/cache.dart';
+import 'command/check_resolution_up_to_date.dart';
 import 'command/deps.dart';
 import 'command/downgrade.dart';
 import 'command/get.dart';
@@ -151,6 +152,7 @@
     addCommand(OutdatedCommand());
     addCommand(RemoveCommand());
     addCommand(RunCommand());
+    addCommand(CheckResolutionUpToDateCommand());
     addCommand(UpgradeCommand());
     addCommand(UnpackCommand());
     addCommand(UploaderCommand());
diff --git a/lib/src/dart.dart b/lib/src/dart.dart
index 07d50eb..8592781 100644
--- a/lib/src/dart.dart
+++ b/lib/src/dart.dart
@@ -47,8 +47,8 @@
     path = p.normalize(p.absolute(path));
     final parseResult = _session.getParsedUnit(path);
     if (parseResult is ParsedUnitResult) {
-      if (parseResult.errors.isNotEmpty) {
-        throw AnalyzerErrorGroup(parseResult.errors);
+      if (parseResult.diagnostics.isNotEmpty) {
+        throw AnalyzerErrorGroup(parseResult.diagnostics);
       }
       return parseResult.unit;
     } else {
diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart
index 2eda882..62dfa78 100644
--- a/lib/src/entrypoint.dart
+++ b/lib/src/entrypoint.dart
@@ -792,23 +792,47 @@
     }
   }
 
-  /// Does a fast-pass check to see if the resolution is up-to-date. If not, run
-  /// a resolution with `pub get` semantics.
+  /// The [PackageConfig] object representing `.dart_tool/package_config.json`
+  /// along with the dir where it resides, if it and `pubspec.lock` exist and
+  /// are up to date with respect to pubspec.yaml and its dependencies. Or
+  /// `null` if it is outdated.
   ///
-  /// If [summaryOnly] is `true` (the default) only a short summary is shown of
-  /// the solve.
+  /// Always returns `null` if `.dart_tool/package_config.json` was generated
+  /// with a different PUB_CACHE location, a different $FLUTTER_ROOT or a
+  /// different Dart or Flutter SDK version.
   ///
-  /// If [onlyOutputWhenTerminal] is `true` (the default) there will be no
-  /// output if no terminal is attached.
+  /// Otherwise first the `modified` timestamps are compared, and if
+  /// `.dart_tool/package_config.json` is newer than `pubspec.lock` that is
+  /// newer than all pubspec.yamls of all packages in
+  /// `.dart_tool/package_config.json` we short-circuit and return true.
   ///
-  /// When succesfull returns the found/created `PackageConfig` and the
-  /// directory containing it.
-  static Future<({PackageConfig packageConfig, String rootDir})> ensureUpToDate(
-    String dir, {
-    required SystemCache cache,
-    bool summaryOnly = true,
-    bool onlyOutputWhenTerminal = true,
-  }) async {
+  /// If any of the timestamps are out of order, the resolution in
+  /// pubspec.lock is validated against constraints of all pubspec.yamls, and
+  /// the packages of `.dart_tool/package_config.json` is validated against
+  /// pubspec.lock. We do this extra round of checking to accommodate for cases
+  /// where version control or other processes mess up the timestamp order.
+  ///
+  /// If the resolution is still valid, the timestamps are updated and this
+  /// returns the package configuration and the root dir. Otherwise this
+  /// returns `null`.
+  ///
+  /// This check is on the fast-path of `dart run` and should do as little
+  /// work as possible. Specifically we avoid parsing any yaml when the
+  /// timestamps are in the right order.
+  ///
+  /// `.dart_tool/package_config.json` is read and parsed. In the case of `dart
+  /// run` this is acceptable: we speculate that it brings it to the file
+  /// system cache and the dart VM is going to read the file anyways.
+  ///
+  /// Note this procedure will give false positives if the timestamps are
+  /// artificially brought in the "right" order. (eg. by manually running
+  /// `touch pubspec.lock; touch .dart_tool/package_config.json`) - that is
+  /// hard to avoid, but also unlikely to happen by accident because
+  /// `.dart_tool/package_config.json` is not checked into version control.
+  static (PackageConfig, String)? isResolutionUpToDate(
+    String dir,
+    SystemCache cache,
+  ) {
     late final wasRelative = p.isRelative(dir);
     String relativeIfNeeded(String path) =>
         wasRelative ? p.relative(path) : path;
@@ -1044,271 +1068,254 @@
       return true;
     }
 
-    /// The [PackageConfig] object representing `.dart_tool/package_config.json`
-    /// along with the dir where it resides, if it and `pubspec.lock` exist and
-    /// are up to date with respect to pubspec.yaml and its dependencies. Or
-    /// `null` if it is outdated.
-    ///
-    /// Always returns `null` if `.dart_tool/package_config.json` was generated
-    /// with a different PUB_CACHE location, a different $FLUTTER_ROOT or a
-    /// different Dart or Flutter SDK version.
-    ///
-    /// Otherwise first the `modified` timestamps are compared, and if
-    /// `.dart_tool/package_config.json` is newer than `pubspec.lock` that is
-    /// newer than all pubspec.yamls of all packages in
-    /// `.dart_tool/package_config.json` we short-circuit and return true.
-    ///
-    /// If any of the timestamps are out of order, the resolution in
-    /// pubspec.lock is validated against constraints of all pubspec.yamls, and
-    /// the packages of `.dart_tool/package_config.json` is validated against
-    /// pubspec.lock. We do this extra round of checking to accomodate for cases
-    /// where version control or other processes mess up the timestamp order.
-    ///
-    /// If the resolution is still valid, the timestamps are updated and this
-    /// returns the package configuration and the root dir. Otherwise this
-    /// returns `null`.
-    ///
-    /// This check is on the fast-path of `dart run` and should do as little
-    /// work as possible. Specifically we avoid parsing any yaml when the
-    /// timestamps are in the right order.
-    ///
-    /// `.dart_tool/package_config.json` is read parsed. In the case of `dart
-    /// run` this is acceptable: we speculate that it brings it to the file
-    /// system cache and the dart VM is going to read the file anyways.
-    ///
-    /// Note this procedure will give false positives if the timestamps are
-    /// artificially brought in the "right" order. (eg. by manually running
-    /// `touch pubspec.lock; touch .dart_tool/package_config.json`) - that is
-    /// hard to avoid, but also unlikely to happen by accident because
-    /// `.dart_tool/package_config.json` is not checked into version control.
-    (PackageConfig, String)? isResolutionUpToDate() {
-      FileStat? packageConfigStat;
-      late final String packageConfigPath;
-      late final String rootDir;
-      for (final parent in parentDirs(dir)) {
-        final potentialPackageConfigPath = p.normalize(
-          p.join(parent, '.dart_tool', 'package_config.json'),
+    FileStat? packageConfigStat;
+    late final String packageConfigPath;
+    late final String rootDir;
+    for (final parent in parentDirs(dir)) {
+      final potentialPackageConfigPath = p.normalize(
+        p.join(parent, '.dart_tool', 'package_config.json'),
+      );
+      packageConfigStat = tryStatFile(potentialPackageConfigPath);
+
+      if (packageConfigStat != null) {
+        packageConfigPath = potentialPackageConfigPath;
+        rootDir = parent;
+        break;
+      }
+      final potentialPubspecPath = p.join(parent, 'pubspec.yaml');
+      if (tryStatFile(potentialPubspecPath) == null) {
+        // No package at [parent] continue to next dir.
+        continue;
+      }
+
+      final potentialWorkspaceRefPath = p.normalize(
+        p.join(parent, '.dart_tool', 'pub', 'workspace_ref.json'),
+      );
+
+      final workspaceRefText = tryReadTextFile(potentialWorkspaceRefPath);
+      if (workspaceRefText == null) {
+        log.fine(
+          '`$potentialPubspecPath` exists without corresponding '
+          '`$potentialPubspecPath` or `$potentialWorkspaceRefPath`.',
         );
-        packageConfigStat = tryStatFile(potentialPackageConfigPath);
-
-        if (packageConfigStat != null) {
-          packageConfigPath = potentialPackageConfigPath;
-          rootDir = parent;
-          break;
-        }
-        final potentialPubspecPath = p.join(parent, 'pubspec.yaml');
-        if (tryStatFile(potentialPubspecPath) == null) {
-          // No package at [parent] continue to next dir.
-          continue;
-        }
-
-        final potentialWorkspaceRefPath = p.normalize(
-          p.join(parent, '.dart_tool', 'pub', 'workspace_ref.json'),
-        );
-
-        final workspaceRefText = tryReadTextFile(potentialWorkspaceRefPath);
-        if (workspaceRefText == null) {
-          log.fine(
-            '`$potentialPubspecPath` exists without corresponding '
-            '`$potentialPubspecPath` or `$potentialWorkspaceRefPath`.',
-          );
-          return null;
-        } else {
-          try {
-            if (jsonDecode(workspaceRefText) case {
-              'workspaceRoot': final String path,
-            }) {
-              final potentialPackageConfigPath2 = relativeIfNeeded(
+        return null;
+      } else {
+        try {
+          if (jsonDecode(workspaceRefText) case {
+            'workspaceRoot': final String path,
+          }) {
+            final potentialPackageConfigPath2 = relativeIfNeeded(
+              p.normalize(
+                p.absolute(
+                  p.join(
+                    p.dirname(potentialWorkspaceRefPath),
+                    path,
+                    '.dart_tool',
+                    'package_config.json',
+                  ),
+                ),
+              ),
+            );
+            packageConfigStat = tryStatFile(potentialPackageConfigPath2);
+            if (packageConfigStat == null) {
+              log.fine(
+                '`$potentialWorkspaceRefPath` points to non-existing '
+                '`$potentialPackageConfigPath2`',
+              );
+              return null;
+            } else {
+              packageConfigPath = potentialPackageConfigPath2;
+              rootDir = relativeIfNeeded(
                 p.normalize(
                   p.absolute(
-                    p.join(
-                      p.dirname(potentialWorkspaceRefPath),
-                      path,
-                      '.dart_tool',
-                      'package_config.json',
-                    ),
+                    p.join(p.dirname(potentialWorkspaceRefPath), path),
                   ),
                 ),
               );
-              packageConfigStat = tryStatFile(potentialPackageConfigPath2);
-              if (packageConfigStat == null) {
-                log.fine(
-                  '`$potentialWorkspaceRefPath` points to non-existing '
-                  '`$potentialPackageConfigPath2`',
-                );
-                return null;
-              } else {
-                packageConfigPath = potentialPackageConfigPath2;
-                rootDir = relativeIfNeeded(
-                  p.normalize(
-                    p.absolute(
-                      p.join(p.dirname(potentialWorkspaceRefPath), path),
-                    ),
-                  ),
-                );
 
-                break;
-              }
-            } else {
-              log.fine(
-                '`$potentialWorkspaceRefPath` '
-                'is missing "workspaceRoot" property',
-              );
-              return null;
+              break;
             }
-          } on FormatException catch (e) {
-            log.fine('`$potentialWorkspaceRefPath` not valid json: $e.');
+          } else {
+            log.fine(
+              '`$potentialWorkspaceRefPath` '
+              'is missing "workspaceRoot" property',
+            );
             return null;
           }
-        }
-      }
-      if (packageConfigStat == null) {
-        log.fine(
-          'Found no .dart_tool/package_config.json - no existing resolution.',
-        );
-        return null;
-      }
-      final lockFilePath = p.normalize(p.join(rootDir, 'pubspec.lock'));
-      final packageConfig = _loadPackageConfig(packageConfigPath);
-      if (p.isWithin(cache.rootDir, packageConfigPath)) {
-        // We always consider a global package (inside the cache) up-to-date.
-        return (packageConfig, rootDir);
-      }
-
-      /// Whether or not the `.dart_tool/package_config.json` file is was
-      /// generated by a different sdk down to changes in minor versions.
-      bool isPackageConfigGeneratedBySameDartSdk() {
-        final generatorVersion = packageConfig.generatorVersion;
-        if (generatorVersion == null ||
-            generatorVersion.major != sdk.version.major ||
-            generatorVersion.minor != sdk.version.minor) {
-          log.fine('The Dart SDK was updated since last package resolution.');
-          return false;
-        }
-        return true;
-      }
-
-      final flutter = FlutterSdk();
-      // If Flutter has moved since last invocation, we want to have new
-      // sdk-packages, and therefore do a new resolution.
-      //
-      // This also counts if Flutter was introduced or removed.
-      final flutterRoot =
-          flutter.rootDirectory == null
-              ? null
-              : p.toUri(p.absolute(flutter.rootDirectory!)).toString();
-      if (packageConfig.additionalProperties['flutterRoot'] != flutterRoot) {
-        log.fine('Flutter has moved since last invocation.');
-        return null;
-      }
-      if (packageConfig.additionalProperties['flutterVersion'] !=
-          (flutter.isAvailable ? flutter.version.toString() : null)) {
-        log.fine('Flutter has updated since last invocation.');
-        return null;
-      }
-      // If the pub cache was moved we should have a new resolution.
-      final rootCacheUrl = p.toUri(p.absolute(cache.rootDir)).toString();
-      if (packageConfig.additionalProperties['pubCache'] != rootCacheUrl) {
-        final previousPubCachePath =
-            packageConfig.additionalProperties['pubCache'];
-        log.fine(
-          'The pub cache has moved from $previousPubCachePath to $rootCacheUrl '
-          'since last invocation.',
-        );
-        return null;
-      }
-      // If the Dart sdk was updated we want a new resolution.
-      if (!isPackageConfigGeneratedBySameDartSdk()) {
-        return null;
-      }
-      final lockFileStat = tryStatFile(lockFilePath);
-      if (lockFileStat == null) {
-        log.fine('No $lockFilePath file found.');
-        return null;
-      }
-
-      final lockFileModified = lockFileStat.modified;
-      var lockfileNewerThanPubspecs = true;
-
-      // Check that all packages in packageConfig exist and their pubspecs have
-      // not been updated since the lockfile was written.
-      for (var package in packageConfig.packages) {
-        final pubspecPath = p.normalize(
-          p.join(
-            rootDir,
-            '.dart_tool',
-            package.rootUri
-                // Important to use `toFilePath()` here rather than `path`, as
-                // it handles Url-decoding.
-                .toFilePath(),
-            'pubspec.yaml',
-          ),
-        );
-        if (p.isWithin(cache.rootDir, pubspecPath)) {
-          continue;
-        }
-        final pubspecStat = tryStatFile(pubspecPath);
-        if (pubspecStat == null) {
-          log.fine('Could not find `$pubspecPath`');
-          // A dependency is missing - do a full new resolution.
+        } on FormatException catch (e) {
+          log.fine('`$potentialWorkspaceRefPath` not valid json: $e.');
           return null;
         }
-
-        if (pubspecStat.modified.isAfter(lockFileModified)) {
-          log.fine('`$pubspecPath` is newer than `$lockFilePath`');
-          lockfileNewerThanPubspecs = false;
-          break;
-        }
-        final pubspecOverridesPath = p.join(
-          package.rootUri.path,
-          'pubspec_overrides.yaml',
-        );
-        final pubspecOverridesStat = tryStatFile(pubspecOverridesPath);
-        if (pubspecOverridesStat != null) {
-          // This will wrongly require you to reresolve if a
-          // `pubspec_overrides.yaml` in a path-dependency is updated. That
-          // seems acceptable.
-          if (pubspecOverridesStat.modified.isAfter(lockFileModified)) {
-            log.fine('`$pubspecOverridesPath` is newer than `$lockFilePath`');
-            lockfileNewerThanPubspecs = false;
-          }
-        }
       }
-      var touchedLockFile = false;
-      late final lockFile = _loadLockFile(lockFilePath, cache);
-      late final root = Package.load(
-        dir,
-        loadPubspec: Pubspec.loadRootWithSources(cache.sources),
+    }
+    if (packageConfigStat == null) {
+      log.fine(
+        'Found no .dart_tool/package_config.json - no existing resolution.',
       );
-
-      if (!lockfileNewerThanPubspecs) {
-        if (isLockFileUpToDate(lockFile, root, lockFilePath: lockFilePath)) {
-          touch(lockFilePath);
-          touchedLockFile = true;
-        } else {
-          return null;
-        }
-      }
-
-      if (touchedLockFile ||
-          lockFileModified.isAfter(packageConfigStat.modified)) {
-        log.fine('`$lockFilePath` is newer than `$packageConfigPath`');
-        if (isPackageConfigUpToDate(
-          packageConfig,
-          lockFile,
-          root,
-          packageConfigPath: packageConfigPath,
-          lockFilePath: lockFilePath,
-        )) {
-          touch(packageConfigPath);
-        } else {
-          return null;
-        }
-      }
+      return null;
+    }
+    final lockFilePath = p.normalize(p.join(rootDir, 'pubspec.lock'));
+    final packageConfig = _loadPackageConfig(packageConfigPath);
+    if (p.isWithin(cache.rootDir, packageConfigPath)) {
+      // We always consider a global package (inside the cache) up-to-date.
       return (packageConfig, rootDir);
     }
 
-    if (isResolutionUpToDate() case (
+    /// Whether or not the `.dart_tool/package_config.json` file was
+    /// generated by a different sdk down to changes in minor versions.
+    bool isPackageConfigGeneratedBySameDartSdk() {
+      final generatorVersion = packageConfig.generatorVersion;
+      if (generatorVersion == null ||
+          generatorVersion.major != sdk.version.major ||
+          generatorVersion.minor != sdk.version.minor) {
+        log.fine('The Dart SDK was updated since last package resolution.');
+        return false;
+      }
+      return true;
+    }
+
+    final flutter = FlutterSdk();
+    // If Flutter has moved since last invocation, we want to have new
+    // sdk-packages, and therefore do a new resolution.
+    //
+    // This also counts if Flutter was introduced or removed.
+    final flutterRoot =
+        flutter.rootDirectory == null
+            ? null
+            : p.toUri(p.absolute(flutter.rootDirectory!)).toString();
+    if (packageConfig.additionalProperties['flutterRoot'] != flutterRoot) {
+      log.fine('Flutter has moved since last invocation.');
+      return null;
+    }
+    if (packageConfig.additionalProperties['flutterVersion'] !=
+        (flutter.isAvailable ? flutter.version.toString() : null)) {
+      log.fine('Flutter has updated since last invocation.');
+      return null;
+    }
+    // If the pub cache was moved we should have a new resolution.
+    final rootCacheUrl = p.toUri(p.absolute(cache.rootDir)).toString();
+    if (packageConfig.additionalProperties['pubCache'] != rootCacheUrl) {
+      final previousPubCachePath =
+          packageConfig.additionalProperties['pubCache'];
+      log.fine(
+        'The pub cache has moved from $previousPubCachePath to $rootCacheUrl '
+        'since last invocation.',
+      );
+      return null;
+    }
+    // If the Dart sdk was updated we want a new resolution.
+    if (!isPackageConfigGeneratedBySameDartSdk()) {
+      return null;
+    }
+    final lockFileStat = tryStatFile(lockFilePath);
+    if (lockFileStat == null) {
+      log.fine('No $lockFilePath file found.');
+      return null;
+    }
+
+    final lockFileModified = lockFileStat.modified;
+    var lockfileNewerThanPubspecs = true;
+
+    // Check that all packages in packageConfig exist and their pubspecs have
+    // not been updated since the lockfile was written.
+    for (var package in packageConfig.packages) {
+      final pubspecPath = p.normalize(
+        p.join(
+          rootDir,
+          '.dart_tool',
+          package.rootUri
+              // Important to use `toFilePath()` here rather than `path`, as
+              // it handles Url-decoding.
+              .toFilePath(),
+          'pubspec.yaml',
+        ),
+      );
+      if (p.isWithin(cache.rootDir, pubspecPath)) {
+        continue;
+      }
+      final pubspecStat = tryStatFile(pubspecPath);
+      if (pubspecStat == null) {
+        log.fine('Could not find `$pubspecPath`');
+        // A dependency is missing - do a full new resolution.
+        return null;
+      }
+
+      if (pubspecStat.modified.isAfter(lockFileModified)) {
+        log.fine('`$pubspecPath` is newer than `$lockFilePath`');
+        lockfileNewerThanPubspecs = false;
+        break;
+      }
+      final pubspecOverridesPath = p.join(
+        package.rootUri.path,
+        'pubspec_overrides.yaml',
+      );
+      final pubspecOverridesStat = tryStatFile(pubspecOverridesPath);
+      if (pubspecOverridesStat != null) {
+        // This will wrongly require you to reresolve if a
+        // `pubspec_overrides.yaml` in a path-dependency is updated. That
+        // seems acceptable.
+        if (pubspecOverridesStat.modified.isAfter(lockFileModified)) {
+          log.fine('`$pubspecOverridesPath` is newer than `$lockFilePath`');
+          lockfileNewerThanPubspecs = false;
+        }
+      }
+    }
+    var touchedLockFile = false;
+    late final lockFile = _loadLockFile(lockFilePath, cache);
+    late final root = Package.load(
+      dir,
+      loadPubspec: Pubspec.loadRootWithSources(cache.sources),
+    );
+
+    if (!lockfileNewerThanPubspecs) {
+      if (isLockFileUpToDate(lockFile, root, lockFilePath: lockFilePath)) {
+        touch(lockFilePath);
+        touchedLockFile = true;
+      } else {
+        return null;
+      }
+    }
+
+    if (touchedLockFile ||
+        lockFileModified.isAfter(packageConfigStat.modified)) {
+      log.fine('`$lockFilePath` is newer than `$packageConfigPath`');
+      if (isPackageConfigUpToDate(
+        packageConfig,
+        lockFile,
+        root,
+        packageConfigPath: packageConfigPath,
+        lockFilePath: lockFilePath,
+      )) {
+        touch(packageConfigPath);
+      } else {
+        return null;
+      }
+    }
+    return (packageConfig, rootDir);
+  }
+
+  /// Does a fast-pass check to see if the resolution is up-to-date. If not, run
+  /// a resolution with `pub get` semantics.
+  ///
+  /// If [summaryOnly] is `true` (the default) only a short summary is shown of
+  /// the solve.
+  ///
+  /// If [onlyOutputWhenTerminal] is `true` (the default) there will be no
+  /// output if no terminal is attached.
+  ///
+  /// When succesfull returns the found/created `PackageConfig` and the
+  /// directory containing it.
+  static Future<({PackageConfig packageConfig, String rootDir})> ensureUpToDate(
+    String dir, {
+    required SystemCache cache,
+    bool summaryOnly = true,
+    bool onlyOutputWhenTerminal = true,
+  }) async {
+    late final wasRelative = p.isRelative(dir);
+    String relativeIfNeeded(String path) =>
+        wasRelative ? p.relative(path) : path;
+
+    if (isResolutionUpToDate(dir, cache) case (
       final PackageConfig packageConfig,
       final String rootDir,
     )) {
diff --git a/lib/src/pub_embeddable_command.dart b/lib/src/pub_embeddable_command.dart
index 408d00a..0ed7723 100644
--- a/lib/src/pub_embeddable_command.dart
+++ b/lib/src/pub_embeddable_command.dart
@@ -7,6 +7,7 @@
 import 'command/add.dart';
 import 'command/bump.dart';
 import 'command/cache.dart';
+import 'command/check_resolution_up_to_date.dart';
 import 'command/deps.dart';
 import 'command/downgrade.dart';
 import 'command/get.dart';
@@ -83,6 +84,7 @@
     addSubcommand(LishCommand());
     addSubcommand(OutdatedCommand());
     addSubcommand(RemoveCommand());
+    addSubcommand(CheckResolutionUpToDateCommand());
     addSubcommand(RunCommand(deprecated: true, alwaysUseSubprocess: true));
     addSubcommand(UnpackCommand());
     addSubcommand(UpgradeCommand());
diff --git a/lib/src/source/git.dart b/lib/src/source/git.dart
index 7f03059..9087944 100644
--- a/lib/src/source/git.dart
+++ b/lib/src/source/git.dart
@@ -290,7 +290,7 @@
     String? ref,
     String? path,
     SystemCache cache, {
-    required String relativeTo,
+    required String? relativeTo,
     required String? tagPattern,
   }) async {
     assert(
diff --git a/pubspec.lock b/pubspec.lock
index 5bae051..c4db465 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -5,18 +5,18 @@
     dependency: transitive
     description:
       name: _fe_analyzer_shared
-      sha256: c81659312e021e3b780a502206130ea106487b34793bce61e26dc0f9b84807af
+      sha256: dd3d2ad434b9510001d089e8de7556d50c834481b9abc2891a0184a8493a19dc
       url: "https://pub.dev"
     source: hosted
-    version: "83.0.0"
+    version: "89.0.0"
   analyzer:
     dependency: "direct main"
     description:
       name: analyzer
-      sha256: "9c35a79bf2a150b3ea0d40010fbbb45b5ebea143d47096e0f82fd922a324b49b"
+      sha256: c22b6e7726d1f9e5db58c7251606076a71ca0dbcf76116675edfadbec0c9e875
       url: "https://pub.dev"
     source: hosted
-    version: "7.4.6"
+    version: "8.2.0"
   args:
     dependency: "direct main"
     description:
@@ -45,10 +45,10 @@
     dependency: "direct dev"
     description:
       name: checks
-      sha256: aad431b45a8ae2fa26db8c22e385b9cdec73f72986a1d9d9f2017f4c39ecf5c9
+      sha256: "016871c84732c1ac9856b8940236d5a5802ba638b3bd3e0ea7027b51a35f7aa7"
       url: "https://pub.dev"
     source: hosted
-    version: "0.3.0"
+    version: "0.3.1"
   cli_config:
     dependency: transitive
     description:
@@ -85,10 +85,10 @@
     dependency: transitive
     description:
       name: coverage
-      sha256: aa07dbe5f2294c827b7edb9a87bba44a9c15a3cc81bc8da2ca19b37322d30080
+      sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d"
       url: "https://pub.dev"
     source: hosted
-    version: "1.14.1"
+    version: "1.15.0"
   crypto:
     dependency: "direct main"
     description:
@@ -141,10 +141,10 @@
     dependency: "direct main"
     description:
       name: http
-      sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b"
+      sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
       url: "https://pub.dev"
     source: hosted
-    version: "1.4.0"
+    version: "1.5.0"
   http_multi_server:
     dependency: "direct main"
     description:
@@ -365,26 +365,26 @@
     dependency: "direct dev"
     description:
       name: test
-      sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb"
+      sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7"
       url: "https://pub.dev"
     source: hosted
-    version: "1.26.2"
+    version: "1.26.3"
   test_api:
     dependency: transitive
     description:
       name: test_api
-      sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
+      sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
       url: "https://pub.dev"
     source: hosted
-    version: "0.7.6"
+    version: "0.7.7"
   test_core:
     dependency: transitive
     description:
       name: test_core
-      sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a"
+      sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0"
       url: "https://pub.dev"
     source: hosted
-    version: "0.6.11"
+    version: "0.6.12"
   test_descriptor:
     dependency: "direct dev"
     description:
@@ -421,10 +421,10 @@
     dependency: transitive
     description:
       name: watcher
-      sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a"
+      sha256: "5bf046f41320ac97a469d506261797f35254fa61c641741ef32dacda98b7d39c"
       url: "https://pub.dev"
     source: hosted
-    version: "1.1.2"
+    version: "1.1.3"
   web:
     dependency: transitive
     description:
@@ -474,4 +474,4 @@
     source: hosted
     version: "2.2.2"
 sdks:
-  dart: ">=3.8.0 <4.0.0"
+  dart: ">=3.9.0 <4.0.0"
diff --git a/pubspec.yaml b/pubspec.yaml
index 808dda2..38570c1 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -4,7 +4,7 @@
   sdk: ^3.7.0
 
 dependencies:
-  analyzer: ^7.4.5
+  analyzer: ^8.2.0
   args: ^2.7.0
   async: ^2.13.0
   cli_util: ^0.4.2
@@ -13,7 +13,7 @@
   crypto: ^3.0.6
   frontend_server_client: ^4.0.0
   graphs: ^2.3.2
-  http: ^1.4.0
+  http: ^1.5.0
   http_multi_server: ^3.2.2
   http_parser: ^4.1.2
   meta: ^1.17.0
@@ -29,9 +29,9 @@
   yaml_edit: ^2.2.2
 
 dev_dependencies:
-  checks: ^0.3.0
+  checks: ^0.3.1
   dart_flutter_team_lints: ^3.5.2
   shelf_test_handler: ^2.0.2
-  test: ^1.26.2
+  test: ^1.26.3
   test_descriptor: ^2.0.2
   test_process: ^2.1.1
diff --git a/test/check_resolution_up_to_date_test.dart b/test/check_resolution_up_to_date_test.dart
new file mode 100644
index 0000000..f9a6f33
--- /dev/null
+++ b/test/check_resolution_up_to_date_test.dart
@@ -0,0 +1,112 @@
+// Copyright (c) 2025, 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:path/path.dart' as p;
+import 'package:test/test.dart';
+
+import 'descriptor.dart' as d;
+import 'test_pub.dart';
+
+void main() {
+  test('Will exit non-zero if there are changes', () async {
+    final server = await servePackages();
+    server.serve('foo', '1.0.0');
+
+    await d.appDir(dependencies: {'foo': '1.0.0'}).create();
+
+    await runPub(
+      args: ['check-resolution-up-to-date'],
+      error: contains('Resolution needs updating. Run `dart pub get`'),
+
+      exitCode: 1,
+    );
+
+    await d.dir(appPath, [
+      d.nothing('pubspec.lock'),
+      d.nothing('.dart_tool/package_config.json'),
+    ]).validate();
+
+    await pubGet();
+
+    await runPub(
+      args: ['check-resolution-up-to-date'],
+      output: contains('Resolution is up-to-date'),
+      exitCode: 0,
+    );
+
+    // Timestamp resolution is rather poor especially on windows.
+    await Future<Null>.delayed(const Duration(seconds: 1));
+
+    await d.appDir(dependencies: {'foo': '2.0.0'}).create();
+
+    await runPub(
+      args: ['check-resolution-up-to-date'],
+      error: contains('Resolution needs updating. Run `dart pub get`'),
+      exitCode: 1,
+    );
+  });
+
+  test('Works in a workspace', () async {
+    final server = await servePackages();
+    server.serve('foo', '1.0.0');
+
+    await d.dir(appPath, [
+      d.libPubspec(
+        'myapp',
+        '1.0.0',
+        sdk: '^3.5.0',
+        deps: {'foo': '1.0.0'},
+        extras: {
+          'workspace': ['pkg'],
+        },
+      ),
+      d.dir('pkg', [d.libPubspec('pkg', '1.0.0', resolutionWorkspace: true)]),
+    ]).create();
+
+    await runPub(
+      args: ['check-resolution-up-to-date'],
+      environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'},
+      workingDirectory: p.join(d.sandbox, appPath, 'pkg'),
+      error: contains('Resolution needs updating. Run `dart pub get`'),
+      exitCode: 1,
+    );
+
+    await pubGet(
+      environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'},
+      workingDirectory: p.join(d.sandbox, appPath, 'pkg'),
+      output: contains('+ foo 1.0.0'),
+    );
+
+    await runPub(
+      args: ['check-resolution-up-to-date'],
+      environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'},
+      workingDirectory: p.join(d.sandbox, appPath, 'pkg'),
+      output: contains('Resolution is up-to-date'),
+      exitCode: 0,
+    );
+
+    // Timestamp resolution is rather poor especially on windows.
+    await Future<Null>.delayed(const Duration(seconds: 1));
+
+    await d.dir(appPath, [
+      d.libPubspec(
+        'myapp',
+        '1.0.0',
+        sdk: '^3.5.0',
+        deps: {'foo': '1.0.0'},
+        extras: {
+          'workspace': ['pkg'],
+        },
+      ),
+    ]).create();
+
+    await runPub(
+      args: ['check-resolution-up-to-date'],
+      environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'},
+      workingDirectory: p.join(d.sandbox, appPath, 'pkg'),
+      error: contains('Resolution needs updating. Run `dart pub get`'),
+      exitCode: 1,
+    );
+  });
+}
diff --git a/test/testdata/goldens/help_test/pub add --help.txt b/test/testdata/goldens/help_test/pub add --help.txt
index f6f73fb..4aca35f 100644
--- a/test/testdata/goldens/help_test/pub add --help.txt
+++ b/test/testdata/goldens/help_test/pub add --help.txt
@@ -43,6 +43,8 @@
 -n, --dry-run            Report what dependencies would change but don't change any.
     --[no-]precompile    Build executables in immediate dependencies.
 -C, --directory=<dir>    Run this in the directory <dir>.
+    --[no-]example       Also update dependencies in `example/` after modifying pubspec.yaml in the root package (if it exists).
+                         (defaults to on)
 
 Run "pub help" to see global options.
 See https://dart.dev/tools/pub/cmd/pub-add for detailed documentation.
diff --git a/test/testdata/goldens/help_test/pub downgrade --help.txt b/test/testdata/goldens/help_test/pub downgrade --help.txt
index a971395..0b86d3e 100644
--- a/test/testdata/goldens/help_test/pub downgrade --help.txt
+++ b/test/testdata/goldens/help_test/pub downgrade --help.txt
@@ -10,6 +10,8 @@
 -h, --help               Print this usage information.
     --[no-]offline       Use cached packages instead of accessing the network.
 -n, --dry-run            Report what dependencies would change but don't change any.
+    --[no-]example       Also run in `example/` (if it exists).
+                         (defaults to on)
 -C, --directory=<dir>    Run this in the directory <dir>.
     --tighten            Updates lower bounds in pubspec.yaml to match the resolved version.
 
diff --git a/test/testdata/goldens/help_test/pub get --help.txt b/test/testdata/goldens/help_test/pub get --help.txt
index dd5aaf5..3c68ab1 100644
--- a/test/testdata/goldens/help_test/pub get --help.txt
+++ b/test/testdata/goldens/help_test/pub get --help.txt
@@ -12,6 +12,8 @@
                           has changed.
                           Useful for CI or deploying to production.
     --[no-]precompile     Build executables in immediate dependencies.
+    --[no-]example        Also run in `example/` (if it exists).
+                          (defaults to on)
 -C, --directory=<dir>     Run this in the directory <dir>.
 
 Run "pub help" to see global options.
diff --git a/test/testdata/goldens/help_test/pub remove --help.txt b/test/testdata/goldens/help_test/pub remove --help.txt
index 2032abf..34e1463 100644
--- a/test/testdata/goldens/help_test/pub remove --help.txt
+++ b/test/testdata/goldens/help_test/pub remove --help.txt
@@ -16,6 +16,8 @@
     --[no-]offline       Use cached packages instead of accessing the network.
 -n, --dry-run            Report what dependencies would change but don't change any.
     --[no-]precompile    Precompile executables in immediate dependencies.
+    --[no-]example       Also update dependencies in `example/` (if it exists).
+                         (defaults to on)
 -C, --directory=<dir>    Run this in the directory <dir>.
 
 Run "pub help" to see global options.
diff --git a/test/testdata/goldens/help_test/pub upgrade --help.txt b/test/testdata/goldens/help_test/pub upgrade --help.txt
index ade5a8d..90030c7 100644
--- a/test/testdata/goldens/help_test/pub upgrade --help.txt
+++ b/test/testdata/goldens/help_test/pub upgrade --help.txt
@@ -12,6 +12,8 @@
     --tighten              Updates lower bounds in pubspec.yaml to match the resolved version.
     --unlock-transitive    Also upgrades the transitive dependencies of the listed [dependencies]
     --major-versions       Upgrades packages to their latest resolvable versions, and updates pubspec.yaml.
+    --[no-]example         Also run in `example/` (if it exists).
+                           (defaults to on)
 -C, --directory=<dir>      Run this in the directory <dir>.
 
 Run "pub help" to see global options.
diff --git a/tool/create_version_tags_from_sdk.dart b/tool/create_version_tags_from_sdk.dart
new file mode 100644
index 0000000..3c67f33
--- /dev/null
+++ b/tool/create_version_tags_from_sdk.dart
@@ -0,0 +1,196 @@
+// Copyright (c) 2025, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+
+/// Tool for finding all tagged versions of the dart sdk, and in turn tag this
+/// repository with `SDK-$version` for each of these.
+library;
+
+import 'dart:io';
+
+import 'package:args/args.dart';
+import 'package:path/path.dart' as p;
+import 'package:pub_semver/pub_semver.dart';
+
+Future<void> main(List<String> args) async {
+  final argParser =
+      ArgParser()
+        ..addFlag(
+          'create',
+          help: 'Create the missing tags, otherwise only list them',
+          negatable: false,
+        )
+        ..addFlag(
+          'push',
+          help: 'Push missing sdk tags to remote',
+          negatable: false,
+        )
+        ..addOption('sdk-dir');
+  final ArgResults argResults;
+  try {
+    argResults = argParser.parse(args);
+  } on FormatException catch (e) {
+    stderr.writeln('${e.message}\n${argParser.usage}');
+    stderr.writeln('''
+Will find all tagged sdk versions, and tag the corresponding revision in this 
+repository with `--create`.
+And can push these tags to the remote with `--push`.
+
+Usage: create_version_tags_from_sdk [--create] [--push] [--sdk-dir <path>]
+''');
+    exit(-1);
+  }
+  final create = argResults.flag('create');
+  final push = argResults.flag('push');
+
+  Directory? tempDir;
+  String sdkDir;
+  try {
+    if (argResults.option('sdk-dir') == null) {
+      tempDir = Directory.systemTemp.createTempSync();
+      final cloneResult = Process.runSync('git', [
+        'clone',
+        // Using a treeless clone is faster up-front, but slower for
+        // showing a specific revision.
+        // We assume we only miss a few tags.
+        '--filter=tree:0',
+        '-n',
+        'https://github.com/dart-lang/sdk',
+      ], workingDirectory: tempDir.path);
+      if (cloneResult.exitCode != 0) {
+        throw Exception(
+          'Failed to clone sdk ${cloneResult.stderr} ${cloneResult.stdout}',
+        );
+      }
+      sdkDir = p.join(tempDir.path, 'sdk');
+    } else {
+      sdkDir = argResults.option('sdk-dir')!;
+    }
+
+    final sdkTags =
+        (Process.runSync('git', [
+                  'ls-remote',
+                  '--tags',
+                  '--refs',
+                  'origin',
+                ], workingDirectory: sdkDir).stdout
+                as String)
+            .split('\n')
+            .where((line) => line.isNotEmpty)
+            .map((line) => line.split('\t')[1])
+            .map((x) => x.substring('refs/tags/'.length))
+            .where((x) {
+              try {
+                Version.parse(x);
+              } on FormatException {
+                return false;
+              }
+              return true;
+            })
+            .toSet();
+    final alreadyTagged =
+        (Process.runSync('git', [
+                  'ls-remote',
+                  '--tags',
+                  '--refs',
+                  'origin',
+                ], workingDirectory: Directory.current.path).stdout
+                as String)
+            .split('\n')
+            .where((line) => line.isNotEmpty)
+            .map((line) => line.split('\t')[1])
+            .map((x) => x.substring('refs/tags/'.length))
+            .where((x) => x.startsWith('SDK-'))
+            .map((x) => x.substring('SDK-'.length))
+            .toSet();
+    final missing = sdkTags.difference(alreadyTagged);
+    var createdTagCount = 0;
+    var pushedTagCount = 0;
+
+    if (missing.isNotEmpty) {
+      for (final sdkTag in missing) {
+        final version = Version.parse(sdkTag);
+        if (
+        // Old versions of the sdk had no pub or no DEPS file.
+        version <= (Version.parse('1.11.3')) ||
+            // These version seems to have a broken DEPS file.
+            version == Version.parse('1.12.0-dev.5.6') ||
+            version == Version.parse('1.12.0-dev.5.7')) {
+          continue;
+        }
+        final depsResult = Process.runSync('git', [
+          'show',
+          '$sdkTag:DEPS',
+        ], workingDirectory: sdkDir);
+        if (depsResult.exitCode != 0) {
+          stderr.writeln(
+            'Failed to get deps for $sdkTag ${depsResult.stderr} '
+            '${depsResult.stdout}',
+          );
+          continue;
+        }
+
+        // Could use `gclient getdep -r sdk/third_party/pkg/pub` instead of a
+        // regexp. But for some versions that seems to not work well.
+        // The regexp
+        var pubRev = RegExp(
+          '"pub_rev": "([^"]*)"',
+        ).firstMatch(depsResult.stdout as String)?.group(1);
+        if (pubRev == null || pubRev.isEmpty) {
+          stderr.writeln('Failed to get pub rev for $sdkTag ');
+          continue;
+        }
+        if (pubRev.startsWith('@')) {
+          pubRev = pubRev.substring(1);
+        }
+
+        stdout.writeln('$sdkTag uses pub: $pubRev');
+        if (create) {
+          final tagResult = Process.runSync('git', [
+            '-c', 'user.email=support@pub.dev', //
+            '-c', 'user.name=Pub tagging bot', //
+            'tag',
+            'SDK-$sdkTag',
+            pubRev,
+            '--annotate',
+            '--force',
+            '--message', 'SDK $sdkTag', //
+          ], workingDirectory: Directory.current.path);
+          if (tagResult.exitCode != 0) {
+            stderr.writeln(
+              'Failed to tag sdk ${tagResult.stderr} ${tagResult.stdout}',
+            );
+            continue;
+          }
+          createdTagCount++;
+        }
+        if (push) {
+          final pushResult = Process.runSync('git', [
+            'push',
+            'origin',
+            // Don't run any hooks before pushing.
+            '--no-verify',
+            'tag',
+            'SDK-$sdkTag',
+          ], workingDirectory: Directory.current.path);
+          if (pushResult.exitCode != 0) {
+            stderr.writeln(
+              'Failed to push sdk ${pushResult.stderr} ${pushResult.stdout}',
+            );
+            continue;
+          }
+          pushedTagCount++;
+        }
+      }
+    }
+    if (!create) {
+      stdout.writeln('Would have created $createdTagCount tags');
+    } else {
+      stdout.writeln('Created $createdTagCount tags');
+    }
+    if (push) {
+      stdout.writeln('Pushed $pushedTagCount tags');
+    }
+  } finally {
+    tempDir?.deleteSync(recursive: true);
+  }
+}