Avoid failing in gitignore validator (#3354)

diff --git a/lib/src/git.dart b/lib/src/git.dart
index dc45edc..ded45e9 100644
--- a/lib/src/git.dart
+++ b/lib/src/git.dart
@@ -5,6 +5,7 @@
 /// Helper functionality for invoking Git.
 import 'dart:async';
 
+import 'package:collection/collection.dart';
 import 'package:path/path.dart' as p;
 import 'package:pub_semver/pub_semver.dart';
 
@@ -88,24 +89,9 @@
   return result.stdout;
 }
 
-/// Returns the name of the git command-line app, or `null` if Git could not be
-/// found on the user's PATH.
-String? get command {
-  if (_commandCache != null) return _commandCache;
-
-  if (_tryGitCommand('git')) {
-    _commandCache = 'git';
-  } else if (_tryGitCommand('git.cmd')) {
-    _commandCache = 'git.cmd';
-  } else {
-    return null;
-  }
-
-  log.fine('Determined git command $_commandCache.');
-  return _commandCache;
-}
-
-String? _commandCache;
+/// The name of the git command-line app, or `null` if Git could not be found on
+/// the user's PATH.
+final String? command = ['git', 'git.cmd'].firstWhereOrNull(_tryGitCommand);
 
 /// Returns the root of the git repo [dir] belongs to. Returns `null` if not
 /// in a git repo or git is not installed.
@@ -150,6 +136,7 @@
 for $topLevelProgram it is recommended to use git version 2.14 or newer.
 ''');
     }
+    log.fine('Determined git command $command.');
     return true;
   } on RunProcessException catch (err) {
     // If the process failed, they probably don't have it.
diff --git a/lib/src/validator/gitignore.dart b/lib/src/validator/gitignore.dart
index 4fb49b1..532178b 100644
--- a/lib/src/validator/gitignore.dart
+++ b/lib/src/validator/gitignore.dart
@@ -11,6 +11,7 @@
 import '../git.dart' as git;
 import '../ignore.dart';
 import '../io.dart';
+import '../log.dart' as log;
 import '../utils.dart';
 import '../validator.dart';
 
@@ -23,12 +24,21 @@
   @override
   Future<void> validate() async {
     if (entrypoint.root.inGitRepo) {
-      final checkedIntoGit = git.runSync([
-        'ls-files',
-        '--cached',
-        '--exclude-standard',
-        '--recurse-submodules'
-      ], workingDir: entrypoint.root.dir);
+      late final List<String> checkedIntoGit;
+      try {
+        checkedIntoGit = git.runSync([
+          'ls-files',
+          '--cached',
+          '--exclude-standard',
+          '--recurse-submodules'
+        ], workingDir: entrypoint.root.dir);
+      } on git.GitException catch (e) {
+        log.fine('Could not run `git ls-files` files in repo (${e.message}).');
+        // This validation is only a warning.
+        // If git is not supported on the platform, or too old to support
+        // --recurse-submodules we just continue silently.
+        return;
+      }
       final root = git.repoRoot(entrypoint.root.dir) ?? entrypoint.root.dir;
       var beneath = p.posix.joinAll(
           p.split(p.normalize(p.relative(entrypoint.root.dir, from: root))));
diff --git a/test/get/git/git_not_installed_test.dart b/test/get/git/git_not_installed_test.dart
index e146f0e..e590770 100644
--- a/test/get/git/git_not_installed_test.dart
+++ b/test/get/git/git_not_installed_test.dart
@@ -5,42 +5,11 @@
 @TestOn('linux')
 import 'dart:io';
 
-import 'package:path/path.dart' as p;
-import 'package:pub/src/io.dart' show runProcess;
 import 'package:test/test.dart';
-import 'package:test_descriptor/test_descriptor.dart' show sandbox;
 
 import '../../descriptor.dart' as d;
 import '../../test_pub.dart';
 
-/// Create temporary folder 'bin/' containing a 'git' script in [sandbox]
-/// By adding the bin/ folder to the search `$PATH` we can prevent `pub` from
-/// detecting the installed 'git' binary and we can test that it prints
-/// a useful error message.
-Future<void> setUpFakeGitScript(
-    {required String bash, required String batch}) async {
-  await d.dir('bin', [
-    if (!Platform.isWindows) d.file('git', bash),
-    if (Platform.isWindows) d.file('git.bat', batch),
-  ]).create();
-  if (!Platform.isWindows) {
-    // Make the script executable.
-
-    await runProcess('chmod', ['+x', p.join(sandbox, 'bin', 'git')]);
-  }
-}
-
-/// Returns an environment where PATH is extended with `$sandbox/bin`.
-Map<String, String> extendedPathEnv() {
-  final separator = Platform.isWindows ? ';' : ':';
-  final binFolder = p.join(sandbox, 'bin');
-
-  return {
-    // Override 'PATH' to ensure that we can't detect a working "git" binary
-    'PATH': '$binFolder$separator${Platform.environment['PATH']}',
-  };
-}
-
 void main() {
   test('reports failure if Git is not installed', () async {
     await setUpFakeGitScript(bash: '''
