Enabling pre-push checks on Windows (#36123)

Re-submit the changes to enable windows pre-push checks.

This patch changes how `ci/bin/format.dart` generate diffs from `diff` and `patch` commands to `git diff` and `git apply` in order to have a common method for these operations on all platforms. Windows installations don't have diff and patch commands available by default and many implementations which provide such commands work differently than the UN*X tools. Git however works consistently across all platforms.

Additionally, this patch also changes the python executable in some of the pre-push components affected by this to `vpython3` to continue the effort started at flutter/flutter#108474 and I also removed the `--no-sound-null-safety` parameter in the ci/format.sh, ci/format.bat files

NOTE: Since the original patch caused some issues, I suggest that this should be tested more carefully before it is merged.

### Issues fixed by this PR
* flutter/flutter#108122
* flutter/flutter#107920
* flutter/flutter#86506
* flutter/flutter#106615

### [flutter/tests] repo impact
None.

writing and running engine tests.

[C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
diff --git a/ci/bin/format.dart b/ci/bin/format.dart
index c67f9c0..1e4de14 100644
--- a/ci/bin/format.dart
+++ b/ci/bin/format.dart
@@ -6,9 +6,6 @@
 //
 // Run with --help for usage.
 
-// TODO(gspencergoog): Support clang formatting on Windows.
-// TODO(gspencergoog): Support Java formatting on Windows.
-
 import 'dart:io';
 
 import 'package:args/args.dart';
@@ -84,14 +81,9 @@
 }
 
 List<String> formatCheckNames() {
-  List<FormatCheck> allowed;
-  if (!Platform.isWindows) {
-    allowed = FormatCheck.values;
-  } else {
-    allowed = <FormatCheck>[FormatCheck.gn, FormatCheck.whitespace];
-  }
-  return allowed
-      .map<String>((FormatCheck check) => check.toString().replaceFirst('$FormatCheck.', ''))
+  return FormatCheck.values
+      .map<String>((FormatCheck check) =>
+          check.toString().replaceFirst('$FormatCheck.', ''))
       .toList();
 }
 
@@ -226,7 +218,7 @@
     );
     final List<WorkerJob> jobs = patches.map<WorkerJob>((String patch) {
       return WorkerJob(
-        <String>['patch', '-p0'],
+        <String>['git', 'apply', '--ignore-space-change'],
         stdinRaw: codeUnitsAsStream(patch.codeUnits),
       );
     }).toList();
@@ -318,6 +310,8 @@
       clangOs = 'linux-x64';
     } else if (Platform.isMacOS) {
       clangOs = 'mac-x64';
+    } else if (Platform.isWindows) {
+      clangOs = 'windows-x64';
     } else {
       throw FormattingException(
           "Unknown operating system: don't know how to run clang-format here.");
@@ -401,9 +395,23 @@
     await for (final WorkerJob completedJob in completedClangFormats) {
       if (completedJob.result.exitCode == 0) {
         diffJobs.add(
-          WorkerJob(<String>['diff', '-u', completedJob.command.last, '-'],
+          WorkerJob(<String>[
+            'git',
+            'diff',
+            '--no-index',
+            '--no-color',
+            '--ignore-cr-at-eol',
+            '--',
+            completedJob.command.last,
+            '-',
+          ],
               stdinRaw: codeUnitsAsStream(completedJob.result.stdoutRaw)),
         );
+      } else {
+        final String formatterCommand = completedJob.command.join(' ');
+        error("Formatter command '$formatterCommand' failed with exit code "
+            '${completedJob.result.exitCode}. Command output follows:\n\n'
+            '${completedJob.result.output}');
       }
     }
     final ProcessPool diffPool = ProcessPool(
@@ -425,9 +433,11 @@
             ' which ${plural ? 'were' : 'was'} formatted incorrectly.');
         stdout.writeln('To fix, run:');
         stdout.writeln();
-        stdout.writeln('patch -p0 <<DONE');
+        stdout.writeln('git apply <<DONE');
         for (final WorkerJob job in failed) {
-          stdout.write(job.result.stdout);
+          stdout.write(job.result.stdout
+              .replaceFirst('b/-', 'b/${job.command[job.command.length - 2]}')
+              .replaceFirst('b/-', 'b/${job.command[job.command.length - 2]}'));
         }
         stdout.writeln('DONE');
         stdout.writeln();
@@ -436,7 +446,9 @@
       message('Completed checking ${diffJobs.length} C++/ObjC/Shader files with no formatting problems.');
     }
     return failed.map<String>((WorkerJob job) {
-      return job.result.stdout;
+      return job.result.stdout
+          .replaceFirst('b/-', 'b/${job.command[job.command.length - 2]}')
+          .replaceFirst('b/-', 'b/${job.command[job.command.length - 2]}');
     }).toList();
   }
 }
