[tool] Improve check version ci so that it enforces the version in CHANGELOG and pubspec matches. (#3678)

diff --git a/.cirrus.yml b/.cirrus.yml
index 8d992d5..8f61fab 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -6,10 +6,13 @@
   env:
     INTEGRATION_TEST_PATH: "./packages/integration_test"
     CHANNEL: "master" # Default to master when not explicitly set by a task.
+    PLUGIN_TOOLS: "dart run ./script/tool/lib/src/main.dart"
   setup_script:
     - flutter channel $CHANNEL
     - flutter upgrade
     - git fetch origin master # To set FETCH_HEAD for "git merge-base" to work
+    - cd script/tool
+    - pub get
 
 
 # Light-workload Linux tasks.
@@ -24,10 +27,14 @@
     - name: plugin_tools_tests
       script:
         - cd script/tool
-        - pub get
         - CIRRUS_BUILD_ID=null pub run test
     - name: publishable
       script:
+        - if [[ "$CIRRUS_BRANCH" == "master" ]]; then
+        -   $PLUGIN_TOOLS version-check
+        - else
+        -   $PLUGIN_TOOLS version-check --run-on-changed-packages
+        - fi
         - ./script/check_publish.sh
     - name: format
       format_script: ./script/incremental_build.sh format --fail-on-change
diff --git a/script/incremental_build.sh b/script/incremental_build.sh
index 826229b..38a4d2d 100755
--- a/script/incremental_build.sh
+++ b/script/incremental_build.sh
@@ -49,8 +49,5 @@
   else
     echo running "${ACTIONS[@]}"
     (cd "$REPO_DIR" && plugin_tools "${ACTIONS[@]}" --plugins="$CHANGED_PACKAGES" --exclude="$ALL_EXCLUDED" ${PLUGIN_SHARDING[@]})
-    echo "Running version check for changed packages"
-    # TODO(egarciad): Enable this check once in master.
-    # (cd "$REPO_DIR" && $PUB global run flutter_plugin_tools version-check --base_sha="$(get_branch_base_sha)")
   fi
 fi
diff --git a/script/tool/lib/src/common.dart b/script/tool/lib/src/common.dart
index 08622df..ce4f378 100644
--- a/script/tool/lib/src/common.dart
+++ b/script/tool/lib/src/common.dart
@@ -7,8 +7,12 @@
 import 'dart:math';
 
 import 'package:args/command_runner.dart';
+import 'package:colorize/colorize.dart';
 import 'package:file/file.dart';
+import 'package:git/git.dart';
+import 'package:meta/meta.dart';
 import 'package:path/path.dart' as p;
+import 'package:pub_semver/pub_semver.dart';
 import 'package:yaml/yaml.dart';
 
 typedef void Print(Object object);
@@ -140,6 +144,13 @@
   return pluginSupportsPlatform(kLinux, entity, fileSystem);
 }
 
+/// Throws a [ToolExit] with `exitCode` and log the `errorMessage` in red.
+void printErrorAndExit({@required String errorMessage, int exitCode = 1}) {
+  final Colorize redError = Colorize(errorMessage)..red();
+  print(redError);
+  throw ToolExit(exitCode);
+}
+
 /// Error thrown when a command needs to exit with a non-zero exit code.
 class ToolExit extends Error {
   ToolExit(this.exitCode);
@@ -152,6 +163,7 @@
     this.packagesDir,
     this.fileSystem, {
     this.processRunner = const ProcessRunner(),
+    this.gitDir,
   }) {
     argParser.addMultiOption(
       _pluginsArg,
@@ -179,12 +191,23 @@
       help: 'Exclude packages from this command.',
       defaultsTo: <String>[],
     );
+    argParser.addFlag(_runOnChangedPackagesArg,
+        help: 'Run the command on changed packages/plugins.\n'
+            'If the $_pluginsArg is specified, this flag is ignored.\n'
+            'The packages excluded with $_excludeArg is also excluded even if changed.\n'
+            'See $_kBaseSha if a custom base is needed to determine the diff.');
+    argParser.addOption(_kBaseSha,
+        help: 'The base sha used to determine git diff. \n'
+            'This is useful when $_runOnChangedPackagesArg is specified.\n'
+            'If not specified, merge-base is used as base sha.');
   }
 
   static const String _pluginsArg = 'plugins';
   static const String _shardIndexArg = 'shardIndex';
   static const String _shardCountArg = 'shardCount';
   static const String _excludeArg = 'exclude';
+  static const String _runOnChangedPackagesArg = 'run-on-changed-packages';
+  static const String _kBaseSha = 'base-sha';
 
   /// The directory containing the plugin packages.
   final Directory packagesDir;
@@ -199,6 +222,11 @@
   /// This can be overridden for testing.
   final ProcessRunner processRunner;
 
+  /// The git directory to use. By default it uses the parent directory.
+  ///
+  /// This can be mocked for testing.
+  final GitDir gitDir;
+
   int _shardIndex;
   int _shardCount;
 