diff --git a/test/test_pub.dart b/test/test_pub.dart
index fcba6a6..6968f4e 100644
--- a/test/test_pub.dart
+++ b/test/test_pub.dart
@@ -980,3 +980,30 @@
   });
   return server;
 }
+
+/// Create temporary folder 'bin/' containing a 'git' script in [sandbox]
+/// By adding the bin/ folder to the search `$PATH` we can prevent `pub` from
+/// detecting the installed 'git' binary and we can test that it prints
+/// a useful error message.
+Future<void> setUpFakeGitScript(
+    {required String bash, required String batch}) async {
+  await d.dir('bin', [
+    if (!Platform.isWindows) d.file('git', bash),
+    if (Platform.isWindows) d.file('git.bat', batch),
+  ]).create();
+  if (!Platform.isWindows) {
+    // Make the script executable.
+    await runProcess('chmod', ['+x', p.join(d.sandbox, 'bin', 'git')]);
+  }
+}
+
+/// Returns an environment where PATH is extended with `$sandbox/bin`.
+Map<String, String> extendedPathEnv() {
+  final separator = Platform.isWindows ? ';' : ':';
+  final binFolder = p.join(d.sandbox, 'bin');
+
+  return {
+    // Override 'PATH' to ensure that we can't detect a working "git" binary
+    'PATH': '$binFolder$separator${Platform.environment['PATH']}',
+  };
+}
diff --git a/test/validator/gitignore_test.dart b/test/validator/gitignore_test.dart
index f7b02fe..3cd4a11 100644
--- a/test/validator/gitignore_test.dart
+++ b/test/validator/gitignore_test.dart
@@ -14,12 +14,13 @@
 Future<void> expectValidation(
   error,
   int exitCode, {
+  Map<String, String> environment = const {},
   String? workingDirectory,
 }) async {
   await runPub(
     error: error,
     args: ['publish', '--dry-run'],
-    environment: {'_PUB_TEST_SDK_VERSION': '2.12.0'},
+    environment: {'_PUB_TEST_SDK_VERSION': '2.12.0', ...environment},
     workingDirectory: workingDirectory ?? d.path(appPath),
     exitCode: exitCode,
   );
@@ -52,6 +53,20 @@
         exit_codes.DATA);
   });
 
+  test('should not fail on missing git', () async {
+    await d.git('myapp', [
+      ...d.validPackage.contents,
+      d.file('.gitignore', '*.txt'),
+      d.file('foo.txt'),
+    ]).create();
+
+    await pubGet(environment: {'_PUB_TEST_SDK_VERSION': '1.12.0'});
+    await setUpFakeGitScript(bash: 'echo "Not git"', batch: 'echo "Not git"');
+    await expectValidation(
+        allOf([contains('Package has 0 warnings.')]), exit_codes.SUCCESS,
+        environment: extendedPathEnv());
+  });
+
   test('Should also consider gitignores from above the package root', () async {
     await d.git('reporoot', [
       d.dir(