Flutter 1.17.0.dev.3.3 cherrypicks (#55871)

* [flutter_tools] fix type error in symbolize (#55212)

* [flutter_tools] check first for stable tag, then dev tag (#55342)

* [flutter_tools] enable `flutter upgrade` to support force pushed branches (#55594)

* [flutter_tools] catch ProcessException and throw ToolExit during upgrade (#55759)

* Update engine version to 376ad6a64b08aa26005e3f82aed26de2e290b572

Co-authored-by: Jonah Williams <campfish91@gmail.com>
Co-authored-by: Christopher Fujino <fujino@google.com>
Co-authored-by: Christopher Fujino <christopherfujino@gmail.com>
diff --git a/bin/internal/engine.version b/bin/internal/engine.version
index 7b9898d..0827024 100644
--- a/bin/internal/engine.version
+++ b/bin/internal/engine.version
@@ -1 +1 @@
-4c8c31f591882b3c668992d2e9da761118899f38
+376ad6a64b08aa26005e3f82aed26de2e290b572
diff --git a/packages/flutter_tools/lib/src/commands/symbolize.dart b/packages/flutter_tools/lib/src/commands/symbolize.dart
index c15a9ef..21cf3fa 100644
--- a/packages/flutter_tools/lib/src/commands/symbolize.dart
+++ b/packages/flutter_tools/lib/src/commands/symbolize.dart
@@ -113,9 +113,46 @@
   }
 }
 
+typedef SymbolsTransformer = StreamTransformer<String, String> Function(Uint8List);
+
+StreamTransformer<String, String> _defaultTransformer(Uint8List symbols) {
+  final Dwarf dwarf = Dwarf.fromBytes(symbols);
+  if (dwarf == null) {
+    throwToolExit('Failed to decode symbols file');
+  }
+  return DwarfStackTraceDecoder(dwarf, includeInternalFrames: true);
+}
+
+// A no-op transformer for `DwarfSymbolizationService.test`
+StreamTransformer<String, String> _testTransformer(Uint8List buffer) {
+  return StreamTransformer<String, String>.fromHandlers(
+    handleData: (String data, EventSink<String> sink) {
+      sink.add(data);
+    },
+    handleDone: (EventSink<String> sink) {
+      sink.close();
+    },
+    handleError: (dynamic error, StackTrace stackTrace, EventSink<String> sink) {
+      sink.addError(error, stackTrace);
+    }
+  );
+}
+
 /// A service which decodes stack traces from Dart applications.
 class DwarfSymbolizationService {
-  const DwarfSymbolizationService();
+  const DwarfSymbolizationService({
+    SymbolsTransformer symbolsTransformer = _defaultTransformer,
+  }) : _transformer = symbolsTransformer;
+
+  /// Create a DwarfSymbolizationService with a no-op transformer for testing.
+  @visibleForTesting
+  factory DwarfSymbolizationService.test() {
+    return const DwarfSymbolizationService(
+      symbolsTransformer: _testTransformer
+    );
+  }
+
+  final SymbolsTransformer _transformer;
 
   /// Decode a stack trace from [input] and place the results in [output].
   ///
@@ -129,17 +166,13 @@
     @required IOSink output,
     @required Uint8List symbols,
   }) async {
-    final Dwarf dwarf = Dwarf.fromBytes(symbols);
-    if (dwarf == null) {
-      throwToolExit('Failed to decode symbols file');
-    }
-
     final Completer<void> onDone = Completer<void>();
     StreamSubscription<void> subscription;
     subscription = input
+      .cast<List<int>>()
       .transform(const Utf8Decoder())
       .transform(const LineSplitter())
-      .transform(DwarfStackTraceDecoder(dwarf, includeInternalFrames: true))
+      .transform(_transformer(symbols))
       .listen((String line) {
         try {
           output.writeln(line);
diff --git a/packages/flutter_tools/lib/src/commands/upgrade.dart b/packages/flutter_tools/lib/src/commands/upgrade.dart
index 7b51c3b..21d1569 100644
--- a/packages/flutter_tools/lib/src/commands/upgrade.dart
+++ b/packages/flutter_tools/lib/src/commands/upgrade.dart
@@ -100,7 +100,12 @@
     @required FlutterVersion flutterVersion,
     @required bool testFlow,
   }) async {
-    await verifyUpstreamConfigured();
+    final String upstreamRevision = await fetchRemoteRevision();
+    if (flutterVersion.frameworkRevision == upstreamRevision) {
+      globals.printStatus('Flutter is already up to date on channel ${flutterVersion.channel}');
+      globals.printStatus('$flutterVersion');
+      return;
+    }
     if (!force && gitTagVersion == const GitTagVersion.unknown()) {
       // If the commit is a recognized branch and not master,
       // explain that we are avoiding potential damage.
@@ -132,12 +137,8 @@
     }
     recordState(flutterVersion);
     await upgradeChannel(flutterVersion);
-    final bool alreadyUpToDate = await attemptFastForward(flutterVersion);
-    if (alreadyUpToDate) {
-      // If the upgrade was a no op, then do not continue with the second half.
-      globals.printStatus('Flutter is already up to date on channel ${flutterVersion.channel}');
-      globals.printStatus('$flutterVersion');
-    } else if (!testFlow) {
+    await attemptReset(upstreamRevision);
+    if (!testFlow) {
       await flutterUpgradeContinue();
     }
   }
@@ -199,16 +200,25 @@
     return false;
   }
 
-  /// Check if there is an upstream repository configured.
+  /// Returns the remote HEAD revision.
   ///
   /// Exits tool if there is no upstream.
-  Future<void> verifyUpstreamConfigured() async {
+  Future<String> fetchRemoteRevision() async {
+    String revision;
     try {
+      // Fetch upstream branch's commits and tags
       await processUtils.run(
-        <String>[ 'git', 'rev-parse', '@{u}'],
+        <String>['git', 'fetch', '--tags'],
         throwOnError: true,
         workingDirectory: workingDirectory,
       );
+      // '@{u}' means upstream HEAD
+      final RunResult result = await processUtils.run(
+          <String>[ 'git', 'rev-parse', '--verify', '@{u}'],
+          throwOnError: true,
+          workingDirectory: workingDirectory,
+      );
+      revision = result.stdout.trim();
     } on Exception {
       throwToolExit(
         'Unable to upgrade Flutter: no origin repository configured. '
@@ -216,6 +226,7 @@
         "https://github.com/flutter/flutter' in $workingDirectory",
       );
     }
+    return revision;
   }
 
   /// Attempts to upgrade the channel.
@@ -227,33 +238,21 @@
     await ChannelCommand.upgradeChannel();
   }
 
-  /// Attempts to rebase the upstream onto the local branch.
+  /// Attempts a hard reset to the given revision.
   ///
-  /// If there haven't been any hot fixes or local changes, this is equivalent
-  /// to a fast-forward.
-  ///
-  /// If the fast forward lands us on the same channel and revision, then
-  /// returns true, otherwise returns false.
-  Future<bool> attemptFastForward(FlutterVersion oldFlutterVersion) async {
-    final int code = await processUtils.stream(
-      <String>['git', 'pull', '--ff-only'],
-      workingDirectory: workingDirectory,
-      mapFunction: (String line) => matchesGitLine(line) ? null : line,
-    );
-    if (code != 0) {
-      throwToolExit(null, exitCode: code);
-    }
-
-    // Check if the upgrade did anything.
-    bool alreadyUpToDate = false;
+  /// This is a reset instead of fast forward because if we are on a release
+  /// branch with cherry picks, there may not be a direct fast-forward route
+  /// to the next release.
+  Future<void> attemptReset(String newRevision) async {
     try {
-      final FlutterVersion newFlutterVersion = FlutterVersion(const SystemClock(), workingDirectory);
-      alreadyUpToDate = newFlutterVersion.channel == oldFlutterVersion.channel &&
-        newFlutterVersion.frameworkRevision == oldFlutterVersion.frameworkRevision;
-    } on Exception catch (e) {
-      globals.printTrace('Failed to determine FlutterVersion after upgrade fast-forward: $e');
+      await processUtils.run(
+        <String>['git', 'reset', '--hard', newRevision],
+        throwOnError: true,
+        workingDirectory: workingDirectory,
+      );
+    } on ProcessException catch (e) {
+      throwToolExit(e.message, exitCode: e.errorCode);
     }
-    return alreadyUpToDate;
   }
 
   /// Update the engine repository and precache all artifacts.
@@ -300,18 +299,4 @@
       allowReentrantFlutter: true,
     );
   }
-
-  //  dev/benchmarks/complex_layout/lib/main.dart        |  24 +-
-  static final RegExp _gitDiffRegex = RegExp(r' (\S+)\s+\|\s+\d+ [+-]+');
-
-  //  rename {packages/flutter/doc => dev/docs}/styles.html (92%)
-  //  delete mode 100644 doc/index.html
-  //  create mode 100644 dev/integration_tests/flutter_gallery/lib/gallery/demo.dart
-  static final RegExp _gitChangedRegex = RegExp(r' (rename|delete mode|create mode) .+');
-
-  static bool matchesGitLine(String line) {
-    return _gitDiffRegex.hasMatch(line)
-      || _gitChangedRegex.hasMatch(line)
-      || line == 'Fast-forward';
-  }
 }
diff --git a/packages/flutter_tools/lib/src/version.dart b/packages/flutter_tools/lib/src/version.dart
index bbd6cef..75b186e 100644
--- a/packages/flutter_tools/lib/src/version.dart
+++ b/packages/flutter_tools/lib/src/version.dart
@@ -752,80 +752,83 @@
         _runGit('git fetch $_flutterGit --tags', processUtils, workingDirectory);
       }
     }