@@ -273,9 +301,13 @@
   ///    "client library" package, which declares the API for the plugin, as
   ///    well as one or more platform-specific implementations.
   Stream<Directory> _getAllPlugins() async* {
-    final Set<String> plugins = Set<String>.from(argResults[_pluginsArg]);
+    Set<String> plugins = Set<String>.from(argResults[_pluginsArg]);
     final Set<String> excludedPlugins =
         Set<String>.from(argResults[_excludeArg]);
+    final bool runOnChangedPackages = argResults[_runOnChangedPackagesArg];
+    if (plugins.isEmpty && runOnChangedPackages) {
+      plugins = await _getChangedPackages();
+    }
 
     await for (FileSystemEntity entity
         in packagesDir.list(followLinks: false)) {
@@ -363,6 +395,50 @@
             (FileSystemEntity entity) => isFlutterPackage(entity, fileSystem))
         .cast<Directory>();
   }
+
+  /// Retrieve an instance of [GitVersionFinder] based on `_kBaseSha` and [gitDir].
+  ///
+  /// Throws tool exit if [gitDir] nor root directory is a git directory.
+  Future<GitVersionFinder> retrieveVersionFinder() async {
+    final String rootDir = packagesDir.parent.absolute.path;
+    String baseSha = argResults[_kBaseSha];
+
+    GitDir baseGitDir = gitDir;
+    if (baseGitDir == null) {
+      if (!await GitDir.isGitDir(rootDir)) {
+        printErrorAndExit(
+            errorMessage: '$rootDir is not a valid Git repository.',
+            exitCode: 2);
+      }
+      baseGitDir = await GitDir.fromExisting(rootDir);
+    }
+
+    final GitVersionFinder gitVersionFinder =
+        GitVersionFinder(baseGitDir, baseSha);
+    return gitVersionFinder;
+  }
+
+  Future<Set<String>> _getChangedPackages() async {
+    final GitVersionFinder gitVersionFinder = await retrieveVersionFinder();
+
+    final List<String> allChangedFiles =
+        await gitVersionFinder.getChangedFiles();
+    final Set<String> packages = <String>{};
+    allChangedFiles.forEach((String path) {
+      final List<String> pathComponents = path.split('/');
+      final int packagesIndex =
+          pathComponents.indexWhere((String element) => element == 'packages');
+      if (packagesIndex != -1) {
+        packages.add(pathComponents[packagesIndex + 1]);
+      }
+    });
+    if (packages.isNotEmpty) {
+      final String changedPackages = packages.join(',');
+      print(changedPackages);
+    }
+    print('No changed packages.');
+    return packages;
+  }
 }
 
 /// A class used to run processes.
@@ -466,3 +542,68 @@
     return 'ERROR: Unable to execute "$executable ${args.join(' ')}"$workdir.';
   }
 }
+
+/// Finding diffs based on `baseGitDir` and `baseSha`.
+class GitVersionFinder {
+  /// Constructor
+  GitVersionFinder(this.baseGitDir, this.baseSha);
+
+  /// The top level directory of the git repo.
+  ///
+  /// That is where the .git/ folder exists.
+  final GitDir baseGitDir;
+
+  /// The base sha used to get diff.
+  final String baseSha;
+
+  static bool _isPubspec(String file) {
+    return file.trim().endsWith('pubspec.yaml');
+  }
+
+  /// Get a list of all the pubspec.yaml file that is changed.
+  Future<List<String>> getChangedPubSpecs() async {
+    return (await getChangedFiles()).where(_isPubspec).toList();
+  }
+
+  /// Get a list of all the changed files.
+  Future<List<String>> getChangedFiles() async {
+    final String baseSha = await _getBaseSha();
+    final io.ProcessResult changedFilesCommand = await baseGitDir
+        .runCommand(<String>['diff', '--name-only', '$baseSha', 'HEAD']);
+    print('Determine diff with base sha: $baseSha');
+    final String changedFilesStdout = changedFilesCommand.stdout.toString()  ?? '';
+    if (changedFilesStdout.isEmpty) {
+      return <String>[];
+    }
+    final List<String> changedFiles = changedFilesStdout
+        .split('\n')
+          ..removeWhere((element) => element.isEmpty);
+    return changedFiles.toList();
+  }
+
+  /// Get the package version specified in the pubspec file in `pubspecPath` and at the revision of `gitRef`.
+  Future<Version> getPackageVersion(String pubspecPath, String gitRef) async {
+    final io.ProcessResult gitShow =
+        await baseGitDir.runCommand(<String>['show', '$gitRef:$pubspecPath']);
+    final String fileContent = gitShow.stdout;
+    final String versionString = loadYaml(fileContent)['version'];
+    return versionString == null ? null : Version.parse(versionString);
+  }
+
+  Future<String> _getBaseSha() async {
+    if (baseSha != null && baseSha.isNotEmpty) {
+      return baseSha;
+    }
+
+    io.ProcessResult baseShaFromMergeBase = await baseGitDir.runCommand(
+        <String>['merge-base', '--fork-point', 'FETCH_HEAD', 'HEAD'],
+        throwOnError: false);
+    if (baseShaFromMergeBase == null ||
+        baseShaFromMergeBase.stderr != null ||
+        baseShaFromMergeBase.stdout == null) {
+      baseShaFromMergeBase = await baseGitDir
+          .runCommand(<String>['merge-base', 'FETCH_HEAD', 'HEAD']);
+    }
+    return (baseShaFromMergeBase.stdout as String).trim();
+  }
+}
diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart
index 2c6b92b..111239f 100644
--- a/script/tool/lib/src/version_check_command.dart
+++ b/script/tool/lib/src/version_check_command.dart
@@ -6,43 +6,14 @@
 import 'dart:io' as io;
 
 import 'package:meta/meta.dart';
