// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:io'; import 'package:process/process.dart'; import './globals.dart'; /// A wrapper around git process calls that can be mocked for unit testing. class Git { const Git(this.processManager); final ProcessManager processManager; Future<String> getOutput( List<String> args, String explanation, { required String workingDirectory, bool allowFailures = false, }) async { final ProcessResult result = await _run(args, workingDirectory); if (result.exitCode == 0) { return stdoutToString(result.stdout); } _reportFailureAndExit(args, workingDirectory, result, explanation); } Future<int> run( List<String> args, String explanation, { bool allowNonZeroExitCode = false, required String workingDirectory, }) async { late final ProcessResult result; try { result = await _run(args, workingDirectory); } on ProcessException { _reportFailureAndExit(args, workingDirectory, result, explanation); } if (result.exitCode != 0 && !allowNonZeroExitCode) { _reportFailureAndExit(args, workingDirectory, result, explanation); } return result.exitCode; } Future<ProcessResult> _run(List<String> args, String workingDirectory) async { return processManager.run( <String>['git', ...args], workingDirectory: workingDirectory, environment: <String, String>{'GIT_TRACE': '1'}, ); } Never _reportFailureAndExit( List<String> args, String workingDirectory, ProcessResult result, String explanation, ) { final StringBuffer message = StringBuffer(); if (result.exitCode != 0) { message.writeln( 'Command "git ${args.join(' ')}" failed in directory "$workingDirectory" to ' '$explanation. Git exited with error code ${result.exitCode}.', ); } else { message.writeln('Command "git ${args.join(' ')}" failed to $explanation.'); } if ((result.stdout as String).isNotEmpty) { message.writeln('stdout from git:\n${result.stdout}\n'); } if ((result.stderr as String).isNotEmpty) { message.writeln('stderr from git:\n${result.stderr}\n'); } throw GitException(message.toString(), args); } } enum GitExceptionType { /// Git push failed because the remote branch contained commits the local did /// not. /// /// Either the local branch was wrong, and needs a rebase before pushing /// again, or the remote branch needs to be overwritten with a force push. /// /// Example output: /// /// ``` /// To github.com:user/engine.git /// /// ! [rejected] HEAD -> cherrypicks-flutter-2.8-candidate.3 (non-fast-forward) /// error: failed to push some refs to 'github.com:user/engine.git' /// hint: Updates were rejected because the tip of your current branch is behind /// hint: its remote counterpart. Integrate the remote changes (e.g. /// hint: 'git pull ...') before pushing again. /// hint: See the 'Note about fast-forwards' in 'git push --help' for details. /// ``` PushRejected, } /// An exception created because a git subprocess failed. /// /// Known git failures will be assigned a [GitExceptionType] in the [type] /// field. If this field is null it means and unknown git failure. class GitException implements Exception { GitException(this.message, this.args) { if (_pushRejectedPattern.hasMatch(message)) { type = GitExceptionType.PushRejected; } else { // because type is late final, it must be explicitly set before it is // accessed. type = null; } } static final RegExp _pushRejectedPattern = RegExp( r'Updates were rejected because the tip of your current branch is behind', ); final String message; final List<String> args; late final GitExceptionType? type; @override String toString() => 'Exception on command "${args.join(' ')}": $message'; }