Repository: VeryGoodOpenSource/very_good_cli (dart)

Issue:
CLI - feat: support excluding directories
As a developer, I may have directories which I wish to exclude from 'packages get' and Test when using the recursive flag.

- [ ] Packages Get
- [ ] Test

Pull request description:
<!--
  Thanks for contributing!

  Provide a description of your changes below and a general summary in the title

  Please look at the following checklist to ensure that your PR can be accepted quickly:
-->

## Description

Adds support for excluding packages when running `packages get`.

- `very_good packages get -r --ignore=pluginA,pluginB`

Closes #636 

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [x] ✨ New feature (non-breaking change which adds functionality)
- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change)
- [ ] 🧹 Code refactor
- [ ] ✅ Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore

Patch:
diff --git a/lib/src/cli/cli.dart b/lib/src/cli/cli.dart
index 49ec55b09..da35b7dd5 100644
--- a/lib/src/cli/cli.dart
+++ b/lib/src/cli/cli.dart
@@ -162,8 +162,22 @@ const _ignoredDirectories = {
 };
 
 bool _isPubspec(FileSystemEntity entity) {
-  final segments = p.split(entity.path).toSet();
-  if (segments.intersection(_ignoredDirectories).isNotEmpty) return false;
   if (entity is! File) return false;
   return p.basename(entity.path) == 'pubspec.yaml';
 }
