| // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| /// Helper functionality for invoking Git. |
| library; |
| |
| import 'dart:async'; |
| import 'dart:convert'; |
| import 'dart:io'; |
| import 'dart:typed_data'; |
| |
| import 'package:collection/collection.dart'; |
| 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; |
| import 'utils.dart'; |
| |
| /// An exception thrown because a git command failed. |
| class GitException implements ApplicationException { |
| /// The arguments to the git command. |
| final List<String> args; |
| |
| /// The standard error emitted by git. |
| final dynamic stderr; |
| |
| /// The standard out emitted by git. |
| final dynamic stdout; |
| |
| /// The error code |
| final int exitCode; |
| |
| @override |
| String get message => |
| 'Git error. Command: `git ${args.join(' ')}`\n' |
| 'stdout: ${stdout is String ? stdout : '<binary>'}\n' |
| 'stderr: ${stderr is String ? stderr : '<binary>'}\n' |
| 'exit code: $exitCode'; |
| |
| GitException(Iterable<String> args, this.stdout, this.stderr, this.exitCode) |
| : args = args.toList(); |
| |
| @override |
| String toString() => message; |
| } |
| |
| /// Tests whether or not the git command-line app is available for use. |
| bool get isInstalled => command != null; |
| |
| /// Splits the [output] of a git -z command at \0. |
| /// |
| /// The first [skipPrefix] bytes of each substring will be ignored (useful for |
| /// `git status -z`). If there are not enough bytes to skip, throws a |
| /// [FormatException]. |
| List<Uint8List> splitZeroTerminated(Uint8List output, {int skipPrefix = 0}) { |
| final result = <Uint8List>[]; |
| var start = 0; |
| |
| for (var i = 0; i < output.length; i++) { |
| if (output[i] != 0) { |
| continue; |
| } |
| if (start + skipPrefix > i) { |
| throw FormatException('Substring too short for prefix at $start'); |
| } |
| result.add( |
| Uint8List.sublistView( |
| output, |
| // The first 3 bytes are the modification status. |
| // Skip those. |
| start + skipPrefix, |
| i, |
| ), |
| ); |
| |
| start = i + 1; |
| } |
| return result; |
| } |
| |
| /// Run a git process with [args] from [workingDir]. |
| /// |
| /// Returns the stdout if it succeeded. Completes to ans exception if it failed. |
| Future<String> run( |
| List<String> args, { |
| String? workingDir, |
| Map<String, String>? environment, |
| Encoding stdoutEncoding = systemEncoding, |
| Encoding stderrEncoding = systemEncoding, |
| }) async { |
| if (!isInstalled) { |
| fail( |
| 'Cannot find a Git executable.\n' |
| 'Please ensure Git is correctly installed.', |
| ); |
| } |
| |
| log.muteProgress(); |
| try { |
| final result = await runProcess( |
| command!, |
| args, |
| workingDir: workingDir, |
| environment: {...?environment, 'LANG': 'en_GB'}, |
| stdoutEncoding: stdoutEncoding, |
| stderrEncoding: stderrEncoding, |
| ); |
| if (!result.success) { |
| throw GitException(args, result.stdout, result.stderr, result.exitCode); |
| } |
| return result.stdout; |
| } finally { |
| log.unmuteProgress(); |
| } |
| } |
| |
| /// Like [run], but synchronous. |
| String runSync( |
| List<String> args, { |
| String? workingDir, |
| Map<String, String>? environment, |
| Encoding stdoutEncoding = systemEncoding, |
| Encoding stderrEncoding = systemEncoding, |
| }) { |
| if (!isInstalled) { |
| fail( |
| 'Cannot find a Git executable.\n' |
| 'Please ensure Git is correctly installed.', |
| ); |
| } |
| |
| final result = runProcessSync( |
| command!, |
| args, |
| workingDir: workingDir, |
| environment: environment, |
| stdoutEncoding: stdoutEncoding, |
| stderrEncoding: stderrEncoding, |
| ); |
| if (!result.success) { |
| throw GitException(args, result.stdout, result.stderr, result.exitCode); |
| } |
| |
| return result.stdout; |
| } |
| |
| /// Like [run], but synchronous. Returns raw stdout as `Uint8List`. |
| Uint8List runSyncBytes( |
| List<String> args, { |
| String? workingDir, |
| Map<String, String>? environment, |
| Encoding stderrEncoding = systemEncoding, |
| }) { |
| if (!isInstalled) { |
| fail( |
| 'Cannot find a Git executable.\n' |
| 'Please ensure Git is correctly installed.', |
| ); |
| } |
| |
| final result = runProcessSyncBytes( |
| command!, |
| args, |
| workingDir: workingDir, |
| environment: environment, |
| stderrEncoding: stderrEncoding, |
| ); |
| if (!result.success) { |
| throw GitException(args, result.stdout, result.stderr, result.exitCode); |
| } |
| |
| return result.stdout; |
| } |
| |
| /// 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. |
| String? repoRoot(String dir) { |
| if (isInstalled) { |
| try { |
| return p.normalize( |
| runSync(['rev-parse', '--show-toplevel'], workingDir: dir).trim(), |
| ); |
| } on GitException { |
| // Not in a git folder. |
| return null; |
| } |
| } |
| 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 { |
| final result = runProcessSync(command, ['--version']); |
| final output = result.stdout; |
| |
| // Some users may have configured commands such as autorun, which may |
| // produce additional output, so we need to look for "git version" |
| // in every line of the output. |
| final match = RegExp( |
| r'^git version (\d+)\.(\d+)\..*$', |
| multiLine: true, |
| ).matchAsPrefix(output); |
| if (match == null) return false; |
| final versionString = match[0]!.substring('git version '.length); |
| // 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 $versionString), |
| 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. |
| log.error('Git command is not "$command": $err'); |
| return false; |
| } |
| } |