-import 'package:colorize/colorize.dart';
 import 'package:file/file.dart';
 import 'package:git/git.dart';
 import 'package:pub_semver/pub_semver.dart';
 import 'package:pubspec_parse/pubspec_parse.dart';
-import 'package:yaml/yaml.dart';
 
 import 'common.dart';
 
-const String _kBaseSha = 'base_sha';
-
-class GitVersionFinder {
-  GitVersionFinder(this.baseGitDir, this.baseSha);
-
-  final GitDir baseGitDir;
-  final String baseSha;
-
-  static bool isPubspec(String file) {
-    return file.trim().endsWith('pubspec.yaml');
-  }
-
-  Future<List<String>> getChangedPubSpecs() async {
-    final io.ProcessResult changedFilesCommand = await baseGitDir
-        .runCommand(<String>['diff', '--name-only', '$baseSha', 'HEAD']);
-    final List<String> changedFiles =
-        changedFilesCommand.stdout.toString().split('\n');
-    return changedFiles.where(isPubspec).toList();
-  }
-
-  Future<Version> getPackageVersion(String pubspecPath, String gitRef) async {
-    final io.ProcessResult gitShow =
-        await baseGitDir.runCommand(<String>['show', '$gitRef:$pubspecPath']);
-    final String fileContent = gitShow.stdout;
-    final String versionString = loadYaml(fileContent)['version'];
-    return versionString == null ? null : Version.parse(versionString);
-  }
-}
+const String _kBaseSha = 'base-sha';
 
 enum NextVersionType {
   BREAKING_MAJOR,
@@ -128,46 +99,28 @@
     Directory packagesDir,
     FileSystem fileSystem, {
     ProcessRunner processRunner = const ProcessRunner(),
-    this.gitDir,
-  }) : super(packagesDir, fileSystem, processRunner: processRunner) {
-    argParser.addOption(_kBaseSha);
-  }
-
-  /// The git directory to use. By default it uses the parent directory.
-  ///
-  /// This can be mocked for testing.
-  final GitDir gitDir;
+    GitDir gitDir,
+  }) : super(packagesDir, fileSystem,
+            processRunner: processRunner, gitDir: gitDir);
 
   @override
   final String name = 'version-check';
 
   @override
   final String description =
-      'Checks if the versions of the plugins have been incremented per pub specification.\n\n'
+      'Checks if the versions of the plugins have been incremented per pub specification.\n'
+      'Also checks if the latest version in CHANGELOG matches the version in pubspec.\n\n'
       'This command requires "pub" and "flutter" to be in your path.';
 
   @override
   Future<Null> run() async {
     checkSharding();
-
-    final String rootDir = packagesDir.parent.absolute.path;
-    final String baseSha = argResults[_kBaseSha];
-
-    GitDir baseGitDir = gitDir;
-    if (baseGitDir == null) {
-      if (!await GitDir.isGitDir(rootDir)) {
-        print('$rootDir is not a valid Git repository.');
-        throw ToolExit(2);
-      }
-      baseGitDir = await GitDir.fromExisting(rootDir);
-    }
-
-    final GitVersionFinder gitVersionFinder =
-        GitVersionFinder(baseGitDir, baseSha);
+    final GitVersionFinder gitVersionFinder = await retrieveVersionFinder();
 
     final List<String> changedPubspecs =
         await gitVersionFinder.getChangedPubSpecs();
 
+    final String baseSha = argResults[_kBaseSha];
     for (final String pubspecPath in changedPubspecs) {
       try {
         final File pubspecFile = fileSystem.file(pubspecPath);
@@ -194,9 +147,7 @@
           final String error = '$pubspecPath incorrectly updated version.\n'
               'HEAD: $headVersion, master: $masterVersion.\n'
               'Allowed versions: $allowedNextVersions';
-          final Colorize redError = Colorize(error)..red();
-          print(redError);
-          throw ToolExit(1);
+          printErrorAndExit(errorMessage: error);
         }
 
         bool isPlatformInterface = pubspec.name.endsWith("_platform_interface");
@@ -205,9 +156,7 @@
                 NextVersionType.BREAKING_MAJOR) {
           final String error = '$pubspecPath breaking change detected.\n'
               'Breaking changes to platform interfaces are strongly discouraged.\n';
-          final Colorize redError = Colorize(error)..red();
-          print(redError);
-          throw ToolExit(1);
+          printErrorAndExit(errorMessage: error);
         }
       } on io.ProcessException {
         print('Unable to find pubspec in master for $pubspecPath.'
@@ -215,6 +164,74 @@
       }
     }
 
