Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 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
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
30 changes: 30 additions & 0 deletions script/githooks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Git Hooks

This directory contains Git hooks for the `flutter/packages` repository.

## Installation

To install the Git hooks, run the following commands from the root of the repository:

```bash
# Fetch dependencies for the githooks package
dart pub get -C script/githooks

# Run the installation script
dart script/githooks/bin/install_hooks.dart
```

## Available Hooks

### pre-commit

The `pre-commit` hook runs automatically when you run `git commit` and performs the following checks on any staged changes:

1. **Formatting**: It runs `dart run script/tool/bin/flutter_plugin_tools.dart format --run-on-staged-packages` to verify that all staged files in the targeted packages are correctly formatted.
2. **Static Analysis**: If formatting passes, it runs `dart run script/tool/bin/flutter_plugin_tools.dart analyze --run-on-staged-packages --dart` to run static analysis on the staged packages.

If either check fails, it aborts the commit. To bypass the hook (for a WIP commit, for example), you can use the `--no-verify` flag:

```bash
git commit -m "WIP" --no-verify
```
34 changes: 34 additions & 0 deletions script/githooks/bin/install_hooks.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// ignore_for_file: avoid_print

import 'dart:io';
import 'package:path/path.dart' as p;

void main() async {
Directory repoRoot = Directory.current;
while (repoRoot.path != repoRoot.parent.path &&
!(Directory(p.join(repoRoot.path, '.git')).existsSync() ||
File(p.join(repoRoot.path, '.git')).existsSync())) {
repoRoot = repoRoot.parent;
}
if (!(Directory(p.join(repoRoot.path, '.git')).existsSync() ||
File(p.join(repoRoot.path, '.git')).existsSync())) {
print('Installation failed because .git directory could not be found.');
exit(1);
}
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;
}
180 changes: 180 additions & 0 deletions script/githooks/lib/src/pre_commit_command.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// ignore_for_file: avoid_print

import 'dart:io';
import 'package:args/command_runner.dart';
import 'package:path/path.dart' as p;

