Skip to content
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
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"';

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 {

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.

Add dart doc

// 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.
Comment thread
camsim99 marked this conversation as resolved.

// 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) {

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.

With your ACM filter there could be a commit that was just a deletion but I dont think it would give any files. What error code is returned in that situation or is that a success? Looks like empty sets are handled below.

print('❌ Failed to get staged files');

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.

Do you think we should include the command that failed and the error code?

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.

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 a bit confused as to what this does. It looks like it is building a set of packages from staged files. But if that is the case then where does the filtering happen.

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(

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.

What code determines if this list needs to change?

(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.
final bool hasJava = stagedFiles.any((f) => f.endsWith('.java'));

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.

Recommend structuring this a bit more formally. What information is needed per language? How do you execute per language. Lets make the blast radius clear and the file manipulations happen outside of any language logic.

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');
}
Comment thread
camsim99 marked this conversation as resolved.
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

'--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.
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',
);
}
Comment thread
camsim99 marked this conversation as resolved.

// 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.
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.

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.
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