+    await for (Directory plugin in getPlugins()) {
+      await _checkVersionsMatch(plugin);
+    }
+
     print('No version check errors found!');
   }
+
+  Future<void> _checkVersionsMatch(Directory plugin) async {
+    // get version from pubspec
+    final String packageName = plugin.basename;
+    print('-----------------------------------------');
+    print(
+        'Checking the first version listed in CHANGELOG.MD matches the version in pubspec.yaml for $packageName.');
+
+    final Pubspec pubspec = _tryParsePubspec(plugin);
+    if (pubspec == null) {
+      final String error = 'Cannot parse version from pubspec.yaml';
+      printErrorAndExit(errorMessage: error);
+    }
+    final Version fromPubspec = pubspec.version;
+
+    // get first version from CHANGELOG
+    final File changelog = plugin.childFile('CHANGELOG.md');
+    final List<String> lines = changelog.readAsLinesSync();
+    String firstLineWithText;
+    final Iterator iterator = lines.iterator;
+    while (iterator.moveNext()) {
+      if ((iterator.current as String).trim().isNotEmpty) {
+        firstLineWithText = iterator.current;
+        break;
+      }
+    }
+    // Remove all leading mark down syntax from the version line.
+    final String versionString = firstLineWithText.split(' ').last;
+    Version fromChangeLog = Version.parse(versionString);
+    if (fromChangeLog == null) {
+      final String error =
+          'Cannot find version on the first line of ${plugin.path}/CHANGELOG.md';
+      printErrorAndExit(errorMessage: error);
+    }
+
+    if (fromPubspec != fromChangeLog) {
+      final String error = '''
+versions for $packageName in CHANGELOG.md and pubspec.yaml do not match.
+The version in pubspec.yaml is $fromPubspec.
+The first version listed in CHANGELOG.md is $fromChangeLog.
+''';
+      printErrorAndExit(errorMessage: error);
+    }
+    print('${packageName} passed version check');
+  }
+
+  Pubspec _tryParsePubspec(Directory package) {
+    final File pubspecFile = package.childFile('pubspec.yaml');
+
+    try {
+      Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync());
+      if (pubspec == null) {
+        final String error =
+            'Failed to parse `pubspec.yaml` at ${pubspecFile.path}';
+        printErrorAndExit(errorMessage: error);
+      }
+      return pubspec;
+    } on Exception catch (exception) {
+      final String error =
+          'Failed to parse `pubspec.yaml` at ${pubspecFile.path}: $exception}';
+      printErrorAndExit(errorMessage: error);
+    }
+    return null;
+  }
 }
diff --git a/script/tool/test/common_test.dart b/script/tool/test/common_test.dart
index b3504c2..0fb3ce7 100644
--- a/script/tool/test/common_test.dart
+++ b/script/tool/test/common_test.dart
@@ -1,6 +1,10 @@
+import 'dart:io';
+
 import 'package:args/command_runner.dart';
 import 'package:file/file.dart';
 import 'package:flutter_plugin_tools/src/common.dart';
+import 'package:git/git.dart';
+import 'package:mockito/mockito.dart';
 import 'package:test/test.dart';
 
 import 'util.dart';
@@ -9,8 +13,21 @@
   RecordingProcessRunner processRunner;
   CommandRunner runner;
   List<String> plugins;
+  List<List<String>> gitDirCommands;
+  String gitDiffResponse;
 
   setUp(() {
+    gitDirCommands = <List<String>>[];
+    gitDiffResponse = '';
+    final MockGitDir gitDir = MockGitDir();
+    when(gitDir.runCommand(any)).thenAnswer((Invocation invocation) {
+      gitDirCommands.add(invocation.positionalArguments[0]);
+      final MockProcessResult mockProcessResult = MockProcessResult();
+      if (invocation.positionalArguments[0][0] == 'diff') {
+        when<String>(mockProcessResult.stdout).thenReturn(gitDiffResponse);
+      }
+      return Future<ProcessResult>.value(mockProcessResult);
+    });
     initializeFakePackages();
     processRunner = RecordingProcessRunner();
     plugins = [];
@@ -19,6 +36,7 @@
       mockPackagesDir,
       mockFileSystem,
       processRunner: processRunner,
+      gitDir: gitDir,
     );
     runner =
         CommandRunner<Null>('common_command', 'Test for common functionality');
@@ -73,6 +91,207 @@
     ]);
     expect(plugins, unorderedEquals(<String>[plugin2.path]));
   });
