Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
0a65ef8
first pass
camsim99 Jun 18, 2026
ccda0df
rippling revisions
camsim99 Jun 18, 2026
09a89f2
use plugin tool instead
camsim99 Jun 18, 2026
04253cc
-- debugging stuff
camsim99 Jun 18, 2026
e078f24
fake commit
camsim99 Jun 18, 2026
d61f271
test + command fix
camsim99 Jun 18, 2026
a487e22
fake commit
camsim99 Jun 18, 2026
85219e8
try optimizing by cutting out native toolchains
camsim99 Jun 22, 2026
021dd71
fake commit
camsim99 Jun 22, 2026
4908982
run dart analyze directly
camsim99 Jun 22, 2026
d05c928
undo fake commit
camsim99 Jun 22, 2026
2d89dd8
run format and analyze directly on staged files
camsim99 Jun 22, 2026
7e53502
fake commit
camsim99 Jun 22, 2026
5b8c9a8
use ANSI escape codes to erase status logs
camsim99 Jun 22, 2026
f4eed3d
fake commit
camsim99 Jun 22, 2026
638ce34
change emoji to runner
camsim99 Jun 22, 2026
c19c8f3
undo fake commit
camsim99 Jun 22, 2026
f8837ba
make unit tests stricter
camsim99 Jun 22, 2026
352580c
self review
camsim99 Jun 22, 2026
a6a2a78
expect args
camsim99 Jun 22, 2026
5f94b32
gemini review
camsim99 Jun 22, 2026
bc0ce9f
fix print
camsim99 Jun 22, 2026
38e5327
address gemini review
camsim99 Jun 22, 2026
d725d68
first agentic pass at refactoring
camsim99 Jun 23, 2026
e344ca7
implement --run-on-staged-packages
camsim99 Jun 25, 2026
814caf1
small optimizations
camsim99 Jun 25, 2026
32faa24
msg
camsim99 Jun 25, 2026
9dc4b9d
address reid review that is still relevant
camsim99 Jun 25, 2026
1ccc307
fix analysis
camsim99 Jun 26, 2026
3858e8d
clarify if test skipped
camsim99 Jun 26, 2026
dc86fa7
little tweaks
camsim99 Jun 26, 2026
7a17ad8
self review
camsim99 Jun 26, 2026
55d6235
test nits
camsim99 Jun 26, 2026
db09b06
address review
camsim99 Jun 29, 2026
220baab
self review
camsim99 Jun 29, 2026
4432af2
ignore
camsim99 Jun 30, 2026
befac5a
add docs + todo for plugin tool overhead
camsim99 Jul 1, 2026
006ead1
Merge remote-tracking branch 'upstream/main' into cos_precommit
camsim99 Jul 6, 2026
8719555
correct license
camsim99 Jul 6, 2026
e1d0372
bump version
camsim99 Jul 6, 2026
6940eee
test fixes
camsim99 Jul 7, 2026
37ccc08
Merge remote-tracking branch 'upstream/main' into cos_precommit
camsim99 Jul 7, 2026
f85c2fe
format & analyze
camsim99 Jul 7, 2026
3d9dc63
Merge remote-tracking branch 'upstream/main' into cos_precommit
camsim99 Jul 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions script/githooks/bin/install_hooks.dart
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 != repoRoot.parent.path &&
!Directory(p.join(repoRoot.path, '.git')).existsSync()) {
repoRoot = repoRoot.parent;
}
if (!Directory(p.join(repoRoot.path, '.git')).existsSync()) {
print('❌ Could not find .git directory.');
exit(1);
}
Comment thread
camsim99 marked this conversation as resolved.
Comment thread
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);
}
}
11 changes: 11 additions & 0 deletions script/githooks/bin/main.dart
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);
}
16 changes: 16 additions & 0 deletions script/githooks/lib/githooks.dart
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;
}
241 changes: 241 additions & 0 deletions script/githooks/lib/src/pre_commit_command.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
// 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"';
Comment thread
camsim99 marked this conversation as resolved.
Outdated

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 currentDir.path;
}
if (p.equals(repoRoot, currentDir.path)) {
break;
}
currentDir = currentDir.parent;
}
return null;
}

@override
Future<bool> run() async {
Comment thread
camsim99 marked this conversation as resolved.
// 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()) {
repoRoot = repoRoot.parent;
}
if (!Directory(p.join(repoRoot.path, '.git')).existsSync()) {
print('❌ Could not find .git directory.');
return false;
}
Comment thread
camsim99 marked this conversation as resolved.
Outdated

// Get all staged files that are added, copied, or modified.
final ProcessResult diffResult = await processRunner('git', [
'diff',
'--cached',
'--name-only',
'--diff-filter=ACM',
], workingDirectory: repoRoot.path);

if (diffResult.exitCode != 0) {
Comment thread
camsim99 marked this conversation as resolved.
Outdated
print('❌ Failed to get staged files');
Comment thread
camsim99 marked this conversation as resolved.
Outdated
return false;
}

