Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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 != '/' && !Directory(p.join(repoRoot.path, '.git')).existsSync()) {
repoRoot = repoRoot.parent;
}

if (repoRoot.path == '/') {
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;
}
158 changes: 158 additions & 0 deletions script/githooks/lib/src/pre_commit_command.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// ignore_for_file: avoid_print

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

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

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

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

@override
final String description = 'Checks to run before a "git commit"';
Comment thread
camsim99 marked this conversation as resolved.
Outdated

String? _findPackageName(String filePath, String repoRoot) {
Directory currentDir = File(p.join(repoRoot, filePath)).parent;
while (p.isWithin(repoRoot, currentDir.path) || p.equals(repoRoot, currentDir.path)) {
final String dirName = p.basename(currentDir.path);
if (dirName != 'example' && File(p.join(currentDir.path, 'pubspec.yaml')).existsSync()) {
return dirName;
}
if (p.equals(repoRoot, currentDir.path)) {
break;
}
currentDir = currentDir.parent;
}
return null;
}

@override
Future<bool> run() async {
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 != '/' && !Directory(p.join(repoRoot.path, '.git')).existsSync()) {
repoRoot = repoRoot.parent;
}

if (repoRoot.path == '/') {
print('❌ Could not find .git directory.');
return false;
}
Comment thread
camsim99 marked this conversation as resolved.
Outdated

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

if (diffResult.exitCode != 0) {
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;

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.

I made this decision because I assume there must be a Git setup issue if this happens, but open to pushback if we think this could actually block valid commits.

}

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

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

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

if (targetPackages.isEmpty) {
return true; // None of the changed files are part of a package we care about.
}

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

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

// Check formatting.
print('Checking formatting...');
final ProcessResult formatResult = await processRunner('dart', [
'run',
toolScript,
'format',
packageArgs,
'--fail-on-change',
], workingDirectory: repoRoot.path);

if (formatResult.exitCode != 0) {
if (formatResult.stdout.toString().isNotEmpty) {
print(formatResult.stdout);
}
if (formatResult.stderr.toString().isNotEmpty) {
print(formatResult.stderr);
}
print(
'❌ Formatting issues found. Please run "dart run script/tool/bin/flutter_plugin_tools.dart format $packageArgs" to fix them.',
);
hasError = true;
} else {
print('✅ Formatting looks good.');
}

// Run static analysis.
print('Running static analysis...');
final ProcessResult analyzeResult = await processRunner('dart', [
'run',
toolScript,
'analyze',
packageArgs,
], workingDirectory: repoRoot.path);

if (analyzeResult.exitCode != 0) {
if (analyzeResult.stdout.toString().isNotEmpty) print(analyzeResult.stdout);
if (analyzeResult.stderr.toString().isNotEmpty) print(analyzeResult.stderr);
print('❌ Static analysis errors found. Please fix the errors listed above.');
hasError = true;
} else {
print('✅ Static analysis looks good.');
}

return !hasError;
}
}
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.
78 changes: 78 additions & 0 deletions script/githooks/test/pre_commit_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:io';

import 'package:githooks/src/pre_commit_command.dart';
import 'package:test/test.dart';

void main() {
group('pre-commit hook', () {
test('passes when both format and analyze succeed', () async {
final command = PreCommitCommand(
processRunner:
(String executable, List<String> arguments, {String? workingDirectory}) async {
if (executable == 'git') {
return ProcessResult(0, 0, 'script/githooks/lib/githooks.dart\n', '');
}
return ProcessResult(0, 0, 'Success', '');
},
);

final bool result = await command.run();
expect(result, isTrue);
});

test('fails when formatting fails', () async {
final command = PreCommitCommand(
processRunner:
(String executable, List<String> arguments, {String? workingDirectory}) async {
if (executable == 'git') {
return ProcessResult(0, 0, 'script/githooks/lib/githooks.dart\n', '');
}
if (arguments.contains('format')) {
return ProcessResult(0, 1, 'bad_file.dart', '');
}
return ProcessResult(0, 0, 'Success', '');
},
);

final bool result = await command.run();
expect(result, isFalse);
});

test('fails when analysis fails', () async {
final command = PreCommitCommand(
processRunner:
(String executable, List<String> arguments, {String? workingDirectory}) async {
if (executable == 'git') {
return ProcessResult(0, 0, 'script/githooks/lib/githooks.dart\n', '');
}
if (arguments.contains('analyze')) {
return ProcessResult(0, 1, 'error in file.dart', '');
}
return ProcessResult(0, 0, 'Success', '');
},
);

final bool result = await command.run();
expect(result, isFalse);
});

test('ignores non-dart files', () async {
final command = PreCommitCommand(
processRunner:
(String executable, List<String> arguments, {String? workingDirectory}) async {
if (executable == 'git') {
return ProcessResult(0, 0, 'README.md\n', '');
}
return ProcessResult(0, 0, 'Success', '');
},
);

final bool result = await command.run();
expect(result, isTrue);
});
});
}
Loading