+
+  group('test run-on-changed-packages', () {
+    test('all plugins should be tested if there are no changes.', () async {
+      final Directory plugin1 = createFakePlugin('plugin1');
+      final Directory plugin2 = createFakePlugin('plugin2');
+      await runner.run(
+          <String>['sample', '--base-sha=master', '--run-on-changed-packages']);
+
+      expect(plugins, unorderedEquals(<String>[plugin1.path, plugin2.path]));
+    });
+
+    test('all plugins should be tested if there are no plugin related changes.',
+        () async {
+      gitDiffResponse = ".cirrus";
+      final Directory plugin1 = createFakePlugin('plugin1');
+      final Directory plugin2 = createFakePlugin('plugin2');
+      await runner.run(
+          <String>['sample', '--base-sha=master', '--run-on-changed-packages']);
+
+      expect(plugins, unorderedEquals(<String>[plugin1.path, plugin2.path]));
+    });
+
+    test('Only changed plugin should be tested.', () async {
+      gitDiffResponse = "packages/plugin1/plugin1.dart";
+      final Directory plugin1 = createFakePlugin('plugin1');
+      createFakePlugin('plugin2');
+      await runner.run(
+          <String>['sample', '--base-sha=master', '--run-on-changed-packages']);
+
+      expect(plugins, unorderedEquals(<String>[plugin1.path]));
+    });
+
+    test('multiple files in one plugin should also test the plugin', () async {
+      gitDiffResponse = '''
+packages/plugin1/plugin1.dart
+packages/plugin1/ios/plugin1.m
+''';
+      final Directory plugin1 = createFakePlugin('plugin1');
+      createFakePlugin('plugin2');
+      await runner.run(
+          <String>['sample', '--base-sha=master', '--run-on-changed-packages']);
+
+      expect(plugins, unorderedEquals(<String>[plugin1.path]));
+    });
+
+    test('multiple plugins changed should test all the changed plugins',
+        () async {
+      gitDiffResponse = '''
+packages/plugin1/plugin1.dart
+packages/plugin2/ios/plugin2.m
+''';
+      final Directory plugin1 = createFakePlugin('plugin1');
+      final Directory plugin2 = createFakePlugin('plugin2');
+      createFakePlugin('plugin3');
+      await runner.run(
+          <String>['sample', '--base-sha=master', '--run-on-changed-packages']);
+
+      expect(plugins, unorderedEquals(<String>[plugin1.path, plugin2.path]));
+    });
+
+    test(
+        'multiple plugins inside the same plugin group changed should output the plugin group name',
+        () async {
+      gitDiffResponse = '''
+packages/plugin1/plugin1/plugin1.dart
+packages/plugin1/plugin1_platform_interface/plugin1_platform_interface.dart
+packages/plugin1/plugin1_web/plugin1_web.dart
+''';
+      final Directory plugin1 =
+          createFakePlugin('plugin1', parentDirectoryName: 'plugin1');
+      createFakePlugin('plugin2');
+      createFakePlugin('plugin3');
+      await runner.run(
+          <String>['sample', '--base-sha=master', '--run-on-changed-packages']);
+
+      expect(plugins, unorderedEquals(<String>[plugin1.path]));
+    });
+
+    test('--plugins flag overrides the behavior of --run-on-changed-packages',
+        () async {
+      gitDiffResponse = '''
+packages/plugin1/plugin1.dart
+packages/plugin2/ios/plugin2.m
+packages/plugin3/plugin3.dart
+''';
+      final Directory plugin1 =
+          createFakePlugin('plugin1', parentDirectoryName: 'plugin1');
+      final Directory plugin2 = createFakePlugin('plugin2');
+      createFakePlugin('plugin3');
+      await runner.run(<String>[
+        'sample',
+        '--plugins=plugin1,plugin2',
+        '--base-sha=master',
+        '--run-on-changed-packages'
+      ]);
+
+      expect(plugins, unorderedEquals(<String>[plugin1.path, plugin2.path]));
+    });
+
+    test('--exclude flag works with --run-on-changed-packages', () async {
+      gitDiffResponse = '''
+packages/plugin1/plugin1.dart
+packages/plugin2/ios/plugin2.m
+packages/plugin3/plugin3.dart
+''';
+      final Directory plugin1 =
+          createFakePlugin('plugin1', parentDirectoryName: 'plugin1');
+      createFakePlugin('plugin2');
+      createFakePlugin('plugin3');
+      await runner.run(<String>[
+        'sample',
+        '--exclude=plugin2,plugin3',
+        '--base-sha=master',
+        '--run-on-changed-packages'
+      ]);
+
+      expect(plugins, unorderedEquals(<String>[plugin1.path]));
+    });
+  });
+
+  group('$GitVersionFinder', () {
+    List<List<String>> gitDirCommands;
+    String gitDiffResponse;
+    String mergeBaseResponse;
+    MockGitDir gitDir;
+
+    setUp(() {
+      gitDirCommands = <List<String>>[];
+      gitDiffResponse = '';
+      gitDir = MockGitDir();
+      when(gitDir.runCommand(any)).thenAnswer((Invocation invocation) {
+        gitDirCommands.add(invocation.positionalArguments[0]);
+        final MockProcessResult mockProcessResult = MockProcessResult();
+        if (invocation.positionalArguments[0][0] == 'diff') {
+          when<String>(mockProcessResult.stdout).thenReturn(gitDiffResponse);
+        } else if (invocation.positionalArguments[0][0] == 'merge-base') {
+          when<String>(mockProcessResult.stdout).thenReturn(mergeBaseResponse);
+        }
+        return Future<ProcessResult>.value(mockProcessResult);
+      });
+      initializeFakePackages();
+      processRunner = RecordingProcessRunner();
+    });
+
+    tearDown(() {
+      cleanupPackages();
+    });
+
+    test('No git diff should result no files changed', () async {
+      final GitVersionFinder finder = GitVersionFinder(gitDir, 'some base sha');
+      List<String> changedFiles = await finder.getChangedFiles();
+
+      expect(changedFiles, isEmpty);
+    });
+
+    test('get correct files changed based on git diff', () async {
+      gitDiffResponse = '''
+file1/file1.cc
+file2/file2.cc
+''';
+      final GitVersionFinder finder = GitVersionFinder(gitDir, 'some base sha');
+      List<String> changedFiles = await finder.getChangedFiles();
+
+      expect(
+          changedFiles, equals(<String>['file1/file1.cc', 'file2/file2.cc']));
+    });
+
+    test('get correct pubspec change based on git diff', () async {
+      gitDiffResponse = '''
+file1/pubspec.yaml
+file2/file2.cc
+''';
+      final GitVersionFinder finder = GitVersionFinder(gitDir, 'some base sha');
+      List<String> changedFiles = await finder.getChangedPubSpecs();
+
+      expect(changedFiles, equals(<String>['file1/pubspec.yaml']));
+    });
+
+    test('use correct base sha if not specified', () async {
+      mergeBaseResponse = 'shaqwiueroaaidf12312jnadf123nd';
+      gitDiffResponse = '''
+file1/pubspec.yaml
+file2/file2.cc
+''';
+      final GitVersionFinder finder = GitVersionFinder(gitDir, null);
+      await finder.getChangedFiles();
+      verify(gitDir
+          .runCommand(['diff', '--name-only', mergeBaseResponse, 'HEAD']));
+    });
+
+    test('use correct base sha if specified', () async {
+      final String customBaseSha = 'aklsjdcaskf12312';
+      gitDiffResponse = '''
+file1/pubspec.yaml
+file2/file2.cc
+''';
+      final GitVersionFinder finder = GitVersionFinder(gitDir, customBaseSha);
+      await finder.getChangedFiles();
+      verify(gitDir.runCommand(['diff', '--name-only', customBaseSha, 'HEAD']));
+    });
+  });
 }
 
 class SamplePluginCommand extends PluginCommand {
@@ -81,7 +300,9 @@
     Directory packagesDir,
     FileSystem fileSystem, {
     ProcessRunner processRunner = const ProcessRunner(),
-  }) : super(packagesDir, fileSystem, processRunner: processRunner);
+    GitDir gitDir,
+  }) : super(packagesDir, fileSystem,
+            processRunner: processRunner, gitDir: gitDir);
 
   List<String> plugins_;
 
