@@ -7,8 +7,12 @@ import 'dart:io' as io;
7
7
import 'dart:math' ;
8
8
9
9
import 'package:args/command_runner.dart' ;
10
+ import 'package:colorize/colorize.dart' ;
10
11
import 'package:file/file.dart' ;
12
+ import 'package:git/git.dart' ;
13
+ import 'package:meta/meta.dart' ;
11
14
import 'package:path/path.dart' as p;
15
+ import 'package:pub_semver/pub_semver.dart' ;
12
16
import 'package:yaml/yaml.dart' ;
13
17
14
18
typedef void Print (Object object);
@@ -140,6 +144,13 @@ bool isLinuxPlugin(FileSystemEntity entity, FileSystem fileSystem) {
140
144
return pluginSupportsPlatform (kLinux, entity, fileSystem);
141
145
}
142
146
147
+ /// Throws a [ToolExit] with `exitCode` and log the `errorMessage` in red.
148
+ void printErrorAndExit ({@required String errorMessage, int exitCode = 1 }) {
149
+ final Colorize redError = Colorize (errorMessage)..red ();
150
+ print (redError);
151
+ throw ToolExit (exitCode);
152
+ }
153
+
143
154
/// Error thrown when a command needs to exit with a non-zero exit code.
144
155
class ToolExit extends Error {
145
156
ToolExit (this .exitCode);
@@ -152,6 +163,7 @@ abstract class PluginCommand extends Command<Null> {
152
163
this .packagesDir,
153
164
this .fileSystem, {
154
165
this .processRunner = const ProcessRunner (),
166
+ this .gitDir,
155
167
}) {
156
168
argParser.addMultiOption (
157
169
_pluginsArg,
@@ -179,12 +191,23 @@ abstract class PluginCommand extends Command<Null> {
179
191
help: 'Exclude packages from this command.' ,
180
192
defaultsTo: < String > [],
181
193
);
194
+ argParser.addFlag (_runOnChangedPackagesArg,
195
+ help: 'Run the command on changed packages/plugins.\n '
196
+ 'If the $_pluginsArg is specified, this flag is ignored.\n '
197
+ 'The packages excluded with $_excludeArg is also excluded even if changed.\n '
198
+ 'See $_kBaseSha if a custom base is needed to determine the diff.' );
199
+ argParser.addOption (_kBaseSha,
200
+ help: 'The base sha used to determine git diff. \n '
201
+ 'This is useful when $_runOnChangedPackagesArg is specified.\n '
202
+ 'If not specified, merge-base is used as base sha.' );
182
203
}
183
204
184
205
static const String _pluginsArg = 'plugins' ;
185
206
static const String _shardIndexArg = 'shardIndex' ;
186
207
static const String _shardCountArg = 'shardCount' ;
187
208
static const String _excludeArg = 'exclude' ;
209
+ static const String _runOnChangedPackagesArg = 'run-on-changed-packages' ;
210
+ static const String _kBaseSha = 'base-sha' ;
188
211
189
212
/// The directory containing the plugin packages.
190
213
final Directory packagesDir;
@@ -199,6 +222,11 @@ abstract class PluginCommand extends Command<Null> {
199
222
/// This can be overridden for testing.
200
223
final ProcessRunner processRunner;
201
224
225
+ /// The git directory to use. By default it uses the parent directory.
226
+ ///
227
+ /// This can be mocked for testing.
228
+ final GitDir gitDir;
229
+
202
230
int _shardIndex;
203
231
int _shardCount;
204
232
@@ -273,9 +301,13 @@ abstract class PluginCommand extends Command<Null> {
273
301
/// "client library" package, which declares the API for the plugin, as
274
302
/// well as one or more platform-specific implementations.
275
303
Stream <Directory > _getAllPlugins () async * {
276
- final Set <String > plugins = Set <String >.from (argResults[_pluginsArg]);
304
+ Set <String > plugins = Set <String >.from (argResults[_pluginsArg]);
277
305
final Set <String > excludedPlugins =
278
306
Set <String >.from (argResults[_excludeArg]);
307
+ final bool runOnChangedPackages = argResults[_runOnChangedPackagesArg];
308
+ if (plugins.isEmpty && runOnChangedPackages) {
309
+ plugins = await _getChangedPackages ();
310
+ }
279
311
280
312
await for (FileSystemEntity entity
281
313
in packagesDir.list (followLinks: false )) {
@@ -363,6 +395,50 @@ abstract class PluginCommand extends Command<Null> {
363
395
(FileSystemEntity entity) => isFlutterPackage (entity, fileSystem))
364
396
.cast <Directory >();
365
397
}
398
+
399
+ /// Retrieve an instance of [GitVersionFinder] based on `_kBaseSha` and [gitDir] .
400
+ ///
401
+ /// Throws tool exit if [gitDir] nor root directory is a git directory.
402
+ Future <GitVersionFinder > retrieveVersionFinder () async {
403
+ final String rootDir = packagesDir.parent.absolute.path;
404
+ String baseSha = argResults[_kBaseSha];
405
+
406
+ GitDir baseGitDir = gitDir;
407
+ if (baseGitDir == null ) {
408
+ if (! await GitDir .isGitDir (rootDir)) {
409
+ printErrorAndExit (
410
+ errorMessage: '$rootDir is not a valid Git repository.' ,
411
+ exitCode: 2 );
412
+ }
413
+ baseGitDir = await GitDir .fromExisting (rootDir);
414
+ }
415
+
416
+ final GitVersionFinder gitVersionFinder =
417
+ GitVersionFinder (baseGitDir, baseSha);
418
+ return gitVersionFinder;
419
+ }
420
+
421
+ Future <Set <String >> _getChangedPackages () async {
422
+ final GitVersionFinder gitVersionFinder = await retrieveVersionFinder ();
423
+
424
+ final List <String > allChangedFiles =
425
+ await gitVersionFinder.getChangedFiles ();
426
+ final Set <String > packages = < String > {};
427
+ allChangedFiles.forEach ((String path) {
428
+ final List <String > pathComponents = path.split ('/' );
429
+ final int packagesIndex =
430
+ pathComponents.indexWhere ((String element) => element == 'packages' );
431
+ if (packagesIndex != - 1 ) {
432
+ packages.add (pathComponents[packagesIndex + 1 ]);
433
+ }
434
+ });
435
+ if (packages.isNotEmpty) {
436
+ final String changedPackages = packages.join (',' );
437
+ print (changedPackages);
438
+ }
439
+ print ('No changed packages.' );
440
+ return packages;
441
+ }
366
442
}
367
443
368
444
/// A class used to run processes.
@@ -466,3 +542,68 @@ class ProcessRunner {
466
542
return 'ERROR: Unable to execute "$executable ${args .join (' ' )}"$workdir .' ;
467
543
}
468
544
}
545
+
546
+ /// Finding diffs based on `baseGitDir` and `baseSha` .
547
+ class GitVersionFinder {
548
+ /// Constructor
549
+ GitVersionFinder (this .baseGitDir, this .baseSha);
550
+
551
+ /// The top level directory of the git repo.
552
+ ///
553
+ /// That is where the .git/ folder exists.
554
+ final GitDir baseGitDir;
555
+
556
+ /// The base sha used to get diff.
557
+ final String baseSha;
558
+
559
+ static bool _isPubspec (String file) {
560
+ return file.trim ().endsWith ('pubspec.yaml' );
561
+ }
562
+
563
+ /// Get a list of all the pubspec.yaml file that is changed.
564
+ Future <List <String >> getChangedPubSpecs () async {
565
+ return (await getChangedFiles ()).where (_isPubspec).toList ();
566
+ }
567
+
568
+ /// Get a list of all the changed files.
569
+ Future <List <String >> getChangedFiles () async {
570
+ final String baseSha = await _getBaseSha ();
571
+ final io.ProcessResult changedFilesCommand = await baseGitDir
572
+ .runCommand (< String > ['diff' , '--name-only' , '$baseSha ' , 'HEAD' ]);
573
+ print ('Determine diff with base sha: $baseSha ' );
574
+ final String changedFilesStdout = changedFilesCommand.stdout.toString () ?? '' ;
575
+ if (changedFilesStdout.isEmpty) {
576
+ return < String > [];
577
+ }
578
+ final List <String > changedFiles = changedFilesStdout
579
+ .split ('\n ' )
580
+ ..removeWhere ((element) => element.isEmpty);
581
+ return changedFiles.toList ();
582
+ }
583
+
584
+ /// Get the package version specified in the pubspec file in `pubspecPath` and at the revision of `gitRef` .
585
+ Future <Version > getPackageVersion (String pubspecPath, String gitRef) async {
586
+ final io.ProcessResult gitShow =
587
+ await baseGitDir.runCommand (< String > ['show' , '$gitRef :$pubspecPath ' ]);
588
+ final String fileContent = gitShow.stdout;
589
+ final String versionString = loadYaml (fileContent)['version' ];
590
+ return versionString == null ? null : Version .parse (versionString);
591
+ }
592
+
593
+ Future <String > _getBaseSha () async {
594
+ if (baseSha != null && baseSha.isNotEmpty) {
595
+ return baseSha;
596
+ }
597
+
598
+ io.ProcessResult baseShaFromMergeBase = await baseGitDir.runCommand (
599
+ < String > ['merge-base' , '--fork-point' , 'FETCH_HEAD' , 'HEAD' ],
600
+ throwOnError: false );
601
+ if (baseShaFromMergeBase == null ||
602
+ baseShaFromMergeBase.stderr != null ||
603
+ baseShaFromMergeBase.stdout == null ) {
604
+ baseShaFromMergeBase = await baseGitDir
605
+ .runCommand (< String > ['merge-base' , 'FETCH_HEAD' , 'HEAD' ]);
606
+ }
607
+ return (baseShaFromMergeBase.stdout as String ).trim ();
608
+ }
609
+ }
0 commit comments