Skip to content

Commit 1890678

Browse files
authored
Add pre-push git hook (flutter#26699)
1 parent 1eb8a34 commit 1890678

File tree

12 files changed

+431
-2
lines changed

12 files changed

+431
-2
lines changed

DEPS

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,5 +712,13 @@ hooks = [
712712
'python3',
713713
'src/build/win/generate_winrt_headers.py',
714714
]
715+
},
716+
{
717+
'name': 'Setup githooks',
718+
'pattern': '.',
719+
'action': [
720+
'python3',
721+
'src/flutter/tools/githooks/setup.py',
722+
]
715723
}
716724
]

ci/analyze.sh

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ SRC_DIR="$(cd "$SCRIPT_DIR/../.."; pwd -P)"
3232
FLUTTER_DIR="$SRC_DIR/flutter"
3333
DART_BIN="$SRC_DIR/third_party/dart/tools/sdks/dart-sdk/bin"
3434
PUB="$DART_BIN/pub"
35+
DART="$DART_BIN/dart"
3536
DART_ANALYZER="$DART_BIN/dartanalyzer"
3637

3738
echo "Using analyzer from $DART_ANALYZER"
@@ -125,7 +126,13 @@ analyze \
125126
--options "$FLUTTER_DIR/analysis_options.yaml" \
126127
"$FLUTTER_DIR/testing/symbols"
127128

129+
echo "Analyzing githooks..."
130+
analyze \
131+
--packages="$FLUTTER_DIR/tools/githooks/.dart_tool/package_config.json" \
132+
--options "$FLUTTER_DIR/analysis_options.yaml" \
133+
"$FLUTTER_DIR/tools/githooks"
134+
128135
# Check that dart libraries conform.
129136
echo "Checking web_ui api conformance..."
130-
(cd "$FLUTTER_DIR/web_sdk"; pub get)
131-
(cd "$FLUTTER_DIR"; dart "web_sdk/test/api_conform_test.dart")
137+
(cd "$FLUTTER_DIR/web_sdk"; "$PUB" get)
138+
(cd "$FLUTTER_DIR"; "$DART" "web_sdk/test/api_conform_test.dart")

testing/run_tests.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,21 @@ def RunBenchmarkTests(build_dir):
485485
cwd=test_dir)
486486

487487

488+
def RunGithooksTests(build_dir):
489+
test_dir = os.path.join(buildroot_dir, 'flutter', 'tools', 'githooks')
490+
dart_tests = glob.glob('%s/test/*_test.dart' % test_dir)
491+
for dart_test_file in dart_tests:
492+
opts = [
493+
'--disable-dart-dev',
494+
dart_test_file]
495+
RunEngineExecutable(
496+
build_dir,
497+
os.path.join('dart-sdk', 'bin', 'dart'),
498+
None,
499+
flags=opts,
500+
cwd=test_dir)
501+
502+
488503
def main():
489504
parser = argparse.ArgumentParser()
490505

@@ -526,6 +541,7 @@ def main():
526541
dart_filter = args.dart_filter.split(',') if args.dart_filter else None
527542
RunDartSmokeTest(build_dir, args.verbose_dart_snapshot)
528543
RunLitetestTests(build_dir)
544+
RunGithooksTests(build_dir)
529545
RunDartTests(build_dir, dart_filter, args.verbose_dart_snapshot)
530546
RunConstFinderTests(build_dir)
531547
RunFrontEndServerTests(build_dir)

tools/githooks/README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Git Hooks
2+
3+
The behavior of `git` commands can be customized through the use of "hooks".
4+
These hooks are described in detail in git's
5+
[documentation](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks).
6+
7+
`git` looks for an executables by name in the directory specified by
8+
the `core.hooksPath` `git config` setting. The script `setup.py` here points
9+
`core.hooksPath` at this directory. It runs during a `gclient sync` or a
10+
`gclient runhooks`.
11+
12+
The hooks here are implemented in Dart by the program with
13+
entrypoint `bin/main.dart` in this directory. The commands of the program
14+
are the implementation of the different hooks, for example
15+
`bin/main.dart pre-push ...`. Since the Dart program itself isn't an executable,
16+
these commands are invoked by small Python wrapper scripts. These wrapper
17+
scripts have the names that `git` will look for.
18+
19+
## pre-push
20+
21+
This hooks runs when pushing commits to a remote branch, for example to
22+
create or update a pull request: `git push origin my-local-branch`.
23+
24+
The `pre-push` hook runs `ci/lint.sh` and `ci/format.sh`. `ci/analyze.sh` and
25+
`ci/licenses.sh` are more expensive and are not run.
26+
27+
### Adding new pre-push checks
28+
29+
Since the pre-push checks run on every `git push`, they should run quickly.
30+
New checks can be added by modifying the `run()` method of the `PrePushCommand`
31+
class in `lib/src/pre_push_command.dart`.
32+
33+
## Creating a new hook
34+
35+
1. Check the `git` documentation, and copy `pre-push` into a script with
36+
the right name.
37+
1. Make sure the script has the executable bit set
38+
(`chmod +x <script>`).
39+
1. Add a new `Command` implementation under `lib/src`. Give the new
40+
`Command` the same name as the new hook.
41+
1. Add the new `Command` to the `CommandRunner` in `lib/githooks.dart`.
42+
1. Make sure the script from step (1) is passing the new command to the Dart
43+
program.