@@ -536,16 +548,30 @@
       processRunner: _processRunner,
       printReport: namedReport('Java format'),
     );
-    final Stream<WorkerJob> completedClangFormats = formatPool.startWorkers(formatJobs);
+    final Stream<WorkerJob> completedJavaFormats = formatPool.startWorkers(formatJobs);
     final List<WorkerJob> diffJobs = <WorkerJob>[];
-    await for (final WorkerJob completedJob in completedClangFormats) {
+    await for (final WorkerJob completedJob in completedJavaFormats) {
       if (completedJob.result.exitCode == 0) {
         diffJobs.add(
           WorkerJob(
-            <String>['diff', '-u', completedJob.command.last, '-'],
+            <String>[
+              'git',
+              'diff',
+              '--no-index',
+              '--no-color',
+              '--ignore-cr-at-eol',
+              '--',
+              completedJob.command.last,
+              '-',
+            ],
             stdinRaw: codeUnitsAsStream(completedJob.result.stdoutRaw),
           ),
         );
+      } else {
+        final String formatterCommand = completedJob.command.join(' ');
+        error("Formatter command '$formatterCommand' failed with exit code "
+            '${completedJob.result.exitCode}. Command output follows:\n\n'
+            '${completedJob.result.output}');
       }
     }
     final ProcessPool diffPool = ProcessPool(
@@ -567,9 +593,11 @@
             ' which ${plural ? 'were' : 'was'} formatted incorrectly.');
         stdout.writeln('To fix, run:');
         stdout.writeln();
-        stdout.writeln('patch -p0 <<DONE');
+        stdout.writeln('git apply <<DONE');
         for (final WorkerJob job in failed) {
-          stdout.write(job.result.stdout);
+          stdout.write(job.result.stdout
+              .replaceFirst('b/-', 'b/${job.command[job.command.length - 2]}')
+              .replaceFirst('b/-', 'b/${job.command[job.command.length - 2]}'));
         }
         stdout.writeln('DONE');
         stdout.writeln();
@@ -578,7 +606,9 @@
       message('Completed checking ${diffJobs.length} Java files with no formatting problems.');
     }
     return failed.map<String>((WorkerJob job) {
-      return job.result.stdout;
+      return job.result.stdout
+          .replaceFirst('b/-', 'b/${job.command[job.command.length - 2]}')
+          .replaceFirst('b/-', 'b/${job.command[job.command.length - 2]}');
     }).toList();
   }
 }
@@ -627,45 +657,90 @@
     final List<String> cmd = <String>[
       gnBinary.path,
       'format',
-      if (!fixing) '--dry-run',
+      if (!fixing) '--stdin',
     ];
     final List<WorkerJob> jobs = <WorkerJob>[];
     for (final String file in filesToCheck) {
-      jobs.add(WorkerJob(<String>[...cmd, file]));
+      if (fixing) {
+        jobs.add(WorkerJob(
+          <String>[...cmd, file],
+          name: <String>[...cmd, file].join(' '),
+        ));
+      } else {
+        final WorkerJob job = WorkerJob(
+          cmd,
+          stdinRaw: codeUnitsAsStream(
+            File(path.join(repoDir.absolute.path, file)).readAsBytesSync(),
+          ),
+          name: <String>[...cmd, file].join(' '),
+        );
+        jobs.add(job);
+      }
     }
     final ProcessPool gnPool = ProcessPool(
       processRunner: _processRunner,
       printReport: namedReport('gn format'),
     );