-    // `--match` glob must match old version tag `v1.2.3` and new `1.2.3-dev.4.5`
-    return parse(_runGit('git describe --match *.*.* --first-parent --long --tags', processUtils, workingDirectory));
-  }
+    final List<String> tags = _runGit(
+      'git tag --contains HEAD', processUtils, workingDirectory).split('\n');
 
-  // TODO(fujino): Deprecate this https://github.com/flutter/flutter/issues/53850
-  /// Check for the release tag format of the form x.y.z-dev.m.n
-  static GitTagVersion parseLegacyVersion(String version) {
-    final RegExp versionPattern = RegExp(
-      r'^([0-9]+)\.([0-9]+)\.([0-9]+)(-dev\.[0-9]+\.[0-9]+)?-([0-9]+)-g([a-f0-9]+)$');
-    final List<String> parts = versionPattern.matchAsPrefix(version)?.groups(<int>[1, 2, 3, 4, 5, 6]);
-    if (parts == null) {
-      return const GitTagVersion.unknown();
+    // Check first for a stable tag
+    final RegExp stableTagPattern = RegExp(r'^\d+\.\d+\.\d+$');
+    for (final String tag in tags) {
+      final String trimmedTag = tag.trim();
+      if (stableTagPattern.hasMatch(trimmedTag)) {
+        return parse(trimmedTag);
+      }
     }
-    final List<int> parsedParts = parts.take(5).map<int>(
-      (String source) => source == null ? null : int.tryParse(source)).toList();
-    List<int> devParts = <int>[null, null];
-    if (parts[3] != null) {
-      devParts = RegExp(r'^-dev\.(\d+)\.(\d+)')
-        .matchAsPrefix(parts[3])
-        ?.groups(<int>[1, 2])
-        ?.map<int>(
-          (String source) => source == null ? null : int.tryParse(source)
-        )?.toList() ?? <int>[null, null];
+    // Next check for a dev tag
+    final RegExp devTagPattern = RegExp(r'^\d+\.\d+\.\d+-\d+\.\d+\.pre$');
+    for (final String tag in tags) {
+      final String trimmedTag = tag.trim();
+      if (devTagPattern.hasMatch(trimmedTag)) {
+        return parse(trimmedTag);
+      }
     }
-    return GitTagVersion(
-      x: parsedParts[0],
-      y: parsedParts[1],
-      z: parsedParts[2],
-      devVersion: devParts[0],
-      devPatch: devParts[1],
-      commits: parsedParts[4],
-      hash: parts[5],
-      gitTag: '${parts[0]}.${parts[1]}.${parts[2]}${parts[3] ?? ''}', // x.y.z-dev.m.n
+
+    // If we're not currently on a tag, use git describe to find the most
+    // recent tag and number of commits past.
+    return parse(
+      _runGit(
+        'git describe --match *.*.*-*.*.pre --first-parent --long --tags',
+        processUtils,
+        workingDirectory,
+      )
     );
   }
 
-  /// Check for the release tag format of the form x.y.z-m.n.pre
+  /// Parse a version string.
+  ///
+  /// The version string can either be an exact release tag (e.g. '1.2.3' for
+  /// stable or 1.2.3-4.5.pre for a dev) or the output of `git describe` (e.g.
+  /// for commit abc123 that is 6 commits after tag 1.2.3-4.5.pre, git would
+  /// return '1.2.3-4.5.pre-6-gabc123').
   static GitTagVersion parseVersion(String version) {
     final RegExp versionPattern = RegExp(
-      r'^(\d+)\.(\d+)\.(\d+)(-\d+\.\d+\.pre)?-(\d+)-g([a-f0-9]+)$');
-    final List<String> parts = versionPattern.matchAsPrefix(version)?.groups(<int>[1, 2, 3, 4, 5, 6]);
-    if (parts == null) {
+      r'^(\d+)\.(\d+)\.(\d+)(-\d+\.\d+\.pre)?(?:-(\d+)-g([a-f0-9]+))?$');
+    final Match match = versionPattern.firstMatch(version.trim());
+    if (match == null) {
       return const GitTagVersion.unknown();
     }
-    final List<int> parsedParts = parts.take(5).map<int>(
-      (String source) => source == null ? null : int.tryParse(source)).toList();
-    List<int> devParts = <int>[null, null];
-    if (parts[3] != null) {
-      devParts = RegExp(r'^-(\d+)\.(\d+)\.pre')
-        .matchAsPrefix(parts[3])
-        ?.groups(<int>[1, 2])
-        ?.map<int>(
-          (String source) => source == null ? null : int.tryParse(source)
-        )?.toList() ?? <int>[null, null];
+
+    final List<String> matchGroups = match.groups(<int>[1, 2, 3, 4, 5, 6]);
+    final int x = matchGroups[0] == null ? null : int.tryParse(matchGroups[0]);
+    final int y = matchGroups[1] == null ? null : int.tryParse(matchGroups[1]);
+    final int z = matchGroups[2] == null ? null : int.tryParse(matchGroups[2]);
+    final String devString = matchGroups[3];
+    int devVersion, devPatch;
+    if (devString != null) {
+      final Match devMatch = RegExp(r'^-(\d+)\.(\d+)\.pre$')
+        .firstMatch(devString);
+      final List<String> devGroups = devMatch.groups(<int>[1, 2]);
+      devVersion = devGroups[0] == null ? null : int.tryParse(devGroups[0]);
+      devPatch = devGroups[1] == null ? null : int.tryParse(devGroups[1]);
     }
+    // count of commits past last tagged version
+    final int commits = matchGroups[4] == null ? 0 : int.tryParse(matchGroups[4]);
+    final String hash = matchGroups[5] ?? '';
+
     return GitTagVersion(
-      x: parsedParts[0],
-      y: parsedParts[1],
-      z: parsedParts[2],
-      devVersion: devParts[0],
-      devPatch: devParts[1],
-      commits: parsedParts[4],
-      hash: parts[5],
-      gitTag: '${parts[0]}.${parts[1]}.${parts[2]}${parts[3] ?? ''}', // x.y.z-m.n.pre
+      x: x,
+      y: y,
+      z: z,
+      devVersion: devVersion,
+      devPatch: devPatch,
+      commits: commits,
+      hash: hash,
+      gitTag: '$x.$y.$z${devString ?? ''}', // e.g. 1.2.3-4.5.pre
     );
   }
 
   static GitTagVersion parse(String version) {
     GitTagVersion gitTagVersion;
 
-    gitTagVersion = parseLegacyVersion(version);
-    if (gitTagVersion != const GitTagVersion.unknown()) {
-      return gitTagVersion;
-    }
     gitTagVersion = parseVersion(version);
     if (gitTagVersion != const GitTagVersion.unknown()) {
       return gitTagVersion;
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/symbolize_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/symbolize_test.dart
index 9211837..17c1026 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/symbolize_test.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/symbolize_test.dart
@@ -2,10 +2,12 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:async';
 import 'dart:typed_data';
 
 import 'package:file/memory.dart';
 import 'package:flutter_tools/src/base/common.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
 import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/commands/symbolize.dart';
 import 'package:flutter_tools/src/convert.dart';
@@ -38,6 +40,24 @@
     applyMocksToCommand(command);
   });
 
+  testUsingContext('Regression test for type error in codec', () async {
+    final DwarfSymbolizationService symbolizationService = DwarfSymbolizationService.test();
+    final StreamController<List<int>> output = StreamController<List<int>>();
+
+    unawaited(symbolizationService.decode(
+      input: Stream<Uint8List>.fromIterable(<Uint8List>[
+        utf8.encode('Hello, World\n') as Uint8List,
+      ]),
+      symbols: Uint8List(0),
+      output: IOSink(output.sink),
+    ));
+
+    await expectLater(
+      output.stream.transform(utf8.decoder),
+      emits('Hello, World'),
+    );
+  });
+
 
   testUsingContext('symbolize exits when --debug-info argument is missing', () async {
     final Future<void> result = createTestCommandRunner(command)
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/version_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/version_test.dart
index 0365e10..4ff197e 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/version_test.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/version_test.dart
@@ -259,7 +259,7 @@
       return ProcessResult(0, 0, '000000000000000000000', '');
     }
     if (commandStr ==
-        'git describe --match *.*.* --first-parent --long --tags') {
+        'git describe --match *.*.*-*.*.pre --first-parent --long --tags') {
       if (version.isNotEmpty) {
         return ProcessResult(0, 0, '$version-0-g00000000', '');
       }
diff --git a/packages/flutter_tools/test/commands.shard/permeable/upgrade_test.dart b/packages/flutter_tools/test/commands.shard/permeable/upgrade_test.dart
index 150dc26..633ff27 100644
--- a/packages/flutter_tools/test/commands.shard/permeable/upgrade_test.dart
+++ b/packages/flutter_tools/test/commands.shard/permeable/upgrade_test.dart
@@ -5,7 +5,6 @@
 import 'package:flutter_tools/src/base/file_system.dart';
 import 'package:flutter_tools/src/base/io.dart';
 
-import 'package:flutter_tools/src/base/os.dart';
 import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/commands/upgrade.dart';
 import 'package:flutter_tools/src/convert.dart';
@@ -134,7 +133,10 @@
     });
 
     testUsingContext("Doesn't continue on known tag, dev branch, no force, already up-to-date", () async {
+      const String revision = 'abc123';
+      when(flutterVersion.frameworkRevision).thenReturn(revision);
       fakeCommandRunner.alreadyUpToDate = true;
+      fakeCommandRunner.remoteRevision = revision;
       final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
         force: false,
         continueFlow: false,
@@ -159,16 +161,46 @@
       Platform: () => fakePlatform,
     });
 
-    testUsingContext('verifyUpstreamConfigured', () async {
-      when(globals.processManager.run(
-        <String>['git', 'rev-parse', '@{u}'],
+    testUsingContext('fetchRemoteRevision', () async {
+      const String revision = 'abc123';
+      when(processManager.run(
+        <String>['git', 'fetch', '--tags'],
         environment:anyNamed('environment'),
         workingDirectory: anyNamed('workingDirectory')),
       ).thenAnswer((Invocation invocation) async {
         return FakeProcessResult()
           ..exitCode = 0;
       });
-      await realCommandRunner.verifyUpstreamConfigured();
+      when(processManager.run(
+        <String>['git', 'rev-parse', '--verify', '@{u}'],
+        environment:anyNamed('environment'),
+        workingDirectory: anyNamed('workingDirectory')),
+      ).thenAnswer((Invocation invocation) async {
+        return FakeProcessResult()
+          ..exitCode = 0
+          ..stdout = revision;
+      });
+      expect(await realCommandRunner.fetchRemoteRevision(), revision);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => processManager,
+      Platform: () => fakePlatform,
+    });
+
+    testUsingContext('git exception during attemptReset throwsToolExit', () async {
+      const String revision = 'abc123';
+      const String errorMessage = 'fatal: Could not parse object ´$revision´';
+      when(processManager.run(
+        <String>['git', 'reset', '--hard', revision]
+      )).thenThrow(const ProcessException(
+        'git',
+        <String>['reset', '--hard', revision],
+        errorMessage,
+      ));
+
+      expect(
+        () async => await realCommandRunner.attemptReset(revision),
+        throwsToolExit(message: errorMessage),
+      );
     }, overrides: <Type, Generator>{
       ProcessManager: () => processManager,
       Platform: () => fakePlatform,
@@ -238,7 +270,13 @@
         fakeProcessManager = FakeProcessManager.list(<FakeCommand>[
           const FakeCommand(
             command: <String>[
-              'git', 'describe', '--match', '*.*.*', '--first-parent', '--long', '--tags',
+              'git', 'tag', '--contains', 'HEAD',
+            ],
+            stdout: '',
+          ),
+          const FakeCommand(
+            command: <String>[
+              'git', 'describe', '--match', '*.*.*-*.*.pre', '--first-parent', '--long', '--tags',
             ],
             stdout: 'v1.12.16-19-gb45b676af',
           ),
@@ -278,58 +316,6 @@
       });
     });
   });
-
-  group('matchesGitLine', () {
-    setUpAll(() {
-      Cache.disableLocking();
-    });
-
-    bool _match(String line) => UpgradeCommandRunner.matchesGitLine(line);
-
-    test('regex match', () {
-      expect(_match(' .../flutter_gallery/lib/demo/buttons_demo.dart    | 10 +--'), true);
-      expect(_match(' dev/benchmarks/complex_layout/lib/main.dart        |  24 +-'), true);
-
-      expect(_match(' rename {packages/flutter/doc => dev/docs}/styles.html (92%)'), true);
-      expect(_match(' delete mode 100644 doc/index.html'), true);
-      expect(_match(' create mode 100644 dev/integration_tests/flutter_gallery/lib/gallery/demo.dart'), true);
-
-      expect(_match('Fast-forward'), true);
-    });
-
-    test("regex doesn't match", () {
-      expect(_match('Updating 79cfe1e..5046107'), false);
-      expect(_match('229 files changed, 6179 insertions(+), 3065 deletions(-)'), false);
-    });
-
-    group('findProjectRoot', () {
-      Directory tempDir;
-
-      setUp(() async {
-        tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_tools_upgrade_test.');
-      });
-
-      tearDown(() {
-        tryToDelete(tempDir);
-      });
-
-      testUsingContext('in project', () async {
-        final String projectPath = await createProject(tempDir);
-        expect(findProjectRoot(projectPath), projectPath);
-        expect(findProjectRoot(globals.fs.path.join(projectPath, 'lib')), projectPath);
-
-        final String hello = globals.fs.path.join(Cache.flutterRoot, 'examples', 'hello_world');
-        expect(findProjectRoot(hello), hello);
-        expect(findProjectRoot(globals.fs.path.join(hello, 'lib')), hello);
-      });
-
-      testUsingContext('outside project', () async {
-        final String projectPath = await createProject(tempDir);
-        expect(findProjectRoot(globals.fs.directory(projectPath).parent.path), null);
-        expect(findProjectRoot(Cache.flutterRoot), null);
-      });
-    });
-  });
 }
 
 class FakeUpgradeCommandRunner extends UpgradeCommandRunner {
@@ -337,8 +323,10 @@
 
   bool alreadyUpToDate = false;
 
+  String remoteRevision = '';
+
   @override
-  Future<void> verifyUpstreamConfigured() async {}
+  Future<String> fetchRemoteRevision() async => remoteRevision;
 
   @override
   Future<bool> hasUncomittedChanges() async => willHaveUncomittedChanges;
@@ -347,7 +335,7 @@
   Future<void> upgradeChannel(FlutterVersion flutterVersion) async {}
 
   @override
-  Future<bool> attemptFastForward(FlutterVersion flutterVersion) async => alreadyUpToDate;
+  Future<void> attemptReset(String newRevision) async {}
 
   @override
   Future<void> precacheArtifacts() async {}
diff --git a/packages/flutter_tools/test/general.shard/runner/flutter_command_runner_test.dart b/packages/flutter_tools/test/general.shard/runner/flutter_command_runner_test.dart
index 3779ff8..cd51758 100644
--- a/packages/flutter_tools/test/general.shard/runner/flutter_command_runner_test.dart
+++ b/packages/flutter_tools/test/general.shard/runner/flutter_command_runner_test.dart
@@ -179,7 +179,9 @@
           workingDirectory: Cache.flutterRoot)).thenReturn(result);
         when(processManager.runSync('git fetch https://github.com/flutter/flutter.git --tags'.split(' '),
           workingDirectory: Cache.flutterRoot)).thenReturn(result);
-        when(processManager.runSync('git describe --match *.*.* --first-parent --long --tags'.split(' '),
+        when(processManager.runSync('git tag --contains HEAD'.split(' '),
+          workingDirectory: Cache.flutterRoot)).thenReturn(result);
+        when(processManager.runSync('git describe --match *.*.*-*.*.pre --first-parent --long --tags'.split(' '),
           workingDirectory: Cache.flutterRoot)).thenReturn(result);
         when(processManager.runSync(FlutterVersion.gitLog('-n 1 --pretty=format:%ad --date=iso'.split(' ')),
           workingDirectory: Cache.flutterRoot)).thenReturn(result);
diff --git a/packages/flutter_tools/test/general.shard/version_test.dart b/packages/flutter_tools/test/general.shard/version_test.dart
index f47e5a2..6d7fa7f 100644
--- a/packages/flutter_tools/test/general.shard/version_test.dart
+++ b/packages/flutter_tools/test/general.shard/version_test.dart
@@ -394,16 +394,25 @@
     const String hash = 'abcdef';
     GitTagVersion gitTagVersion;
 
-    // legacy tag format (x.y.z-dev.m.n), master channel
-    gitTagVersion = GitTagVersion.parse('1.2.3-dev.4.5-4-g$hash');
-    expect(gitTagVersion.frameworkVersionFor(hash), '1.2.3-5.0.pre.4');
-    expect(gitTagVersion.gitTag, '1.2.3-dev.4.5');
+    // Master channel
+    gitTagVersion = GitTagVersion.parse('1.2.3-4.5.pre-13-g$hash');
+    expect(gitTagVersion.frameworkVersionFor(hash), '1.2.3-5.0.pre.13');
+    expect(gitTagVersion.gitTag, '1.2.3-4.5.pre');
     expect(gitTagVersion.devVersion, 4);
     expect(gitTagVersion.devPatch, 5);
 
-    // new tag release format, master channel
-    gitTagVersion = GitTagVersion.parse('1.2.3-4.5.pre-13-g$hash');
-    expect(gitTagVersion.frameworkVersionFor(hash), '1.2.3-5.0.pre.13');
+    // Stable channel
+    gitTagVersion = GitTagVersion.parse('1.2.3');
+    expect(gitTagVersion.frameworkVersionFor(hash), '1.2.3');
+    expect(gitTagVersion.x, 1);
+    expect(gitTagVersion.y, 2);
+    expect(gitTagVersion.z, 3);
+    expect(gitTagVersion.devVersion, null);
+    expect(gitTagVersion.devPatch, null);
+
+    // Dev channel
+    gitTagVersion = GitTagVersion.parse('1.2.3-4.5.pre');
+    expect(gitTagVersion.frameworkVersionFor(hash), '1.2.3-4.5.pre');
     expect(gitTagVersion.gitTag, '1.2.3-4.5.pre');
     expect(gitTagVersion.devVersion, 4);
     expect(gitTagVersion.devPatch, 5);
@@ -448,6 +457,29 @@
     );
   });
 
+  testUsingContext('determine favors stable tags over dev tags', () {
+    final MockProcessUtils mockProcessUtils = MockProcessUtils();
+    when(mockProcessUtils.runSync(
+      <String>['git', 'tag', '--contains', 'HEAD'],
+      workingDirectory: anyNamed('workingDirectory'),
+      environment: anyNamed('environment'),
+    )).thenReturn(RunResult(
+      ProcessResult(1, 0, '1.2.3-0.0.pre\n1.2.3\n1.2.3-0.1.pre', ''),
+      <String>['git', 'tag', '--contains', 'HEAD'],
+    ));
+    final GitTagVersion version = GitTagVersion.determine(mockProcessUtils, workingDirectory: '.');
+    expect(version.gitTag, '1.2.3');
+    expect(version.devPatch, null);
+    expect(version.devVersion, null);
+    // We shouldn't have to fallback to git describe, because we are exactly
+    // on a release tag.
+    verifyNever(mockProcessUtils.runSync(
+      <String>['git', 'describe', '--match', '*.*.*-*.*.pre', '--first-parent', '--long', '--tags'],
+      workingDirectory: anyNamed('workingDirectory'),
+      environment: anyNamed('environment'),
+    ));
+  });
+
   testUsingContext('determine does not call fetch --tags', () {
     final MockProcessUtils processUtils = MockProcessUtils();
     when(processUtils.runSync(
@@ -456,10 +488,18 @@
       environment: anyNamed('environment'),
     )).thenReturn(RunResult(ProcessResult(105, 0, '', ''), <String>['git', 'fetch']));
     when(processUtils.runSync(
-      <String>['git', 'describe', '--match', '*.*.*', '--first-parent', '--long', '--tags'],
+      <String>['git', 'describe', '--match', '*.*.*-*.*.pre', '--first-parent', '--long', '--tags'],
       workingDirectory: anyNamed('workingDirectory'),
       environment: anyNamed('environment'),
     )).thenReturn(RunResult(ProcessResult(106, 0, 'v0.1.2-3-1234abcd', ''), <String>['git', 'describe']));
+    when(processUtils.runSync(
+      <String>['git', 'tag', '--contains', 'HEAD'],
+      workingDirectory: anyNamed('workingDirectory'),
+      environment: anyNamed('environment'),
+    )).thenReturn(
+      RunResult(ProcessResult(110, 0, '', ''),
+      <String>['git', 'tag', '--contains', 'HEAD'],
+    ));
 
     GitTagVersion.determine(processUtils, workingDirectory: '.');
 
@@ -474,7 +514,7 @@
       environment: anyNamed('environment'),
     ));
     verify(processUtils.runSync(
-      <String>['git', 'describe', '--match', '*.*.*', '--first-parent', '--long', '--tags'],
+      <String>['git', 'describe', '--match', '*.*.*-*.*.pre', '--first-parent', '--long', '--tags'],
       workingDirectory: anyNamed('workingDirectory'),
       environment: anyNamed('environment'),
     )).called(1);
@@ -493,10 +533,18 @@
       environment: anyNamed('environment'),
     )).thenReturn(RunResult(ProcessResult(106, 0, '', ''), <String>['git', 'fetch']));
     when(processUtils.runSync(
-      <String>['git', 'describe', '--match', '*.*.*', '--first-parent', '--long', '--tags'],
+      <String>['git', 'describe', '--match', '*.*.*-*.*.pre', '--first-parent', '--long', '--tags'],
       workingDirectory: anyNamed('workingDirectory'),
       environment: anyNamed('environment'),
     )).thenReturn(RunResult(ProcessResult(107, 0, 'v0.1.2-3-1234abcd', ''), <String>['git', 'describe']));
+    when(processUtils.runSync(
+      <String>['git', 'tag', '--contains', 'HEAD'],
+      workingDirectory: anyNamed('workingDirectory'),
+      environment: anyNamed('environment'),
+    )).thenReturn(
+      RunResult(ProcessResult(108, 0, '', ''),
+      <String>['git', 'tag', '--contains', 'HEAD'],
+    ));
 
     GitTagVersion.determine(processUtils, workingDirectory: '.', fetchTags: true);
 
@@ -511,7 +559,7 @@
       environment: anyNamed('environment'),
     ));
     verify(processUtils.runSync(
-      <String>['git', 'describe', '--match', '*.*.*', '--first-parent', '--long', '--tags'],
+      <String>['git', 'describe', '--match', '*.*.*-*.*.pre', '--first-parent', '--long', '--tags'],
       workingDirectory: anyNamed('workingDirectory'),
       environment: anyNamed('environment'),
     )).called(1);