tools/githooks/bin/main.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
// @dart = 2.12
6+
7+
import 'package:githooks/githooks.dart';
8+
9+
Future<int> main(List<String> args) async {
10+
return run(args);
11+
}

tools/githooks/lib/githooks.dart

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
// @dart = 2.12
6+
7+
import 'dart:io' as io;
8+
9+
import 'package:args/args.dart';
10+
import 'package:args/command_runner.dart';
11+
12+
import 'src/pre_push_command.dart';
13+
14+
/// Runs the githooks
15+
Future<int> run(List<String> args) async {
16+
final CommandRunner<bool> runner = CommandRunner<bool> (
17+
'githooks',
18+
'Githooks implementation for the flutter/engine repo.',
19+
)
20+
..addCommand(PrePushCommand());
21+
22+
// Add top-level arguments.
23+
runner.argParser
24+
..addOption(
25+
'flutter',
26+
abbr: 'f',
27+
help: 'The absolute path to the root of the flutter/engine checkout.',
28+
)
29+
..addFlag(
30+
'verbose',
31+
abbr: 'v',
32+
help: 'Runs with verbose logging',
33+
defaultsTo: false,
34+
);
35+
36+
if (args.isEmpty) {
37+
// The tool was invoked with no arguments. Print usage.
38+
runner.printUsage();
39+
return 1;
40+
}
41+
42+
final ArgResults argResults = runner.parse(args);
43+
final String? argMessage = _checkArgs(argResults);
44+
if (argMessage != null) {
45+
io.stderr.writeln(argMessage);
46+
runner.printUsage();
47+
return 1;
48+
}
49+
50+
final bool commandResult = await runner.runCommand(argResults) ?? false;
51+
return commandResult ? 0 : 1;
52+
}
53+
54+
String? _checkArgs(ArgResults argResults) {
55+
if (argResults.command?.name == 'help') {
56+
return null;
57+
}
58+
59+
if (argResults['help'] as bool) {
60+
return null;
61+
}
62+
63+
if (argResults['flutter'] == null) {
64+
return 'The --flutter option is required';
65+
}
66+
67+
final io.Directory dir = io.Directory(argResults['flutter'] as String);
68+
if (!dir.isAbsolute) {
69+
return 'The --flutter option must be an absolute path';
70+
}
71+
72+
if (!dir.existsSync()) {
73+
return 'The directory specified by the --flutter option must exist';
74+
}
75+
76+
return null;
77+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
// @dart = 2.12
6+
7+
import 'dart:io' as io;
8+
9+
import 'package:args/command_runner.dart';
10+
import 'package:path/path.dart' as path;
11+
12+
/// The command that implements the pre-push githook
13+
class PrePushCommand extends Command<bool> {
14+
@override
15+
final String name = 'pre-push';
16+
17+
@override
18+
final String description = 'Checks to run before a "git push"';
19+
20+
@override
21+
Future<bool> run() async {
22+
final Stopwatch sw = Stopwatch()..start();
23+
final bool verbose = globalResults!['verbose']! as bool;
24+
final String flutterRoot = globalResults!['flutter']! as String;
25+
final List<bool> checkResults = await Future.wait<bool>(<Future<bool>>[
26+
_runLinter(flutterRoot, verbose),
27+
_runFormatter(flutterRoot, verbose),
28+
]);
29+
sw.stop();
30+
io.stdout.writeln('pre-push checks finished in ${sw.elapsed}');
31+
return !checkResults.contains(false);
32+
}
33+
34+
Future<bool> _runLinter(String flutterRoot, bool verbose) async {
35+
if (io.Platform.isWindows) {
36+
return true;
37+
}
38+
return _runCheck(
39+
flutterRoot,
40+
path.join(flutterRoot, 'ci', 'lint.sh'),
41+
<String>[],
42+
'Linting check',
43+
verbose: verbose,
44+
);
45+
}
46+
47+
Future<bool> _runFormatter(String flutterRoot, bool verbose) {
48+
final String ext = io.Platform.isWindows ? '.bat' : '.sh';
49+
return _runCheck(
50+
flutterRoot,
51+
path.join(flutterRoot, 'ci', 'format$ext'),
52+
<String>[],
53+
'Formatting check',
54+
verbose: verbose,
55+
);
56+
}
57+
58+
Future<bool> _runCheck(
59+
String flutterRoot,
60+
String scriptPath,
61+
List<String> scriptArgs,
62+
String checkName, {
63+
bool verbose = false,
64+
}) async {
65+
if (verbose) {
66+
io.stdout.writeln('Starting "$checkName": $scriptPath');
67+
}
68+
final io.ProcessResult result = await io.Process.run(
69+
scriptPath,
70+
scriptArgs,
71+
workingDirectory: flutterRoot,
72+
);
73+
if (result.exitCode != 0) {
74+
final StringBuffer message = StringBuffer();
75+
message.writeln('Check "$checkName" failed.');
76+
message.writeln('command: $scriptPath ${scriptArgs.join(" ")}');
77+
message.writeln('working directory: $flutterRoot');
78+
message.writeln('exit code: ${result.exitCode}');
79+
message.writeln('stdout:');
80+
message.writeln(result.stdout);
81+
message.writeln('stderr:');
82+
message.writeln(result.stderr);
83+
io.stderr.write(message.toString());
84+
return false;
85+
}
86+
if (verbose) {
87+
io.stdout.writeln('Check "$checkName" finished successfully.');
88+
}
89+
return true;
90+
}
91+
}

tools/githooks/pre-push

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/usr/bin/env python3
2+
# Copyright 2013 The Flutter Authors. All rights reserved.
3+
# Use of this source code is governed by a BSD-style license that can be
4+
# found in the LICENSE file.
5+
6+
'''
7+
Runs the pre-push githooks.
8+
'''
9+
10+
import os
11+
import subprocess
12+
import sys
13+
14+
15+
SRC_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
16+
FLUTTER_DIR = os.path.join(SRC_ROOT, 'flutter')
17+
DART_BIN = os.path.join(SRC_ROOT, 'third_party', 'dart', 'tools', 'sdks', 'dart-sdk', 'bin')
18+
19+
20+
def Main(argv):
21+
result = subprocess.run([
22+
os.path.join(DART_BIN, 'dart'),
23+
'--disable-dart-dev',
24+
os.path.join(FLUTTER_DIR, 'tools', 'githooks', 'bin', 'main.dart'),
25+
'--flutter',
26+
FLUTTER_DIR,
27+
'pre-push',
28+
], cwd=SRC_ROOT)
29+
return result.returncode
30+
31+
32+
if __name__ == '__main__':
33+
sys.exit(Main(sys.argv))

tools/githooks/pubspec.yaml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Copyright 2013 The Flutter Authors. All rights reserved.
2+
# Use of this source code is governed by a BSD-style license that can be
3+
# found in the LICENSE file.
4+
5+
name: githooks
6+
publish_to: none
7+
environment:
8+
sdk: '>=2.12.0-0.0.dev <3.0.0'
9+
10+
# Do not add any dependencies that require more than what is provided in
11+
# //third_party.pkg, //third_party/dart/pkg, or
12+
# //third_party/dart/third_party/pkg. In particular, package:test is not usable
13+
# here.
14+
15+
# If you do add packages here, make sure you can run `pub get --offline`, and
16+
# check the .packages and .package_config to make sure all the paths are
17+
# relative to this directory into //third_party/dart
18+
19+
dependencies:
20+
args: any
21+
meta: any
22+
path: any
23+
24+
dev_dependencies:
25+
async_helper: any
26+
expect: any
27+
litetest: any
28+
29+
dependency_overrides:
30+
args:
31+
path: ../../../third_party/dart/third_party/pkg/args
32+
async_helper:
33+
path: ../../../third_party/dart/pkg/async_helper
34+
expect:
35+
path: ../../../third_party/dart/pkg/expect
36+
litetest:
37+
path: ../../testing/litetest
38+
meta:
39+
path: ../../../third_party/dart/pkg/meta
40+
path:
41+
path: ../../../third_party/dart/third_party/pkg/path

0 commit comments

Comments
 (0)