diff --git a/packages/shorebird_cli/lib/src/commands/upgrade_command.dart b/packages/shorebird_cli/lib/src/commands/upgrade_command.dart index f9ce125a..6598caa3 100644 --- a/packages/shorebird_cli/lib/src/commands/upgrade_command.dart +++ b/packages/shorebird_cli/lib/src/commands/upgrade_command.dart @@ -3,8 +3,7 @@ import 'dart:io'; import 'package:mason_logger/mason_logger.dart'; import 'package:shorebird_cli/src/command.dart'; import 'package:shorebird_cli/src/logger.dart'; -import 'package:shorebird_cli/src/process.dart'; -import 'package:shorebird_cli/src/shorebird_env.dart'; +import 'package:shorebird_cli/src/shorebird_flutter_manager.dart'; import 'package:shorebird_cli/src/shorebird_version_manager.dart'; /// {@template upgrade_command} @@ -64,7 +63,16 @@ class UpgradeCommand extends ShorebirdCommand { } try { - await _pruneFlutterOrigin(); + // Intended to fix an issue caused by a change in our remote branches. + // We deleted (origin/shorebird) and created (origin/shorebird/main) + // + // The error manifested at: + // $ shorebird --version + // Updating Flutter... + // error: cannot lock ref 'refs/remotes/origin/shorebird/main': 'refs/remotes/origin/shorebird' exists; cannot create 'refs/remotes/origin/shorebird/main' + // From https://github.com/shorebirdtech/flutter + // ! [new branch] shorebird/main -> origin/shorebird/main (unable to update local ref) + await shorebirdFlutterManager.pruneRemoteOrigin(revision: latestVersion); } on ProcessException catch (error) { updateProgress.fail(); logger.err('Updating failed: ${error.message}'); @@ -75,32 +83,4 @@ class UpgradeCommand extends ShorebirdCommand { return ExitCode.success.code; } - - // Intended to fix an issue caused by a change in our remote branches. - // We deleted (origin/shorebird) and created (origin/shorebird/main) - // - // The error manifested at: - // $ shorebird --version - // Updating Flutter... - // error: cannot lock ref 'refs/remotes/origin/shorebird/main': 'refs/remotes/origin/shorebird' exists; cannot create 'refs/remotes/origin/shorebird/main' - // From https://github.com/shorebirdtech/flutter - // ! [new branch] shorebird/main -> origin/shorebird/main (unable to update local ref) - Future _pruneFlutterOrigin() async { - const executable = 'git'; - final args = ['remote', 'prune', 'origin']; - final result = await process.run( - executable, - args, - workingDirectory: shorebirdEnv.flutterDirectory.path, - ); - - if (result.exitCode != 0) { - throw ProcessException( - executable, - args, - '${result.stderr}', - result.exitCode, - ); - } - } } diff --git a/packages/shorebird_cli/lib/src/shorebird_flutter_manager.dart b/packages/shorebird_cli/lib/src/shorebird_flutter_manager.dart index bbf505c3..bb6c3909 100644 --- a/packages/shorebird_cli/lib/src/shorebird_flutter_manager.dart +++ b/packages/shorebird_cli/lib/src/shorebird_flutter_manager.dart @@ -23,10 +23,13 @@ class ShorebirdFlutterManager { static const String flutterGitUrl = 'https://github.com/shorebirdtech/flutter.git'; + String _workingDirectory({String? revision}) { + revision ??= shorebirdEnv.flutterRevision; + return p.join(shorebirdEnv.flutterDirectory.parent.path, revision); + } + Future installRevision({required String revision}) async { - final targetDirectory = Directory( - p.join(shorebirdEnv.flutterDirectory.parent.path, revision), - ); + final targetDirectory = Directory(_workingDirectory(revision: revision)); if (targetDirectory.existsSync()) return; // Clone the Shorebird Flutter repo into the target directory. @@ -40,9 +43,14 @@ class ShorebirdFlutterManager { ); // Checkout the correct revision. - await git.checkout( - directory: targetDirectory.path, - revision: revision, + await git.checkout(directory: targetDirectory.path, revision: revision); + } + + /// Prunes stale remote branches from the repository. + Future pruneRemoteOrigin({String? revision}) async { + return git.remotePrune( + name: 'origin', + directory: _workingDirectory(revision: revision), ); } } diff --git a/packages/shorebird_cli/test/src/commands/upgrade_command_test.dart b/packages/shorebird_cli/test/src/commands/upgrade_command_test.dart index 3a35a7e9..573c89ca 100644 --- a/packages/shorebird_cli/test/src/commands/upgrade_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/upgrade_command_test.dart @@ -5,20 +5,16 @@ import 'package:mocktail/mocktail.dart'; import 'package:scoped/scoped.dart'; import 'package:shorebird_cli/src/commands/commands.dart'; import 'package:shorebird_cli/src/logger.dart'; -import 'package:shorebird_cli/src/process.dart'; -import 'package:shorebird_cli/src/shorebird_env.dart'; +import 'package:shorebird_cli/src/shorebird_flutter_manager.dart'; import 'package:shorebird_cli/src/shorebird_version_manager.dart'; import 'package:test/test.dart'; class _MockLogger extends Mock implements Logger {} -class _MockProcessResult extends Mock implements ShorebirdProcessResult {} - class _MockProgress extends Mock implements Progress {} -class _MockShorebirdProcess extends Mock implements ShorebirdProcess {} - -class _MockShorebirdEnv extends Mock implements ShorebirdEnv {} +class _MockShorebirdFlutterManager extends Mock + implements ShorebirdFlutterManager {} class _MockShorebirdVersionManager extends Mock implements ShorebirdVersionManager {} @@ -29,9 +25,7 @@ void main() { group('upgrade', () { late Logger logger; - late ShorebirdProcessResult pruneFlutterOriginResult; - late ShorebirdProcess shorebirdProcess; - late ShorebirdEnv shorebirdEnv; + late ShorebirdFlutterManager shorebirdFlutterManager; late ShorebirdVersionManager shorebirdVersionManager; late UpgradeCommand command; @@ -40,8 +34,9 @@ void main() { body, values: { loggerRef.overrideWith(() => logger), - processRef.overrideWith(() => shorebirdProcess), - shorebirdEnvRef.overrideWith(() => shorebirdEnv), + shorebirdFlutterManagerRef.overrideWith( + () => shorebirdFlutterManager, + ), shorebirdVersionManagerRef.overrideWith( () => shorebirdVersionManager, ), @@ -54,15 +49,15 @@ void main() { final progressLogs = []; logger = _MockLogger(); - pruneFlutterOriginResult = _MockProcessResult(); - shorebirdProcess = _MockShorebirdProcess(); - shorebirdEnv = _MockShorebirdEnv(); + shorebirdFlutterManager = _MockShorebirdFlutterManager(); shorebirdVersionManager = _MockShorebirdVersionManager(); command = runWithOverrides(UpgradeCommand.new); when( - () => shorebirdEnv.flutterDirectory, - ).thenReturn(Directory('flutter')); + () => shorebirdFlutterManager.pruneRemoteOrigin( + revision: any(named: 'revision'), + ), + ).thenAnswer((_) async {}); when( shorebirdVersionManager.fetchCurrentGitHash, ).thenAnswer((_) async => currentShorebirdRevision); @@ -75,17 +70,6 @@ void main() { ), ).thenAnswer((_) async => {}); - when( - () => shorebirdProcess.run( - 'git', - ['remote', 'prune', 'origin'], - workingDirectory: any(named: 'workingDirectory'), - ), - ).thenAnswer((_) async => pruneFlutterOriginResult); - when( - () => pruneFlutterOriginResult.exitCode, - ).thenReturn(ExitCode.success.code); - when(() => progress.complete(any())).thenAnswer((_) { final message = _.positionalArguments.elementAt(0) as String?; if (message != null) progressLogs.add(message); @@ -156,12 +140,22 @@ void main() { }); test('handles errors on failure to prune Flutter branches', () async { - when(() => pruneFlutterOriginResult.exitCode).thenReturn(1); + const exception = ProcessException('git', ['remote', 'prune'], 'oops'); + when( + () => shorebirdFlutterManager.pruneRemoteOrigin( + revision: any(named: 'revision'), + ), + ).thenThrow(exception); when(() => logger.progress(any())).thenReturn(_MockProgress()); final result = await runWithOverrides(command.run); expect(result, equals(ExitCode.software.code)); + verify( + () => shorebirdFlutterManager.pruneRemoteOrigin( + revision: newerShorebirdRevision, + ), + ).called(1); }); test( diff --git a/packages/shorebird_cli/test/src/shorebird_flutter_manager_test.dart b/packages/shorebird_cli/test/src/shorebird_flutter_manager_test.dart index d144bbaf..8b697949 100644 --- a/packages/shorebird_cli/test/src/shorebird_flutter_manager_test.dart +++ b/packages/shorebird_cli/test/src/shorebird_flutter_manager_test.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:mason_logger/mason_logger.dart'; import 'package:mocktail/mocktail.dart'; import 'package:path/path.dart' as p; import 'package:scoped/scoped.dart'; @@ -14,6 +15,7 @@ class _MockShorebirdEnv extends Mock implements ShorebirdEnv {} void main() { group(ShorebirdFlutterManager, () { + const flutterRevision = 'flutter-revision'; late Directory shorebirdRoot; late Directory flutterDirectory; late Git git; @@ -50,7 +52,14 @@ void main() { revision: any(named: 'revision'), ), ).thenAnswer((_) async => {}); + when( + () => git.remotePrune( + name: any(named: 'name'), + directory: any(named: 'directory'), + ), + ).thenAnswer((_) async {}); when(() => shorebirdEnv.flutterDirectory).thenReturn(flutterDirectory); + when(() => shorebirdEnv.flutterRevision).thenReturn(flutterRevision); }); group('installRevision', () { @@ -138,5 +147,68 @@ void main() { ); }); }); + + group('pruneRemoteOrigin', () { + test('completes when git command exits with code 0', () async { + await expectLater( + runWithOverrides(() => shorebirdFlutterManager.pruneRemoteOrigin()), + completes, + ); + verify( + () => git.remotePrune( + name: 'origin', + directory: p.join(flutterDirectory.parent.path, flutterRevision), + ), + ).called(1); + }); + + test('completes when git command exits with code 0 (custom revision)', + () async { + const customRevision = 'custom-revision'; + await expectLater( + runWithOverrides( + () => shorebirdFlutterManager.pruneRemoteOrigin( + revision: customRevision, + ), + ), + completes, + ); + verify( + () => git.remotePrune( + name: 'origin', + directory: p.join(flutterDirectory.parent.path, customRevision), + ), + ).called(1); + }); + + test('throws ProcessException when git command exits non-zero code', + () async { + const errorMessage = 'oh no!'; + when( + () => git.remotePrune( + name: any(named: 'name'), + directory: any(named: 'directory'), + ), + ).thenThrow( + ProcessException( + 'git', + ['remote', 'prune', 'origin'], + errorMessage, + ExitCode.software.code, + ), + ); + + expect( + runWithOverrides(() => shorebirdFlutterManager.pruneRemoteOrigin()), + throwsA( + isA().having( + (e) => e.message, + 'message', + errorMessage, + ), + ), + ); + }); + }); }); } diff --git a/packages/shorebird_cli/test/src/shorebird_version_manager_test.dart b/packages/shorebird_cli/test/src/shorebird_version_manager_test.dart index f2818585..dbe1fa87 100644 --- a/packages/shorebird_cli/test/src/shorebird_version_manager_test.dart +++ b/packages/shorebird_cli/test/src/shorebird_version_manager_test.dart @@ -49,6 +49,12 @@ void main() { args: any(named: 'args'), ), ).thenAnswer((_) async {}); + when( + () => git.remotePrune( + name: any(named: 'name'), + directory: any(named: 'directory'), + ), + ).thenAnswer((_) async {}); }); group('isShorebirdVersionCurrent', () { @@ -76,48 +82,44 @@ void main() { ).called(1); }); - test( - 'returns false if current and latest git hashes differ', - () async { - when( - () => git.revParse( - revision: any(named: 'revision'), - directory: any(named: 'directory'), - ), - ).thenAnswer((invocation) async { - final revision = invocation.namedArguments[#revision] as String; - if (revision == 'HEAD') { - return currentShorebirdRevision; - } else if (revision == '@{upstream}') { - return newerShorebirdRevision; - } - throw UnsupportedError('Unexpected revision: $revision'); - }); + test('returns false if current and latest git hashes differ', () async { + when( + () => git.revParse( + revision: any(named: 'revision'), + directory: any(named: 'directory'), + ), + ).thenAnswer((invocation) async { + final revision = invocation.namedArguments[#revision] as String; + if (revision == 'HEAD') { + return currentShorebirdRevision; + } else if (revision == '@{upstream}') { + return newerShorebirdRevision; + } + throw UnsupportedError('Unexpected revision: $revision'); + }); - expect( - await runWithOverrides( - shorebirdVersionManager.isShorebirdVersionCurrent, - ), - isFalse, - ); - verify( - () => - git.fetch(directory: any(named: 'directory'), args: ['--tags']), - ).called(1); - verify( - () => git.revParse( - revision: 'HEAD', - directory: any(named: 'directory'), - ), - ).called(1); - verify( - () => git.revParse( - revision: '@{upstream}', - directory: any(named: 'directory'), - ), - ).called(1); - }, - ); + expect( + await runWithOverrides( + shorebirdVersionManager.isShorebirdVersionCurrent, + ), + isFalse, + ); + verify( + () => git.fetch(directory: any(named: 'directory'), args: ['--tags']), + ).called(1); + verify( + () => git.revParse( + revision: 'HEAD', + directory: any(named: 'directory'), + ), + ).called(1); + verify( + () => git.revParse( + revision: '@{upstream}', + directory: any(named: 'directory'), + ), + ).called(1); + }); test( 'throws ProcessException if git command exits with code other than 0', @@ -162,39 +164,37 @@ void main() { ); }); - test( - 'throws ProcessException when git command exits with code other than 0', - () async { - const errorMessage = 'oh no!'; - when( - () => git.reset( - revision: any(named: 'revision'), - directory: any(named: 'directory'), - args: any(named: 'args'), - ), - ).thenThrow( - ProcessException( - 'git', - ['reset', '--hard', 'HEAD'], - errorMessage, - ExitCode.software.code, - ), - ); + test('throws ProcessException when git command exits with non-zero code', + () async { + const errorMessage = 'oh no!'; + when( + () => git.reset( + revision: any(named: 'revision'), + directory: any(named: 'directory'), + args: any(named: 'args'), + ), + ).thenThrow( + ProcessException( + 'git', + ['reset', '--hard', 'HEAD'], + errorMessage, + ExitCode.software.code, + ), + ); - expect( - runWithOverrides( - () => shorebirdVersionManager.attemptReset(newRevision: 'HEAD'), + expect( + runWithOverrides( + () => shorebirdVersionManager.attemptReset(newRevision: 'HEAD'), + ), + throwsA( + isA().having( + (e) => e.message, + 'message', + errorMessage, ), - throwsA( - isA().having( - (e) => e.message, - 'message', - errorMessage, - ), - ), - ); - }, - ); + ), + ); + }); }); }); }