@@ -530,10 +578,18 @@
       environment: anyNamed('environment'),
     )).thenReturn(RunResult(ProcessResult(109, 0, '', ''), <String>['git', 'fetch']));
     when(processUtils.runSync(
-      <String>['git', 'describe', '--match', '*.*.*', '--first-parent', '--long', '--tags'],
+      <String>['git', 'tag', '--contains', 'HEAD'],
       workingDirectory: anyNamed('workingDirectory'),
       environment: anyNamed('environment'),
-    )).thenReturn(RunResult(ProcessResult(110, 0, 'v0.1.2-3-1234abcd', ''), <String>['git', 'describe']));
+    )).thenReturn(
+      RunResult(ProcessResult(110, 0, '', ''),
+      <String>['git', 'tag', '--contains', 'HEAD'],
+    ));
+    when(processUtils.runSync(
+      <String>['git', 'describe', '--match', '*.*.*-*.*.pre', '--first-parent', '--long', '--tags'],
+      workingDirectory: anyNamed('workingDirectory'),
+      environment: anyNamed('environment'),
+    )).thenReturn(RunResult(ProcessResult(111, 0, 'v0.1.2-3-1234abcd', ''), <String>['git', 'describe']));
 
     GitTagVersion.determine(processUtils, workingDirectory: '.', fetchTags: true);
 
