Warn if git version is not high enough for supporting all features (#3332)

diff --git a/lib/src/git.dart b/lib/src/git.dart
index 5fa7536..dc45edc 100644
--- a/lib/src/git.dart
+++ b/lib/src/git.dart
@@ -6,7 +6,9 @@
 import 'dart:async';
 
 import 'package:path/path.dart' as p;
+import 'package:pub_semver/pub_semver.dart';
 
+import 'command_runner.dart';
 import 'exceptions.dart';
 import 'io.dart';
 import 'log.dart' as log;
@@ -121,13 +123,34 @@
   return null;
 }
 
+/// '--recourse-submodules' was introduced in Git 2.14
+/// (https://git-scm.com/book/en/v2/Git-Tools-Submodules).
+final _minSupportedGitVersion = Version(2, 14, 0);
+
 /// Checks whether [command] is the Git command for this computer.
 bool _tryGitCommand(String command) {
   // If "git --version" prints something familiar, git is working.
   try {
     var result = runProcessSync(command, ['--version']);
-    var regexp = RegExp('^git version');
-    return result.stdout.length == 1 && regexp.hasMatch(result.stdout.single);
+
+    if (result.stdout.length != 1) return false;
+    final output = result.stdout.single;
+    final match = RegExp(r'^git version (\d+)\.(\d+)\.').matchAsPrefix(output);
+
+    if (match == null) return false;
+    // Git seems to use many parts in the version number. We just check the
+    // first two.
+    final major = int.parse(match[1]!);
+    final minor = int.parse(match[2]!);
+    if (Version(major, minor, 0) < _minSupportedGitVersion) {
+      // We just warn here, as some features might work with older versions of
+      // git.
+      log.warning('''
+You have a very old version of git (version ${output.substring('git version '.length)}),
+for $topLevelProgram it is recommended to use git version 2.14 or newer.
+''');
+    }
+    return true;
   } on RunProcessException catch (err) {
     // If the process failed, they probably don't have it.
     log.error('Git command is not "$command": $err');
diff --git a/test/get/git/git_not_installed_test.dart b/test/get/git/git_not_installed_test.dart
index f00d795..e146f0e 100644
--- a/test/get/git/git_not_installed_test.dart
+++ b/test/get/git/git_not_installed_test.dart
@@ -13,42 +13,84 @@
 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 {
-    // 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.
-    await d.dir('bin', [
-      d.file('git', '''
+    await setUpFakeGitScript(bash: '''
 #!/bin/bash -e
 echo "not git"
 exit 1
-'''),
-      d.file('git.bat', '''
+''', batch: '''
 echo "not git"
-''')
-    ]).create();
-    final binFolder = p.join(sandbox, 'bin');
-    // chmod the git script
-    if (!Platform.isWindows) {
-      await runProcess('chmod', ['+x', p.join(sandbox, 'bin', 'git')]);
-    }
+''');
+    await d.appDir({
+      'foo': {'git': '../foo.git'}
+    }).create();
+
+    await pubGet(
+      environment: extendedPathEnv(),
+      error: contains('Cannot find a Git executable'),
+      exitCode: 1,
+    );
+  });
+
+  test('warns if git version is too old', () async {
+    await setUpFakeGitScript(bash: '''
+#!/bin/bash -e
+if [ "\$1" == "--version" ]
+then
+  echo "git version 2.13.1.616"
+  exit 1
+else
+  PATH=${Platform.environment['PATH']} git \$*
+fi
+''', batch: '''
+if "%1"=="--version" (
+  echo "git version 2.13.1.616"
+) else (
+  set path="${Platform.environment['PATH']}"
+  git %*
+)
+''');
+
+    await d.git('foo.git', [d.libPubspec('foo', '1.0.0')]).create();
 
     await d.appDir({
       'foo': {'git': '../foo.git'}
     }).create();
 
-    final separator = Platform.isWindows ? ';' : ':';
-
     await pubGet(
-      environment: {
-        // Override 'PATH' to ensure that we can't detect a working "git" binary
-        'PATH': '$binFolder$separator${Platform.environment['PATH']}',
-      },
-      // We wish to verify that this error message is printed.
-      error: contains('Cannot find a Git executable'),
-      exitCode: 1, // exit code is non-zero.
+      environment: extendedPathEnv(),
+      warning:
+          contains('You have a very old version of git (version 2.13.1.616)'),
+      exitCode: 0,
     );
   });
 }