@@ -7,8 +7,12 @@ import 'dart:io' as io;
77import 'dart:math' ;
88
99import 'package:args/command_runner.dart' ;
10+ import 'package:colorize/colorize.dart' ;
1011import 'package:file/file.dart' ;
12+ import 'package:git/git.dart' ;
13+ import 'package:meta/meta.dart' ;
1114import 'package:path/path.dart' as p;
15+ import 'package:pub_semver/pub_semver.dart' ;
1216import 'package:yaml/yaml.dart' ;
1317
1418typedef void Print (Object object);
@@ -140,6 +144,13 @@ bool isLinuxPlugin(FileSystemEntity entity, FileSystem fileSystem) {
140144 return pluginSupportsPlatform (kLinux, entity, fileSystem);
141145}
142146
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+
143154/// Error thrown when a command needs to exit with a non-zero exit code.
144155class ToolExit extends Error {
145156 ToolExit (this .exitCode);
@@ -152,6 +163,7 @@ abstract class PluginCommand extends Command<Null> {
152163 this .packagesDir,
153164 this .fileSystem, {
154165 this .processRunner = const ProcessRunner (),
166+ this .gitDir,
155167 }) {
156168 argParser.addMultiOption (
157169 _pluginsArg,
@@ -179,12 +191,23 @@ abstract class PluginCommand extends Command<Null> {
179191 help: 'Exclude packages from this command.' ,
180192 defaultsTo: < String > [],
181193 );
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.' );
182203 }
183204
184205 static const String _pluginsArg = 'plugins' ;
185206 static const String _shardIndexArg = 'shardIndex' ;
186207 static const String _shardCountArg = 'shardCount' ;
187208 static const String _excludeArg = 'exclude' ;
209+ static const String _runOnChangedPackagesArg = 'run-on-changed-packages' ;
210+ static const String _kBaseSha = 'base-sha' ;
188211
189212 /// The directory containing the plugin packages.
190213 final Directory packagesDir;
@@ -199,6 +222,11 @@ abstract class PluginCommand extends Command<Null> {
199222 /// This can be overridden for testing.
200223 final ProcessRunner processRunner;
201224
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+
202230 int _shardIndex;
203231 int _shardCount;
204232
@@ -273,9 +301,13 @@ abstract class PluginCommand extends Command<Null> {
273301 /// "client library" package, which declares the API for the plugin, as
274302 /// well as one or more platform-specific implementations.
275303 Stream <Directory > _getAllPlugins () async * {
276- final Set <String > plugins = Set <String >.from (argResults[_pluginsArg]);
304+ Set <String > plugins = Set <String >.from (argResults[_pluginsArg]);
277305 final Set <String > excludedPlugins =
278306 Set <String >.from (argResults[_excludeArg]);
307+ final bool runOnChangedPackages = argResults[_runOnChangedPackagesArg];
308+ if (plugins.isEmpty && runOnChangedPackages) {
309+ plugins = await _getChangedPackages ();
310+ }
279311
280312 await for (FileSystemEntity entity
281313 in packagesDir.list (followLinks: false )) {
@@ -363,6 +395,50 @@ abstract class PluginCommand extends Command<Null> {
363395 (FileSystemEntity entity) => isFlutterPackage (entity, fileSystem))
364396 .cast <Directory >();
365397 }
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+ }
366442}
367443
368444/// A class used to run processes.
@@ -466,3 +542,68 @@ class ProcessRunner {
466542 return 'ERROR: Unable to execute "$executable ${args .join (' ' )}"$workdir .' ;
467543 }
468544}
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