From b064f1e76fff9ebbb5ea990f936305afc782a838 Mon Sep 17 00:00:00 2001 From: Eric Seidel Date: Mon, 26 Jan 2026 14:51:18 -0800 Subject: [PATCH] fix: --flutter-version=hash stopped working (#3478) --- .../lib/src/shorebird_flutter.dart | 34 +++++---- .../test/src/shorebird_flutter_test.dart | 75 ++++++++++--------- 2 files changed, 59 insertions(+), 50 deletions(-) diff --git a/packages/shorebird_cli/lib/src/shorebird_flutter.dart b/packages/shorebird_cli/lib/src/shorebird_flutter.dart index 89d72f27..bfba3df2 100644 --- a/packages/shorebird_cli/lib/src/shorebird_flutter.dart +++ b/packages/shorebird_cli/lib/src/shorebird_flutter.dart @@ -214,29 +214,37 @@ class ShorebirdFlutter { .firstOrNull; } + /// Pattern for a valid git hash (4-40 hex characters). + /// Git allows short hashes as long as they're unambiguous. + static final _gitHashPattern = RegExp(r'^[0-9a-fA-F]{4,40}$'); + /// Translates [versionOrHash] into a Flutter revision. If this is a semver - /// version, it will simply parse that into a [Version]. If not, it will - /// attempt to look up the Flutter version for the provided revision hash and - /// return the hash if a version is found, or null if not. + /// version, it will look up the git revision for that version. If not, it + /// will check if it's a valid git hash that exists in the local Flutter repo. + /// + /// Returns the full hash if valid, or null if it's neither a valid semver + /// version nor a valid git hash that exists locally. Future resolveFlutterRevision(String versionOrHash) async { final parsedVersion = tryParseVersion(versionOrHash); if (parsedVersion != null) { return getRevisionForVersion(versionOrHash); } - // If we were unable to parse the version, assume it's a revision hash. - try { - final version = await getVersionForRevision( - flutterRevision: versionOrHash, - ); - if (version != null) { - return versionOrHash; - } - } on Exception { + // If we were unable to parse the version, check if it's a valid git hash. + if (!_gitHashPattern.hasMatch(versionOrHash)) { return null; } - return null; + // Verify the hash exists locally by resolving it to its full hash. + try { + final fullHash = await git.revParse( + revision: versionOrHash, + directory: _workingDirectory(), + ); + return fullHash; + } on ProcessException { + return null; + } } /// Translates [versionOrHash] into a Flutter [Version]. If [versionOrHash] diff --git a/packages/shorebird_cli/test/src/shorebird_flutter_test.dart b/packages/shorebird_cli/test/src/shorebird_flutter_test.dart index a2b01d46..43acaa6a 100644 --- a/packages/shorebird_cli/test/src/shorebird_flutter_test.dart +++ b/packages/shorebird_cli/test/src/shorebird_flutter_test.dart @@ -313,62 +313,63 @@ Tools • Dart 3.0.6 • DevTools 2.23.1'''); ); }); - group('when input is a commit hash that maps to a Flutter version', () { + group('when input is a valid git hash that exists locally', () { + const fullHash = 'eead750584a909f506eb6fc111ece5d8fed4aa39'; + setUp(() { when( - () => git.forEachRef( + () => git.revParse( + revision: any(named: 'revision'), directory: any(named: 'directory'), - contains: any(named: 'contains'), - format: any(named: 'format'), - pattern: any(named: 'pattern'), ), - ).thenAnswer((_) async => 'origin/flutter_release/1.2.3'); + ).thenAnswer((_) async => fullHash); }); - test('returns the input string', () async { + test('returns full hash for full input', () async { + final revision = await runWithOverrides( + () => shorebirdFlutter.resolveFlutterRevision(fullHash), + ); + expect(revision, equals(fullHash)); + }); + + test('returns full hash for short input', () async { final revision = await runWithOverrides( () => shorebirdFlutter.resolveFlutterRevision('deadbeef'), ); - expect(revision, equals('deadbeef')); + expect(revision, equals(fullHash)); }); }); - group( - 'when input is not a commit hash that maps to a Flutter version', - () { - setUp(() { - when( - () => git.forEachRef( - directory: any(named: 'directory'), - contains: any(named: 'contains'), - format: any(named: 'format'), - pattern: any(named: 'pattern'), - ), - ).thenAnswer((_) async => ''); - }); - - test('returns the input string', () async { - final revision = await runWithOverrides( - () => shorebirdFlutter.resolveFlutterRevision('not-a-version'), - ); - expect(revision, isNull); - }); - }, - ); - - group('when exception occurs doing revision lookup', () { + group('when input is a valid git hash format but does not exist', () { setUp(() { when( - () => git.forEachRef( + () => git.revParse( + revision: any(named: 'revision'), directory: any(named: 'directory'), - contains: any(named: 'contains'), - format: any(named: 'format'), - pattern: any(named: 'pattern'), ), - ).thenThrow(Exception('oops')); + ).thenThrow( + const ProcessException('git', ['rev-parse']), + ); }); test('returns null', () async { + const validHash = 'eead750584a909f506eb6fc111ece5d8fed4aa39'; + final revision = await runWithOverrides( + () => shorebirdFlutter.resolveFlutterRevision(validHash), + ); + expect(revision, isNull); + }); + }); + + group('when input is not a valid git hash format', () { + test('returns null for too-short hash', () async { + final revision = await runWithOverrides( + () => shorebirdFlutter.resolveFlutterRevision('abc'), + ); + expect(revision, isNull); + }); + + test('returns null for non-hex string', () async { final revision = await runWithOverrides( () => shorebirdFlutter.resolveFlutterRevision('not-a-version'), );