blob: 73d7819fba44ec9b2c6ee73d7d410d7a2d757f58 [file]
// Copyright 2013 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// ignore_for_file: avoid_print
import 'dart:io';
import 'package:args/command_runner.dart';
import 'package:path/path.dart' as p;
/// The command that implements the pre-commit githook.
class PreCommitCommand extends Command<bool> {
/// Creates a [PreCommitCommand].
PreCommitCommand({
Future<ProcessResult> Function(
String executable,
List<String> arguments, {
String? workingDirectory,
})?
processRunner,
}) : processRunner = processRunner ?? Process.run;
/// The process runner injected for testing.
final Future<ProcessResult> Function(
String executable,
List<String> arguments, {
String? workingDirectory,
})
processRunner;
@override
final String name = 'pre-commit';
@override
final String description =
'Runs formatting and static analysis checks on staged changes before a "git commit"';
/// Runs a pre-commit check for correct formatting and static analysis.
///
/// It runs the plugin tool format and analyze commands on all packages that have staged changes.
/// If any of the commands fail, it will return false; otherwise, it will return true.
@override
Future<bool> run() async {
final Directory? repoRoot = await _findRepoRoot();
if (repoRoot == null) {
print('Could not find git repository.');
return false;
}
// Skips check if there are no staged package changes because this
// makes a ~5s difference in runtime.
// TODO(camsim99): Investigate why the plugins tool adds overhead:
// https://github.com/flutter/flutter/issues/188870.
final bool? hasStaged = await _hasStagedPackages(repoRoot);
if (hasStaged == null) {
return false;
}
if (!hasStaged) {
print('No staged package changes to check.');
return true;
}
final String toolScript = p.join(
repoRoot.path,
'script',
'tool',
'bin',
'flutter_plugin_tools.dart',
);
print('Running pre-commit format and static analysis checks for staged changes...');
// Check format.
final bool formatPassed = await _executeCheckFormatting(repoRoot, toolScript);
if (!formatPassed) {
return false;
}
return _executeCheckStaticAnalysis(repoRoot, toolScript);
}
/// Finds the repository root directory using git.
///
/// Returns null if git fails or if the directory is not a git repository.
Future<Directory?> _findRepoRoot() async {
final ProcessResult rootResult = await processRunner('git', <String>[
'rev-parse',
'--show-toplevel',
], workingDirectory: Directory.current.path);
if (rootResult.exitCode != 0) {
return null;
}
return Directory((rootResult.stdout as String).trim());
}
/// Checks if there are any staged package changes in the repository.
///
/// Returns true if at least one staged file is located within a package directory
/// (under packages/ or third_party/packages/), false if there are none, or null
/// if the git command fails.
Future<bool?> _hasStagedPackages(Directory repoRoot) async {
final ProcessResult diffResult = await processRunner('git', <String>[
'diff',
'--cached',
'-z',
'--name-only',
], workingDirectory: repoRoot.path);
if (diffResult.exitCode != 0) {
print('Failed to check staged changes.');
if (diffResult.stderr.toString().isNotEmpty) {
print(diffResult.stderr);
}
// If we cannot determine the diff, abort pre-commit check by returning null.
return null;
}
final stdoutStr = diffResult.stdout as String;
if (stdoutStr.isEmpty) {
return false;
}
final List<String> lines = stdoutStr.split('\u0000')
..removeWhere((String element) => element.isEmpty);
return lines.any(
(String path) => path.startsWith('packages/') || path.startsWith('third_party/packages/'),
);
}
/// Runs the formatting check on staged files.
///
/// Returns true if all staged files are correctly formatted or false otherwise.
Future<bool> _executeCheckFormatting(Directory repoRoot, String toolScript) async {
final ProcessResult formatResult = await processRunner('dart', [
'run',
toolScript,
'format',
'--run-on-staged-packages',
'--fail-on-change',
], workingDirectory: repoRoot.path);
if (formatResult.exitCode != 0) {
print('''
Formatting check failed.
To fix formatting automatically, run:
dart run script/tool/bin/flutter_plugin_tools.dart format --run-on-staged-packages
To bypass this check, commit with --no-verify.''');
return false;
}
print('Formatting looks good!');
return true;
}
/// Runs the static analysis check on staged files.
///
/// Returns true if all staged files pass analysis or false otherwise.
Future<bool> _executeCheckStaticAnalysis(Directory repoRoot, String toolScript) async {
final ProcessResult analyzeResult = await processRunner('dart', [
'run',
toolScript,
'analyze',
'--run-on-staged-packages',
'--dart',
], workingDirectory: repoRoot.path);
if (analyzeResult.exitCode != 0) {
print('''
Static analysis check failed.
To view and fix analysis errors, run:
dart run script/tool/bin/flutter_plugin_tools.dart analyze --run-on-staged-packages --dart
To bypass this check, commit with --no-verify.''');
return false;
}
print('Static analysis looks good!');
return true;
}
}