77
88import 'dart:async' show Completer;
99import 'dart:convert' show jsonDecode, utf8, LineSplitter;
10- import 'dart:io'
11- show
12- File,
13- Process,
14- ProcessResult,
15- ProcessException,
16- exit,
17- Directory,
18- FileSystemEntity,
19- Platform,
20- stderr,
21- stdout;
10+ import 'dart:io' show File, exit, Directory, FileSystemEntity, Platform, stderr;
2211
2312import 'package:args/args.dart' ;
2413import 'package:path/path.dart' as path;
25-
26- Platform defaultPlatform = Platform ();
27-
28- /// Exception class for when a process fails to run, so we can catch
29- /// it and provide something more readable than a stack trace.
30- class ProcessRunnerException implements Exception {
31- ProcessRunnerException (this .message, {this .result});
32-
33- final String message;
34- final ProcessResult result;
35-
36- int get exitCode => result? .exitCode ?? - 1 ;
37-
38- @override
39- String toString () {
40- String output = runtimeType.toString ();
41- output += ': $message ' ;
42- final String stderr = (result? .stderr ?? '' ) as String ;
43- if (stderr.isNotEmpty) {
44- output += ':\n $stderr ' ;
45- }
46- return output;
47- }
48- }
49-
50- class ProcessRunnerResult {
51- const ProcessRunnerResult (this .exitCode, this .stdout, this .stderr, this .output);
52- final int exitCode;
53- final List <int > stdout;
54- final List <int > stderr;
55- final List <int > output;
56- }
57-
58- /// A helper class for classes that want to run a process, optionally have the
59- /// stderr and stdout reported as the process runs, and capture the stdout
60- /// properly without dropping any.
61- class ProcessRunner {
62- ProcessRunner ({
63- this .defaultWorkingDirectory,
64- });
65-
66- /// Sets the default directory used when `workingDirectory` is not specified
67- /// to [runProcess] .
68- final Directory defaultWorkingDirectory;
69-
70- /// The environment to run processes with.
71- Map <String , String > environment = Map <String , String >.from (Platform .environment);
72-
73- /// Run the command and arguments in `commandLine` as a sub-process from
74- /// `workingDirectory` if set, or the [defaultWorkingDirectory] if not. Uses
75- /// [Directory.current] if [defaultWorkingDirectory] is not set.
76- ///
77- /// Set `failOk` if [runProcess] should not throw an exception when the
78- /// command completes with a a non-zero exit code.
79- Future <ProcessRunnerResult > runProcess (
80- List <String > commandLine, {
81- Directory workingDirectory,
82- bool printOutput = false ,
83- bool failOk = false ,
84- Stream <List <int >> stdin,
85- }) async {
86- workingDirectory ?? = defaultWorkingDirectory ?? Directory .current;
87- if (printOutput) {
88- stderr.write ('Running "${commandLine .join (' ' )}" in ${workingDirectory .path }.\n ' );
89- }
90- final List <int > stdoutOutput = < int > [];
91- final List <int > stderrOutput = < int > [];
92- final List <int > combinedOutput = < int > [];
93- final Completer <void > stdoutComplete = Completer <void >();
94- final Completer <void > stderrComplete = Completer <void >();
95- final Completer <void > stdinComplete = Completer <void >();
96-
97- Process process;
98- Future <int > allComplete () async {
99- if (stdin != null ) {
100- await stdinComplete.future;
101- await process? .stdin? .close ();
102- }
103- await stderrComplete.future;
104- await stdoutComplete.future;
105- return process? .exitCode ?? Future <int >.value (0 );
106- }
107-
108- try {
109- process = await Process .start (
110- commandLine.first,
111- commandLine.sublist (1 ),
112- workingDirectory: workingDirectory.absolute.path,
113- environment: environment,
114- runInShell: false ,
115- );
116- if (stdin != null ) {
117- stdin.listen ((List <int > data) {
118- process? .stdin? .add (data);
119- }, onDone: () async => stdinComplete.complete ());
120- }
121- process.stdout.listen (
122- (List <int > event) {
123- stdoutOutput.addAll (event);
124- combinedOutput.addAll (event);
125- if (printOutput) {
126- stdout.add (event);
127- }
128- },
129- onDone: () async => stdoutComplete.complete (),
130- );
131- process.stderr.listen (
132- (List <int > event) {
133- stderrOutput.addAll (event);
134- combinedOutput.addAll (event);
135- if (printOutput) {
136- stderr.add (event);
137- }
138- },
139- onDone: () async => stderrComplete.complete (),
140- );
141- } on ProcessException catch (e) {
142- final String message = 'Running "${commandLine .join (' ' )}" in ${workingDirectory .path } '
143- 'failed with:\n ${e .toString ()}' ;
144- throw ProcessRunnerException (message);
145- } on ArgumentError catch (e) {
146- final String message = 'Running "${commandLine .join (' ' )}" in ${workingDirectory .path } '
147- 'failed with:\n ${e .toString ()}' ;
148- throw ProcessRunnerException (message);
149- }
150-
151- final int exitCode = await allComplete ();
152- if (exitCode != 0 && ! failOk) {
153- final String message =
154- 'Running "${commandLine .join (' ' )}" in ${workingDirectory .path } failed' ;
155- throw ProcessRunnerException (
156- message,
157- result: ProcessResult (
158- 0 , exitCode, null , 'exited with code $exitCode \n ${utf8 .decode (combinedOutput )}' ),
159- );
160- }
161- return ProcessRunnerResult (exitCode, stdoutOutput, stderrOutput, combinedOutput);
162- }
163- }
164-
165- class WorkerJob {
166- WorkerJob (
167- this .name,
168- this .args, {
169- this .workingDirectory,
170- this .printOutput = false ,
171- });
172-
173- /// The name of the job.
174- final String name;
175-
176- /// The arguments for the process, including the command name as args[0] .
177- final List <String > args;
178-
179- /// The working directory that the command should be executed in.
180- final Directory workingDirectory;
181-
182- /// Whether or not this command should print it's stdout when it runs.
183- final bool printOutput;
184-
185- @override
186- String toString () {
187- return args.join (' ' );
188- }
189- }
190-
191- /// A pool of worker processes that will keep [numWorkers] busy until all of the
192- /// (presumably single-threaded) processes are finished.
193- class ProcessPool {
194- ProcessPool ({int numWorkers}) : numWorkers = numWorkers ?? Platform .numberOfProcessors;
195-
196- ProcessRunner processRunner = ProcessRunner ();
197- int numWorkers;
198- List <WorkerJob > pendingJobs = < WorkerJob > [];
199- List <WorkerJob > failedJobs = < WorkerJob > [];
200- Map <WorkerJob , Future <List <int >>> inProgressJobs = < WorkerJob , Future <List <int >>> {};
201- Map <WorkerJob , ProcessRunnerResult > completedJobs = < WorkerJob , ProcessRunnerResult > {};
202- Completer <Map <WorkerJob , ProcessRunnerResult >> completer =
203- Completer <Map <WorkerJob , ProcessRunnerResult >>();
204-
205- void _printReport () {
206- final int totalJobs = completedJobs.length + inProgressJobs.length + pendingJobs.length;
207- final String percent =
208- totalJobs == 0 ? '100' : ((100 * completedJobs.length) ~ / totalJobs).toString ().padLeft (3 );
209- final String completed = completedJobs.length.toString ().padLeft (3 );
210- final String total = totalJobs.toString ().padRight (3 );
211- final String inProgress = inProgressJobs.length.toString ().padLeft (2 );
212- final String pending = pendingJobs.length.toString ().padLeft (3 );
213- stdout.write ('Jobs: $percent % done, $completed /$total completed, $inProgress in '
214- 'progress, $pending pending. \r ' );
215- }
216-
217- Future <List <int >> _scheduleJob (WorkerJob job) async {
218- final Completer <List <int >> jobDone = Completer <List <int >>();
219- final List <int > output = < int > [];
220- try {
221- completedJobs[job] = await processRunner.runProcess (
222- job.args,
223- workingDirectory: job.workingDirectory,
224- printOutput: job.printOutput,
225- );
226- } catch (e) {
227- failedJobs.add (job);
228- if (e is ProcessRunnerException ) {
229- print (e.toString ());
230- print ('${utf8 .decode (output )}' );
231- } else {
232- print ('\n Job $job failed: $e ' );
233- }
234- } finally {
235- inProgressJobs.remove (job);
236- if (pendingJobs.isNotEmpty) {
237- final WorkerJob newJob = pendingJobs.removeAt (0 );
238- inProgressJobs[newJob] = _scheduleJob (newJob);
239- } else {
240- if (inProgressJobs.isEmpty) {
241- completer.complete (completedJobs);
242- }
243- }
244- jobDone.complete (output);
245- _printReport ();
246- }
247- return jobDone.future;
248- }
249-
250- Future <Map <WorkerJob , ProcessRunnerResult >> startWorkers (List <WorkerJob > jobs) async {
251- assert (inProgressJobs.isEmpty);
252- assert (failedJobs.isEmpty);
253- assert (completedJobs.isEmpty);
254- if (jobs.isEmpty) {
255- return < WorkerJob , ProcessRunnerResult > {};
256- }
257- pendingJobs = jobs;
258- for (int i = 0 ; i < numWorkers; ++ i) {
259- if (pendingJobs.isEmpty) {
260- break ;
261- }
262- final WorkerJob job = pendingJobs.removeAt (0 );
263- inProgressJobs[job] = _scheduleJob (job);
264- }
265- return completer.future.then ((Map <WorkerJob , ProcessRunnerResult > result) {
266- stdout.flush ();
267- return result;
268- });
269- }
270- }
14+ import 'package:process_runner/process_runner.dart' ;
27115
27216String _linterOutputHeader = '''
27317┌──────────────────────────┐
@@ -313,20 +57,33 @@ bool containsAny(File file, Iterable<File> queries) {
31357 return queries.where ((File query) => path.equals (query.path, file.path)).isNotEmpty;
31458}
31559
316- /// Returns a list of all files with current changes or differ from `master` .
60+ /// Returns a list of all non-deleted files which differ from the nearest
61+ /// merge-base with `master` . If it can't find a fork point, uses the default
62+ /// merge-base.
31763Future <List <File >> getListOfChangedFiles (Directory repoPath) async {
31864 final ProcessRunner processRunner = ProcessRunner (defaultWorkingDirectory: repoPath);
319- String branch = 'upstream/master' ;
320- final ProcessResult fetchResult = Process .runSync ('git' , < String > ['fetch' , 'upstream' , 'master' ]);
65+ final ProcessRunnerResult fetchResult = await processRunner.runProcess (
66+ < String > ['git' , 'fetch' , 'upstream' , 'master' ],
67+ failOk: true ,
68+ );
32169 if (fetchResult.exitCode != 0 ) {
322- branch = 'origin/master' ;
323- Process .runSync ('git' , < String > ['fetch' , 'origin' , 'master' ]);
70+ await processRunner.runProcess (< String > ['git' , 'fetch' , 'origin' , 'master' ]);
32471 }
32572 final Set <String > result = < String > {};
326- final ProcessRunnerResult diffResult = await processRunner.runProcess (
327- < String > ['git' , 'diff' , '--name-only' , '--diff-filter=ACMRT' , if (branch.isNotEmpty) branch]);
328-
329- result.addAll (utf8.decode (diffResult.stdout).split ('\n ' ).where (isNonEmptyString));
73+ ProcessRunnerResult mergeBaseResult = await processRunner.runProcess (
74+ < String > ['git' , 'merge-base' , '--fork-point' , 'FETCH_HEAD' , 'HEAD' ],
75+ failOk: true );
76+ if (mergeBaseResult.exitCode != 0 ) {
77+ if (verbose) {
78+ stderr.writeln ("Didn't find a fork point, falling back to default merge base." );
79+ }
80+ mergeBaseResult = await processRunner
81+ .runProcess (< String > ['git' , 'merge-base' , 'FETCH_HEAD' , 'HEAD' ], failOk: false );
82+ }
83+ final String mergeBase = mergeBaseResult.stdout.trim ();
84+ final ProcessRunnerResult masterResult = await processRunner
85+ .runProcess (< String > ['git' , 'diff' , '--name-only' , '--diff-filter=ACMRT' , mergeBase]);
86+ result.addAll (masterResult.stdout.split ('\n ' ).where (isNonEmptyString));
33087 return result.map <File >((String filePath) => File (path.join (repoPath.path, filePath))).toList ();
33188}
33289
@@ -409,6 +166,8 @@ void main(List<String> arguments) async {
409166 _usage (parser);
410167 }
411168
169+ print (_linterOutputHeader);
170+
412171 final String checksArg = options.wasParsed ('checks' ) ? options['checks' ] as String : '' ;
413172 final String checks = checksArg.isNotEmpty ? '--checks=$checksArg ' : '--config=' ;
414173 final bool lintAll =
@@ -439,12 +198,17 @@ void main(List<String> arguments) async {
439198 final List <Command > changedFileBuildCommands =
440199 buildCommands.where ((Command x) => containsAny (x.file, changedFiles)).toList ();
441200
201+ if (changedFileBuildCommands.isEmpty) {
202+ print ('No changed files that have build commands associated with them '
203+ 'were found.' );
204+ exit (0 );
205+ }
206+
442207 if (verbose) {
443208 print ('Found ${changedFileBuildCommands .length } files that have build '
444209 'commands associated with them and can be lint checked.' );
445210 }
446211
447- print (_linterOutputHeader);
448212 int exitCode = 0 ;
449213 final List <WorkerJob > jobs = < WorkerJob > [];
450214 for (Command command in changedFileBuildCommands) {
@@ -453,23 +217,23 @@ void main(List<String> arguments) async {
453217 final List <String > args = < String > [command.file.path, checks, '--' ];
454218 args.addAll (tidyArgs? .split (' ' ) ?? < String > []);
455219 print ('🔶 linting ${command .file }' );
456- jobs.add (WorkerJob (command.file.path, < String > [tidyPath, ...args],
457- workingDirectory: command.directory));
220+ jobs.add (WorkerJob (< String > [tidyPath, ...args],
221+ workingDirectory: command.directory, name : 'clang-tidy on ${ command . file . path }' ));
458222 } else {
459223 print ('🔷 ignoring ${command .file }' );
460224 }
461225 }
462226 final ProcessPool pool = ProcessPool ();
463- final Map <WorkerJob , ProcessRunnerResult > results = await pool.startWorkers (jobs);
464- print ('\n ' );
465- for (final WorkerJob job in results.keys) {
466- if (results[job].stdout.isEmpty) {
227+
228+ await for (final WorkerJob job in pool.startWorkers (jobs)) {
229+ if (job.result.stdout.isEmpty) {
467230 continue ;
468231 }
469232 print ('❌ Failures for ${job .name }:' );
470- print (utf8. decode (results[ job]. stdout) );
233+ print (job.result. stdout);
471234 exitCode = 1 ;
472235 }
236+ print ('\n ' );
473237 if (exitCode == 0 ) {
474238 print ('No lint problems found.' );
475239 }
0 commit comments