| // 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; |
| } |
| } |