/// The command that implements the pre-commit githook.
class PreCommitCommand extends Command<bool> {
/// Creates a [PreCommitCommand].
PreCommitCommand({
Future<ProcessResult> Function(
String executable,
List<String> arguments, {
String? workingDirectory,
})?
processRunner,
}) : processRunner = processRunner ?? Process.run;

/// The process runner injected for testing.
final Future<ProcessResult> Function(
String executable,
List<String> arguments, {
String? workingDirectory,
})
processRunner;

@override
final String name = 'pre-commit';

@override
final String description =
'Runs formatting and static analysis checks on staged changes before a "git commit"';

/// Runs a pre-commit check for correct formatting and static analysis.
///
/// It runs the plugin tool format and analyze commands on all packages that have staged changes.
/// If any of the commands fail, it will return false; otherwise, it will return true.
@override
Future<bool> run() async {
Comment thread
camsim99 marked this conversation as resolved.
final Directory? repoRoot = await _findRepoRoot();
if (repoRoot == null) {
print('Could not find git repository.');
return false;
}

// Skips check if there are no staged package changes because this
// makes a ~5s difference in runtime.
// TODO(camsim99): Investigate why the plugins tool adds overhead:
// https://github.com/flutter/flutter/issues/188870.
final bool? hasStaged = await _hasStagedPackages(repoRoot);
if (hasStaged == null) {
return false;
}
if (!hasStaged) {
print('No staged package changes to check.');
return true;
}

final String toolScript = p.join(
repoRoot.path,
'script',
'tool',
'bin',
'flutter_plugin_tools.dart',
);

print('Running pre-commit format and static analysis checks for staged changes...');

// Check format.
final bool formatPassed = await _executeCheckFormatting(repoRoot, toolScript);
if (!formatPassed) {
return false;
}

return _executeCheckStaticAnalysis(repoRoot, toolScript);
}

/// Finds the repository root directory using git.
///
/// Returns null if git fails or if the directory is not a git repository.
Future<Directory?> _findRepoRoot() async {
final ProcessResult rootResult = await processRunner('git', <String>[
'rev-parse',
'--show-toplevel',
], workingDirectory: Directory.current.path);

if (rootResult.exitCode != 0) {
return null;
}
return Directory((rootResult.stdout as String).trim());
}

/// Checks if there are any staged package changes in the repository.
///
/// Returns true if at least one staged file is located within a package directory
/// (under packages/ or third_party/packages/), false if there are none, or null
/// if the git command fails.
Future<bool?> _hasStagedPackages(Directory repoRoot) async {

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 think this logic was moved to the format/package commands. If so consider removing it. If we add logic here to filter then it could be confusing if it diverges from what is in the other tooling.

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.

The optimization was moved to the format/package commands but this is an additional optimization--when there are no relevant changes to have this hook check, let's skip calling into the plugins tool altogether. Calling into at all adds steps like checking package dependencies that we can skip if we aren't going to end up running the checks at all.

I don't see the risk of divergence being high, but if you're concerned we can remove it! I don't feel super strongly since we don't necessarily expect high human usage.

@reidbaker reidbaker Jun 30, 2026

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.

Ok in that case can you give me the situation and performance delta of this optimization? My understanding having not run it is that we are still spinning up dart and we are still filtering using the same git commands so I don't see how it could improve efficiency. Is there a step I am not seeing?

The most convincing evidence would be a common situation, maybe a merge conflict, and showing the difference is a multiple in speed.

I guess my larger concern is that the filtering should happen in one place unless we get a really large performance or ux gain and on my mind once we are in dart land we should be able to make the performance the same.

final ProcessResult diffResult = await processRunner('git', <String>[
'diff',
'--cached',
'-z',
'--name-only',
], workingDirectory: repoRoot.path);

if (diffResult.exitCode != 0) {
print('Failed to check staged changes.');
if (diffResult.stderr.toString().isNotEmpty) {
print(diffResult.stderr);
}
// If we cannot determine the diff, abort pre-commit check by returning null.
return null;
}

final stdoutStr = diffResult.stdout as String;
if (stdoutStr.isEmpty) {
return false;
}

final List<String> lines = stdoutStr.split('\u0000')
..removeWhere((String element) => element.isEmpty);
return lines.any(
(String path) => path.startsWith('packages/') || path.startsWith('third_party/packages/'),
);
}

/// Runs the formatting check on staged files.
///
/// Returns true if all staged files are correctly formatted or false otherwise.
Future<bool> _executeCheckFormatting(Directory repoRoot, String toolScript) async {
final ProcessResult formatResult = await processRunner('dart', [
'run',
toolScript,
'format',
'--run-on-staged-packages',
'--fail-on-change',
], workingDirectory: repoRoot.path);

if (formatResult.exitCode != 0) {
print('''
Formatting check failed.
To fix formatting automatically, run:
dart run script/tool/bin/flutter_plugin_tools.dart format --run-on-staged-packages
To bypass this check, commit with --no-verify.''');
return false;
}

print('Formatting looks good!');
return true;
}

/// Runs the static analysis check on staged files.
///
/// Returns true if all staged files pass analysis or false otherwise.
Future<bool> _executeCheckStaticAnalysis(Directory repoRoot, String toolScript) async {
final ProcessResult analyzeResult = await processRunner('dart', [
'run',
toolScript,
'analyze',
'--run-on-staged-packages',
'--dart',
], workingDirectory: repoRoot.path);

if (analyzeResult.exitCode != 0) {
print('''
Static analysis check failed.
To view and fix analysis errors, run:
dart run script/tool/bin/flutter_plugin_tools.dart analyze --run-on-staged-packages --dart
To bypass this check, commit with --no-verify.''');
return false;
}

print('Static analysis looks good!');
return true;
}
}
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. For more information on the hooks and how to install them, see README.md
publish_to: none

environment:
sdk: ^3.10.0-0

dependencies:
args: any
path: any

dev_dependencies:
test: any
Comment thread
camsim99 marked this conversation as resolved.
Loading
Loading