diff --git a/packages/shorebird_cli/lib/src/code_push_client_wrapper.dart b/packages/shorebird_cli/lib/src/code_push_client_wrapper.dart index 22c0b9cc..4b5c0457 100644 --- a/packages/shorebird_cli/lib/src/code_push_client_wrapper.dart +++ b/packages/shorebird_cli/lib/src/code_push_client_wrapper.dart @@ -261,8 +261,8 @@ Please create a release using "shorebird release" and try again. required ReleasePlatform platform, }) async { final createReleaseProgress = logger.progress('Creating release'); - final flutterVersion = await shorebirdFlutter.getVersionString( - revision: flutterRevision, + final flutterVersion = await shorebirdFlutter.getVersionForRevision( + flutterRevision: flutterRevision, ); try { final release = await codePushClient.createRelease( diff --git a/packages/shorebird_cli/lib/src/commands/release/release_command.dart b/packages/shorebird_cli/lib/src/commands/release/release_command.dart index 2f7f02a6..02c5e40f 100644 --- a/packages/shorebird_cli/lib/src/commands/release/release_command.dart +++ b/packages/shorebird_cli/lib/src/commands/release/release_command.dart @@ -358,19 +358,33 @@ Use `shorebird flutter versions list` to list available versions. // All artifacts associated with a given release must be built // with the same Flutter revision. if (existingRelease.flutterRevision != flutterRevision) { + final flutterVersion = await shorebirdFlutter.getVersionForRevision( + flutterRevision: flutterRevision, + ); + + final formattedCurrentReleaseVersion = shorebirdFlutter.formatVersion( + revision: flutterRevision, + version: flutterVersion, + ); + + final formattedExistingReleaseVersion = shorebirdFlutter.formatVersion( + revision: existingRelease.flutterRevision, + version: existingRelease.flutterVersion, + ); + logger ..err(''' ${styleBold.wrap(lightRed.wrap('A release with version $version already exists but was built using a different Flutter revision.'))} ''') ..info(''' - Existing release built with: ${lightCyan.wrap(existingRelease.flutterRevision)} - Current release built with: ${lightCyan.wrap(flutterRevision)} + Existing release built with: ${lightCyan.wrap(formattedExistingReleaseVersion)} + Current release built with: ${lightCyan.wrap(formattedCurrentReleaseVersion)} ${styleBold.wrap(lightRed.wrap('All platforms for a given release must be built using the same Flutter revision.'))} To resolve this issue, you can: - * Re-run the release command with "${lightCyan.wrap('--flutter-version=${existingRelease.flutterRevision}')}". + * Re-run the release command with "${lightCyan.wrap('--flutter-version=${existingRelease.flutterVersion ?? existingRelease.flutterRevision}')}". * Delete the existing release and re-run the release command with the desired Flutter version. * Bump the release version and re-run the release command with the desired Flutter version.'''); throw ProcessExit(ExitCode.software.code); diff --git a/packages/shorebird_cli/lib/src/shorebird_flutter.dart b/packages/shorebird_cli/lib/src/shorebird_flutter.dart index c92ee9ff..4063e750 100644 --- a/packages/shorebird_cli/lib/src/shorebird_flutter.dart +++ b/packages/shorebird_cli/lib/src/shorebird_flutter.dart @@ -44,7 +44,7 @@ class ShorebirdFlutter { final targetDirectory = Directory(_workingDirectory(revision: revision)); if (targetDirectory.existsSync()) return; - final version = await getVersionString(revision: revision); + final version = await getVersionForRevision(flutterRevision: revision); final installProgress = logger.progress( 'Installing Flutter $version (${shortRevisionString(revision)})', @@ -133,33 +133,40 @@ class ShorebirdFlutter { /// Converts a full git revision to a short revision string. String shortRevisionString(String revision) => revision.substring(0, 10); + /// Given a revision and a version, formats them into a single string. + /// + /// e.g. 3.16.3 and b9b2390296b9b2390296 -> 3.16.3 (b9b2390296) + String formatVersion({required String revision, required String? version}) { + version ??= 'unknown'; + return '$version (${shortRevisionString(revision)})'; + } + /// Returns the current Shorebird Flutter version and revision. /// Returns unknown if the version check fails. Future getVersionAndRevision() async { - String? version = 'unknown'; + late final String? version; + try { version = await getVersionString(); - } catch (_) {} + } catch (_) { + version = 'unknown'; + } - return '$version (${shortRevisionString(shorebirdEnv.flutterRevision)})'; + return formatVersion( + version: version, + revision: shorebirdEnv.flutterRevision, + ); } /// Returns the current Shorebird Flutter version. /// Throws a [ProcessException] if the version check fails. /// Returns `null` if the version check succeeds but the version cannot be /// parsed. - Future getVersionString({String? revision}) async { - final result = await git.forEachRef( - contains: revision ?? shorebirdEnv.flutterRevision, - format: '%(refname:short)', - pattern: 'refs/remotes/origin/flutter_release/*', - directory: _workingDirectory(), + Future getVersionString() async { + final flutterRevision = shorebirdEnv.flutterRevision; + return getVersionForRevision( + flutterRevision: flutterRevision, ); - - return LineSplitter.split(result) - .map((e) => e.replaceFirst('origin/flutter_release/', '')) - .toList() - .firstOrNull; } /// The current Shorebird Flutter version as a [Version]. Returns null if the @@ -180,6 +187,24 @@ class ShorebirdFlutter { return version; } + /// Returns the human readable version for a given git revision + /// e.g. b9b2390296b9b2390296 -> 3.16.3 + Future getVersionForRevision({ + required String flutterRevision, + }) async { + final result = await git.forEachRef( + contains: flutterRevision, + format: '%(refname:short)', + pattern: 'refs/remotes/origin/flutter_release/*', + directory: _workingDirectory(), + ); + + return LineSplitter.split(result) + .map((e) => e.replaceFirst('origin/flutter_release/', '')) + .toList() + .firstOrNull; + } + /// Returns the git revision for the provided [version]. /// e.g. 3.16.3 -> b9b23902966504a9778f4c07e3a3487fa84dcb2a Future getRevisionForVersion(String version) async { @@ -213,7 +238,7 @@ class ShorebirdFlutter { Future useRevision({required String revision}) async { await installRevision(revision: revision); - final version = await getVersionString(revision: revision); + final version = await getVersionForRevision(flutterRevision: revision); final useFlutterProgress = logger.progress('Using Flutter $version'); shorebirdEnv.flutterRevision = revision; useFlutterProgress.complete(); diff --git a/packages/shorebird_cli/test/src/code_push_client_wrapper_test.dart b/packages/shorebird_cli/test/src/code_push_client_wrapper_test.dart index 848500de..f5c719ed 100644 --- a/packages/shorebird_cli/test/src/code_push_client_wrapper_test.dart +++ b/packages/shorebird_cli/test/src/code_push_client_wrapper_test.dart @@ -55,8 +55,8 @@ void main() { when(() => logger.progress(any())).thenReturn(progress); when( - () => shorebirdFlutter.getVersionString( - revision: any(named: 'revision'), + () => shorebirdFlutter.getVersionForRevision( + flutterRevision: any(named: 'flutterRevision'), ), ).thenAnswer( (_) async => '3.22.0', @@ -207,7 +207,9 @@ void main() { ); when( - () => shorebirdFlutter.getVersionString(revision: flutterRevision), + () => shorebirdFlutter.getVersionForRevision( + flutterRevision: flutterRevision, + ), ).thenAnswer((_) async => flutterVersion); }); diff --git a/packages/shorebird_cli/test/src/commands/release/release_command_test.dart b/packages/shorebird_cli/test/src/commands/release/release_command_test.dart index 58e1ba0c..6418e0c1 100644 --- a/packages/shorebird_cli/test/src/commands/release/release_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/release/release_command_test.dart @@ -370,6 +370,25 @@ Note: ${lightCyan.wrap('shorebird patch --platforms=android --flavor=$flavor --t createdAt: DateTime(2023), updatedAt: DateTime(2023), ); + + when( + () => shorebirdFlutter.getVersionForRevision( + flutterRevision: flutterRevision, + ), + ).thenAnswer((_) async => flutterVersion); + + when( + () => shorebirdFlutter.formatVersion( + revision: flutterRevision, + version: flutterVersion, + ), + ).thenReturn('3.12.1'); + when( + () => shorebirdFlutter.formatVersion( + revision: existingRelease.flutterRevision, + version: existingRelease.flutterVersion, + ), + ).thenReturn('3.12.1'); }); test('logs error and exits with code 70', () async { diff --git a/packages/shorebird_cli/test/src/shorebird_flutter_test.dart b/packages/shorebird_cli/test/src/shorebird_flutter_test.dart index f0f9942b..9fe0f31e 100644 --- a/packages/shorebird_cli/test/src/shorebird_flutter_test.dart +++ b/packages/shorebird_cli/test/src/shorebird_flutter_test.dart @@ -201,33 +201,37 @@ Tools • Dart 3.0.6 • DevTools 2.23.1'''); }); group('getVersionAndRevision', () { - test('returns unknown () when unable to determine version', - () async { + group('when unable to determine version', () { const error = 'oops'; - when( - () => git.forEachRef( - directory: any(named: 'directory'), - contains: any(named: 'contains'), - format: any(named: 'format'), - pattern: any(named: 'pattern'), - ), - ).thenThrow( - ProcessException( - 'git', - [ - 'for-each-ref', - '--format', - '%(refname:short)', - 'refs/remotes/origin/flutter_release/*', - ], - error, - ExitCode.software.code, - ), - ); - await expectLater( - runWithOverrides(shorebirdFlutter.getVersionAndRevision), - completion(equals('unknown (${flutterRevision.substring(0, 10)})')), - ); + setUp(() { + when( + () => git.forEachRef( + directory: any(named: 'directory'), + contains: any(named: 'contains'), + format: any(named: 'format'), + pattern: any(named: 'pattern'), + ), + ).thenThrow( + ProcessException( + 'git', + [ + 'for-each-ref', + '--format', + '%(refname:short)', + 'refs/remotes/origin/flutter_release/*', + ], + error, + ExitCode.software.code, + ), + ); + }); + + test('returns unknown ()', () async { + await expectLater( + runWithOverrides(shorebirdFlutter.getVersionAndRevision), + completion(equals('unknown (${flutterRevision.substring(0, 10)})')), + ); + }); }); test('returns correct version and revision', () async { @@ -240,151 +244,178 @@ Tools • Dart 3.0.6 • DevTools 2.23.1'''); group('getRevisionForVersion', () { const version = '3.16.3'; + const exception = ProcessException('git', ['rev-parse']); - test('throws exception when process exits with non-zero code', () async { - const exception = ProcessException('git', ['rev-parse']); - when( - () => git.revParse( - revision: any(named: 'revision'), - directory: any(named: 'directory'), - ), - ).thenThrow(exception); - await expectLater( - runWithOverrides( - () => shorebirdFlutter.getRevisionForVersion(version), - ), - throwsA(exception), - ); - verify( - () => git.revParse( - revision: 'refs/remotes/origin/flutter_release/$version', - directory: any(named: 'directory'), - ), - ).called(1); + group('when process exits with non-zero code', () { + setUp(() { + when( + () => git.revParse( + revision: any(named: 'revision'), + directory: any(named: 'directory'), + ), + ).thenThrow(exception); + }); + + test('throws exception', () async { + await expectLater( + runWithOverrides( + () => shorebirdFlutter.getRevisionForVersion(version), + ), + throwsA(exception), + ); + verify( + () => git.revParse( + revision: 'refs/remotes/origin/flutter_release/$version', + directory: any(named: 'directory'), + ), + ).called(1); + }); }); - test('returns null when cannot parse revision', () async { - when( - () => git.revParse( - revision: any(named: 'revision'), - directory: any(named: 'directory'), - ), - ).thenAnswer((_) async => ''); - await expectLater( - runWithOverrides( - () => shorebirdFlutter.getRevisionForVersion(version), - ), - completion(isNull), - ); - verify( - () => git.revParse( - revision: 'refs/remotes/origin/flutter_release/$version', - directory: any(named: 'directory'), - ), - ).called(1); + group('when cannot parse revision', () { + setUp(() { + when( + () => git.revParse( + revision: any(named: 'revision'), + directory: any(named: 'directory'), + ), + ).thenAnswer((_) async => ''); + }); + + test('returns null', () async { + await expectLater( + runWithOverrides( + () => shorebirdFlutter.getRevisionForVersion(version), + ), + completion(isNull), + ); + verify( + () => git.revParse( + revision: 'refs/remotes/origin/flutter_release/$version', + directory: any(named: 'directory'), + ), + ).called(1); + }); }); - test('returns revision when able to parse the string', () async { + group('when able to parse the string', () { const revision = '771d07b2cf97cf107bae6eeedcf41bdc9db772fa'; - when( - () => git.revParse( - revision: any(named: 'revision'), - directory: any(named: 'directory'), - ), - ).thenAnswer( - (_) async => ''' + setUp(() { + when( + () => git.revParse( + revision: any(named: 'revision'), + directory: any(named: 'directory'), + ), + ).thenAnswer( + (_) async => ''' $revision ''', - ); - await expectLater( - runWithOverrides( - () => shorebirdFlutter.getRevisionForVersion(version), - ), - completion(equals(revision)), - ); - verify( - () => git.revParse( - revision: 'refs/remotes/origin/flutter_release/$version', - directory: any(named: 'directory'), - ), - ).called(1); + ); + }); + + test('returns revision', () async { + await expectLater( + runWithOverrides( + () => shorebirdFlutter.getRevisionForVersion(version), + ), + completion(equals(revision)), + ); + verify( + () => git.revParse( + revision: 'refs/remotes/origin/flutter_release/$version', + directory: any(named: 'directory'), + ), + ).called(1); + }); }); }); group('getVersionString', () { - test('throws ProcessException when process exits with non-zero code', - () async { + group('when process exits with non-zero code', () { const error = 'oops'; - when( - () => git.forEachRef( - directory: any(named: 'directory'), - contains: any(named: 'contains'), - format: any(named: 'format'), - pattern: any(named: 'pattern'), - ), - ).thenThrow( - ProcessException( - 'git', - [ - 'for-each-ref', - '--format', - '%(refname:short)', - 'refs/remotes/origin/flutter_release/*', - ], - error, - ExitCode.software.code, - ), - ); - await expectLater( - runWithOverrides(shorebirdFlutter.getVersionString), - throwsA(isA()), - ); - verify( - () => git.forEachRef( - directory: p.join(flutterDirectory.parent.path, flutterRevision), - contains: flutterRevision, - format: '%(refname:short)', - pattern: 'refs/remotes/origin/flutter_release/*', - ), - ).called(1); + + setUp(() { + when( + () => git.forEachRef( + directory: any(named: 'directory'), + contains: any(named: 'contains'), + format: any(named: 'format'), + pattern: any(named: 'pattern'), + ), + ).thenThrow( + ProcessException( + 'git', + [ + 'for-each-ref', + '--format', + '%(refname:short)', + 'refs/remotes/origin/flutter_release/*', + ], + error, + ExitCode.software.code, + ), + ); + }); + + test('throws ProcessException', () async { + await expectLater( + runWithOverrides(shorebirdFlutter.getVersionString), + throwsA(isA()), + ); + verify( + () => git.forEachRef( + directory: p.join(flutterDirectory.parent.path, flutterRevision), + contains: flutterRevision, + format: '%(refname:short)', + pattern: 'refs/remotes/origin/flutter_release/*', + ), + ).called(1); + }); }); - test('returns null when cannot parse version', () async { - when( - () => git.forEachRef( - directory: any(named: 'directory'), - contains: any(named: 'contains'), - format: any(named: 'format'), - pattern: any(named: 'pattern'), - ), - ).thenAnswer((_) async => ''); - await expectLater( - runWithOverrides(shorebirdFlutter.getVersionString), - completion(isNull), - ); - verify( - () => git.forEachRef( - directory: p.join(flutterDirectory.parent.path, flutterRevision), - contains: flutterRevision, - format: '%(refname:short)', - pattern: 'refs/remotes/origin/flutter_release/*', - ), - ).called(1); + group('when cannot parse 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 null', () async { + await expectLater( + runWithOverrides(shorebirdFlutter.getVersionString), + completion(isNull), + ); + verify( + () => git.forEachRef( + directory: p.join(flutterDirectory.parent.path, flutterRevision), + contains: flutterRevision, + format: '%(refname:short)', + pattern: 'refs/remotes/origin/flutter_release/*', + ), + ).called(1); + }); }); - test('returns version when able to parse the string', () async { - await expectLater( - runWithOverrides(shorebirdFlutter.getVersionString), - completion(equals('3.10.6')), - ); - verify( - () => git.forEachRef( - directory: p.join(flutterDirectory.parent.path, flutterRevision), - contains: flutterRevision, - format: '%(refname:short)', - pattern: 'refs/remotes/origin/flutter_release/*', - ), - ).called(1); + group('when able to parse the string', () { + test('returns version', () async { + await expectLater( + runWithOverrides(shorebirdFlutter.getVersionString), + completion(equals('3.10.6')), + ); + verify( + () => git.forEachRef( + directory: p.join(flutterDirectory.parent.path, flutterRevision), + contains: flutterRevision, + format: '%(refname:short)', + pattern: 'refs/remotes/origin/flutter_release/*', + ), + ).called(1); + }); }); }); @@ -409,7 +440,7 @@ $revision }); }); - group('when getVersionStringReturns an invalid string', () { + group('when getVersionString returns an invalid string', () { setUp(() { when( () => git.forEachRef( @@ -429,7 +460,7 @@ $revision }); }); - group('when getVersionStringReturns a valid string', () { + group('when getVersionString returns a valid string', () { setUp(() { when( () => git.forEachRef( @@ -878,5 +909,37 @@ origin/flutter_release/3.10.6'''; verify(() => shorebirdEnv.flutterRevision = newRevision).called(1); }); }); + + group('formatVersion', () { + test('returns the correct formated value', () { + expect( + runWithOverrides( + () => shorebirdFlutter.formatVersion( + version: '3.10.6', + revision: '771d07b2cf97cf107bae6eeedcf41bdc9db772fa', + ), + ), + equals( + '3.10.6 (771d07b2cf)', + ), + ); + }); + + group('when version is null', () { + test('returns unknown for the version', () { + expect( + runWithOverrides( + () => shorebirdFlutter.formatVersion( + version: null, + revision: '771d07b2cf97cf107bae6eeedcf41bdc9db772fa', + ), + ), + equals( + 'unknown (771d07b2cf)', + ), + ); + }); + }); + }); }); }