@@ -98,3 +319,7 @@
     }
   }
 }
+
+class MockGitDir extends Mock implements GitDir {}
+
+class MockProcessResult extends Mock implements ProcessResult {}
diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart
index ec0000d..1538d9b 100644
--- a/script/tool/test/util.dart
+++ b/script/tool/test/util.dart
@@ -37,6 +37,8 @@
   bool isLinuxPlugin = false,
   bool isMacOsPlugin = false,
   bool isWindowsPlugin = false,
+  bool includeChangeLog = false,
+  bool includeVersion = false,
   String parentDirectoryName = '',
 }) {
   assert(!(withSingleExample && withExamples.isNotEmpty),
@@ -57,7 +59,14 @@
     isLinuxPlugin: isLinuxPlugin,
     isMacOsPlugin: isMacOsPlugin,
     isWindowsPlugin: isWindowsPlugin,
+    includeVersion: includeVersion,
   );
+  if (includeChangeLog) {
+    createFakeCHANGELOG(pluginDirectory, '''
+## 0.0.1
+  * Some changes.
+  ''');
+  }
 
   if (withSingleExample) {
     final Directory exampleDir = pluginDirectory.childDirectory('example')
@@ -85,6 +94,11 @@
   return pluginDirectory;
 }
 
+void createFakeCHANGELOG(Directory parent, String texts) {
+  parent.childFile('CHANGELOG.md').createSync();
+  parent.childFile('CHANGELOG.md').writeAsStringSync(texts);
+}
+
 /// Creates a `pubspec.yaml` file with a flutter dependency.
 void createFakePubspec(
   Directory parent, {
@@ -97,6 +111,7 @@
   bool isLinuxPlugin = false,
   bool isMacOsPlugin = false,
   bool isWindowsPlugin = false,
+  String version = '0.0.1',
 }) {
   parent.childFile('pubspec.yaml').createSync();
   String yaml = '''
@@ -152,8 +167,8 @@
   }
   if (includeVersion) {
     yaml += '''
-version: 0.0.1
-publish_to: none # Hardcoded safeguard to prevent this from somehow being published by a broken test.
+version: $version
+publish_to: http://no_pub_server.com # Hardcoded safeguard to prevent this from somehow being published by a broken test.
 ''';
   }
   parent.childFile('pubspec.yaml').writeAsStringSync(yaml);
diff --git a/script/tool/test/version_check_test.dart b/script/tool/test/version_check_test.dart
index b9ace38..ac0d378 100644
--- a/script/tool/test/version_check_test.dart
+++ b/script/tool/test/version_check_test.dart
@@ -2,6 +2,7 @@
 import 'dart:io';
 
 import 'package:args/command_runner.dart';
+import 'package:flutter_plugin_tools/src/common.dart';
 import 'package:git/git.dart';
 import 'package:mockito/mockito.dart';
 import "package:test/test.dart";
@@ -74,18 +75,18 @@
     });
 
     test('allows valid version', () async {
-      createFakePlugin('plugin');
+      createFakePlugin('plugin', includeChangeLog: true, includeVersion: true);
       gitDiffResponse = "packages/plugin/pubspec.yaml";
       gitShowResponses = <String, String>{
         'master:packages/plugin/pubspec.yaml': 'version: 1.0.0',
         'HEAD:packages/plugin/pubspec.yaml': 'version: 2.0.0',
       };
       final List<String> output = await runCapturingPrint(
-          runner, <String>['version-check', '--base_sha=master']);
+          runner, <String>['version-check', '--base-sha=master']);
 
       expect(
         output,
-        orderedEquals(<String>[
+        containsAllInOrder(<String>[
           'No version check errors found!',
         ]),
       );
@@ -99,14 +100,14 @@
     });
 
     test('denies invalid version', () async {
-      createFakePlugin('plugin');
+      createFakePlugin('plugin', includeChangeLog: true, includeVersion: true);
       gitDiffResponse = "packages/plugin/pubspec.yaml";
       gitShowResponses = <String, String>{
         'master:packages/plugin/pubspec.yaml': 'version: 0.0.1',
         'HEAD:packages/plugin/pubspec.yaml': 'version: 0.2.0',
       };
       final Future<List<String>> result = runCapturingPrint(
-          runner, <String>['version-check', '--base_sha=master']);
+          runner, <String>['version-check', '--base-sha=master']);
 
       await expectLater(
         result,
@@ -122,7 +123,7 @@
     });
 
     test('gracefully handles missing pubspec.yaml', () async {
-      createFakePlugin('plugin');
+      createFakePlugin('plugin', includeChangeLog: true, includeVersion: true);
       gitDiffResponse = "packages/plugin/pubspec.yaml";
       mockFileSystem.currentDirectory
           .childDirectory('packages')
@@ -130,11 +131,12 @@
           .childFile('pubspec.yaml')
           .deleteSync();
       final List<String> output = await runCapturingPrint(
-          runner, <String>['version-check', '--base_sha=master']);
+          runner, <String>['version-check', '--base-sha=master']);
 
       expect(
         output,
         orderedEquals(<String>[
+          'Determine diff with base sha: master',
           'No version check errors found!',
         ]),
       );
@@ -144,7 +146,8 @@
     });
 
     test('allows minor changes to platform interfaces', () async {
-      createFakePlugin('plugin_platform_interface');
+      createFakePlugin('plugin_platform_interface',
+          includeChangeLog: true, includeVersion: true);
       gitDiffResponse = "packages/plugin_platform_interface/pubspec.yaml";
       gitShowResponses = <String, String>{
         'master:packages/plugin_platform_interface/pubspec.yaml':
@@ -153,10 +156,10 @@
             'version: 1.1.0',
       };
       final List<String> output = await runCapturingPrint(
-          runner, <String>['version-check', '--base_sha=master']);
+          runner, <String>['version-check', '--base-sha=master']);
       expect(
         output,
-        orderedEquals(<String>[
+        containsAllInOrder(<String>[
           'No version check errors found!',
         ]),
       );
@@ -172,7 +175,8 @@
     });
 
     test('disallows breaking changes to platform interfaces', () async {
-      createFakePlugin('plugin_platform_interface');
+      createFakePlugin('plugin_platform_interface',
+          includeChangeLog: true, includeVersion: true);
       gitDiffResponse = "packages/plugin_platform_interface/pubspec.yaml";
       gitShowResponses = <String, String>{
         'master:packages/plugin_platform_interface/pubspec.yaml':
@@ -181,7 +185,7 @@
             'version: 2.0.0',
       };
       final Future<List<String>> output = runCapturingPrint(
-          runner, <String>['version-check', '--base_sha=master']);
+          runner, <String>['version-check', '--base-sha=master']);
       await expectLater(
         output,
         throwsA(const TypeMatcher<Error>()),
@@ -196,6 +200,138 @@
       expect(gitDirCommands[2].join(' '),
           equals('show HEAD:packages/plugin_platform_interface/pubspec.yaml'));
     });
+
+    test('Allow empty lines in front of the first version in CHANGELOG',
+        () async {
+      createFakePlugin('plugin', includeChangeLog: true, includeVersion: true);
+
+      final Directory pluginDirectory =
+          mockPackagesDir.childDirectory('plugin');
+
+      createFakePubspec(pluginDirectory,
+          isFlutter: true, includeVersion: true, version: '1.0.1');
+      String changelog = '''
+      
+
+
+## 1.0.1
+
+* Some changes.
+''';
+      createFakeCHANGELOG(pluginDirectory, changelog);
+      final List<String> output = await runCapturingPrint(
+          runner, <String>['version-check', '--base-sha=master']);
+      await expect(
+        output,
+        containsAllInOrder([
+          'Checking the first version listed in CHANGELOG.MD matches the version in pubspec.yaml for plugin.',
+          'plugin passed version check',
+          'No version check errors found!'
+        ]),
+      );
+    });
+
+    test('Throws if versions in changelog and pubspec do not match', () async {
+      createFakePlugin('plugin', includeChangeLog: true, includeVersion: true);
+
+      final Directory pluginDirectory =
+          mockPackagesDir.childDirectory('plugin');
+
+      createFakePubspec(pluginDirectory,
+          isFlutter: true, includeVersion: true, version: '1.0.1');
+      String changelog = '''
+## 1.0.2
+
+* Some changes.
+''';
+      createFakeCHANGELOG(pluginDirectory, changelog);
+      final Future<List<String>> output = runCapturingPrint(
+          runner, <String>['version-check', '--base-sha=master']);
+      await expectLater(
+        output,
+        throwsA(const TypeMatcher<Error>()),
+      );
+      try {
+        List<String> outputValue = await output;
+        await expectLater(
+          outputValue,
+          containsAllInOrder([
+            '''
+  versions for plugin in CHANGELOG.md and pubspec.yaml do not match.
+  The version in pubspec.yaml is 1.0.1.
+  The first version listed in CHANGELOG.md is 1.0.2.
+  ''',
+          ]),
+        );
+      } on ToolExit catch (_) {}
+    });
+
+    test('Success if CHANGELOG and pubspec versions match', () async {
+      createFakePlugin('plugin', includeChangeLog: true, includeVersion: true);
+
+      final Directory pluginDirectory =
+          mockPackagesDir.childDirectory('plugin');
+
+      createFakePubspec(pluginDirectory,
+          isFlutter: true, includeVersion: true, version: '1.0.1');
+      String changelog = '''
+## 1.0.1
+
+* Some changes.
+''';
+      createFakeCHANGELOG(pluginDirectory, changelog);
+      final List<String> output = await runCapturingPrint(
+          runner, <String>['version-check', '--base-sha=master']);
+      await expect(
+        output,
+        containsAllInOrder([
+          'Checking the first version listed in CHANGELOG.MD matches the version in pubspec.yaml for plugin.',
+          'plugin passed version check',
+          'No version check errors found!'
+        ]),
+      );
+    });
+
+    test(
+        'Fail if pubspec version only matches an older version listed in CHANGELOG',
+        () async {
+      createFakePlugin('plugin', includeChangeLog: true, includeVersion: true);
+
+      final Directory pluginDirectory =
+          mockPackagesDir.childDirectory('plugin');
+
+      createFakePubspec(pluginDirectory,
+          isFlutter: true, includeVersion: true, version: '1.0.0');
+      String changelog = '''
+## 1.0.1
+
+* Some changes.
+
+## 1.0.0
+
+* Some other changes.
+''';
+      createFakeCHANGELOG(pluginDirectory, changelog);
+      Future<List<String>> output = runCapturingPrint(
+          runner, <String>['version-check', '--base-sha=master']);
+      await expectLater(
+        output,
+        throwsA(const TypeMatcher<Error>()),
+      );
+      try {
+        List<String> outputValue = await output;
+        await expectLater(
+          outputValue,
+          containsAllInOrder([
+            '''
+  versions for plugin in CHANGELOG.md and pubspec.yaml do not match.
+  The version in pubspec.yaml is 1.0.0.
+  The first version listed in CHANGELOG.md is 1.0.1.
+  ''',
+          ]),
+        );
+      } on ToolExit catch (_) {}
+    });
   });
 
   group("Pre 1.0", () {