refactor(shorebird_cli): shorebird upgrade use prune from ShorebirdFlutterManager (#1057)

This commit is contained in:
Felix Angelov
2023-08-08 11:40:16 -05:00
committed by GitHub
parent 253a6904d5
commit aa32096a29
5 changed files with 192 additions and 138 deletions
@@ -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<void> _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,
);
}
}
}
@@ -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<void> 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<void> pruneRemoteOrigin({String? revision}) async {
return git.remotePrune(
name: 'origin',
directory: _workingDirectory(revision: revision),
);
}
}
@@ -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 = <String>[];
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(
@@ -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<ProcessException>().having(
(e) => e.message,
'message',
errorMessage,
),
),
);
});
});
});
}
@@ -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<ProcessException>().having(
(e) => e.message,
'message',
errorMessage,
),
throwsA(
isA<ProcessException>().having(
(e) => e.message,
'message',
errorMessage,
),
),
);
},
);
),
);
});
});
});
}