Add --force to `roll_dev.dart` (#56501)

diff --git a/dev/tools/lib/roll_dev.dart b/dev/tools/lib/roll_dev.dart
index cc766b3..12ead14 100644
--- a/dev/tools/lib/roll_dev.dart
+++ b/dev/tools/lib/roll_dev.dart
@@ -10,6 +10,7 @@
 import 'dart:io';
 
 import 'package:args/args.dart';
+import 'package:meta/meta.dart';
 
 const String kIncrement = 'increment';
 const String kX = 'x';
@@ -20,12 +21,119 @@
 const String kJustPrint = 'just-print';
 const String kYes = 'yes';
 const String kHelp = 'help';
+const String kForce = 'force';
 
 const String kUpstreamRemote = 'git@github.com:flutter/flutter.git';
 
 void main(List<String> args) {
   final ArgParser argParser = ArgParser(allowTrailingOptions: false);
 
+  ArgResults argResults;
+  try {
+    argResults = parseArguments(argParser, args);
+  } on ArgParserException catch (error) {
+    print(error.message);
+    print(argParser.usage);
+    exit(1);
+  }
+
+  try {
+    run(
+      usage: argParser.usage,
+      argResults: argResults,
+      git: const Git(),
+    );
+  } on Exception catch (e) {
+    print(e.toString());
+    exit(1);
+  }
+}
+
+/// Main script execution.
+///
+/// Returns true if publishing was successful, else false.
+bool run({
+  @required String usage,
+  @required ArgResults argResults,
+  @required Git git,
+}) {
+  final String level = argResults[kIncrement] as String;
+  final String commit = argResults[kCommit] as String;
+  final String origin = argResults[kOrigin] as String;
+  final bool justPrint = argResults[kJustPrint] as bool;
+  final bool autoApprove = argResults[kYes] as bool;
+  final bool help = argResults[kHelp] as bool;
+  final bool force = argResults[kForce] as bool;
+
+  if (help || level == null || commit == null) {
+    print(
+      'roll_dev.dart --increment=level --commit=hash • update the version tags '
+      'and roll a new dev build.\n$usage'
+    );
+    return false;
+  }
+
+  final String remote = git.getOutput(
+    'remote get-url $origin',
+    'check whether this is a flutter checkout',
+  );
+  if (remote != kUpstreamRemote) {
+    throw Exception(
+      'The current directory is not a Flutter repository checkout with a '
+      'correctly configured upstream remote.\nFor more details see: '
+      'https://github.com/flutter/flutter/wiki/Release-process'
+    );
+  }
+
+  if (git.getOutput('status --porcelain', 'check status of your local checkout') != '') {
+    throw Exception(
+      'Your git repository is not clean. Try running "git clean -fd". Warning, '
+      'this will delete files! Run with -n to find out which ones.'
+    );
+  }
+
+  // TODO(fujino): move this after `justPrint`
+  git.run('fetch $origin', 'fetch $origin');
+  git.run('reset $commit --hard', 'reset to the release commit');
+
+  String version = getFullTag(git);
+
+  version = incrementLevel(version, level);
+
+  if (justPrint) {
+    print(version);
+    return false;
+  }
+
+  final String hash = git.getOutput('rev-parse HEAD', 'Get git hash for $commit');
+
+  git.run('tag $version', 'tag the commit with the version label');
+
+  // PROMPT
+
+  if (autoApprove) {
+    print('Publishing Flutter $version (${hash.substring(0, 10)}) to the "dev" channel.');
+  } else {
+    print('Your tree is ready to publish Flutter $version (${hash.substring(0, 10)}) '
+      'to the "dev" channel.');
+    stdout.write('Are you? [yes/no] ');
+    if (stdin.readLineSync() != 'yes') {
+      git.run('tag -d $version', 'remove the tag you did not want to publish');
+      print('The dev roll has been aborted.');
+      return false;
+    }
+  }
+
+  git.run('push $origin $version', 'publish the version');
+  git.run(
+    'push ${force ? "--force " : ""}$origin HEAD:dev',
+    'land the new version on the "dev" branch',
+  );
+  print('Flutter version $version has been rolled to the "dev" channel!');
+  return true;
+}
+
+ArgResults parseArguments(ArgParser argParser, List<String> args) {
   argParser.addOption(
     kIncrement,
     help: 'Specifies which part of the x.y.z version number to increment. Required.',
@@ -50,6 +158,12 @@
     defaultsTo: 'upstream',
   );
   argParser.addFlag(
+    kForce,
+    abbr: 'f',
+    help: 'Force push. Necessary when the previous release had cherry-picks.',
+    negatable: false,
+  );
+  argParser.addFlag(
     kJustPrint,
     negatable: false,
     help:
@@ -59,59 +173,92 @@
   argParser.addFlag(kYes, negatable: false, abbr: 'y', help: 'Skip the confirmation prompt.');
   argParser.addFlag(kHelp, negatable: false, help: 'Show this help message.', hide: true);
 
-  ArgResults argResults;
-  try {
-    argResults = argParser.parse(args);
-  } on ArgParserException catch (error) {
-    print(error.message);
-    print(argParser.usage);
+  return argParser.parse(args);
+}
+
+/// Obtain the version tag of the previous dev release.
+String getFullTag(Git git) {
+  const String glob = '*.*.*-*.*.pre';
+  // describe the latest dev release
+  const String ref = 'refs/heads/dev';
+  return git.getOutput(
+    'describe --match $glob --exact-match --tags $ref',
+    'obtain last released version number',
+  );
+}
+
+Match parseFullTag(String version) {
+  // of the form: x.y.z-m.n.pre
+  final RegExp versionPattern = RegExp(
+    r'^(\d+)\.(\d+)\.(\d+)-(\d+)\.(\d+)\.pre$');
+  return versionPattern.matchAsPrefix(version);
+}
+
+String getVersionFromParts(List<int> parts) {
+  // where parts correspond to [x, y, z, m, n] from tag
+  assert(parts.length == 5);
+  final StringBuffer buf = StringBuffer()
+    // take x, y, and z
+    ..write(parts.take(3).join('.'))
+    ..write('-')
+    // skip x, y, and z, take m and n
+    ..write(parts.skip(3).take(2).join('.'))
+    ..write('.pre');
+  // return a string that looks like: '1.2.3-4.5.pre'
+  return buf.toString();
+}
+
+class Git {
+  const Git();
+
+  String getOutput(String command, String explanation) {
+    final ProcessResult result = _run(command);
+    if ((result.stderr as String).isEmpty && result.exitCode == 0)
+      return (result.stdout as String).trim();
+    _reportFailureAndExit(result, explanation);
+    return null; // for the analyzer's sake
+  }
+
+  void run(String command, String explanation) {
+    final ProcessResult result = _run(command);
+    if (result.exitCode != 0)
+      _reportFailureAndExit(result, explanation);
+  }
+
+  ProcessResult _run(String command) {
+    return Process.runSync('git', command.split(' '));
+  }
+
+  void _reportFailureAndExit(ProcessResult result, String explanation) {
+    if (result.exitCode != 0) {
+      print('Failed to $explanation. Git exited with error code ${result.exitCode}.');
+    } else {
+      print('Failed to $explanation.');
+    }
+    if ((result.stdout as String).isNotEmpty)
+      print('stdout from git:\n${result.stdout}\n');
+    if ((result.stderr as String).isNotEmpty)
+      print('stderr from git:\n${result.stderr}\n');
     exit(1);
   }
+}
 
-  final String level = argResults[kIncrement] as String;
-  final String commit = argResults[kCommit] as String;
-  final String origin = argResults[kOrigin] as String;
-  final bool justPrint = argResults[kJustPrint] as bool;
-  final bool autoApprove = argResults[kYes] as bool;
-  final bool help = argResults[kHelp] as bool;
-
-  if (help || level == null || commit == null) {
-    print('roll_dev.dart --increment=level --commit=hash • update the version tags and roll a new dev build.\n');
-    print(argParser.usage);
-    exit(0);
-  }
-
-  if (getGitOutput('remote get-url $origin', 'check whether this is a flutter checkout') != kUpstreamRemote) {
-    print('The current directory is not a Flutter repository checkout with a correctly configured upstream remote.');
-    print('For more details see: https://github.com/flutter/flutter/wiki/Release-process');
-    exit(1);
-  }
-
-  if (getGitOutput('status --porcelain', 'check status of your local checkout') != '') {
-    print('Your git repository is not clean. Try running "git clean -fd". Warning, this ');
-    print('will delete files! Run with -n to find out which ones.');
-    exit(1);
-  }
-
-  runGit('fetch $origin', 'fetch $origin');
-  runGit('reset $commit --hard', 'reset to the release commit');
-
-  String version = getFullTag();
+/// Return a copy of the [version] with [level] incremented by one.
+String incrementLevel(String version, String level) {
   final Match match = parseFullTag(version);
   if (match == null) {
-    print('Could not determine the version for this build.');
-    if (version.isNotEmpty)
-      print('Git reported the latest version as "$version", which does not fit the expected pattern.');
-    exit(1);
+    String errorMessage;
+    if (version.isEmpty) {
+      errorMessage = 'Could not determine the version for this build.';
+    } else {
+      errorMessage = 'Git reported the latest version as "$version", which '
+          'does not fit the expected pattern.';
+    }
+    throw Exception(errorMessage);
   }
 
   final List<int> parts = match.groups(<int>[1, 2, 3, 4, 5]).map<int>(int.parse).toList();
 
-  if (match.group(6) == '0') {
-    print('This commit has already been released, as version ${getVersionFromParts(parts)}.');
-    exit(0);
-  }
-
   switch (level) {
     case kX:
       parts[0] += 1;
@@ -132,96 +279,7 @@
       parts[4] = 0;
       break;
     default:
-      print('Unknown increment level. The valid values are "$kX", "$kY", and "$kZ".');
-      exit(1);
+      throw Exception('Unknown increment level. The valid values are "$kX", "$kY", and "$kZ".');
   }
-  version = getVersionFromParts(parts);
-
-  if (justPrint) {
-    print(version);
-    exit(0);
-  }
-
-  final String hash = getGitOutput('rev-parse HEAD', 'Get git hash for $commit');
-
-  runGit('tag $version', 'tag the commit with the version label');
-
-  // PROMPT
-
-  if (autoApprove) {
-    print('Publishing Flutter $version (${hash.substring(0, 10)}) to the "dev" channel.');
-  } else {
-    print('Your tree is ready to publish Flutter $version (${hash.substring(0, 10)}) '
-      'to the "dev" channel.');
-    stdout.write('Are you? [yes/no] ');
-    if (stdin.readLineSync() != 'yes') {
-      runGit('tag -d $version', 'remove the tag you did not want to publish');
-      print('The dev roll has been aborted.');
-      exit(0);
-    }
-  }
-
-  runGit('push $origin $version', 'publish the version');
-  runGit('push $origin HEAD:dev', 'land the new version on the "dev" branch');
-  print('Flutter version $version has been rolled to the "dev" channel!');
-}
-
-String getFullTag() {
-  const String glob = '*.*.*-*.*.pre';
-  return getGitOutput(
-    'describe --match $glob --first-parent --long --tags',
-    'obtain last released version number',
-  );
-}
-
-Match parseFullTag(String version) {
-  // of the form: x.y.z-m.n.pre-c-g<revision>
-  final RegExp versionPattern = RegExp(
-    r'^(\d+)\.(\d+)\.(\d+)-(\d+)\.(\d+)\.pre-(\d+)-g([a-f0-9]+)$');
-  return versionPattern.matchAsPrefix(version);
-}
-
-String getVersionFromParts(List<int> parts) {
-  // where parts correspond to [x, y, z, m, n] from tag
-  assert(parts.length == 5);
-  final StringBuffer buf = StringBuffer()
-    // take x, y, and z
-    ..write(parts.take(3).join('.'))
-    ..write('-')
-    // skip x, y, and z, take m and n
-    ..write(parts.skip(3).take(2).join('.'))
-    ..write('.pre');
-  // return a string that looks like: '1.2.3-4.5.pre'
-  return buf.toString();
-}
-
-String getGitOutput(String command, String explanation) {
-  final ProcessResult result = _runGit(command);
-  if ((result.stderr as String).isEmpty && result.exitCode == 0)
-    return (result.stdout as String).trim();
-  _reportGitFailureAndExit(result, explanation);
-  return null; // for the analyzer's sake
-}
-
-void runGit(String command, String explanation) {
-  final ProcessResult result = _runGit(command);
-  if (result.exitCode != 0)
-    _reportGitFailureAndExit(result, explanation);
-}
-
-ProcessResult _runGit(String command) {
-  return Process.runSync('git', command.split(' '));
-}
-
-void _reportGitFailureAndExit(ProcessResult result, String explanation) {
-  if (result.exitCode != 0) {
-    print('Failed to $explanation. Git exited with error code ${result.exitCode}.');
-  } else {
-    print('Failed to $explanation.');
-  }
-  if ((result.stdout as String).isNotEmpty)
-    print('stdout from git:\n${result.stdout}\n');
-  if ((result.stderr as String).isNotEmpty)
-    print('stderr from git:\n${result.stderr}\n');
-  exit(1);
+  return getVersionFromParts(parts);
 }
diff --git a/dev/tools/test/roll_dev_test.dart b/dev/tools/test/roll_dev_test.dart
index f456fa4..600dda8 100644
--- a/dev/tools/test/roll_dev_test.dart
+++ b/dev/tools/test/roll_dev_test.dart
@@ -2,20 +2,227 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'package:args/args.dart';
 import 'package:dev_tools/roll_dev.dart';
+import 'package:mockito/mockito.dart';
+
 import './common.dart';
 
 void main() {
+  group('run()', () {
+    const String usage = 'usage info...';
+    const String level = 'z';
+    const String commit = 'abcde012345';
+    const String origin = 'upstream';
+    FakeArgResults fakeArgResults;
+    MockGit mockGit;
+
+    setUp(() {
+      mockGit = MockGit();
+    });
+
+    test('returns false if help requested', () {
+      fakeArgResults = FakeArgResults(
+        level: level,
+        commit: commit,
+        origin: origin,
+        justPrint: false,
+        autoApprove: true,
+        help: true,
+      );
+      expect(
+        run(
+          usage: usage,
+          argResults: fakeArgResults,
+          git: mockGit,
+        ),
+        false,
+      );
+    });
+
+    test('returns false if level not provided', () {
+      fakeArgResults = FakeArgResults(
+        level: level,
+        commit: commit,
+        origin: origin,
+        justPrint: false,
+        autoApprove: true,
+        help: true,
+      );
+      expect(
+        run(
+          usage: usage,
+          argResults: fakeArgResults,
+          git: mockGit,
+        ),
+        false,
+      );
+    });
+
+    test('returns false if commit not provided', () {
+      fakeArgResults = FakeArgResults(
+        level: level,
+        commit: commit,
+        origin: origin,
+        justPrint: false,
+        autoApprove: true,
+        help: true,
+      );
+      expect(
+        run(
+          usage: usage,
+          argResults: fakeArgResults,
+          git: mockGit,
+        ),
+        false,
+      );
+    });
+
+    test('throws exception if upstream remote wrong', () {
+      when(mockGit.getOutput('remote get-url $origin', any)).thenReturn('wrong-remote');
+      fakeArgResults = FakeArgResults(
+        level: level,
+        commit: commit,
+        origin: origin,
+        justPrint: false,
+        autoApprove: true,
+        help: false,
+      );
+      Exception exception;
+      try {
+        run(
+          usage: usage,
+          argResults: fakeArgResults,
+          git: mockGit,
+        );
+      } on Exception catch (e) {
+        exception = e;
+      }
+      const String pattern = r'The current directory is not a Flutter '
+        'repository checkout with a correctly configured upstream remote.';
+      expect(exception?.toString(), contains(pattern));
+    });
+
+    test('throws exception if git checkout not clean', () {
+      when(mockGit.getOutput('remote get-url $origin', any)).thenReturn(kUpstreamRemote);
+      when(mockGit.getOutput('status --porcelain', any)).thenReturn(
+        ' M dev/tools/test/roll_dev_test.dart',
+      );
+      fakeArgResults = FakeArgResults(
+        level: level,
+        commit: commit,
+        origin: origin,
+        justPrint: false,
+        autoApprove: true,
+        help: false,
+      );
+      Exception exception;
+      try {
+        run(
+          usage: usage,
+          argResults: fakeArgResults,
+          git: mockGit,
+        );
+      } on Exception catch (e) {
+        exception = e;
+      }
+      const String pattern = r'Your git repository is not clean. Try running '
+        '"git clean -fd". Warning, this will delete files! Run with -n to find '
+        'out which ones.';
+      expect(exception?.toString(), contains(pattern));
+    });
+
+    test('does not tag if --just-print is specified', () {
+      when(mockGit.getOutput('remote get-url $origin', any)).thenReturn(kUpstreamRemote);
+      when(mockGit.getOutput('status --porcelain', any)).thenReturn('');
+      when(mockGit.getOutput(
+        'describe --match *.*.*-*.*.pre --exact-match --tags refs/heads/dev',
+        any,
+      )).thenReturn('1.2.3-0.0.pre');
+      fakeArgResults = FakeArgResults(
+        level: level,
+        commit: commit,
+        origin: origin,
+        justPrint: true,
+        autoApprove: true,
+        help: false,
+      );
+      expect(run(
+        usage: usage,
+        argResults: fakeArgResults,
+        git: mockGit,
+      ), false);
+      verify(mockGit.run('fetch $origin', any));
+      verify(mockGit.run('reset $commit --hard', any));
+      verifyNever(mockGit.getOutput('rev-parse HEAD', any));
+    });
+
+    test('successfully tags and publishes release', () {
+      when(mockGit.getOutput('remote get-url $origin', any)).thenReturn(kUpstreamRemote);
+      when(mockGit.getOutput('status --porcelain', any)).thenReturn('');
+      when(mockGit.getOutput(
+        'describe --match *.*.*-*.*.pre --exact-match --tags refs/heads/dev',
+        any,
+      )).thenReturn('1.2.3-0.0.pre');
+      when(mockGit.getOutput('rev-parse HEAD', any)).thenReturn(commit);
+      fakeArgResults = FakeArgResults(
+        level: level,
+        commit: commit,
+        origin: origin,
+        justPrint: false,
+        autoApprove: true,
+        help: false,
+      );
+      expect(run(
+        usage: usage,
+        argResults: fakeArgResults,
+        git: mockGit,
+      ), true);
+      verify(mockGit.run('fetch $origin', any));
+      verify(mockGit.run('reset $commit --hard', any));
+      verify(mockGit.run('tag 1.2.0-1.0.pre', any));
+      verify(mockGit.run('push $origin HEAD:dev', any));
+    });
+
+    test('successfully publishes release with --force', () {
+      when(mockGit.getOutput('remote get-url $origin', any)).thenReturn(kUpstreamRemote);
+      when(mockGit.getOutput('status --porcelain', any)).thenReturn('');
+      when(mockGit.getOutput(
+        'describe --match *.*.*-*.*.pre --exact-match --tags refs/heads/dev',
+        any,
+      )).thenReturn('1.2.3-0.0.pre');
+      when(mockGit.getOutput('rev-parse HEAD', any)).thenReturn(commit);
+      fakeArgResults = FakeArgResults(
+        level: level,
+        commit: commit,
+        origin: origin,
+        justPrint: false,
+        autoApprove: true,
+        help: false,
+        force: true,
+      );
+      expect(run(
+        usage: usage,
+        argResults: fakeArgResults,
+        git: mockGit,
+      ), true);
+      verify(mockGit.run('fetch $origin', any));
+      verify(mockGit.run('reset $commit --hard', any));
+      verify(mockGit.run('tag 1.2.0-1.0.pre', any));
+      verify(mockGit.run('push --force $origin HEAD:dev', any));
+    });
+  });
+
   group('parseFullTag', () {
     test('returns match on valid version input', () {
       final List<String> validTags = <String>[
-        '1.2.3-1.2.pre-3-gabc123',
-        '10.2.30-12.22.pre-45-gabc123',
-        '1.18.0-0.0.pre-0-gf0adb240a',
-        '2.0.0-1.99.pre-45-gf0adb240a',
-        '12.34.56-78.90.pre-12-g9db2703a2',
-        '0.0.1-0.0.pre-1-g07601eb95ff82f01e870566586340ed2e87b9cbb',
-        '958.80.144-6.224.pre-7803-g06e90',
+        '1.2.3-1.2.pre',
+        '10.2.30-12.22.pre',
+        '1.18.0-0.0.pre',
+        '2.0.0-1.99.pre',
+        '12.34.56-78.90.pre',
+        '0.0.1-0.0.pre',
+        '958.80.144-6.224.pre',
       ];
       for (final String validTag in validTags) {
         final Match match = parseFullTag(validTag);
@@ -25,15 +232,15 @@
 
     test('returns null on invalid version input', () {
       final List<String> invalidTags = <String>[
-        '1.2.3-dev.1.2-3-gabc123',
-        '1.2.3-1.2-3-gabc123',
+        '1.2.3-1.2.pre-3-gabc123',
+        '1.2.3-1.2.3.pre',
+        '1.2.3.1.2.pre',
+        '1.2.3-dev.1.2',
+        '1.2.3-1.2-3',
         'v1.2.3',
         '2.0.0',
-        'v1.2.3-1.2.pre-3-gabc123',
-        '10.0.1-0.0.pre-gf0adb240a',
-        '10.0.1-0.0.pre-3-gggggggggg',
-        '1.2.3-1.2.pre-3-abc123',
-        '1.2.3-1.2.pre-3-gabc123_',
+        'v1.2.3-1.2.pre',
+        '1.2.3-1.2.pre_',
       ];
       for (final String invalidTag in invalidTags) {
         final Match match = parseFullTag(invalidTag);
@@ -51,4 +258,124 @@
       expect(getVersionFromParts(parts), '11.2.33-1.0.pre');
     });
   });
+
+  group('incrementLevel()', () {
+    const String hash = 'abc123';
+
+    test('throws exception if hash is not valid release candidate', () {
+      String level = 'z';
+
+      String version = '1.0.0-0.0.pre-1-g$hash';
+      expect(
+        () => incrementLevel(version, level),
+        throwsException,
+        reason: 'should throw because $version should be an exact tag',
+      );
+
+      version = '1.2.3';
+      expect(
+        () => incrementLevel(version, level),
+        throwsException,
+        reason: 'should throw because $version should be a dev tag, not stable.'
+      );
+
+      version = '1.0.0-0.0.pre-1-g$hash';
+      level = 'q';
+      expect(
+        () => incrementLevel(version, level),
+        throwsException,
+        reason: 'should throw because $level is unsupported',
+      );
+    });
+
+    test('successfully increments x', () {
+      const String level = 'x';
+
+      String version = '1.0.0-0.0.pre';
+      expect(incrementLevel(version, level), '2.0.0-0.0.pre');
+
+      version = '10.20.0-40.50.pre';
+      expect(incrementLevel(version, level), '11.0.0-0.0.pre');
+
+      version = '1.18.0-3.0.pre';
+      expect(incrementLevel(version, level), '2.0.0-0.0.pre');
+    });
+
+    test('successfully increments y', () {
+      const String level = 'y';
+
+      String version = '1.0.0-0.0.pre';
+      expect(incrementLevel(version, level), '1.1.0-0.0.pre');
+
+      version = '10.20.0-40.50.pre';
+      expect(incrementLevel(version, level), '10.21.0-0.0.pre');
+
+      version = '1.18.0-3.0.pre';
+      expect(incrementLevel(version, level), '1.19.0-0.0.pre');
+    });
+
+    test('successfully increments z', () {
+      const String level = 'z';
+
+      String version = '1.0.0-0.0.pre';
+      expect(incrementLevel(version, level), '1.0.0-1.0.pre');
+
+      version = '10.20.0-40.50.pre';
+      expect(incrementLevel(version, level), '10.20.0-41.0.pre');
+
+      version = '1.18.0-3.0.pre';
+      expect(incrementLevel(version, level), '1.18.0-4.0.pre');
+    });
+  });
 }
+
+class FakeArgResults implements ArgResults {
+  FakeArgResults({
+    String level,
+    String commit,
+    String origin,
+    bool justPrint,
+    bool autoApprove,
+    bool help,
+    bool force = false,
+  }) : _parsedArgs = <String, dynamic>{
+    'increment': level,
+    'commit': commit,
+    'origin': origin,
+    'just-print': justPrint,
+    'yes': autoApprove,
+    'help': help,
+    'force': force,
+  };
+
+  @override
+  String name;
+
+  @override
+  ArgResults command;
+
+  @override
+  final List<String> rest = <String>[];
+
+  @override
+  List<String> arguments;
+
+  final Map<String, dynamic> _parsedArgs;
+
+  @override
+  Iterable<String> get options {
+    return null;
+  }
+
+  @override
+  dynamic operator [](String name) {
+    return _parsedArgs[name];
+  }
+
+  @override
+  bool wasParsed(String name) {
+    return null;
+  }
+}
+
+class MockGit extends Mock implements Git {}