final List<String> stagedFiles = (diffResult.stdout as String)
.split('\n')
.map((e) => e.trim())
.where((e) => e.isNotEmpty)
.toList();

if (stagedFiles.isEmpty) {
// No files changed.
return true;
}

final Set<String> targetPackageDirs = {};
for (final file in stagedFiles) {
final String? packageDir = _findPackagePath(file, repoRoot.path);
if (packageDir != null) {
targetPackageDirs.add(packageDir);
}
}

if (targetPackageDirs.isEmpty) {
// None of the changed files are part of a package we care about.
Comment thread
camsim99 marked this conversation as resolved.
Outdated
return true;
}

final Set<String> targetPackages = targetPackageDirs.map((dir) => p.basename(dir)).toSet();

final String toolScript = p.join(
repoRoot.path,
'script',
'tool',
'bin',
'flutter_plugin_tools.dart',
);
final packageArgs = '--packages=${targetPackages.join(',')}';

// Determine which toolchains are needed based on file extensions to avoid uneccessary slowdown.
final bool hasClang = stagedFiles.any(
Comment thread
camsim99 marked this conversation as resolved.
Outdated
(f) =>
f.endsWith('.c') ||
f.endsWith('.cc') ||
f.endsWith('.cpp') ||
f.endsWith('.h') ||
f.endsWith('.m') ||
f.endsWith('.mm'),
);
Comment thread
camsim99 marked this conversation as resolved.
Outdated
final bool hasJava = stagedFiles.any((f) => f.endsWith('.java'));
Comment thread
camsim99 marked this conversation as resolved.
Outdated
final bool hasKotlin = stagedFiles.any((f) => f.endsWith('.kt'));
final bool hasSwift = stagedFiles.any((f) => f.endsWith('.swift'));

final List<String> dartFiles = stagedFiles.where((f) => f.endsWith('.dart')).toList();

print(
'🏃 Running pre-commit checks on ${targetPackages.length} packages: ${targetPackages.join(', ')}',
);
var hasError = false;

// Check formatting.
stdout.write('Checking formatting...');

// Format staged Dart files.
if (dartFiles.isNotEmpty) {
final ProcessResult dartFormatResult = await processRunner('dart', [
'format',
'--set-exit-if-changed',
...dartFiles,
], workingDirectory: repoRoot.path);

if (dartFormatResult.exitCode != 0) {
if (!hasError) {
stdout.write(stdout.supportsAnsiEscapes ? '\x1B[2K\r' : '\n');
}
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;
}
}

// Format staged native files.
final bool needsNativeFormat = hasClang || hasJava || hasKotlin || hasSwift;
if (needsNativeFormat) {
final nativeFormatFlags = [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets use the args package. https://pub.dev/packages/args

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored quite a bit and don't see how we would use this but if it's applicable here, let me know!

'--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 (!hasError) {
stdout.write('\x1B[2K\r');
}
Comment thread
camsim99 marked this conversation as resolved.
Outdated
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.',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am guessing ai wrote this script because it tends to use these unicode chars for success and failure. Lets remove those unicode chars.

);
hasError = true;
}
}

if (!hasError) {
stdout.write(
stdout.supportsAnsiEscapes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this complexity needed? What value do we get from having ansiExcapes here.

? '\x1B[2K\r✅ Formatting looks good.\n'
: '✅ Formatting looks good.\n',
);
}

// Run static analysis on staged files.
var analyzeHasError = false;
stdout.write('Running static analysis...');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Structurally my gut is organizing code by job type instead of by when in the hooks process the code runs is more likely to be a durable standard.

if (dartFiles.isNotEmpty) {
final ProcessResult analyzeResult = await processRunner('dart', [
'analyze',
'--fatal-infos',
...dartFiles,
], workingDirectory: repoRoot.path);

if (analyzeResult.exitCode != 0) {
if (!analyzeHasError) {
stdout.write('\x1B[2K\r');
}
Comment thread
camsim99 marked this conversation as resolved.
Outdated
if (analyzeResult.stdout.toString().isNotEmpty) {
print(analyzeResult.stdout);
}
if (analyzeResult.stderr.toString().isNotEmpty) {
print(analyzeResult.stderr);
}
print('❌ Static analysis errors found.');
analyzeHasError = true;
hasError = true;
}
}

if (!analyzeHasError) {
stdout.write('\x1B[2K\r✅ Static analysis looks good.\n');
}
Comment thread
camsim99 marked this conversation as resolved.
Outdated

if (hasError) {
print('❌ Please fix the errors listed above.');
}

return !hasError;
}
}
5 changes: 5 additions & 0 deletions script/githooks/pre-commit
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 "$@"
13 changes: 13 additions & 0 deletions script/githooks/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: githooks
description: Git hooks for the flutter/packages repository.
Comment thread
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
Comment thread
camsim99 marked this conversation as resolved.
Loading
Loading