@@ -548,7 +604,7 @@
       environment: anyNamed('environment'),
     )).called(1);
     verify(processUtils.runSync(
-      <String>['git', 'describe', '--match', '*.*.*', '--first-parent', '--long', '--tags'],
+      <String>['git', 'describe', '--match', '*.*.*-*.*.pre', '--first-parent', '--long', '--tags'],
       workingDirectory: anyNamed('workingDirectory'),
       environment: anyNamed('environment'),
     )).called(1);
@@ -669,10 +725,15 @@
     environment: anyNamed('environment'),
   )).thenReturn(ProcessResult(105, 0, '', ''));
   when(pm.runSync(
-    <String>['git', 'describe', '--match', '*.*.*', '--first-parent', '--long', '--tags'],
+    <String>['git', 'tag', '--contains', 'HEAD'],
     workingDirectory: anyNamed('workingDirectory'),
     environment: anyNamed('environment'),
-  )).thenReturn(ProcessResult(106, 0, 'v0.1.2-3-1234abcd', ''));
+  )).thenReturn(ProcessResult(106, 0, '', ''));
+  when(pm.runSync(
+    <String>['git', 'describe', '--match', '*.*.*-*.*.pre', '--first-parent', '--long', '--tags'],
+    workingDirectory: anyNamed('workingDirectory'),
+    environment: anyNamed('environment'),
+  )).thenReturn(ProcessResult(107, 0, 'v0.1.2-3-1234abcd', ''));
 }
 
 class MockProcessManager extends Mock implements ProcessManager {}
