-
Notifications
You must be signed in to change notification settings - Fork 3.8k
Add pre-commit hook to verify static analysis passes & format is correct #11941
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
0a65ef8
ccda0df
09a89f2
04253cc
e078f24
d61f271
a487e22
85219e8
021dd71
4908982
d05c928
2d89dd8
7e53502
5b8c9a8
f4eed3d
638ce34
c19c8f3
f8837ba
352580c
a6a2a78
5f94b32
bc0ce9f
38e5327
d725d68
e344ca7
814caf1
32faa24
9dc4b9d
1ccc307
3858e8d
dc86fa7
7a17ad8
55d6235
db09b06
220baab
4432af2
befac5a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| # Git Hooks | ||
|
|
||
| This directory contains Git hooks for the `flutter/packages` repository. | ||
|
|
||
| ## Installation | ||
|
|
||
| To install the Git hooks, run the following commands from the root of the repository: | ||
|
|
||
| ```bash | ||
| # Fetch dependencies for the githooks package | ||
| dart pub get -C script/githooks | ||
|
|
||
| # Run the installation script | ||
| dart script/githooks/bin/install_hooks.dart | ||
| ``` | ||
|
|
||
| ## Available Hooks | ||
|
|
||
| ### pre-commit | ||
|
|
||
| The `pre-commit` hook runs automatically when you run `git commit` and performs the following checks on any staged changes: | ||
|
|
||
| 1. **Formatting**: It runs `dart run script/tool/bin/flutter_plugin_tools.dart format --run-on-staged-packages` to verify that all staged files in the targeted packages are correctly formatted. | ||
| 2. **Static Analysis**: If formatting passes, it runs `dart run script/tool/bin/flutter_plugin_tools.dart analyze --run-on-staged-packages --dart` to run static analysis on the staged packages. | ||
|
|
||
| If either check fails, it aborts the commit. To bypass the hook (for a WIP commit, for example), you can use the `--no-verify` flag: | ||
|
|
||
| ```bash | ||
| git commit -m "WIP" --no-verify | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| // Copyright 2013 The Flutter Authors. All rights reserved. | ||
| // 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:path/path.dart' as p; | ||
|
|
||
| void main() async { | ||
| Directory repoRoot = Directory.current; | ||
| while (repoRoot.path != repoRoot.parent.path && | ||
| !(Directory(p.join(repoRoot.path, '.git')).existsSync() || | ||
| File(p.join(repoRoot.path, '.git')).existsSync())) { | ||
| repoRoot = repoRoot.parent; | ||
| } | ||
| if (!(Directory(p.join(repoRoot.path, '.git')).existsSync() || | ||
| File(p.join(repoRoot.path, '.git')).existsSync())) { | ||
| print('Installation failed because .git directory could not be found.'); | ||
| exit(1); | ||
| } | ||
|
camsim99 marked this conversation as resolved.
|
||
|
|
||
| final ProcessResult result = await Process.run('git', [ | ||
| 'config', | ||
| 'core.hooksPath', | ||
| 'script/githooks', | ||
| ], workingDirectory: repoRoot.path); | ||
| if (result.exitCode == 0) { | ||
| print('Git hooks installed successfully!'); | ||
| } else { | ||
| print('Failed to install Git hooks: ${result.stderr}'); | ||
| exit(1); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| // Copyright 2013 The Flutter Authors. All rights reserved. | ||
| // Use of this source code is governed by a BSD-style license that can be | ||
| // found in the LICENSE file. | ||
|
|
||
| import 'dart:io' as io; | ||
|
|
||
| import 'package:githooks/githooks.dart'; | ||
|
|
||
| Future<void> main(List<String> args) async { | ||
| io.exitCode = await run(args); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| // Copyright 2013 The Flutter Authors. All rights reserved. | ||
| // Use of this source code is governed by a BSD-style license that can be | ||
| // found in the LICENSE file. | ||
|
|
||
| import 'package:args/command_runner.dart'; | ||
|
|
||
| import 'src/pre_commit_command.dart'; | ||
|
|
||
| /// Runs the githooks command line utility. | ||
| Future<int> run(List<String> args) async { | ||
| final runner = CommandRunner<bool>('githooks', 'Git hooks for flutter/packages') | ||
| ..addCommand(PreCommitCommand()); | ||
|
|
||
| final bool success = await runner.run(args) ?? false; | ||
| return success ? 0 : 1; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,180 @@ | ||
| // Copyright 2013 The Flutter Authors. All rights reserved. | ||
| // 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 { | ||
|
camsim99 marked this conversation as resolved.
|
||
| 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 { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this logic was moved to the format/package commands. If so consider removing it. If we add logic here to filter then it could be confusing if it diverges from what is in the other tooling.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The optimization was moved to the format/package commands but this is an additional optimization--when there are no relevant changes to have this hook check, let's skip calling into the plugins tool altogether. Calling into at all adds steps like checking package dependencies that we can skip if we aren't going to end up running the checks at all. I don't see the risk of divergence being high, but if you're concerned we can remove it! I don't feel super strongly since we don't necessarily expect high human usage.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok in that case can you give me the situation and performance delta of this optimization? My understanding having not run it is that we are still spinning up dart and we are still filtering using the same git commands so I don't see how it could improve efficiency. Is there a step I am not seeing? The most convincing evidence would be a common situation, maybe a merge conflict, and showing the difference is a multiple in speed. I guess my larger concern is that the filtering should happen in one place unless we get a really large performance or ux gain and on my mind once we are in dart land we should be able to make the performance the same. |
||
| 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; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| #!/usr/bin/env bash | ||
| set -e | ||
|
|
||
| HOOKS_DIR="$(dirname "$0")" | ||
| exec dart "$HOOKS_DIR/bin/main.dart" pre-commit "$@" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| name: githooks | ||
| description: Git hooks for the flutter/packages repository. For more information on the hooks and how to install them, see README.md | ||
| publish_to: none | ||
|
|
||
| environment: | ||
| sdk: ^3.10.0-0 | ||
|
|
||
| dependencies: | ||
| args: any | ||
| path: any | ||
|
|
||
| dev_dependencies: | ||
| test: any | ||
|
camsim99 marked this conversation as resolved.
|
||
Uh oh!
There was an error while loading. Please reload this page.