-    final List<WorkerJob> completedJobs = await gnPool.runToCompletion(jobs);
-    reportDone();
-    final List<String> incorrect = <String>[];
-    for (final WorkerJob job in completedJobs) {
-      if (job.result.exitCode == 2) {
-        incorrect.add('  ${job.command.last}');
-      }
-      if (job.result.exitCode == 1) {
-        // GN has exit code 1 if it had some problem formatting/checking the
-        // file.
-        throw FormattingException(
-          'Unable to format ${job.command.last}:\n${job.result.output}',
+    final Stream<WorkerJob> completedJobs = gnPool.startWorkers(jobs);
+    final List<WorkerJob> diffJobs = <WorkerJob>[];
+    await for (final WorkerJob completedJob in completedJobs) {
+      if (completedJob.result.exitCode == 0) {
+        diffJobs.add(
+          WorkerJob(
+            <String>[
+              'git',
+              'diff',
+              '--no-index',
+              '--no-color',
+              '--ignore-cr-at-eol',
+              '--',
+              completedJob.name.split(' ').last,
+              '-'
+            ],
+            stdinRaw: codeUnitsAsStream(completedJob.result.stdoutRaw),
+          ),
         );
+      } else {
+        final String formatterCommand = completedJob.command.join(' ');
+        error("Formatter command '$formatterCommand' failed with exit code "
+            '${completedJob.result.exitCode}. Command output follows:\n\n'
+            '${completedJob.result.output}');
       }
     }
-    if (incorrect.isNotEmpty) {
-      final bool plural = incorrect.length > 1;
+    final ProcessPool diffPool = ProcessPool(
+      processRunner: _processRunner,
+      printReport: namedReport('diff'),
+    );
+    final List<WorkerJob> completedDiffs =
+        await diffPool.runToCompletion(diffJobs);
+    final Iterable<WorkerJob> failed = completedDiffs.where((WorkerJob job) {
+      return job.result.exitCode != 0;
+    });
+    reportDone();
+    if (failed.isNotEmpty) {
+      final bool plural = failed.length > 1;
       if (fixing) {
-        message('Fixed ${incorrect.length} GN file${plural ? 's' : ''}'
+        message('Fixed ${failed.length} GN file${plural ? 's' : ''}'
             ' which ${plural ? 'were' : 'was'} formatted incorrectly.');
       } else {
-        error('Found ${incorrect.length} GN file${plural ? 's' : ''}'
-            ' which ${plural ? 'were' : 'was'} formatted incorrectly:');
-        incorrect.forEach(stderr.writeln);
+        error('Found ${failed.length} GN file${plural ? 's' : ''}'
+            ' which ${plural ? 'were' : 'was'} formatted incorrectly.');
+        stdout.writeln('To fix, run:');
+        stdout.writeln();
+        stdout.writeln('git apply <<DONE');
+        for (final WorkerJob job in failed) {
+          stdout.write(job.result.stdout
+              .replaceFirst('b/-', 'b/${job.command[job.command.length - 2]}')
+              .replaceFirst('b/-', 'b/${job.command[job.command.length - 2]}'));
+        }
+        stdout.writeln('DONE');
+        stdout.writeln();
       }
     } else {
-      message('All GN files formatted correctly.');
+      message('Completed checking ${completedDiffs.length} GN files with no '
+          'formatting problems.');
     }
-    return incorrect.length;
+    return failed.length;
   }
 }
 
@@ -684,7 +759,7 @@
     yapfBin = File(path.join(
       repoDir.absolute.path,
       'tools',
-      'yapf.sh',
+      Platform.isWindows ? 'yapf.bat' : 'yapf.sh',
     ));
     _yapfStyle = File(path.join(
       repoDir.absolute.path,
@@ -937,8 +1012,7 @@
       allowed: formatCheckNames(),
       defaultsTo: formatCheckNames(),
       help: 'Specifies which checks will be performed. Defaults to all checks. '
-          'May be specified more than once to perform multiple types of checks. '
-          'On Windows, only whitespace and gn checks are currently supported.');
+          'May be specified more than once to perform multiple types of checks. ');
   parser.addFlag('verbose', help: 'Print verbose output.', defaultsTo: verbose);
 
   late final ArgResults options;
diff --git a/ci/format.bat b/ci/format.bat
index 3545111..8a12f07 100644
--- a/ci/format.bat
+++ b/ci/format.bat
@@ -20,7 +20,16 @@
 
 SET repo_dir=%SRC_DIR%\flutter
 SET ci_dir=%repo_dir%\ci
-SET dart_sdk_path=%SRC_DIR%\third_party\dart\tools\sdks\dart-sdk
+
+REM Determine which platform we are on and use the right prebuilt Dart SDK
+IF "%PROCESSOR_ARCHITECTURE%"=="AMD64" (
+    SET dart_sdk_path=%SRC_DIR%\flutter\prebuilts\windows-x64\dart-sdk
+) ELSE IF "%PROCESSOR_ARCHITECTURE%"=="ARM64" (
+    SET dart_sdk_path=%SRC_DIR%\flutter\prebuilts\windows-arm64\dart-sdk
+) ELSE (
+    ECHO "Windows x86 (32-bit) is not supported" && EXIT /B 1
+)
+
 SET dart=%dart_sdk_path%\bin\dart.exe
 
 cd "%ci_dir%"
diff --git a/ci/pubspec.yaml b/ci/pubspec.yaml
index 6b09e88..85639d8 100644
--- a/ci/pubspec.yaml
+++ b/ci/pubspec.yaml
@@ -23,15 +23,26 @@
   process_runner: any
   process: any
 
+dev_dependencies:
+  async_helper: any
+  expect: any
+  litetest: any
+
 dependency_overrides:
   args:
     path: ../../third_party/dart/third_party/pkg/args
   async:
     path: ../../third_party/dart/third_party/pkg/async
+  async_helper:
+    path: ../../third_party/dart/pkg/async_helper
   collection:
     path: ../../third_party/dart/third_party/pkg/collection
+  expect:
+    path: ../../third_party/dart/pkg/expect
   file:
     path: ../../third_party/pkg/file/packages/file
+  litetest:
+    path: ../testing/litetest
   meta:
     path: ../../third_party/dart/pkg/meta
   path:
@@ -42,3 +53,5 @@
     path: ../../third_party/pkg/process
   process_runner:
     path: ../../third_party/pkg/process_runner
+  smith:
+    path: ../../third_party/dart/pkg/smith
diff --git a/ci/test/format_test.dart b/ci/test/format_test.dart
new file mode 100644
index 0000000..78af163
--- /dev/null
+++ b/ci/test/format_test.dart
@@ -0,0 +1,227 @@
+// 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:litetest/litetest.dart';
+import 'package:path/path.dart' as path;
+import 'package:process_runner/process_runner.dart';
+
+import '../bin/format.dart' as target;
+
+final io.File script = io.File.fromUri(io.Platform.script).absolute;
+final io.Directory repoDir = script.parent.parent.parent;
+final ProcessPool pool = ProcessPool(
+  numWorkers: 1,
+  processRunner: ProcessRunner(defaultWorkingDirectory: repoDir),
+);
+
+class FileContentPair {
+  FileContentPair(this.original, this.formatted, this.fileExtension);
+
+  final String original;
+  final String formatted;
+  final String fileExtension;
+}
+
+final FileContentPair ccContentPair = FileContentPair(
+    'int main(){return 0;}\n', 'int main() {\n  return 0;\n}\n', '.cc');
+final FileContentPair hContentPair =
+    FileContentPair('int\nmain\n()\n;\n', 'int main();\n', '.h');
+final FileContentPair gnContentPair = FileContentPair(
+    'test\n(){testvar=true}\n', 'test() {\n  testvar = true\n}\n', '.gn');
+final FileContentPair javaContentPair = FileContentPair(
+    'class Test{public static void main(String args[]){System.out.println("Test");}}\n',
+    'class Test {\n  public static void main(String args[]) {\n    System.out.println("Test");\n  }\n}\n',
+    '.java');
+final FileContentPair pythonContentPair = FileContentPair(
+    "if __name__=='__main__':\n  sys.exit(\nMain(sys.argv)\n)\n",
+    "if __name__ == '__main__':\n  sys.exit(Main(sys.argv))\n",
+    '.py');
+final FileContentPair whitespaceContentPair = FileContentPair(
+    'int main() {\n  return 0;       \n}\n',
+    'int main() {\n  return 0;\n}\n',
+    '.c');
+
+class TestFileFixture {
+  TestFileFixture(this.type) {
+    switch (type) {
+      case target.FormatCheck.clang:
+        final io.File ccFile = io.File('${repoDir.path}/format_test.cc');
+        ccFile.writeAsStringSync(ccContentPair.original);
+        files.add(ccFile);
+
+        final io.File hFile = io.File('${repoDir.path}/format_test.h');
+        hFile.writeAsStringSync(hContentPair.original);
+        files.add(hFile);
+      case target.FormatCheck.gn:
+        final io.File gnFile = io.File('${repoDir.path}/format_test.gn');
+        gnFile.writeAsStringSync(gnContentPair.original);
+        files.add(gnFile);
+      case target.FormatCheck.java:
+        final io.File javaFile = io.File('${repoDir.path}/format_test.java');
+        javaFile.writeAsStringSync(javaContentPair.original);
+        files.add(javaFile);
+      case target.FormatCheck.python:
+        final io.File pyFile = io.File('${repoDir.path}/format_test.py');
+        pyFile.writeAsStringSync(pythonContentPair.original);
+        files.add(pyFile);
+      case target.FormatCheck.whitespace:
+        final io.File whitespaceFile = io.File('${repoDir.path}/format_test.c');
+        whitespaceFile.writeAsStringSync(whitespaceContentPair.original);
+        files.add(whitespaceFile);
+    }
+  }
+
+  final target.FormatCheck type;
+  final List<io.File> files = <io.File>[];
+
+  void gitAdd() {
+    final List<String> args = <String>['add'];
+    for (final io.File file in files) {
+      args.add(file.path);
+    }
+
+    io.Process.runSync('git', args);
+  }
+
+  void gitRemove() {
+    final List<String> args = <String>['rm', '-f'];
+    for (final io.File file in files) {
+      args.add(file.path);
+    }
+    io.Process.runSync('git', args);
+  }
+
+  Iterable<FileContentPair> getFileContents() {
+    return files.map((io.File file) {
+      String content = file.readAsStringSync();
+      // Avoid clang-tidy formatting CRLF EOL on Windows
+      content = content.replaceAll('\r\n', '\n');
+      switch (type) {
+        case target.FormatCheck.clang:
+          return FileContentPair(
+            content,
+            path.extension(file.path) == '.cc'
+                ? ccContentPair.formatted
+                : hContentPair.formatted,
+            path.extension(file.path),
+          );
+        case target.FormatCheck.gn:
+          return FileContentPair(
+            content,
+            gnContentPair.formatted,
+            path.extension(file.path),
+          );
+        case target.FormatCheck.java:
+          return FileContentPair(
+            content,
+            javaContentPair.formatted,
+            path.extension(file.path),
+          );
+        case target.FormatCheck.python:
+          return FileContentPair(
+            content,
+            pythonContentPair.formatted,
+            path.extension(file.path),
+          );
+        case target.FormatCheck.whitespace:
+          return FileContentPair(
+            content,
+            whitespaceContentPair.formatted,
+            path.extension(file.path),
+          );
+      }
+    });
+  }
+}
+
+void main() {
+  final String formatterPath =
+      '${repoDir.path}/ci/format.${io.Platform.isWindows ? 'bat' : 'sh'}';
+
+  test('Can fix C++ formatting errors', () {
+    final TestFileFixture fixture = TestFileFixture(target.FormatCheck.clang);
+    try {
+      fixture.gitAdd();
+      io.Process.runSync(formatterPath, <String>['--check', 'clang', '--fix'],
+          workingDirectory: repoDir.path);
+
+      final Iterable<FileContentPair> files = fixture.getFileContents();
+      for (final FileContentPair pair in files) {
+        expect(pair.original, equals(pair.formatted));
+      }
+    } finally {
+      fixture.gitRemove();
+    }
+  });
+
+  test('Can fix GN formatting errors', () {
+    final TestFileFixture fixture = TestFileFixture(target.FormatCheck.gn);
+    try {
+      fixture.gitAdd();
+      io.Process.runSync(formatterPath, <String>['--check', 'gn', '--fix'],
+          workingDirectory: repoDir.path);
+
+      final Iterable<FileContentPair> files = fixture.getFileContents();
+      for (final FileContentPair pair in files) {
+        expect(pair.original, equals(pair.formatted));
+      }
+    } finally {
+      fixture.gitRemove();
+    }
+  });
+
+  test('Can fix Java formatting errors', () {
+    final TestFileFixture fixture = TestFileFixture(target.FormatCheck.java);
+    try {
+      fixture.gitAdd();
+      io.Process.runSync(formatterPath, <String>['--check', 'java', '--fix'],
+          workingDirectory: repoDir.path);
+
+      final Iterable<FileContentPair> files = fixture.getFileContents();
+      for (final FileContentPair pair in files) {
+        expect(pair.original, equals(pair.formatted));
+      }
+    } finally {
+      fixture.gitRemove();
+    }
+    // TODO(mtolmacs): Fails if Java dependency is unavailable,
+    // https://github.com/flutter/flutter/issues/129221
+  }, skip: true);
+
+  test('Can fix Python formatting errors', () {
+    final TestFileFixture fixture = TestFileFixture(target.FormatCheck.python);
+    try {
+      fixture.gitAdd();
+      io.Process.runSync(formatterPath, <String>['--check', 'python', '--fix'],
+          workingDirectory: repoDir.path);
+
+      final Iterable<FileContentPair> files = fixture.getFileContents();
+      for (final FileContentPair pair in files) {
+        expect(pair.original, equals(pair.formatted));
+      }
+    } finally {
+      fixture.gitRemove();
+    }
+  });
+
+  test('Can fix whitespace formatting errors', () {
+    final TestFileFixture fixture =
+        TestFileFixture(target.FormatCheck.whitespace);
+    try {
+      fixture.gitAdd();
+      io.Process.runSync(
+          formatterPath, <String>['--check', 'whitespace', '--fix'],
+          workingDirectory: repoDir.path);
+
+      final Iterable<FileContentPair> files = fixture.getFileContents();
+      for (final FileContentPair pair in files) {
+        expect(pair.original, equals(pair.formatted));
+      }
+    } finally {
+      fixture.gitRemove();
+    }
+  });
+}
diff --git a/testing/run_tests.py b/testing/run_tests.py
index 46e398e..a73182a 100755
--- a/testing/run_tests.py
+++ b/testing/run_tests.py
@@ -979,6 +979,32 @@
     )
 
 
+def gather_ci_tests(build_dir):
+  test_dir = os.path.join(BUILDROOT_DIR, 'flutter', 'ci')
+  dart_tests = glob.glob('%s/test/*_test.dart' % test_dir)
+
+  run_engine_executable(
+      build_dir,
+      os.path.join('dart-sdk', 'bin', 'dart'),
+      None,
+      flags=['pub', 'get', '--offline'],
+      cwd=test_dir,
+  )
+
+  for dart_test_file in dart_tests:
+    opts = [
+        '--disable-dart-dev', dart_test_file,
+        os.path.join(BUILDROOT_DIR, 'flutter')
+    ]
+    yield EngineExecutableTask(
+        build_dir,
+        os.path.join('dart-sdk', 'bin', 'dart'),
+        None,
+        flags=opts,
+        cwd=test_dir
+    )
+
+
 def run_engine_tasks_in_parallel(tasks):
   # Work around a bug in Python.
   #
@@ -1217,6 +1243,7 @@
     tasks += list(gather_path_ops_tests(build_dir))
     tasks += list(gather_const_finder_tests(build_dir))
     tasks += list(gather_front_end_server_tests(build_dir))
+    tasks += list(gather_ci_tests(build_dir))
     tasks += list(gather_dart_tests(build_dir, dart_filter))
     run_engine_tasks_in_parallel(tasks)
 
diff --git a/tools/githooks/pre-push b/tools/githooks/pre-push
index 4e10ce8..0f629a0 100755
--- a/tools/githooks/pre-push
+++ b/tools/githooks/pre-push
@@ -1,4 +1,4 @@
-#!/usr/bin/env python3
+#!/usr/bin/env vpython3
 # 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.
diff --git a/tools/githooks/setup.py b/tools/githooks/setup.py
index ea8edf4..cbf15c7 100755
--- a/tools/githooks/setup.py
+++ b/tools/githooks/setup.py
@@ -29,7 +29,6 @@
   githooks = os.path.join(FLUTTER_DIR, 'tools', 'githooks')
   if IsWindows():
     git = 'git.bat'
-    githooks = os.path.join(githooks, 'windows')
   result = subprocess.run([
       git,
       'config',
diff --git a/tools/githooks/windows/pre-push b/tools/githooks/windows/pre-push
index aa363e3..811a4c9 100644
--- a/tools/githooks/windows/pre-push
+++ b/tools/githooks/windows/pre-push
@@ -3,8 +3,9 @@
 # Use of this source code is governed by a BSD-style license that can be

 # found in the LICENSE file.

 

+YELLOW=''

 RED=''

 NC=''

 BOLD=''

-echo "${BOLD}${RED}Warning:${NC} ${BOLD}Pre-push checks not supported on Windows${NC}"

-echo "${BOLD}See: https://github.com/flutter/flutter/issues/86506${NC}"

+echo "${BOLD}${RED}Warning:${NC} ${BOLD}Pre-push checks are not enabled!${NC}"

+echo "${YELLOW}${BOLD}Note:${NC} ${BOLD}Please run 'gclient sync -D' to enable pre-push checks on Windows${NC}"

diff --git a/tools/yapf.bat b/tools/yapf.bat
new file mode 100644
index 0000000..587989c
--- /dev/null
+++ b/tools/yapf.bat
@@ -0,0 +1,16 @@
+@ECHO off
+REM Copyright 2013 The Flutter Authors. All rights reserved.
+REM Use of this source code is governed by a BSD-style license that can be
+REM found in the LICENSE file.
+
+REM ---------------------------------- NOTE ----------------------------------
+REM
+REM Please keep the logic in this file consistent with the logic in the
+REM `yapf.sh` script in the same directory to ensure that it continues to
+REM work across all platforms!
+REM
+REM --------------------------------------------------------------------------
+
+SET yapf_path=%~dp0\..\..\third_party\yapf
+
+cmd /V /C "SET PYTHONPATH=%yapf_path%&& vpython3 %yapf_path%\yapf %*"
diff --git a/tools/yapf.sh b/tools/yapf.sh
index af6e702..c3cbd55 100755
--- a/tools/yapf.sh
+++ b/tools/yapf.sh
@@ -3,6 +3,14 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+# ---------------------------------- NOTE ----------------------------------
+#
+# Please keep the logic in this file consistent with the logic in the
+# `yapf.bat` script in the same directory to ensure that it continues to
+# work across all platforms!
+#
+# --------------------------------------------------------------------------
+
 # Generates objc docs for Flutter iOS libraries.
 
 set -e