Skip to content

Add pre-commit hook to verify static analysis passes & format is correct#11941

Open
camsim99 wants to merge 20 commits into
flutter:mainfrom
camsim99:cos_precommit
Open

Add pre-commit hook to verify static analysis passes & format is correct#11941
camsim99 wants to merge 20 commits into
flutter:mainfrom
camsim99:cos_precommit

Conversation

@camsim99

@camsim99 camsim99 commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Adds a pre-commit hook that you can install to verify that dart analyze --fatal-infos and the package format are correct when you make a call to git commit. Intended to ensure agents commit code that meets are quality standards.

Miscellaneous notes:

  • script/githooks/bin/install_hooks.dart is the script to install the hook that devs would need to run to use it.
  • I modeled the new githooks directory like https://github.com/flutter/flutter/tree/master/engine/src/flutter/tools/githooks to make it easier for folks to add new hooks
  • The hook separates formatting of Dart and native files as an optimization to avoid all of the configuration that the plugin tool uses to format native files that are not required if no native files have been changed

Pre-Review Checklist

If you need help, consider asking for advice on the #hackers-new channel on Discord.

Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the gemini-code-assist bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.

Footnotes

  1. Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling. 2

@flutter-dashboard flutter-dashboard Bot added the CICD Run CI/CD label Jun 18, 2026
@github-actions github-actions Bot removed the CICD Run CI/CD label Jun 22, 2026
@flutter-dashboard flutter-dashboard Bot added the CICD Run CI/CD label Jun 22, 2026
@camsim99 camsim99 added CICD Run CI/CD and removed CICD Run CI/CD labels Jun 22, 2026
@camsim99 camsim99 marked this pull request as ready for review June 22, 2026 17:58

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request introduces a new githooks Dart package to automate pre-commit checks, including formatting and static analysis, for staged files. The package includes an installation script, a pre-commit command runner, and associated unit tests. Review feedback highlights a potential infinite loop on Windows when locating the .git directory, raw ANSI escape sequences being printed without verifying terminal support, the use of loose any version constraints in pubspec.yaml, and redundant dead code in the test mocks.

Comment on lines +11 to +19
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);
}

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.

high

On Windows, the root directory path is not '/' (e.g., it could be 'C:\'). Since repoRoot.parent of a drive root returns the drive root itself, checking repoRoot.path != '/' will result in an infinite loop if .git is not found. Comparing repoRoot.path to repoRoot.parent.path is a cross-platform way to detect when the file system root has been reached.

Suggested change
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);
}
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 on lines +55 to +63
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;
}

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.

high

On Windows, the root directory path is not '/' (e.g., it could be 'C:\'). Since repoRoot.parent of a drive root returns the drive root itself, checking repoRoot.path != '/' will result in an infinite loop if .git is not found. Comparing repoRoot.path to repoRoot.parent.path is a cross-platform way to detect when the file system root has been reached.

Suggested change
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;
}
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 on lines +146 to +148
if (!hasError) {
stdout.write('\x1B[2K\r');
}

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.

medium

Writing ANSI escape sequences like \x1B[2K\r directly to stdout can produce raw, unreadable characters in environments that do not support them (such as some GUI Git clients or non-interactive CI logs). Checking stdout.supportsAnsiEscapes ensures a clean fallback (like printing a newline) when ANSI escapes are not supported.

Suggested change
if (!hasError) {
stdout.write('\x1B[2K\r');
}
if (!hasError) {
stdout.write(stdout.supportsAnsiEscapes ? '\\x1B[2K\\r' : '\\n');
}

Comment on lines +197 to +199
if (!hasError) {
stdout.write('\x1B[2K\r✅ Formatting looks good.\n');
}

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.

medium

Ensure ANSI escape sequences are only printed when supported by the terminal to avoid raw escape characters in non-ANSI environments.

Suggested change
if (!hasError) {
stdout.write('\x1B[2K\r✅ Formatting looks good.\n');
}
if (!hasError) {
stdout.write(stdout.supportsAnsiEscapes
? '\\x1B[2K\\r✅ Formatting looks good.\\n'
: '✅ Formatting looks good.\\n');
}

Comment on lines +8 to +13
dependencies:
args: any
path: any

dev_dependencies:
test: 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.

medium

Using any version constraints for dependencies is discouraged as it can lead to unexpected breaking changes or build failures when new major versions of these packages are released. It is recommended to use caret constraints to specify compatible version ranges.

dependencies:
  args: ^2.4.2
  path: ^1.9.0

dev_dependencies:
  test: ^1.24.0

Comment on lines +18 to +23
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', '');
}

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.

medium

The check for --show-toplevel is dead code because PreCommitCommand does not invoke git rev-parse --show-toplevel. Removing this block simplifies the test mock.

              if (executable == 'git') {
                return ProcessResult(0, 0, 'script/githooks/lib/githooks.dart\\n', '');
              }

Comment on lines +50 to +55
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', '');
}

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.

medium

The check for --show-toplevel is dead code because PreCommitCommand does not invoke git rev-parse --show-toplevel. Removing this block simplifies the test mock.

              if (executable == 'git') {
                return ProcessResult(0, 0, 'script/githooks/lib/githooks.dart\\n', '');
              }

Comment on lines +80 to +85
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', '');
}

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.

medium

The check for --show-toplevel is dead code because PreCommitCommand does not invoke git rev-parse --show-toplevel. Removing this block simplifies the test mock.

              if (executable == 'git') {
                return ProcessResult(0, 0, 'script/githooks/lib/githooks.dart\\n', '');
              }

Comment on lines +108 to +113
if (executable == 'git') {
if (arguments.contains('--show-toplevel')) {
return ProcessResult(0, 0, '/fake/repo/root\n', '');
}
return ProcessResult(0, 0, 'README.md\n', '');
}

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.

medium

The check for --show-toplevel is dead code because PreCommitCommand does not invoke git rev-parse --show-toplevel. Removing this block simplifies the test mock.

              if (executable == 'git') {
                return ProcessResult(0, 0, 'README.md\\n', '');
              }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CICD Run CI/CD

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant