fix: show friendly error message when GitHub is down (#3655)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
// cspell:words unmatch
|
||||
// cspell:words unmatch githubstatus
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:scoped_deps/scoped_deps.dart';
|
||||
@@ -10,6 +10,44 @@ final gitRef = create(Git.new);
|
||||
/// The [Git] instance available in the current zone.
|
||||
Git get git => read(gitRef);
|
||||
|
||||
/// {@template git_server_unreachable_exception}
|
||||
/// Exception thrown when a git command fails due to a server error.
|
||||
/// {@endtemplate}
|
||||
class GitServerUnreachableException extends ProcessException {
|
||||
/// {@macro git_server_unreachable_exception}
|
||||
GitServerUnreachableException(
|
||||
super.executable,
|
||||
super.arguments, [
|
||||
super.message = '',
|
||||
super.errorCode = 0,
|
||||
]);
|
||||
}
|
||||
|
||||
/// Pattern that matches HTTP server errors (500, 502, 503, 504) or "Internal
|
||||
/// Server Error" in git output.
|
||||
final _serverErrorPattern = RegExp(
|
||||
'The requested URL returned error: (500|502|503|504)|Internal Server Error',
|
||||
);
|
||||
|
||||
/// Pattern that extracts the hostname from a git remote URL in stderr.
|
||||
/// Matches URLs like `https://github.com/org/repo.git/`.
|
||||
final _remoteHostPattern = RegExp(r"https?://([^/']+)");
|
||||
|
||||
String _buildServerErrorMessage(String stderr) {
|
||||
final hostMatch = _remoteHostPattern.firstMatch(stderr);
|
||||
final host = hostMatch?.group(1);
|
||||
final serverName = host ?? 'the remote git server';
|
||||
|
||||
final buffer = StringBuffer('Unable to reach $serverName.');
|
||||
if (host == 'github.com') {
|
||||
buffer.write(
|
||||
'\nIf your network connection is working, check '
|
||||
'https://www.githubstatus.com for service status.',
|
||||
);
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
/// A wrapper around all git related functionality.
|
||||
class Git {
|
||||
/// Name of the git executable.
|
||||
@@ -26,10 +64,19 @@ class Git {
|
||||
workingDirectory: workingDirectory,
|
||||
);
|
||||
if (result.exitCode != 0) {
|
||||
final stderr = '${result.stderr}';
|
||||
if (_serverErrorPattern.hasMatch(stderr)) {
|
||||
throw GitServerUnreachableException(
|
||||
executable,
|
||||
arguments,
|
||||
_buildServerErrorMessage(stderr),
|
||||
result.exitCode,
|
||||
);
|
||||
}
|
||||
throw ProcessException(
|
||||
executable,
|
||||
arguments,
|
||||
'${result.stderr}',
|
||||
stderr,
|
||||
result.exitCode,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// cspell:words githubstatus
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:mason_logger/mason_logger.dart';
|
||||
@@ -37,6 +38,82 @@ void main() {
|
||||
when(() => processResult.exitCode).thenReturn(ExitCode.success.code);
|
||||
});
|
||||
|
||||
group('server error detection', () {
|
||||
test('includes github.com and status link for GitHub URLs', () async {
|
||||
when(() => processResult.exitCode).thenReturn(128);
|
||||
when(() => processResult.stderr).thenReturn(
|
||||
"fatal: unable to access 'https://github.com/"
|
||||
"shorebirdtech/flutter.git/': "
|
||||
'The requested URL returned error: 500',
|
||||
);
|
||||
expect(
|
||||
() => runWithOverrides(
|
||||
() => git.clone(
|
||||
url: 'https://github.com/shorebirdtech/flutter.git',
|
||||
outputDirectory: './output',
|
||||
),
|
||||
),
|
||||
throwsA(
|
||||
isA<GitServerUnreachableException>().having(
|
||||
(e) => e.message,
|
||||
'message',
|
||||
allOf(contains('github.com'), contains('githubstatus.com')),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('includes host name for non-GitHub URLs', () async {
|
||||
when(() => processResult.exitCode).thenReturn(128);
|
||||
when(() => processResult.stderr).thenReturn(
|
||||
"fatal: unable to access 'https://gitlab.com/"
|
||||
"org/repo.git/': "
|
||||
'The requested URL returned error: 502',
|
||||
);
|
||||
expect(
|
||||
() => runWithOverrides(() => git.fetch(directory: 'repo')),
|
||||
throwsA(
|
||||
isA<GitServerUnreachableException>().having(
|
||||
(e) => e.message,
|
||||
'message',
|
||||
allOf(
|
||||
contains('gitlab.com'),
|
||||
isNot(contains('githubstatus.com')),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('falls back to generic message when no URL in stderr', () async {
|
||||
when(() => processResult.exitCode).thenReturn(128);
|
||||
when(
|
||||
() => processResult.stderr,
|
||||
).thenReturn('remote: Internal Server Error');
|
||||
expect(
|
||||
() => runWithOverrides(() => git.fetch(directory: 'repo')),
|
||||
throwsA(
|
||||
isA<GitServerUnreachableException>().having(
|
||||
(e) => e.message,
|
||||
'message',
|
||||
contains('the remote git server'),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('throws ProcessException for non-server errors', () async {
|
||||
when(() => processResult.exitCode).thenReturn(128);
|
||||
when(
|
||||
() => processResult.stderr,
|
||||
).thenReturn('fatal: repository not found');
|
||||
expect(
|
||||
() => runWithOverrides(() => git.fetch(directory: 'repo')),
|
||||
throwsA(isA<ProcessException>()),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('clone', () {
|
||||
const url = 'https://github.com/shorebirdtech/shorebird';
|
||||
const outputDirectory = './output';
|
||||
|
||||
Reference in New Issue
Block a user