diff --git a/packages/flutter_tools/test/integration.shard/downgrade_upgrade_integration_test.dart b/packages/flutter_tools/test/integration.shard/downgrade_upgrade_integration_test.dart
index 438efc3..f6c5383 100644
--- a/packages/flutter_tools/test/integration.shard/downgrade_upgrade_integration_test.dart
+++ b/packages/flutter_tools/test/integration.shard/downgrade_upgrade_integration_test.dart
@@ -57,8 +57,7 @@
       'git', 'config', '--system', 'core.longpaths', 'true',
     ]);
 
-    print('Step 1');
-    // Step 1. Clone the dev branch of flutter into the test directory.
+    print('Step 1 - clone the $_kBranch of flutter into the test directory');
     exitCode = await processUtils.stream(<String>[
       'git',
       'clone',
@@ -66,8 +65,7 @@
     ], workingDirectory: parentDirectory.path, trace: true);
     expect(exitCode, 0);
 
-    print('Step 2');
-    // Step 2. Switch to the dev branch.
+    print('Step 2 - switch to the $_kBranch');
     exitCode = await processUtils.stream(<String>[
       'git',
       'checkout',
@@ -78,8 +76,7 @@
     ], workingDirectory: testDirectory.path, trace: true);
     expect(exitCode, 0);
 