+
+extension on Set<String> {
+  bool excludes(FileSystemEntity entity) {
+    final segments = p.split(entity.path).toSet();
+    if (segments.intersection(_ignoredDirectories).isNotEmpty) return true;
+    if (segments.intersection(this).isNotEmpty) return true;
+
+    for (final value in this) {
+      if (Glob(value).matches(entity.path)) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+}
diff --git a/lib/src/cli/dart_cli.dart b/lib/src/cli/dart_cli.dart
index 89796437e..e79f65d42 100644
--- a/lib/src/cli/dart_cli.dart
+++ b/lib/src/cli/dart_cli.dart
@@ -19,6 +19,7 @@ class Dart {
     required Logger logger,
     String cwd = '.',
     bool recursive = false,
+    Set<String> ignore = const {},
   }) async {
     if (!recursive) {
       final pubspec = File(p.join(cwd, 'pubspec.yaml'));
@@ -40,7 +41,7 @@ class Dart {
         workingDirectory: entity.parent.path,
         logger: logger,
       ),
-      where: _isPubspec,
+      where: (entity) => !ignore.excludes(entity) && _isPubspec(entity),
       cwd: cwd,
     );
 
diff --git a/lib/src/cli/flutter_cli.dart b/lib/src/cli/flutter_cli.dart
index 2097c2409..c4c0667fd 100644
--- a/lib/src/cli/flutter_cli.dart
+++ b/lib/src/cli/flutter_cli.dart
@@ -80,6 +80,7 @@ class Flutter {
     required Logger logger,
     String cwd = '.',
     bool recursive = false,
+    Set<String> ignore = const {},
   }) async {
     await _runCommand(
       cmd: (cwd) async {
@@ -107,6 +108,7 @@ class Flutter {
       },
       cwd: cwd,
       recursive: recursive,
+      ignore: ignore,
     );
   }
 
@@ -115,6 +117,7 @@ class Flutter {
     required Logger logger,
     String cwd = '.',
     bool recursive = false,
+    Set<String> ignore = const {},
   }) async {
     await _runCommand(
       cmd: (cwd) => _Cmd.run(
@@ -125,6 +128,7 @@ class Flutter {
       ),
       cwd: cwd,
       recursive: recursive,
+      ignore: ignore,
     );
   }
 
@@ -136,6 +140,7 @@ class Flutter {
     bool recursive = false,
     bool collectCoverage = false,
     bool optimizePerformance = false,
+    Set<String> ignore = const {},
     double? minCoverage,
     String? excludeFromCoverage,
     String? randomSeed,
@@ -237,6 +242,7 @@ class Flutter {
       },
       cwd: cwd,
       recursive: recursive,
+      ignore: ignore,
     );
   }
 
@@ -287,6 +293,7 @@ Future<List<T>> _runCommand<T>({
   required Future<T> Function(String cwd) cmd,
   required String cwd,
   required bool recursive,
+  required Set<String> ignore,
 }) async {
   if (!recursive) {
     final pubspec = File(p.join(cwd, 'pubspec.yaml'));
@@ -297,7 +304,7 @@ Future<List<T>> _runCommand<T>({
 
   final processes = _Cmd.runWhere<T>(
     run: (entity) => cmd(entity.parent.path),
-    where: _isPubspec,
+    where: (entity) => !ignore.excludes(entity) && _isPubspec(entity),
     cwd: cwd,
   );
 
diff --git a/lib/src/commands/packages.dart b/lib/src/commands/packages.dart
index db6c46d22..ffe43bccf 100644
--- a/lib/src/commands/packages.dart
+++ b/lib/src/commands/packages.dart
@@ -28,12 +28,17 @@ class PackagesCommand extends Command<int> {
 class PackagesGetCommand extends Command<int> {
   /// {@macro packages_get_command}
   PackagesGetCommand({Logger? logger}) : _logger = logger ?? Logger() {
-    argParser.addFlag(
-      'recursive',
-      abbr: 'r',
-      help: 'Install dependencies recursively for all nested packages.',
-      negatable: false,
-    );
+    argParser
+      ..addFlag(
+        'recursive',
+        abbr: 'r',
+        help: 'Install dependencies recursively for all nested packages.',
+        negatable: false,
+      )
+      ..addMultiOption(
+        'ignore',
+        help: 'Exclude packages from installing dependencies.',
+      );
   }
 
   final Logger _logger;
@@ -57,6 +62,7 @@ class PackagesGetCommand extends Command<int> {
     }
 
     final recursive = _argResults['recursive'] as bool;
+    final ignore = (_argResults['ignore'] as List<String>).toSet();
     final target = _argResults.rest.length == 1 ? _argResults.rest[0] : '.';
     final targetPath = path.normalize(Directory(target).absolute.path);
     final isFlutterInstalled = await Flutter.installed(logger: _logger);
@@ -65,6 +71,7 @@ class PackagesGetCommand extends Command<int> {
         await Flutter.packagesGet(
           cwd: targetPath,
           recursive: recursive,
+          ignore: ignore,
           logger: _logger,
         );
       } on PubspecNotFound catch (_) {


Test patch:
diff --git a/test/src/cli/flutter_cli_test.dart b/test/src/cli/flutter_cli_test.dart
index 4b13a2aa6..7eb2c21c8 100644
--- a/test/src/cli/flutter_cli_test.dart
+++ b/test/src/cli/flutter_cli_test.dart
@@ -168,25 +168,61 @@ void main() {
         );
       });
 
-      test('completes when there is a pubspec.yaml (recursive)', () {
-        final directory = Directory.systemTemp.createTempSync();
-        final nestedDirectory = Directory(p.join(directory.path, 'test'))
-          ..createSync();
-        File(p.join(nestedDirectory.path, 'pubspec.yaml'))
-            .writeAsStringSync(_pubspec);
-
-        ProcessOverrides.runZoned(
-          () => expectLater(
-            Flutter.packagesGet(
-              cwd: directory.path,
-              recursive: true,
-              logger: logger,
+      test(
+        'completes when there is a pubspec.yaml and '
+        'directory is ignored (recursive)',
+        () {
+          final directory = Directory.systemTemp.createTempSync();
+          final nestedDirectory = Directory(p.join(directory.path, 'test'))
+            ..createSync();
+          final ignoredDirectory = Directory(
+            p.join(directory.path, 'test_plugin'),
+          )..createSync();
+
+          File(p.join(nestedDirectory.path, 'pubspec.yaml'))
+              .writeAsStringSync(_pubspec);
+          File(p.join(ignoredDirectory.path, 'pubspec.yaml'))
+              .writeAsStringSync(_pubspec);
+
+          ProcessOverrides.runZoned(
+            () => expectLater(
+              Flutter.packagesGet(
+                cwd: directory.path,
+                recursive: true,
+                ignore: {
+                  'test_plugin',
+                  '/**/test_plugin_two/**',
+                },
+                logger: logger,
+              ),
+              completes,
             ),
-            completes,
-          ),
-          runProcess: process.run,
-        );
-      });
+            runProcess: process.run,
+          ).whenComplete(() {
+            verify(() {
+              logger.progress(
+                any(
+                  that: contains(
+                    'Running "flutter packages get" in '
+                    '${nestedDirectory.path}',
+                  ),
+                ),
+              );
+            }).called(1);
+
+            verifyNever(() {
+              logger.progress(
+                any(
+                  that: contains(
+                    'Running "flutter packages get" in '
+                    '${ignoredDirectory.path}',
+                  ),
+                ),
+              );
+            });
+          });
+        },
+      );
     });
 
     group('.pubGet', () {
diff --git a/test/src/commands/packages_test.dart b/test/src/commands/packages_test.dart
index 9db74c149..08d66b193 100644
--- a/test/src/commands/packages_test.dart
+++ b/test/src/commands/packages_test.dart
@@ -34,6 +34,7 @@ const expectedPackagesGetUsage = [
       'Usage: very_good packages get [arguments]\n'
       '-h, --help         Print this usage information.\n'
       '''-r, --recursive    Install dependencies recursively for all nested packages.\n'''
+      '    --ignore       Exclude packages from installing dependencies.\n'
       '\n'
       'Run "very_good help" to see global options.'
 ];
@@ -265,6 +266,77 @@ void main() {
           }).called(2);
         }),
       );
+
+      test(
+        'completes normally '
+        'when pubspec.yaml exists and directory is ignored (recursive)',
+        withRunner((commandRunner, logger, pubUpdater, printLogs) async {
+          final tempDirectory = Directory.systemTemp.createTempSync();
+          final directoryA = Directory(
+            path.join(tempDirectory.path, 'plugin_a'),
+          );
+          final directoryB = Directory(
+            path.join(tempDirectory.path, 'plugin_b'),
+          );
+          final pubspecA = File(
+            path.join(directoryA.path, 'example_a', 'pubspec.yaml'),
+          );
+          final pubspecB = File(
+            path.join(directoryB.path, 'example_b', 'pubspec.yaml'),
+          );
+          pubspecA
+            ..createSync(recursive: true)
+            ..writeAsStringSync(
+              '''
+          name: example_a
+          version: 0.1.0
+          
+          environment:
+            sdk: ">=2.12.0 <3.0.0"
+          ''',
+            );
+          pubspecB
+            ..createSync(recursive: true)
+            ..writeAsStringSync(
+              '''
+          name: example_b
+          version: 0.1.0
+          
+          environment:
+            sdk: ">=2.12.0 <3.0.0"
+          ''',
+            );
+
+          final result = await commandRunner.run(
+            [
+              'packages',
+              'get',
+              '--recursive',
+              '--ignore=plugin_b',
+              tempDirectory.path,
+            ],
+          );
+          expect(result, equals(ExitCode.success.code));
+          verify(() {
+            logger.progress(
+              any(
+                that: contains(
+                  'Running "flutter packages get" in ${directoryA.path}',
+                ),
+              ),
+            );
+          }).called(1);
+          verifyNever(() {
+            logger.progress(
+              any(
+                that: contains(
+                  'Running "flutter packages get" in ${directoryB.path}',
+                ),
+              ),
+            );
+          });
+        }),
+      );
     });
   });
 }