Version 2.12.0-29.10.beta

* Cherry-pick refs/changes/64/172064/1 to beta
* Cherry-pick 0258778e3b20d1f9b2c656b0793322d3c9fa8178 to beta
* Cherry-pick e205acb506cca15228da56b26693e3339a22058a to beta
diff --git a/pkg/nnbd_migration/lib/migration_cli.dart b/pkg/nnbd_migration/lib/migration_cli.dart
index 71b7326..3505c90 100644
--- a/pkg/nnbd_migration/lib/migration_cli.dart
+++ b/pkg/nnbd_migration/lib/migration_cli.dart
@@ -679,6 +679,9 @@
         if (!options.ignoreErrors) {
           throw MigrationExit(1);
         }
+      } else if (analysisResult.allSourcesAlreadyMigrated) {
+        _logAlreadyMigrated();
+        throw MigrationExit(0);
       } else {
         logger.stdout('No analysis issues found.');
       }
@@ -725,6 +728,9 @@
 Use this interactive web view to review, improve, or apply the results.
 When finished with the preview, hit ctrl-c to terminate this process.
 
+If you make edits outside of the web view (in your IDE), use the 'Rerun from
+sources' action.
+
 ''');
 
       // Block until sigint (ctrl-c).
@@ -866,6 +872,10 @@
     }
   }
 
+  void _logAlreadyMigrated() {
+    logger.stdout(migratedAlready);
+  }
+
   void _logErrors(AnalysisResult analysisResult) {
     logger.stdout('');
 
@@ -930,6 +940,14 @@
           _dartFixListener,
           _fixCodeProcessor._task.instrumentationListener,
           analysisResult);
+    } else if (analysisResult.allSourcesAlreadyMigrated) {
+      _logAlreadyMigrated();
+      return MigrationState(
+          _fixCodeProcessor._task.migration,
+          _fixCodeProcessor._task.includedRoot,
+          _dartFixListener,
+          _fixCodeProcessor._task.instrumentationListener,
+          analysisResult);
     } else {
       logger.stdout(ansi.emphasized('Re-generating migration suggestions...'));
       return await _fixCodeProcessor.runLaterPhases();
diff --git a/pkg/nnbd_migration/lib/src/exceptions.dart b/pkg/nnbd_migration/lib/src/exceptions.dart
index 580fcda..54ca2ba 100644
--- a/pkg/nnbd_migration/lib/src/exceptions.dart
+++ b/pkg/nnbd_migration/lib/src/exceptions.dart
@@ -4,9 +4,6 @@
 /// A [StateError] specific to the ways that the NNBD experiment can be
 /// misconfigured which may prevent the tool from working.
 class ExperimentStatusException extends StateError {
-  /// All files included in the migration dir have already been migrated.
-  ExperimentStatusException.migratedAlready() : super(migratedAlready);
-
   /// The SDK was analyzed without NNBD semantics.
   ExperimentStatusException.sdkExperimentDisabled() : super(nnbdExperimentOff);
 
diff --git a/pkg/nnbd_migration/lib/src/messages.dart b/pkg/nnbd_migration/lib/src/messages.dart
index f4796c1..6898c3f 100644
--- a/pkg/nnbd_migration/lib/src/messages.dart
+++ b/pkg/nnbd_migration/lib/src/messages.dart
@@ -5,7 +5,7 @@
 import 'package:nnbd_migration/migration_cli.dart';
 
 const String migratedAlready =
-    "Seem to be migrating code that's already migrated";
+    'All sources appear to be already migrated.  Nothing to do.';
 const String nnbdExperimentOff =
     'Analyzer seems to need the nnbd experiment on in the SDK.';
 const String sdkNnbdOff = 'Analysis seems to have an SDK without NNBD enabled.';
@@ -34,7 +34,7 @@
 
 Please upgrade the packages containing these libraries to null safe versions
 before continuing.  To see what null safe package versions are available, run
-the following command: `dart pub outdated --mode=null-safety --prereleases`.
+the following command: `dart pub outdated --mode=null-safety`.
 
 To skip this check and try to migrate anyway, re-run with the flag
 `$_skipImportCheckFlag`.
diff --git a/pkg/nnbd_migration/lib/src/nullability_migration_impl.dart b/pkg/nnbd_migration/lib/src/nullability_migration_impl.dart
index 81a3079..f3a9fac 100644
--- a/pkg/nnbd_migration/lib/src/nullability_migration_impl.dart
+++ b/pkg/nnbd_migration/lib/src/nullability_migration_impl.dart
@@ -197,12 +197,6 @@
   }
 
   void finish() {
-    if (!_propagated) {
-      // [finalizeInput] sets this field to `true`, so if it's still false, that
-      // means it was never called; this probably means that all the code fed
-      // to the migration tool was already migrated.
-      throw ExperimentStatusException.migratedAlready();
-    }
     _postmortemFileWriter?.write();
     _instrumentation?.finished();
   }
diff --git a/pkg/nnbd_migration/test/migration_cli_test.dart b/pkg/nnbd_migration/test/migration_cli_test.dart
index 355812d..f1352f3 100644
--- a/pkg/nnbd_migration/test/migration_cli_test.dart
+++ b/pkg/nnbd_migration/test/migration_cli_test.dart
@@ -235,13 +235,13 @@
 
   Future<String> assertErrorExit(
       MigrationCliRunner cliRunner, FutureOr<void> Function() callback,
-      {@required bool withUsage, dynamic expectedExitCode = anything}) async {
+      {@required bool withUsage, dynamic expectedExitCode}) async {
+    expectedExitCode ??= isNot(0);
     try {
       await callback();
       fail('Migration succeeded; expected it to abort with an error');
     } on MigrationExit catch (migrationExit) {
       expect(migrationExit.exitCode, isNotNull);
-      expect(migrationExit.exitCode, isNot(0));
       expect(migrationExit.exitCode, expectedExitCode);
     }
     expect(cliRunner.isPreviewServerRunning, isFalse);
@@ -305,7 +305,8 @@
   Future<String> assertRunFailure(List<String> args,
       {MigrationCli cli,
       bool withUsage = false,
-      dynamic expectedExitCode = anything}) async {
+      dynamic expectedExitCode}) async {
+    expectedExitCode ??= isNot(0);
     cli ??= _createCli();
     MigrationCliRunner cliRunner;
     try {
@@ -313,7 +314,6 @@
           cli.decodeCommandLineArgs(MigrationCli.createParser().parse(args));
     } on MigrationExit catch (e) {
       expect(e.exitCode, isNotNull);
-      expect(e.exitCode, isNot(0));
       expect(e.exitCode, expectedExitCode);
       return assertStderr(withUsage: withUsage);
     }
@@ -804,6 +804,15 @@
     expect(output, isNot(contains('package:bar/bar.dart')));
   }
 
+  test_lifecycle_migration_already_performed() async {
+    var projectContents = simpleProject(migrated: true);
+    var projectDir = createProjectDir(projectContents);
+    await assertRunFailure([projectDir], expectedExitCode: 0);
+    var output = logger.stdoutBuffer.toString();
+    expect(output,
+        contains('All sources appear to be already migrated.  Nothing to do.'));
+  }
+
   test_lifecycle_no_preview() async {
     var projectContents = simpleProject();
     var projectDir = createProjectDir(projectContents);
diff --git a/tools/VERSION b/tools/VERSION
index 13e7113..aec0ebb 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -28,4 +28,4 @@
 MINOR 12
 PATCH 0
 PRERELEASE 29
-PRERELEASE_PATCH 7
\ No newline at end of file
+PRERELEASE_PATCH 10
\ No newline at end of file