-
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
Merged
Merged
Changes from 7 commits
Commits
Show all changes
44 commits
Select commit
Hold shift + click to select a range
0a65ef8
first pass
camsim99 ccda0df
rippling revisions
camsim99 09a89f2
use plugin tool instead
camsim99 04253cc
-- debugging stuff
camsim99 e078f24
fake commit
camsim99 d61f271
test + command fix
camsim99 a487e22
fake commit
camsim99 85219e8
try optimizing by cutting out native toolchains
camsim99 021dd71
fake commit
camsim99 4908982
run dart analyze directly
camsim99 d05c928
undo fake commit
camsim99 2d89dd8
run format and analyze directly on staged files
camsim99 7e53502
fake commit
camsim99 5b8c9a8
use ANSI escape codes to erase status logs
camsim99 f4eed3d
fake commit
camsim99 638ce34
change emoji to runner
camsim99 c19c8f3
undo fake commit
camsim99 f8837ba
make unit tests stricter
camsim99 352580c
self review
camsim99 a6a2a78
expect args
camsim99 5f94b32
gemini review
camsim99 bc0ce9f
fix print
camsim99 38e5327
address gemini review
camsim99 d725d68
first agentic pass at refactoring
camsim99 e344ca7
implement --run-on-staged-packages
camsim99 814caf1
small optimizations
camsim99 32faa24
msg
camsim99 9dc4b9d
address reid review that is still relevant
camsim99 1ccc307
fix analysis
camsim99 3858e8d
clarify if test skipped
camsim99 dc86fa7
little tweaks
camsim99 7a17ad8
self review
camsim99 55d6235
test nits
camsim99 db09b06
address review
camsim99 220baab
self review
camsim99 4432af2
ignore
camsim99 befac5a
add docs + todo for plugin tool overhead
camsim99 006ead1
Merge remote-tracking branch 'upstream/main' into cos_precommit
camsim99 8719555
correct license
camsim99 e1d0372
bump version
camsim99 6940eee
test fixes
camsim99 37ccc08
Merge remote-tracking branch 'upstream/main' into cos_precommit
camsim99 f85c2fe
format & analyze
camsim99 3d9dc63
Merge remote-tracking branch 'upstream/main' into cos_precommit
camsim99 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| // 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 != '/' && !Directory(p.join(repoRoot.path, '.git')).existsSync()) { | ||
| repoRoot = repoRoot.parent; | ||
| } | ||
|
|
||
| if (repoRoot.path == '/') { | ||
| print('❌ Could not find .git directory.'); | ||
| 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); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,158 @@ | ||
| // 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 = 'Checks to run before a "git commit"'; | ||
|
camsim99 marked this conversation as resolved.
Outdated
|
||
|
|
||
| String? _findPackageName(String filePath, String repoRoot) { | ||
| Directory currentDir = File(p.join(repoRoot, filePath)).parent; | ||
| while (p.isWithin(repoRoot, currentDir.path) || p.equals(repoRoot, currentDir.path)) { | ||
| final String dirName = p.basename(currentDir.path); | ||
| if (dirName != 'example' && File(p.join(currentDir.path, 'pubspec.yaml')).existsSync()) { | ||
| return dirName; | ||
| } | ||
| if (p.equals(repoRoot, currentDir.path)) { | ||
| break; | ||
| } | ||
| currentDir = currentDir.parent; | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| @override | ||
| Future<bool> run() async { | ||
|
camsim99 marked this conversation as resolved.
|
||
| // Find the repo root where the plugin tool is located. | ||
| Directory repoRoot = Directory.current; | ||
| while (repoRoot.path != '/' && !Directory(p.join(repoRoot.path, '.git')).existsSync()) { | ||
| repoRoot = repoRoot.parent; | ||
| } | ||
|
|
||
| if (repoRoot.path == '/') { | ||
| print('❌ Could not find .git directory.'); | ||
| return false; | ||
| } | ||
|
camsim99 marked this conversation as resolved.
Outdated
|
||
|
|
||
| final ProcessResult diffResult = await processRunner('git', [ | ||
| 'diff', | ||
| '--cached', | ||
| '--name-only', | ||
| '--diff-filter=ACM', | ||
| ], workingDirectory: repoRoot.path); | ||
|
|
||
| if (diffResult.exitCode != 0) { | ||
|
camsim99 marked this conversation as resolved.
Outdated
|
||
| print('❌ Failed to get staged files'); | ||
|
camsim99 marked this conversation as resolved.
Outdated
|
||
| return false; | ||
|
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. I made this decision because I assume there must be a Git setup issue if this happens, but open to pushback if we think this could actually block valid commits. |
||
| } | ||
|
|
||
| final List<String> stagedFiles = (diffResult.stdout as String) | ||
| .split('\n') | ||
| .map((e) => e.trim()) | ||
| .where((e) => e.isNotEmpty) | ||
| .toList(); | ||
|
|
||
| if (stagedFiles.isEmpty) { | ||
| return true; // No files changed. | ||
| } | ||
|
|
||
| final Set<String> targetPackages = {}; | ||
| for (final file in stagedFiles) { | ||
| final String? packageName = _findPackageName(file, repoRoot.path); | ||
| if (packageName != null) { | ||
| targetPackages.add(packageName); | ||
| } | ||
| } | ||
|
|
||
| if (targetPackages.isEmpty) { | ||
| return true; // None of the changed files are part of a package we care about. | ||
| } | ||
|
|
||
| final String toolScript = p.join( | ||
| repoRoot.path, | ||
| 'script', | ||
| 'tool', | ||
| 'bin', | ||
| 'flutter_plugin_tools.dart', | ||
| ); | ||
| final packageArgs = '--packages=${targetPackages.join(',')}'; | ||
|
|
||
| print( | ||
| '🔍 Running pre-commit checks on ${targetPackages.length} packages: ${targetPackages.join(', ')}', | ||
| ); | ||
| var hasError = false; | ||
|
|
||
| // Check formatting. | ||
| print('Checking formatting...'); | ||
| final ProcessResult formatResult = await processRunner('dart', [ | ||
| 'run', | ||
| toolScript, | ||
| 'format', | ||
| packageArgs, | ||
| '--fail-on-change', | ||
| ], workingDirectory: repoRoot.path); | ||
|
|
||
| if (formatResult.exitCode != 0) { | ||
| if (formatResult.stdout.toString().isNotEmpty) { | ||
| print(formatResult.stdout); | ||
| } | ||
| if (formatResult.stderr.toString().isNotEmpty) { | ||
| print(formatResult.stderr); | ||
| } | ||
| print( | ||
| '❌ Formatting issues found. Please run "dart run script/tool/bin/flutter_plugin_tools.dart format $packageArgs" to fix them.', | ||
| ); | ||
| hasError = true; | ||
| } else { | ||
| print('✅ Formatting looks good.'); | ||
| } | ||
|
|
||
| // Run static analysis. | ||
| print('Running static analysis...'); | ||
| final ProcessResult analyzeResult = await processRunner('dart', [ | ||
| 'run', | ||
| toolScript, | ||
| 'analyze', | ||
| packageArgs, | ||
| ], workingDirectory: repoRoot.path); | ||
|
|
||
| if (analyzeResult.exitCode != 0) { | ||
| if (analyzeResult.stdout.toString().isNotEmpty) print(analyzeResult.stdout); | ||
| if (analyzeResult.stderr.toString().isNotEmpty) print(analyzeResult.stderr); | ||
| print('❌ Static analysis errors found. Please fix the errors listed above.'); | ||
| hasError = true; | ||
| } else { | ||
| print('✅ Static analysis looks good.'); | ||
| } | ||
|
|
||
| return !hasError; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 "$@" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| name: githooks | ||
| description: Git hooks for the flutter/packages repository. | ||
|
camsim99 marked this conversation as resolved.
Outdated
|
||
| publish_to: none | ||
|
|
||
| environment: | ||
| sdk: ^3.10.0-0 | ||
|
|
||
| dependencies: | ||
| args: any | ||
| path: any | ||
|
|
||
| dev_dependencies: | ||
| test: any | ||
|
camsim99 marked this conversation as resolved.
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| // 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'; | ||
|
|
||
| import 'package:githooks/src/pre_commit_command.dart'; | ||
| import 'package:test/test.dart'; | ||
|
|
||
| void main() { | ||
| group('pre-commit hook', () { | ||
| test('passes when both format and analyze succeed', () async { | ||
| final command = PreCommitCommand( | ||
| processRunner: | ||
| (String executable, List<String> arguments, {String? workingDirectory}) async { | ||
| if (executable == 'git') { | ||
| return ProcessResult(0, 0, 'script/githooks/lib/githooks.dart\n', ''); | ||
| } | ||
| return ProcessResult(0, 0, 'Success', ''); | ||
| }, | ||
| ); | ||
|
|
||
| final bool result = await command.run(); | ||
| expect(result, isTrue); | ||
| }); | ||
|
|
||
| test('fails when formatting fails', () async { | ||
| final command = PreCommitCommand( | ||
| processRunner: | ||
| (String executable, List<String> arguments, {String? workingDirectory}) async { | ||
| if (executable == 'git') { | ||
| return ProcessResult(0, 0, 'script/githooks/lib/githooks.dart\n', ''); | ||
| } | ||
| if (arguments.contains('format')) { | ||
| return ProcessResult(0, 1, 'bad_file.dart', ''); | ||
| } | ||
| return ProcessResult(0, 0, 'Success', ''); | ||
| }, | ||
| ); | ||
|
|
||
| final bool result = await command.run(); | ||
| expect(result, isFalse); | ||
| }); | ||
|
|
||
| test('fails when analysis fails', () async { | ||
| final command = PreCommitCommand( | ||
| processRunner: | ||
| (String executable, List<String> arguments, {String? workingDirectory}) async { | ||
| if (executable == 'git') { | ||
| return ProcessResult(0, 0, 'script/githooks/lib/githooks.dart\n', ''); | ||
| } | ||
| if (arguments.contains('analyze')) { | ||
| return ProcessResult(0, 1, 'error in file.dart', ''); | ||
| } | ||
| return ProcessResult(0, 0, 'Success', ''); | ||
| }, | ||
| ); | ||
|
|
||
| final bool result = await command.run(); | ||
| expect(result, isFalse); | ||
| }); | ||
|
|
||
| test('ignores non-dart files', () async { | ||
| final command = PreCommitCommand( | ||
| processRunner: | ||
| (String executable, List<String> arguments, {String? workingDirectory}) async { | ||
| if (executable == 'git') { | ||
| return ProcessResult(0, 0, 'README.md\n', ''); | ||
| } | ||
| return ProcessResult(0, 0, 'Success', ''); | ||
| }, | ||
| ); | ||
|
|
||
| final bool result = await command.run(); | ||
| expect(result, isTrue); | ||
| }); | ||
| }); | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.