From 0a65ef84c7548f0243332d3efc8bfc7084cc84d9 Mon Sep 17 00:00:00 2001 From: Camille Simon Date: Thu, 18 Jun 2026 09:18:57 -0700 Subject: [PATCH 01/23] first pass --- script/githooks/bin/install_hooks.dart | 22 +++++ script/githooks/bin/main.dart | 15 +++ .../githooks/lib/src/pre_commit_command.dart | 75 ++++++++++++++ script/githooks/pre-commit | 6 ++ script/githooks/pubspec.yaml | 13 +++ script/githooks/test/pre_commit_test.dart | 98 +++++++++++++++++++ 6 files changed, 229 insertions(+) create mode 100644 script/githooks/bin/install_hooks.dart create mode 100644 script/githooks/bin/main.dart create mode 100644 script/githooks/lib/src/pre_commit_command.dart create mode 100755 script/githooks/pre-commit create mode 100644 script/githooks/pubspec.yaml create mode 100644 script/githooks/test/pre_commit_test.dart diff --git a/script/githooks/bin/install_hooks.dart b/script/githooks/bin/install_hooks.dart new file mode 100644 index 000000000000..87682a2779a8 --- /dev/null +++ b/script/githooks/bin/install_hooks.dart @@ -0,0 +1,22 @@ +import 'dart:io'; +import 'package:path/path.dart' as p; + +void main() async { + var 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); + } + + final 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); + } +} diff --git a/script/githooks/bin/main.dart b/script/githooks/bin/main.dart new file mode 100644 index 000000000000..57100edb8f4d --- /dev/null +++ b/script/githooks/bin/main.dart @@ -0,0 +1,15 @@ +import 'dart:io'; +import 'package:args/command_runner.dart'; +import '../lib/src/pre_commit_command.dart'; + +void main(List args) async { + final runner = CommandRunner('githooks', 'Git hooks for flutter/packages') + ..addCommand(PreCommitCommand()); + + try { + await runner.run(args); + } catch (e) { + print(e); + exit(1); + } +} diff --git a/script/githooks/lib/src/pre_commit_command.dart b/script/githooks/lib/src/pre_commit_command.dart new file mode 100644 index 000000000000..48ba4c970ca0 --- /dev/null +++ b/script/githooks/lib/src/pre_commit_command.dart @@ -0,0 +1,75 @@ +import 'dart:io'; +import 'package:args/command_runner.dart'; + +class PreCommitCommand extends Command { + @override + final String name = 'pre-commit'; + + @override + final String description = 'Runs pre-commit checks like format and analyze.'; + + @override + Future run() async { + // 1. Get the list of staged files + final diffResult = await Process.run('git', ['diff', '--cached', '--name-only', '--diff-filter=ACM']); + if (diffResult.exitCode != 0) { + print('Failed to get staged files.'); + exit(1); + } + + // Filter for Dart files + final stagedDartFiles = (diffResult.stdout as String) + .split('\n') + .map((file) => file.trim()) + .where((file) => file.endsWith('.dart')) + .toList(); + + if (stagedDartFiles.isEmpty) { + // No Dart files are being committed; exit cleanly + return; + } + + print('🔍 Running pre-commit checks on staged Dart files...'); + bool hasError = false; + + // 2. Code Formatting Check + print('Checking formatting...'); + final formatResult = await Process.run('dart', [ + 'format', + '--output=none', + '--set-exit-if-changed', + ...stagedDartFiles, + ]); + + if (formatResult.exitCode != 0) { + print('❌ Formatting issues found in the following files:'); + print((formatResult.stdout as String).trim()); + print('👉 Please run "dart format" on these files to fix them.'); + hasError = true; + } else { + print('✅ Formatting looks good.'); + } + + // 3. Static Analysis Check + print('Running static analysis...'); + final analyzeResult = await Process.run('dart', [ + 'analyze', + '--fatal-infos', + ...stagedDartFiles, + ]); + + if (analyzeResult.exitCode != 0) { + print('❌ Static analysis errors found:'); + print((analyzeResult.stdout as String).trim()); + hasError = true; + } else { + print('✅ Static analysis looks good.'); + } + + // 4. Final Verdict + if (hasError) { + print('🚨 Pre-commit checks failed. Please fix the above errors and try committing again.'); + exit(1); + } + } +} diff --git a/script/githooks/pre-commit b/script/githooks/pre-commit new file mode 100755 index 000000000000..528775e30233 --- /dev/null +++ b/script/githooks/pre-commit @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -e + +echo "HOOK EXECUTED" >&2 +HOOKS_DIR="$(dirname "$0")" +exec dart "$HOOKS_DIR/bin/main.dart" pre-commit "$@" diff --git a/script/githooks/pubspec.yaml b/script/githooks/pubspec.yaml new file mode 100644 index 000000000000..a191a975b763 --- /dev/null +++ b/script/githooks/pubspec.yaml @@ -0,0 +1,13 @@ +name: githooks +description: Git hooks for the flutter/packages repository. +publish_to: none + +environment: + sdk: ^3.10.0-0 + +dependencies: + args: any + +dev_dependencies: + path: any + test: any diff --git a/script/githooks/test/pre_commit_test.dart b/script/githooks/test/pre_commit_test.dart new file mode 100644 index 000000000000..f67165385095 --- /dev/null +++ b/script/githooks/test/pre_commit_test.dart @@ -0,0 +1,98 @@ +import 'dart:io'; +import 'package:test/test.dart'; +import 'package:path/path.dart' as p; + +void main() { + group('pre-commit hook', () { + late Directory tempDir; + late String githooksPath; + + setUp(() async { + tempDir = await Directory.systemTemp.createTemp('pre_commit_test_'); + + await Process.run('git', ['init'], workingDirectory: tempDir.path); + await Process.run('git', ['config', 'user.email', 'test@example.com'], workingDirectory: tempDir.path); + await Process.run('git', ['config', 'user.name', 'Test User'], workingDirectory: tempDir.path); + + var repoRoot = Directory.current; + while (repoRoot.path != '/' && !Directory(p.join(repoRoot.path, '.git')).existsSync()) { + repoRoot = repoRoot.parent; + } + + githooksPath = p.join(repoRoot.path, 'script', 'githooks'); + + // Set the hooksPath to the actual githooks directory + await Process.run('git', ['config', 'core.hooksPath', githooksPath], workingDirectory: tempDir.path); + }); + + tearDown(() async { + await tempDir.delete(recursive: true); + }); + + Future runHook() async { + // Actually run git commit to trigger the hook! + return Process.run('git', ['commit', '-m', 'test'], workingDirectory: tempDir.path); + } + + test('passes on a clean commit', () async { + final file = File(p.join(tempDir.path, 'test_file.dart')); + await file.writeAsString(''' +void main() { + print('Hello, world!'); +} +'''); + + await Process.run('git', ['add', 'test_file.dart'], workingDirectory: tempDir.path); + + final result = await runHook(); + expect(result.exitCode, 0); + final output = '${result.stdout}${result.stderr}'; + expect(output, contains('Formatting looks good.')); + expect(output, contains('Static analysis looks good.')); + }); + + test('fails on formatting error', () async { + final file = File(p.join(tempDir.path, 'test_file.dart')); + await file.writeAsString(''' +void main(){ + print('Hello, world!'); +} +'''); + + await Process.run('git', ['add', 'test_file.dart'], workingDirectory: tempDir.path); + + final result = await runHook(); + expect(result.exitCode, 1); + final output = '${result.stdout}${result.stderr}'; + expect(output, contains('Formatting issues found')); + }); + + test('fails on analysis error', () async { + final file = File(p.join(tempDir.path, 'test_file.dart')); + await file.writeAsString(''' +void main() { + print('Hello, world!') +} +'''); + + await Process.run('git', ['add', 'test_file.dart'], workingDirectory: tempDir.path); + + final result = await runHook(); + expect(result.exitCode, 1); + final output = '${result.stdout}${result.stderr}'; + expect(output, contains('Static analysis errors found')); + }); + + test('ignores non-dart files', () async { + final file = File(p.join(tempDir.path, 'README.md')); + await file.writeAsString('# Hello'); + + await Process.run('git', ['add', 'README.md'], workingDirectory: tempDir.path); + + final result = await runHook(); + expect(result.exitCode, 0); + final output = '${result.stdout}${result.stderr}'; + expect(output, isNot(contains('Checking formatting...'))); + }); + }); +} From ccda0df5c750adc5384a685fd6d5e60053cf7c58 Mon Sep 17 00:00:00 2001 From: Camille Simon Date: Thu, 18 Jun 2026 11:21:37 -0700 Subject: [PATCH 02/23] rippling revisions --- script/githooks/bin/install_hooks.dart | 14 +++++- script/githooks/bin/main.dart | 20 +++----- script/githooks/lib/githooks.dart | 16 ++++++ .../githooks/lib/src/pre_commit_command.dart | 49 +++++++++++-------- script/githooks/pubspec.yaml | 2 +- script/githooks/test/pre_commit_test.dart | 15 +++--- 6 files changed, 74 insertions(+), 42 deletions(-) create mode 100644 script/githooks/lib/githooks.dart diff --git a/script/githooks/bin/install_hooks.dart b/script/githooks/bin/install_hooks.dart index 87682a2779a8..929e03a81263 100644 --- a/script/githooks/bin/install_hooks.dart +++ b/script/githooks/bin/install_hooks.dart @@ -1,8 +1,14 @@ +// 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 { - var repoRoot = Directory.current; + Directory repoRoot = Directory.current; while (repoRoot.path != '/' && !Directory(p.join(repoRoot.path, '.git')).existsSync()) { repoRoot = repoRoot.parent; } @@ -12,7 +18,11 @@ void main() async { exit(1); } - final result = await Process.run('git', ['config', 'core.hooksPath', 'script/githooks'], workingDirectory: repoRoot.path); + 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 { diff --git a/script/githooks/bin/main.dart b/script/githooks/bin/main.dart index 57100edb8f4d..7d734dd82b52 100644 --- a/script/githooks/bin/main.dart +++ b/script/githooks/bin/main.dart @@ -1,15 +1,11 @@ -import 'dart:io'; -import 'package:args/command_runner.dart'; -import '../lib/src/pre_commit_command.dart'; +// 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. -void main(List args) async { - final runner = CommandRunner('githooks', 'Git hooks for flutter/packages') - ..addCommand(PreCommitCommand()); +import 'dart:io' as io; - try { - await runner.run(args); - } catch (e) { - print(e); - exit(1); - } +import 'package:githooks/githooks.dart'; + +Future main(List args) async { + io.exitCode = await run(args); } diff --git a/script/githooks/lib/githooks.dart b/script/githooks/lib/githooks.dart new file mode 100644 index 000000000000..59b3e6fe3fff --- /dev/null +++ b/script/githooks/lib/githooks.dart @@ -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 run(List args) async { + final runner = CommandRunner('githooks', 'Git hooks for flutter/packages') + ..addCommand(PreCommitCommand()); + + final bool success = await runner.run(args) ?? false; + return success ? 0 : 1; +} diff --git a/script/githooks/lib/src/pre_commit_command.dart b/script/githooks/lib/src/pre_commit_command.dart index 48ba4c970ca0..4f7e016a808d 100644 --- a/script/githooks/lib/src/pre_commit_command.dart +++ b/script/githooks/lib/src/pre_commit_command.dart @@ -1,40 +1,51 @@ +// 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'; -class PreCommitCommand extends Command { +/// The command that implements the pre-commit githook +class PreCommitCommand extends Command { @override final String name = 'pre-commit'; @override - final String description = 'Runs pre-commit checks like format and analyze.'; + final String description = 'Checks to run before a "git commit"'; @override - Future run() async { - // 1. Get the list of staged files - final diffResult = await Process.run('git', ['diff', '--cached', '--name-only', '--diff-filter=ACM']); + Future run() async { + // Find changed Dart files. + final ProcessResult diffResult = await Process.run('git', [ + 'diff', + '--cached', + '--name-only', + '--diff-filter=ACM', + ]); if (diffResult.exitCode != 0) { print('Failed to get staged files.'); exit(1); } - // Filter for Dart files - final stagedDartFiles = (diffResult.stdout as String) + final List stagedDartFiles = (diffResult.stdout as String) .split('\n') .map((file) => file.trim()) .where((file) => file.endsWith('.dart')) .toList(); if (stagedDartFiles.isEmpty) { - // No Dart files are being committed; exit cleanly - return; + // No Dart files are being committed. + return true; } print('🔍 Running pre-commit checks on staged Dart files...'); - bool hasError = false; + var hasError = false; - // 2. Code Formatting Check + // Check formatting. print('Checking formatting...'); - final formatResult = await Process.run('dart', [ + final ProcessResult formatResult = await Process.run('dart', [ 'format', '--output=none', '--set-exit-if-changed', @@ -44,15 +55,17 @@ class PreCommitCommand extends Command { if (formatResult.exitCode != 0) { print('❌ Formatting issues found in the following files:'); print((formatResult.stdout as String).trim()); - print('👉 Please run "dart format" on these files to fix them.'); + print( + '👉 Please run "dart format" on these files to fix them.', + ); hasError = true; } else { print('✅ Formatting looks good.'); } - // 3. Static Analysis Check + // Run static analysis. print('Running static analysis...'); - final analyzeResult = await Process.run('dart', [ + final ProcessResult analyzeResult = await Process.run('dart', [ 'analyze', '--fatal-infos', ...stagedDartFiles, @@ -66,10 +79,6 @@ class PreCommitCommand extends Command { print('✅ Static analysis looks good.'); } - // 4. Final Verdict - if (hasError) { - print('🚨 Pre-commit checks failed. Please fix the above errors and try committing again.'); - exit(1); - } + return !hasError; } } diff --git a/script/githooks/pubspec.yaml b/script/githooks/pubspec.yaml index a191a975b763..be8db13c80c0 100644 --- a/script/githooks/pubspec.yaml +++ b/script/githooks/pubspec.yaml @@ -7,7 +7,7 @@ environment: dependencies: args: any + path: any dev_dependencies: - path: any test: any diff --git a/script/githooks/test/pre_commit_test.dart b/script/githooks/test/pre_commit_test.dart index f67165385095..64de7928c025 100644 --- a/script/githooks/test/pre_commit_test.dart +++ b/script/githooks/test/pre_commit_test.dart @@ -1,6 +1,7 @@ import 'dart:io'; -import 'package:test/test.dart'; + import 'package:path/path.dart' as p; +import 'package:test/test.dart'; void main() { group('pre-commit hook', () { @@ -14,7 +15,7 @@ void main() { await Process.run('git', ['config', 'user.email', 'test@example.com'], workingDirectory: tempDir.path); await Process.run('git', ['config', 'user.name', 'Test User'], workingDirectory: tempDir.path); - var repoRoot = Directory.current; + Directory repoRoot = Directory.current; while (repoRoot.path != '/' && !Directory(p.join(repoRoot.path, '.git')).existsSync()) { repoRoot = repoRoot.parent; } @@ -44,7 +45,7 @@ void main() { await Process.run('git', ['add', 'test_file.dart'], workingDirectory: tempDir.path); - final result = await runHook(); + final ProcessResult result = await runHook(); expect(result.exitCode, 0); final output = '${result.stdout}${result.stderr}'; expect(output, contains('Formatting looks good.')); @@ -61,10 +62,10 @@ void main(){ await Process.run('git', ['add', 'test_file.dart'], workingDirectory: tempDir.path); - final result = await runHook(); + final ProcessResult result = await runHook(); expect(result.exitCode, 1); final output = '${result.stdout}${result.stderr}'; - expect(output, contains('Formatting issues found')); + expect(output, contains('Formatting issues found in the following files:')); }); test('fails on analysis error', () async { @@ -77,7 +78,7 @@ void main() { await Process.run('git', ['add', 'test_file.dart'], workingDirectory: tempDir.path); - final result = await runHook(); + final ProcessResult result = await runHook(); expect(result.exitCode, 1); final output = '${result.stdout}${result.stderr}'; expect(output, contains('Static analysis errors found')); @@ -89,7 +90,7 @@ void main() { await Process.run('git', ['add', 'README.md'], workingDirectory: tempDir.path); - final result = await runHook(); + final ProcessResult result = await runHook(); expect(result.exitCode, 0); final output = '${result.stdout}${result.stderr}'; expect(output, isNot(contains('Checking formatting...'))); From 09a89f28ca7dec2102f22b4119cb3b580e86a84b Mon Sep 17 00:00:00 2001 From: Camille Simon Date: Thu, 18 Jun 2026 11:40:49 -0700 Subject: [PATCH 03/23] use plugin tool instead --- .../githooks/lib/src/pre_commit_command.dart | 76 ++++++----- script/githooks/test/pre_commit_test.dart | 127 ++++++------------ 2 files changed, 83 insertions(+), 120 deletions(-) diff --git a/script/githooks/lib/src/pre_commit_command.dart b/script/githooks/lib/src/pre_commit_command.dart index 4f7e016a808d..b4658c7e7c1c 100644 --- a/script/githooks/lib/src/pre_commit_command.dart +++ b/script/githooks/lib/src/pre_commit_command.dart @@ -7,8 +7,26 @@ import 'dart:io'; import 'package:args/command_runner.dart'; -/// The command that implements the pre-commit githook +/// The command that implements the pre-commit githook. class PreCommitCommand extends Command { + /// Creates a [PreCommitCommand]. + PreCommitCommand({ + Future Function( + String executable, + List arguments, { + String? workingDirectory, + })? + processRunner, + }) : processRunner = processRunner ?? Process.run; + + /// The process runner injected for testing. + final Future Function( + String executable, + List arguments, { + String? workingDirectory, + }) + processRunner; + @override final String name = 'pre-commit'; @@ -17,46 +35,35 @@ class PreCommitCommand extends Command { @override Future run() async { - // Find changed Dart files. - final ProcessResult diffResult = await Process.run('git', [ - 'diff', - '--cached', - '--name-only', - '--diff-filter=ACM', - ]); - if (diffResult.exitCode != 0) { - print('Failed to get staged files.'); - exit(1); + // Find the repo root where the plugin tool is located. + Directory repoRoot = Directory.current; + while (repoRoot.path != '/' && !Directory('${repoRoot.path}/.git').existsSync()) { + repoRoot = repoRoot.parent; } - final List stagedDartFiles = (diffResult.stdout as String) - .split('\n') - .map((file) => file.trim()) - .where((file) => file.endsWith('.dart')) - .toList(); - - if (stagedDartFiles.isEmpty) { - // No Dart files are being committed. - return true; + if (repoRoot.path == '/') { + print('❌ Could not find .git directory.'); + return false; } - print('🔍 Running pre-commit checks on staged Dart files...'); + final toolScript = '${repoRoot.path}/script/tool/bin/flutter_plugin_tools.dart'; + + print('🔍 Running pre-commit checks on changed packages using flutter_plugin_tools...'); var hasError = false; // Check formatting. print('Checking formatting...'); - final ProcessResult formatResult = await Process.run('dart', [ + final ProcessResult formatResult = await processRunner('dart', [ + 'run', + toolScript, 'format', - '--output=none', - '--set-exit-if-changed', - ...stagedDartFiles, - ]); + '--run-on-changed-packages', + '--fail-on-change', + ], workingDirectory: repoRoot.path); if (formatResult.exitCode != 0) { - print('❌ Formatting issues found in the following files:'); - print((formatResult.stdout as String).trim()); print( - '👉 Please run "dart format" on these files to fix them.', + '❌ Formatting issues found. Please run "dart run script/tool/bin/flutter_plugin_tools.dart format --run-on-changed-packages" to fix them.', ); hasError = true; } else { @@ -65,15 +72,16 @@ class PreCommitCommand extends Command { // Run static analysis. print('Running static analysis...'); - final ProcessResult analyzeResult = await Process.run('dart', [ + final ProcessResult analyzeResult = await processRunner('dart', [ + 'run', + toolScript, 'analyze', + '--run-on-changed-packages', '--fatal-infos', - ...stagedDartFiles, - ]); + ], workingDirectory: repoRoot.path); if (analyzeResult.exitCode != 0) { - print('❌ Static analysis errors found:'); - print((analyzeResult.stdout as String).trim()); + print('❌ Static analysis errors found. Please fix the errors listed above.'); hasError = true; } else { print('✅ Static analysis looks good.'); diff --git a/script/githooks/test/pre_commit_test.dart b/script/githooks/test/pre_commit_test.dart index 64de7928c025..9178632f794d 100644 --- a/script/githooks/test/pre_commit_test.dart +++ b/script/githooks/test/pre_commit_test.dart @@ -1,99 +1,54 @@ +// 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:path/path.dart' as p; +import 'package:githooks/src/pre_commit_command.dart'; import 'package:test/test.dart'; void main() { group('pre-commit hook', () { - late Directory tempDir; - late String githooksPath; - - setUp(() async { - tempDir = await Directory.systemTemp.createTemp('pre_commit_test_'); - - await Process.run('git', ['init'], workingDirectory: tempDir.path); - await Process.run('git', ['config', 'user.email', 'test@example.com'], workingDirectory: tempDir.path); - await Process.run('git', ['config', 'user.name', 'Test User'], workingDirectory: tempDir.path); - - Directory repoRoot = Directory.current; - while (repoRoot.path != '/' && !Directory(p.join(repoRoot.path, '.git')).existsSync()) { - repoRoot = repoRoot.parent; - } - - githooksPath = p.join(repoRoot.path, 'script', 'githooks'); - - // Set the hooksPath to the actual githooks directory - await Process.run('git', ['config', 'core.hooksPath', githooksPath], workingDirectory: tempDir.path); + test('passes when both format and analyze succeed', () async { + final command = PreCommitCommand( + processRunner: + (String executable, List arguments, {String? workingDirectory}) async { + return ProcessResult(0, 0, 'Success', ''); + }, + ); + + final bool result = await command.run(); + expect(result, isTrue); }); - tearDown(() async { - await tempDir.delete(recursive: true); + test('fails when formatting fails', () async { + final command = PreCommitCommand( + processRunner: + (String executable, List arguments, {String? workingDirectory}) async { + 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); }); - Future runHook() async { - // Actually run git commit to trigger the hook! - return Process.run('git', ['commit', '-m', 'test'], workingDirectory: tempDir.path); - } - - test('passes on a clean commit', () async { - final file = File(p.join(tempDir.path, 'test_file.dart')); - await file.writeAsString(''' -void main() { - print('Hello, world!'); -} -'''); - - await Process.run('git', ['add', 'test_file.dart'], workingDirectory: tempDir.path); - - final ProcessResult result = await runHook(); - expect(result.exitCode, 0); - final output = '${result.stdout}${result.stderr}'; - expect(output, contains('Formatting looks good.')); - expect(output, contains('Static analysis looks good.')); - }); - - test('fails on formatting error', () async { - final file = File(p.join(tempDir.path, 'test_file.dart')); - await file.writeAsString(''' -void main(){ - print('Hello, world!'); -} -'''); - - await Process.run('git', ['add', 'test_file.dart'], workingDirectory: tempDir.path); - - final ProcessResult result = await runHook(); - expect(result.exitCode, 1); - final output = '${result.stdout}${result.stderr}'; - expect(output, contains('Formatting issues found in the following files:')); - }); - - test('fails on analysis error', () async { - final file = File(p.join(tempDir.path, 'test_file.dart')); - await file.writeAsString(''' -void main() { - print('Hello, world!') -} -'''); - - await Process.run('git', ['add', 'test_file.dart'], workingDirectory: tempDir.path); - - final ProcessResult result = await runHook(); - expect(result.exitCode, 1); - final output = '${result.stdout}${result.stderr}'; - expect(output, contains('Static analysis errors found')); - }); - - test('ignores non-dart files', () async { - final file = File(p.join(tempDir.path, 'README.md')); - await file.writeAsString('# Hello'); - - await Process.run('git', ['add', 'README.md'], workingDirectory: tempDir.path); - - final ProcessResult result = await runHook(); - expect(result.exitCode, 0); - final output = '${result.stdout}${result.stderr}'; - expect(output, isNot(contains('Checking formatting...'))); + test('fails when analysis fails', () async { + final command = PreCommitCommand( + processRunner: + (String executable, List arguments, {String? workingDirectory}) async { + 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); }); }); } From 04253cc9f757c61042e27d6d170569f64adb6460 Mon Sep 17 00:00:00 2001 From: Camille Simon Date: Thu, 18 Jun 2026 11:50:56 -0700 Subject: [PATCH 04/23] -- debugging stuff --- script/githooks/pre-commit | 1 - 1 file changed, 1 deletion(-) diff --git a/script/githooks/pre-commit b/script/githooks/pre-commit index 528775e30233..809f62c219df 100755 --- a/script/githooks/pre-commit +++ b/script/githooks/pre-commit @@ -1,6 +1,5 @@ #!/usr/bin/env bash set -e -echo "HOOK EXECUTED" >&2 HOOKS_DIR="$(dirname "$0")" exec dart "$HOOKS_DIR/bin/main.dart" pre-commit "$@" From e078f2442322532d120173736dfc25d25a098e55 Mon Sep 17 00:00:00 2001 From: Camille Simon Date: Thu, 18 Jun 2026 12:28:19 -0700 Subject: [PATCH 05/23] fake commit --- .../githooks/lib/src/pre_commit_command.dart | 76 +++++++++++++++++-- script/githooks/test/pre_commit_test.dart | 24 ++++++ 2 files changed, 93 insertions(+), 7 deletions(-) diff --git a/script/githooks/lib/src/pre_commit_command.dart b/script/githooks/lib/src/pre_commit_command.dart index b4658c7e7c1c..7e438d927e67 100644 --- a/script/githooks/lib/src/pre_commit_command.dart +++ b/script/githooks/lib/src/pre_commit_command.dart @@ -6,6 +6,7 @@ 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 { @@ -33,11 +34,26 @@ class PreCommitCommand extends Command { @override final String description = 'Checks to run before a "git commit"'; + 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 run() async { // Find the repo root where the plugin tool is located. Directory repoRoot = Directory.current; - while (repoRoot.path != '/' && !Directory('${repoRoot.path}/.git').existsSync()) { + while (repoRoot.path != '/' && !Directory(p.join(repoRoot.path, '.git')).existsSync()) { repoRoot = repoRoot.parent; } @@ -46,9 +62,52 @@ class PreCommitCommand extends Command { return false; } - final toolScript = '${repoRoot.path}/script/tool/bin/flutter_plugin_tools.dart'; + final ProcessResult diffResult = await processRunner('git', [ + 'diff', + '--cached', + '--name-only', + '--diff-filter=ACM', + ], workingDirectory: repoRoot.path); + + if (diffResult.exitCode != 0) { + print('❌ Failed to get staged files'); + return false; + } + + final List 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 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 changed packages using flutter_plugin_tools...'); + print( + '🔍 Running pre-commit checks on ${targetPackages.length} packages: ${targetPackages.join(', ')}', + ); var hasError = false; // Check formatting. @@ -57,13 +116,15 @@ class PreCommitCommand extends Command { 'run', toolScript, 'format', - '--run-on-changed-packages', + 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 --run-on-changed-packages" to fix them.', + '❌ Formatting issues found. Please run "dart run script/tool/bin/flutter_plugin_tools.dart format $packageArgs" to fix them.', ); hasError = true; } else { @@ -76,11 +137,12 @@ class PreCommitCommand extends Command { 'run', toolScript, 'analyze', - '--run-on-changed-packages', - '--fatal-infos', + 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 { diff --git a/script/githooks/test/pre_commit_test.dart b/script/githooks/test/pre_commit_test.dart index 9178632f794d..72dbd0c8d694 100644 --- a/script/githooks/test/pre_commit_test.dart +++ b/script/githooks/test/pre_commit_test.dart @@ -13,6 +13,9 @@ void main() { final command = PreCommitCommand( processRunner: (String executable, List arguments, {String? workingDirectory}) async { + if (executable == 'git') { + return ProcessResult(0, 0, 'script/githooks/lib/githooks.dart\n', ''); + } return ProcessResult(0, 0, 'Success', ''); }, ); @@ -25,6 +28,9 @@ void main() { final command = PreCommitCommand( processRunner: (String executable, List 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', ''); } @@ -40,6 +46,9 @@ void main() { final command = PreCommitCommand( processRunner: (String executable, List 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', ''); } @@ -50,5 +59,20 @@ void main() { final bool result = await command.run(); expect(result, isFalse); }); + + test('ignores non-dart files', () async { + final command = PreCommitCommand( + processRunner: + (String executable, List 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); + }); }); } From d61f271cc70cc5af8094adcc4472fbc8dbb21045 Mon Sep 17 00:00:00 2001 From: Camille Simon Date: Thu, 18 Jun 2026 12:30:59 -0700 Subject: [PATCH 06/23] test + command fix --- .../lib/src/android_camera_camerax.dart | 2 +- script/githooks/lib/src/pre_commit_command.dart | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart index 0c84bdf2f2e5..ecd5a4be360f 100644 --- a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart +++ b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart @@ -76,7 +76,7 @@ class AndroidCameraCameraX extends CameraPlatform { }, ); - /// Handles retrieving media orientation for a device. + /// Handles retrieving media orientation for a device.f late final DeviceOrientationManager deviceOrientationManager = DeviceOrientationManager( onDeviceOrientationChanged: (_, String orientation) { final DeviceOrientation deviceOrientation = _deserializeDeviceOrientation(orientation); diff --git a/script/githooks/lib/src/pre_commit_command.dart b/script/githooks/lib/src/pre_commit_command.dart index 7e438d927e67..feb6e2cc31a4 100644 --- a/script/githooks/lib/src/pre_commit_command.dart +++ b/script/githooks/lib/src/pre_commit_command.dart @@ -121,8 +121,12 @@ class PreCommitCommand extends Command { ], workingDirectory: repoRoot.path); if (formatResult.exitCode != 0) { - if (formatResult.stdout.toString().isNotEmpty) print(formatResult.stdout); - if (formatResult.stderr.toString().isNotEmpty) print(formatResult.stderr); + 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.', ); From a487e2209c8d6fd5c32d904dda834aac81d672aa Mon Sep 17 00:00:00 2001 From: Camille Simon Date: Thu, 18 Jun 2026 12:31:29 -0700 Subject: [PATCH 07/23] fake commit --- .../camera_android_camerax/lib/src/android_camera_camerax.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart index ecd5a4be360f..0c84bdf2f2e5 100644 --- a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart +++ b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart @@ -76,7 +76,7 @@ class AndroidCameraCameraX extends CameraPlatform { }, ); - /// Handles retrieving media orientation for a device.f + /// Handles retrieving media orientation for a device. late final DeviceOrientationManager deviceOrientationManager = DeviceOrientationManager( onDeviceOrientationChanged: (_, String orientation) { final DeviceOrientation deviceOrientation = _deserializeDeviceOrientation(orientation); From 85219e8d5af444340752f3de08d2cc7a9a163b32 Mon Sep 17 00:00:00 2001 From: Camille Simon Date: Mon, 22 Jun 2026 10:06:51 -0700 Subject: [PATCH 08/23] try optimizing by cutting out native toolchains --- .../githooks/lib/src/pre_commit_command.dart | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/script/githooks/lib/src/pre_commit_command.dart b/script/githooks/lib/src/pre_commit_command.dart index feb6e2cc31a4..e20cc1abb683 100644 --- a/script/githooks/lib/src/pre_commit_command.dart +++ b/script/githooks/lib/src/pre_commit_command.dart @@ -105,6 +105,29 @@ class PreCommitCommand extends Command { ); final packageArgs = '--packages=${targetPackages.join(',')}'; + // Determine which toolchains are needed based on file extensions + final bool hasDart = stagedFiles.any((f) => f.endsWith('.dart')); + final bool hasClang = stagedFiles.any( + (f) => + f.endsWith('.c') || + f.endsWith('.cc') || + f.endsWith('.cpp') || + f.endsWith('.h') || + f.endsWith('.m') || + f.endsWith('.mm'), + ); + final bool hasJava = stagedFiles.any((f) => f.endsWith('.java')); + final bool hasKotlin = stagedFiles.any((f) => f.endsWith('.kt')); + final bool hasSwift = stagedFiles.any((f) => f.endsWith('.swift')); + + final formatFlags = [ + if (!hasDart) '--no-dart', + if (!hasClang) '--no-clang-format', + if (!hasJava) '--no-java', + if (!hasKotlin) '--no-kotlin', + if (!hasSwift) '--no-swift', + ]; + print( '🔍 Running pre-commit checks on ${targetPackages.length} packages: ${targetPackages.join(', ')}', ); @@ -118,6 +141,7 @@ class PreCommitCommand extends Command { 'format', packageArgs, '--fail-on-change', + ...formatFlags, ], workingDirectory: repoRoot.path); if (formatResult.exitCode != 0) { @@ -145,8 +169,12 @@ class PreCommitCommand extends Command { ], workingDirectory: repoRoot.path); if (analyzeResult.exitCode != 0) { - if (analyzeResult.stdout.toString().isNotEmpty) print(analyzeResult.stdout); - if (analyzeResult.stderr.toString().isNotEmpty) print(analyzeResult.stderr); + 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 { From 021dd712bf95887cc0d8b0948aeac79b2ce5961a Mon Sep 17 00:00:00 2001 From: Camille Simon Date: Mon, 22 Jun 2026 10:07:44 -0700 Subject: [PATCH 09/23] fake commit --- .../camera_android_camerax/lib/src/android_camera_camerax.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart index 0c84bdf2f2e5..820e4a3e0963 100644 --- a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart +++ b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart @@ -24,7 +24,7 @@ class AndroidCameraCameraX extends CameraPlatform { CameraPlatform.instance = AndroidCameraCameraX(); } - /// The [ProcessCameraProvider] instance used to access camera functionality. + /// The [ProcessCameraProvider] instancee used to access camera functionality. @visibleForTesting ProcessCameraProvider? processCameraProvider; From 49089828ca6b46595c7a1055d5829c4cc544d437 Mon Sep 17 00:00:00 2001 From: Camille Simon Date: Mon, 22 Jun 2026 10:11:54 -0700 Subject: [PATCH 10/23] run dart analyze directly --- .../githooks/lib/src/pre_commit_command.dart | 50 +++++++++++-------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/script/githooks/lib/src/pre_commit_command.dart b/script/githooks/lib/src/pre_commit_command.dart index e20cc1abb683..098ae50c2968 100644 --- a/script/githooks/lib/src/pre_commit_command.dart +++ b/script/githooks/lib/src/pre_commit_command.dart @@ -34,12 +34,12 @@ class PreCommitCommand extends Command { @override final String description = 'Checks to run before a "git commit"'; - String? _findPackageName(String filePath, String repoRoot) { + String? _findPackagePath(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; + return currentDir.path; } if (p.equals(repoRoot, currentDir.path)) { break; @@ -84,18 +84,20 @@ class PreCommitCommand extends Command { return true; // No files changed. } - final Set targetPackages = {}; + final Set targetPackageDirs = {}; for (final file in stagedFiles) { - final String? packageName = _findPackageName(file, repoRoot.path); - if (packageName != null) { - targetPackages.add(packageName); + final String? packageDir = _findPackagePath(file, repoRoot.path); + if (packageDir != null) { + targetPackageDirs.add(packageDir); } } - if (targetPackages.isEmpty) { + if (targetPackageDirs.isEmpty) { return true; // None of the changed files are part of a package we care about. } + final Set targetPackages = targetPackageDirs.map((dir) => p.basename(dir)).toSet(); + final String toolScript = p.join( repoRoot.path, 'script', @@ -161,22 +163,26 @@ class PreCommitCommand extends Command { // 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); + for (final packageDir in targetPackageDirs) { + final ProcessResult analyzeResult = await processRunner('dart', [ + 'analyze', + '--fatal-infos', + ], workingDirectory: packageDir); + + 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 in ${p.basename(packageDir)}.'); + hasError = true; } - print('❌ Static analysis errors found. Please fix the errors listed above.'); - hasError = true; + } + + if (hasError) { + print('❌ Please fix the errors listed above.'); } else { print('✅ Static analysis looks good.'); } From d05c928d8111568356401f21bc6db0aa0e6a3726 Mon Sep 17 00:00:00 2001 From: Camille Simon Date: Mon, 22 Jun 2026 10:13:08 -0700 Subject: [PATCH 11/23] undo fake commit --- .../camera_android_camerax/lib/src/android_camera_camerax.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart index 820e4a3e0963..0c84bdf2f2e5 100644 --- a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart +++ b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart @@ -24,7 +24,7 @@ class AndroidCameraCameraX extends CameraPlatform { CameraPlatform.instance = AndroidCameraCameraX(); } - /// The [ProcessCameraProvider] instancee used to access camera functionality. + /// The [ProcessCameraProvider] instance used to access camera functionality. @visibleForTesting ProcessCameraProvider? processCameraProvider; From 2d89dd81d7120bfa3efb9008f1df61a876e20eab Mon Sep 17 00:00:00 2001 From: Camille Simon Date: Mon, 22 Jun 2026 10:18:03 -0700 Subject: [PATCH 12/23] run format and analyze directly on staged files --- .../githooks/lib/src/pre_commit_command.dart | 89 ++++++++++++------- 1 file changed, 59 insertions(+), 30 deletions(-) diff --git a/script/githooks/lib/src/pre_commit_command.dart b/script/githooks/lib/src/pre_commit_command.dart index 098ae50c2968..073a4f28d616 100644 --- a/script/githooks/lib/src/pre_commit_command.dart +++ b/script/githooks/lib/src/pre_commit_command.dart @@ -108,7 +108,6 @@ class PreCommitCommand extends Command { final packageArgs = '--packages=${targetPackages.join(',')}'; // Determine which toolchains are needed based on file extensions - final bool hasDart = stagedFiles.any((f) => f.endsWith('.dart')); final bool hasClang = stagedFiles.any( (f) => f.endsWith('.c') || @@ -122,13 +121,7 @@ class PreCommitCommand extends Command { final bool hasKotlin = stagedFiles.any((f) => f.endsWith('.kt')); final bool hasSwift = stagedFiles.any((f) => f.endsWith('.swift')); - final formatFlags = [ - if (!hasDart) '--no-dart', - if (!hasClang) '--no-clang-format', - if (!hasJava) '--no-java', - if (!hasKotlin) '--no-kotlin', - if (!hasSwift) '--no-swift', - ]; + final List dartFiles = stagedFiles.where((f) => f.endsWith('.dart')).toList(); print( '🔍 Running pre-commit checks on ${targetPackages.length} packages: ${targetPackages.join(', ')}', @@ -137,37 +130,73 @@ class PreCommitCommand extends Command { // Check formatting. print('Checking formatting...'); - final ProcessResult formatResult = await processRunner('dart', [ - 'run', - toolScript, - 'format', - packageArgs, - '--fail-on-change', - ...formatFlags, - ], workingDirectory: repoRoot.path); - if (formatResult.exitCode != 0) { - if (formatResult.stdout.toString().isNotEmpty) { - print(formatResult.stdout); + // Format Dart files instantly using dart format directly + if (dartFiles.isNotEmpty) { + final ProcessResult dartFormatResult = await processRunner('dart', [ + 'format', + '--set-exit-if-changed', + ...dartFiles, + ], workingDirectory: repoRoot.path); + + if (dartFormatResult.exitCode != 0) { + if (dartFormatResult.stdout.toString().isNotEmpty) { + print(dartFormatResult.stdout); + } + if (dartFormatResult.stderr.toString().isNotEmpty) { + print(dartFormatResult.stderr); + } + print('❌ Formatting issues found in Dart files. Please run "dart format" to fix them.'); + hasError = true; } - if (formatResult.stderr.toString().isNotEmpty) { - print(formatResult.stderr); + } + + // Format native files using flutter_plugin_tools + final bool needsNativeFormat = hasClang || hasJava || hasKotlin || hasSwift; + if (needsNativeFormat) { + final nativeFormatFlags = [ + '--no-dart', + if (!hasClang) '--no-clang-format', + if (!hasJava) '--no-java', + if (!hasKotlin) '--no-kotlin', + if (!hasSwift) '--no-swift', + ]; + + final ProcessResult nativeFormatResult = await processRunner('dart', [ + 'run', + toolScript, + 'format', + packageArgs, + '--fail-on-change', + ...nativeFormatFlags, + ], workingDirectory: repoRoot.path); + + if (nativeFormatResult.exitCode != 0) { + if (nativeFormatResult.stdout.toString().isNotEmpty) { + print(nativeFormatResult.stdout); + } + if (nativeFormatResult.stderr.toString().isNotEmpty) { + print(nativeFormatResult.stderr); + } + print( + '❌ Formatting issues found in native files. Please run "dart run script/tool/bin/flutter_plugin_tools.dart format $packageArgs" to fix them.', + ); + hasError = true; } - print( - '❌ Formatting issues found. Please run "dart run script/tool/bin/flutter_plugin_tools.dart format $packageArgs" to fix them.', - ); - hasError = true; - } else { + } + + if (!hasError) { print('✅ Formatting looks good.'); } - // Run static analysis. + // Run static analysis directly on staged files print('Running static analysis...'); - for (final packageDir in targetPackageDirs) { + if (dartFiles.isNotEmpty) { final ProcessResult analyzeResult = await processRunner('dart', [ 'analyze', '--fatal-infos', - ], workingDirectory: packageDir); + ...dartFiles, + ], workingDirectory: repoRoot.path); if (analyzeResult.exitCode != 0) { if (analyzeResult.stdout.toString().isNotEmpty) { @@ -176,7 +205,7 @@ class PreCommitCommand extends Command { if (analyzeResult.stderr.toString().isNotEmpty) { print(analyzeResult.stderr); } - print('❌ Static analysis errors found in ${p.basename(packageDir)}.'); + print('❌ Static analysis errors found.'); hasError = true; } } From 7e53502099dc28694f41f7bd1d0e19f75056ce7f Mon Sep 17 00:00:00 2001 From: Camille Simon Date: Mon, 22 Jun 2026 10:19:13 -0700 Subject: [PATCH 13/23] fake commit --- .../camera_android_camerax/lib/src/android_camera_camerax.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart index 0c84bdf2f2e5..34b8c8ea3168 100644 --- a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart +++ b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart @@ -19,7 +19,7 @@ class AndroidCameraCameraX extends CameraPlatform { /// Constructs an [AndroidCameraCameraX]. AndroidCameraCameraX(); - /// Registers this class as the default instance of [CameraPlatform]. + /// Registers this class as the default instancee of [CameraPlatform]. static void registerWith() { CameraPlatform.instance = AndroidCameraCameraX(); } From 5b8c9a887ac5cc930b09b1a2824eec706efdf6f8 Mon Sep 17 00:00:00 2001 From: Camille Simon Date: Mon, 22 Jun 2026 10:22:29 -0700 Subject: [PATCH 14/23] use ANSI escape codes to erase status logs --- .../githooks/lib/src/pre_commit_command.dart | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/script/githooks/lib/src/pre_commit_command.dart b/script/githooks/lib/src/pre_commit_command.dart index 073a4f28d616..30495778e627 100644 --- a/script/githooks/lib/src/pre_commit_command.dart +++ b/script/githooks/lib/src/pre_commit_command.dart @@ -129,7 +129,7 @@ class PreCommitCommand extends Command { var hasError = false; // Check formatting. - print('Checking formatting...'); + stdout.write('Checking formatting...'); // Format Dart files instantly using dart format directly if (dartFiles.isNotEmpty) { @@ -140,6 +140,9 @@ class PreCommitCommand extends Command { ], workingDirectory: repoRoot.path); if (dartFormatResult.exitCode != 0) { + if (!hasError) { + stdout.write('\x1B[2K\r'); + } if (dartFormatResult.stdout.toString().isNotEmpty) { print(dartFormatResult.stdout); } @@ -172,6 +175,9 @@ class PreCommitCommand extends Command { ], workingDirectory: repoRoot.path); if (nativeFormatResult.exitCode != 0) { + if (!hasError) { + stdout.write('\x1B[2K\r'); + } if (nativeFormatResult.stdout.toString().isNotEmpty) { print(nativeFormatResult.stdout); } @@ -186,11 +192,12 @@ class PreCommitCommand extends Command { } if (!hasError) { - print('✅ Formatting looks good.'); + stdout.write('\x1B[2K\r✅ Formatting looks good.\n'); } // Run static analysis directly on staged files - print('Running static analysis...'); + var analyzeHasError = false; + stdout.write('Running static analysis...'); if (dartFiles.isNotEmpty) { final ProcessResult analyzeResult = await processRunner('dart', [ 'analyze', @@ -199,6 +206,9 @@ class PreCommitCommand extends Command { ], workingDirectory: repoRoot.path); if (analyzeResult.exitCode != 0) { + if (!analyzeHasError) { + stdout.write('\x1B[2K\r'); + } if (analyzeResult.stdout.toString().isNotEmpty) { print(analyzeResult.stdout); } @@ -206,14 +216,17 @@ class PreCommitCommand extends Command { print(analyzeResult.stderr); } print('❌ Static analysis errors found.'); + analyzeHasError = true; hasError = true; } } + if (!analyzeHasError) { + stdout.write('\x1B[2K\r✅ Static analysis looks good.\n'); + } + if (hasError) { print('❌ Please fix the errors listed above.'); - } else { - print('✅ Static analysis looks good.'); } return !hasError; From f4eed3d59fc87103915a3c4e754f7133ba4064c0 Mon Sep 17 00:00:00 2001 From: Camille Simon Date: Mon, 22 Jun 2026 10:23:34 -0700 Subject: [PATCH 15/23] fake commit --- .../io/flutter/plugins/camerax/Camera2CameraInfoProxyApi.java | 2 +- .../camera_android_camerax/lib/src/android_camera_camerax.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/Camera2CameraInfoProxyApi.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/Camera2CameraInfoProxyApi.java index 8f64ad0a50d1..5c426e8f7785 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/Camera2CameraInfoProxyApi.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/Camera2CameraInfoProxyApi.java @@ -16,7 +16,7 @@ /** * ProxyApi implementation for {@link Camera2CameraInfo}. This class may handle instantiating native * object instances that are attached to a Dart instance or handle method calls on the associated - * native class or an instance of that class. + * native class or an instance of that class.f */ @OptIn(markerClass = ExperimentalCamera2Interop.class) class Camera2CameraInfoProxyApi extends PigeonApiCamera2CameraInfo { diff --git a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart index 34b8c8ea3168..0c84bdf2f2e5 100644 --- a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart +++ b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart @@ -19,7 +19,7 @@ class AndroidCameraCameraX extends CameraPlatform { /// Constructs an [AndroidCameraCameraX]. AndroidCameraCameraX(); - /// Registers this class as the default instancee of [CameraPlatform]. + /// Registers this class as the default instance of [CameraPlatform]. static void registerWith() { CameraPlatform.instance = AndroidCameraCameraX(); } From 638ce3434e8ebb32dd99d6b07b4ef3915dc28814 Mon Sep 17 00:00:00 2001 From: Camille Simon Date: Mon, 22 Jun 2026 10:24:31 -0700 Subject: [PATCH 16/23] change emoji to runner --- script/githooks/lib/src/pre_commit_command.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/githooks/lib/src/pre_commit_command.dart b/script/githooks/lib/src/pre_commit_command.dart index 30495778e627..56c9ab48e467 100644 --- a/script/githooks/lib/src/pre_commit_command.dart +++ b/script/githooks/lib/src/pre_commit_command.dart @@ -124,7 +124,7 @@ class PreCommitCommand extends Command { final List dartFiles = stagedFiles.where((f) => f.endsWith('.dart')).toList(); print( - '🔍 Running pre-commit checks on ${targetPackages.length} packages: ${targetPackages.join(', ')}', + '🏃 Running pre-commit checks on ${targetPackages.length} packages: ${targetPackages.join(', ')}', ); var hasError = false; From c19c8f3bbce4a5fe5fb622b9b1d9a5314fea6d09 Mon Sep 17 00:00:00 2001 From: Camille Simon Date: Mon, 22 Jun 2026 10:25:25 -0700 Subject: [PATCH 17/23] undo fake commit --- .../io/flutter/plugins/camerax/Camera2CameraInfoProxyApi.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/Camera2CameraInfoProxyApi.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/Camera2CameraInfoProxyApi.java index 5c426e8f7785..8f64ad0a50d1 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/Camera2CameraInfoProxyApi.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/Camera2CameraInfoProxyApi.java @@ -16,7 +16,7 @@ /** * ProxyApi implementation for {@link Camera2CameraInfo}. This class may handle instantiating native * object instances that are attached to a Dart instance or handle method calls on the associated - * native class or an instance of that class.f + * native class or an instance of that class. */ @OptIn(markerClass = ExperimentalCamera2Interop.class) class Camera2CameraInfoProxyApi extends PigeonApiCamera2CameraInfo { From f8837ba6ac767691bc2c54c85944c84552a4550e Mon Sep 17 00:00:00 2001 From: Camille Simon Date: Mon, 22 Jun 2026 10:36:27 -0700 Subject: [PATCH 18/23] make unit tests stricter --- script/githooks/test/pre_commit_test.dart | 32 +++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/script/githooks/test/pre_commit_test.dart b/script/githooks/test/pre_commit_test.dart index 72dbd0c8d694..a29f7f745eee 100644 --- a/script/githooks/test/pre_commit_test.dart +++ b/script/githooks/test/pre_commit_test.dart @@ -10,10 +10,15 @@ import 'package:test/test.dart'; void main() { group('pre-commit hook', () { test('passes when both format and analyze succeed', () async { + final List> executedArguments = []; final command = PreCommitCommand( processRunner: (String executable, List arguments, {String? workingDirectory}) async { + executedArguments.add(arguments); if (executable == 'git') { + if (arguments.contains('--show-toplevel')) { + return ProcessResult(0, 0, '/fake/repo/root\n', ''); + } return ProcessResult(0, 0, 'script/githooks/lib/githooks.dart\n', ''); } return ProcessResult(0, 0, 'Success', ''); @@ -22,6 +27,18 @@ void main() { final bool result = await command.run(); expect(result, isTrue); + + // Verify the exact arguments passed to format and analyze + expect( + executedArguments, + anyElement( + equals(['format', '--set-exit-if-changed', 'script/githooks/lib/githooks.dart']), + ), + ); + expect( + executedArguments, + anyElement(equals(['analyze', '--fatal-infos', 'script/githooks/lib/githooks.dart'])), + ); }); test('fails when formatting fails', () async { @@ -29,6 +46,9 @@ void main() { processRunner: (String executable, List arguments, {String? workingDirectory}) async { if (executable == 'git') { + if (arguments.contains('--show-toplevel')) { + return ProcessResult(0, 0, '/fake/repo/root\n', ''); + } return ProcessResult(0, 0, 'script/githooks/lib/githooks.dart\n', ''); } if (arguments.contains('format')) { @@ -47,6 +67,9 @@ void main() { processRunner: (String executable, List arguments, {String? workingDirectory}) async { if (executable == 'git') { + if (arguments.contains('--show-toplevel')) { + return ProcessResult(0, 0, '/fake/repo/root\n', ''); + } return ProcessResult(0, 0, 'script/githooks/lib/githooks.dart\n', ''); } if (arguments.contains('analyze')) { @@ -61,10 +84,15 @@ void main() { }); test('ignores non-dart files', () async { + final List> executedArguments = []; final command = PreCommitCommand( processRunner: (String executable, List arguments, {String? workingDirectory}) async { + executedArguments.add(arguments); if (executable == 'git') { + if (arguments.contains('--show-toplevel')) { + return ProcessResult(0, 0, '/fake/repo/root\n', ''); + } return ProcessResult(0, 0, 'README.md\n', ''); } return ProcessResult(0, 0, 'Success', ''); @@ -73,6 +101,10 @@ void main() { final bool result = await command.run(); expect(result, isTrue); + + // Verify that dart format and analyze were NEVER called. + expect(executedArguments.any((args) => args.contains('format')), isFalse); + expect(executedArguments.any((args) => args.contains('analyze')), isFalse); }); }); } From 352580c13e562dab7b186ff5be1b21686ffd4579 Mon Sep 17 00:00:00 2001 From: Camille Simon Date: Mon, 22 Jun 2026 10:48:25 -0700 Subject: [PATCH 19/23] self review --- script/githooks/lib/src/pre_commit_command.dart | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/script/githooks/lib/src/pre_commit_command.dart b/script/githooks/lib/src/pre_commit_command.dart index 56c9ab48e467..8fb5a70f30ca 100644 --- a/script/githooks/lib/src/pre_commit_command.dart +++ b/script/githooks/lib/src/pre_commit_command.dart @@ -62,6 +62,7 @@ class PreCommitCommand extends Command { return false; } + // Get all staged files that are added, copied, or modified. final ProcessResult diffResult = await processRunner('git', [ 'diff', '--cached', @@ -81,7 +82,8 @@ class PreCommitCommand extends Command { .toList(); if (stagedFiles.isEmpty) { - return true; // No files changed. + // No files changed. + return true; } final Set targetPackageDirs = {}; @@ -93,7 +95,8 @@ class PreCommitCommand extends Command { } if (targetPackageDirs.isEmpty) { - return true; // None of the changed files are part of a package we care about. + // None of the changed files are part of a package we care about. + return true; } final Set targetPackages = targetPackageDirs.map((dir) => p.basename(dir)).toSet(); @@ -107,7 +110,7 @@ class PreCommitCommand extends Command { ); final packageArgs = '--packages=${targetPackages.join(',')}'; - // Determine which toolchains are needed based on file extensions + // Determine which toolchains are needed based on file extensions to avoid uneccessary slowdown. final bool hasClang = stagedFiles.any( (f) => f.endsWith('.c') || @@ -131,7 +134,7 @@ class PreCommitCommand extends Command { // Check formatting. stdout.write('Checking formatting...'); - // Format Dart files instantly using dart format directly + // Format staged Dart files. if (dartFiles.isNotEmpty) { final ProcessResult dartFormatResult = await processRunner('dart', [ 'format', @@ -154,7 +157,7 @@ class PreCommitCommand extends Command { } } - // Format native files using flutter_plugin_tools + // Format staged native files. final bool needsNativeFormat = hasClang || hasJava || hasKotlin || hasSwift; if (needsNativeFormat) { final nativeFormatFlags = [ @@ -195,7 +198,7 @@ class PreCommitCommand extends Command { stdout.write('\x1B[2K\r✅ Formatting looks good.\n'); } - // Run static analysis directly on staged files + // Run static analysis on staged files. var analyzeHasError = false; stdout.write('Running static analysis...'); if (dartFiles.isNotEmpty) { From a6a2a789a79a81d3535dc4599cefc8b7b12fdfd1 Mon Sep 17 00:00:00 2001 From: Camille Simon Date: Mon, 22 Jun 2026 10:54:52 -0700 Subject: [PATCH 20/23] expect args --- script/githooks/test/pre_commit_test.dart | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/script/githooks/test/pre_commit_test.dart b/script/githooks/test/pre_commit_test.dart index a29f7f745eee..655857eb6032 100644 --- a/script/githooks/test/pre_commit_test.dart +++ b/script/githooks/test/pre_commit_test.dart @@ -42,16 +42,18 @@ void main() { }); test('fails when formatting fails', () async { + final List> executedArguments = []; final command = PreCommitCommand( processRunner: (String executable, List arguments, {String? workingDirectory}) async { + executedArguments.add(arguments); if (executable == 'git') { if (arguments.contains('--show-toplevel')) { return ProcessResult(0, 0, '/fake/repo/root\n', ''); } return ProcessResult(0, 0, 'script/githooks/lib/githooks.dart\n', ''); } - if (arguments.contains('format')) { + if (arguments.isNotEmpty && arguments[0] == 'format') { return ProcessResult(0, 1, 'bad_file.dart', ''); } return ProcessResult(0, 0, 'Success', ''); @@ -60,19 +62,28 @@ void main() { final bool result = await command.run(); expect(result, isFalse); + + expect( + executedArguments, + anyElement( + equals(['format', '--set-exit-if-changed', 'script/githooks/lib/githooks.dart']), + ), + ); }); test('fails when analysis fails', () async { + final List> executedArguments = []; final command = PreCommitCommand( processRunner: (String executable, List arguments, {String? workingDirectory}) async { + executedArguments.add(arguments); if (executable == 'git') { if (arguments.contains('--show-toplevel')) { return ProcessResult(0, 0, '/fake/repo/root\n', ''); } return ProcessResult(0, 0, 'script/githooks/lib/githooks.dart\n', ''); } - if (arguments.contains('analyze')) { + if (arguments.isNotEmpty && arguments[0] == 'analyze') { return ProcessResult(0, 1, 'error in file.dart', ''); } return ProcessResult(0, 0, 'Success', ''); @@ -81,6 +92,11 @@ void main() { final bool result = await command.run(); expect(result, isFalse); + + expect( + executedArguments, + anyElement(equals(['analyze', '--fatal-infos', 'script/githooks/lib/githooks.dart'])), + ); }); test('ignores non-dart files', () async { From 5f94b320028ed9ed60f0322ef71875eaaf2116d7 Mon Sep 17 00:00:00 2001 From: Camille Simon Date: Mon, 22 Jun 2026 12:37:27 -0700 Subject: [PATCH 21/23] gemini review --- script/githooks/bin/install_hooks.dart | 6 +++--- script/githooks/lib/src/pre_commit_command.dart | 14 +++++++++----- script/githooks/test/pre_commit_test.dart | 12 ------------ 3 files changed, 12 insertions(+), 20 deletions(-) diff --git a/script/githooks/bin/install_hooks.dart b/script/githooks/bin/install_hooks.dart index 929e03a81263..c21f20abdfc1 100644 --- a/script/githooks/bin/install_hooks.dart +++ b/script/githooks/bin/install_hooks.dart @@ -9,11 +9,11 @@ import 'package:path/path.dart' as p; void main() async { Directory repoRoot = Directory.current; - while (repoRoot.path != '/' && !Directory(p.join(repoRoot.path, '.git')).existsSync()) { + while (repoRoot.path != repoRoot.parent.path && + !Directory(p.join(repoRoot.path, '.git')).existsSync()) { repoRoot = repoRoot.parent; } - - if (repoRoot.path == '/') { + if (!Directory(p.join(repoRoot.path, '.git')).existsSync()) { print('❌ Could not find .git directory.'); exit(1); } diff --git a/script/githooks/lib/src/pre_commit_command.dart b/script/githooks/lib/src/pre_commit_command.dart index 8fb5a70f30ca..9db09a0f5032 100644 --- a/script/githooks/lib/src/pre_commit_command.dart +++ b/script/githooks/lib/src/pre_commit_command.dart @@ -53,11 +53,11 @@ class PreCommitCommand extends Command { Future run() async { // Find the repo root where the plugin tool is located. Directory repoRoot = Directory.current; - while (repoRoot.path != '/' && !Directory(p.join(repoRoot.path, '.git')).existsSync()) { + while (repoRoot.path != repoRoot.parent.path && + !Directory(p.join(repoRoot.path, '.git')).existsSync()) { repoRoot = repoRoot.parent; } - - if (repoRoot.path == '/') { + if (!Directory(p.join(repoRoot.path, '.git')).existsSync()) { print('❌ Could not find .git directory.'); return false; } @@ -144,7 +144,7 @@ class PreCommitCommand extends Command { if (dartFormatResult.exitCode != 0) { if (!hasError) { - stdout.write('\x1B[2K\r'); + stdout.write(stdout.supportsAnsiEscapes ? r'\x1B[2K\r' : r'\n'); } if (dartFormatResult.stdout.toString().isNotEmpty) { print(dartFormatResult.stdout); @@ -195,7 +195,11 @@ class PreCommitCommand extends Command { } if (!hasError) { - stdout.write('\x1B[2K\r✅ Formatting looks good.\n'); + stdout.write( + stdout.supportsAnsiEscapes + ? r'\x1B[2K\r✅ Formatting looks good.\n' + : r'✅ Formatting looks good.\n', + ); } // Run static analysis on staged files. diff --git a/script/githooks/test/pre_commit_test.dart b/script/githooks/test/pre_commit_test.dart index 655857eb6032..369b4ba378c7 100644 --- a/script/githooks/test/pre_commit_test.dart +++ b/script/githooks/test/pre_commit_test.dart @@ -16,9 +16,6 @@ void main() { (String executable, List arguments, {String? workingDirectory}) async { executedArguments.add(arguments); if (executable == 'git') { - if (arguments.contains('--show-toplevel')) { - return ProcessResult(0, 0, '/fake/repo/root\n', ''); - } return ProcessResult(0, 0, 'script/githooks/lib/githooks.dart\n', ''); } return ProcessResult(0, 0, 'Success', ''); @@ -48,9 +45,6 @@ void main() { (String executable, List arguments, {String? workingDirectory}) async { executedArguments.add(arguments); if (executable == 'git') { - if (arguments.contains('--show-toplevel')) { - return ProcessResult(0, 0, '/fake/repo/root\n', ''); - } return ProcessResult(0, 0, 'script/githooks/lib/githooks.dart\n', ''); } if (arguments.isNotEmpty && arguments[0] == 'format') { @@ -78,9 +72,6 @@ void main() { (String executable, List arguments, {String? workingDirectory}) async { executedArguments.add(arguments); if (executable == 'git') { - if (arguments.contains('--show-toplevel')) { - return ProcessResult(0, 0, '/fake/repo/root\n', ''); - } return ProcessResult(0, 0, 'script/githooks/lib/githooks.dart\n', ''); } if (arguments.isNotEmpty && arguments[0] == 'analyze') { @@ -106,9 +97,6 @@ void main() { (String executable, List arguments, {String? workingDirectory}) async { executedArguments.add(arguments); if (executable == 'git') { - if (arguments.contains('--show-toplevel')) { - return ProcessResult(0, 0, '/fake/repo/root\n', ''); - } return ProcessResult(0, 0, 'README.md\n', ''); } return ProcessResult(0, 0, 'Success', ''); From bc0ce9f401e3393739615493111241b55c7a758b Mon Sep 17 00:00:00 2001 From: Camille Simon Date: Mon, 22 Jun 2026 12:40:25 -0700 Subject: [PATCH 22/23] fix print --- script/githooks/lib/src/pre_commit_command.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/script/githooks/lib/src/pre_commit_command.dart b/script/githooks/lib/src/pre_commit_command.dart index 9db09a0f5032..81ea620db31d 100644 --- a/script/githooks/lib/src/pre_commit_command.dart +++ b/script/githooks/lib/src/pre_commit_command.dart @@ -144,7 +144,7 @@ class PreCommitCommand extends Command { if (dartFormatResult.exitCode != 0) { if (!hasError) { - stdout.write(stdout.supportsAnsiEscapes ? r'\x1B[2K\r' : r'\n'); + stdout.write(stdout.supportsAnsiEscapes ? '\x1B[2K\r' : '\n'); } if (dartFormatResult.stdout.toString().isNotEmpty) { print(dartFormatResult.stdout); @@ -197,8 +197,8 @@ class PreCommitCommand extends Command { if (!hasError) { stdout.write( stdout.supportsAnsiEscapes - ? r'\x1B[2K\r✅ Formatting looks good.\n' - : r'✅ Formatting looks good.\n', + ? '\x1B[2K\r✅ Formatting looks good.\n' + : '✅ Formatting looks good.\n', ); } From 38e5327cfee265b87d12f76c6c63c0cbcb56de52 Mon Sep 17 00:00:00 2001 From: Camille Simon Date: Mon, 22 Jun 2026 15:44:43 -0700 Subject: [PATCH 23/23] address gemini review --- script/githooks/bin/install_hooks.dart | 6 +++-- .../githooks/lib/src/pre_commit_command.dart | 17 +++++++++---- script/githooks/test/pre_commit_test.dart | 24 +++++++++++++++++++ 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/script/githooks/bin/install_hooks.dart b/script/githooks/bin/install_hooks.dart index c21f20abdfc1..e770cd338c1d 100644 --- a/script/githooks/bin/install_hooks.dart +++ b/script/githooks/bin/install_hooks.dart @@ -10,10 +10,12 @@ 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()) { + !(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()) { + if (!(Directory(p.join(repoRoot.path, '.git')).existsSync() || + File(p.join(repoRoot.path, '.git')).existsSync())) { print('❌ Could not find .git directory.'); exit(1); } diff --git a/script/githooks/lib/src/pre_commit_command.dart b/script/githooks/lib/src/pre_commit_command.dart index 81ea620db31d..30afa285f476 100644 --- a/script/githooks/lib/src/pre_commit_command.dart +++ b/script/githooks/lib/src/pre_commit_command.dart @@ -54,10 +54,12 @@ class PreCommitCommand extends Command { // Find the repo root where the plugin tool is located. Directory repoRoot = Directory.current; while (repoRoot.path != repoRoot.parent.path && - !Directory(p.join(repoRoot.path, '.git')).existsSync()) { + !(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()) { + if (!(Directory(p.join(repoRoot.path, '.git')).existsSync() || + File(p.join(repoRoot.path, '.git')).existsSync())) { print('❌ Could not find .git directory.'); return false; } @@ -117,6 +119,7 @@ class PreCommitCommand extends Command { f.endsWith('.cc') || f.endsWith('.cpp') || f.endsWith('.h') || + f.endsWith('.hpp') || f.endsWith('.m') || f.endsWith('.mm'), ); @@ -179,7 +182,7 @@ class PreCommitCommand extends Command { if (nativeFormatResult.exitCode != 0) { if (!hasError) { - stdout.write('\x1B[2K\r'); + stdout.write(stdout.supportsAnsiEscapes ? '\x1B[2K\r' : '\n'); } if (nativeFormatResult.stdout.toString().isNotEmpty) { print(nativeFormatResult.stdout); @@ -214,7 +217,7 @@ class PreCommitCommand extends Command { if (analyzeResult.exitCode != 0) { if (!analyzeHasError) { - stdout.write('\x1B[2K\r'); + stdout.write(stdout.supportsAnsiEscapes ? '\x1B[2K\r' : '\n'); } if (analyzeResult.stdout.toString().isNotEmpty) { print(analyzeResult.stdout); @@ -229,7 +232,11 @@ class PreCommitCommand extends Command { } if (!analyzeHasError) { - stdout.write('\x1B[2K\r✅ Static analysis looks good.\n'); + stdout.write( + stdout.supportsAnsiEscapes + ? '\x1B[2K\r✅ Static analysis looks good.\n' + : '✅ Static analysis looks good.\n', + ); } if (hasError) { diff --git a/script/githooks/test/pre_commit_test.dart b/script/githooks/test/pre_commit_test.dart index 369b4ba378c7..b0da053a1688 100644 --- a/script/githooks/test/pre_commit_test.dart +++ b/script/githooks/test/pre_commit_test.dart @@ -110,5 +110,29 @@ void main() { expect(executedArguments.any((args) => args.contains('format')), isFalse); expect(executedArguments.any((args) => args.contains('analyze')), isFalse); }); + test('runs native formatter when native files are staged', () async { + final List> executedArguments = []; + final command = PreCommitCommand( + processRunner: + (String executable, List arguments, {String? workingDirectory}) async { + executedArguments.add(arguments); + if (executable == 'git') { + return ProcessResult(0, 0, 'packages/pkg/ios/Classes/Foo.m\n', ''); + } + return ProcessResult(0, 0, 'Success', ''); + }, + ); + + final bool result = await command.run(); + expect(result, isTrue); + + expect( + executedArguments.any( + (args) => + args.contains('format') && args.contains('--no-dart') && args.contains('--no-java'), + ), + isTrue, + ); + }); }); }