-    print('Step 3');
-    // Step 3. Revert to a prior version.
+    print('Step 3 - revert back to $_kInitialVersion');
     exitCode = await processUtils.stream(<String>[
       'git',
       'reset',
@@ -88,9 +85,8 @@
     ], workingDirectory: testDirectory.path, trace: true);
     expect(exitCode, 0);
 
-    print('Step 4');
-    // Step 4. Upgrade to the newest stable. This should update the persistent
-    // tool state with the sha for v1.14.3
+    print('Step 4 - upgrade to the newest $_kBranch');
+    // This should update the persistent tool state with the sha for HEAD
     exitCode = await processUtils.stream(<String>[
       flutterBin,
       'upgrade',
@@ -99,8 +95,7 @@
     ], workingDirectory: testDirectory.path, trace: true);
     expect(exitCode, 0);
 
-    print('Step 5');
-    // Step 5. Verify that the version is different.
+    print('Step 5 - verify that the version is different');
     final RunResult versionResult = await processUtils.run(<String>[
       'git',
       'describe',
@@ -111,8 +106,9 @@
       '--tags',
     ], workingDirectory: testDirectory.path);
     expect(versionResult.stdout, isNot(contains(_kInitialVersion)));
+    print('current version is ${versionResult.stdout.trim()}\ninitial was $_kInitialVersion');
 
-    print('Step 6');
+    print('Step 6 - downgrade back to the initial version');
     // Step 6. Downgrade back to initial version.
     exitCode = await processUtils.stream(<String>[
        flutterBin,
@@ -122,7 +118,7 @@
     ], workingDirectory: testDirectory.path, trace: true);
     expect(exitCode, 0);
 
-    print('Step 7');
+    print('Step 7 - verify downgraded version matches original version');
     // Step 7. Verify downgraded version matches original version.
     final RunResult oldVersionResult = await processUtils.run(<String>[
       'git',
@@ -134,5 +130,6 @@
       '--tags',
     ], workingDirectory: testDirectory.path);
     expect(oldVersionResult.stdout, contains(_kInitialVersion));
+    print('current version is ${oldVersionResult.stdout.trim()}\ninitial was $_kInitialVersion');
   });
 }