import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:process/process.dart';
import 'base/common.dart';
import 'base/file_system.dart';
import 'base/logger.dart';
import 'base/terminal.dart';
const String kDefaultMigrateStagingDirectoryName = 'migrate_staging_dir';
class MigrateUtils {
MigrateUtils({
required Logger logger,
required FileSystem fileSystem,
required ProcessManager processManager,
}) : _processManager = processManager,
_logger = logger,
_fileSystem = fileSystem;
final Logger _logger;
final FileSystem _fileSystem;
final ProcessManager _processManager;
Future<ProcessResult> _runCommand(List<String> command,
{String? workingDirectory, bool runInShell = false}) {
return _processManager.run(command,
workingDirectory: workingDirectory, runInShell: runInShell);
}
Future<DiffResult> diffFiles(File one, File two) async {
if (one.existsSync() && !two.existsSync()) {
return DiffResult(diffType: DiffType.deletion);
}
if (!one.existsSync() && two.existsSync()) {
return DiffResult(diffType: DiffType.addition);
}
final List<String> cmdArgs = <String>[
'git',
'diff',
'--no-index',
one.absolute.path,
two.absolute.path
];
final ProcessResult result = await _runCommand(cmdArgs);
checkForErrors(result,
allowedExitCodes: <int>[0, 1],
commandDescription: 'git ${cmdArgs.join(' ')}');
return DiffResult(
diffType: DiffType.command,
diff: result.stdout as String,
exitCode: result.exitCode);
}
Future<bool> cloneFlutter(String revision, String destination) async {
List<String> cmdArgs = <String>[
'git',
'clone',
'--filter=blob:none',
'https://github.com/flutter/flutter.git',
destination
];
ProcessResult result = await _runCommand(cmdArgs);
checkForErrors(result, commandDescription: cmdArgs.join(' '));
cmdArgs.clear();
cmdArgs = <String>['git', 'reset', '--hard', revision];
result = await _runCommand(cmdArgs, workingDirectory: destination);
if (!checkForErrors(result,
commandDescription: cmdArgs.join(' '), exit: false)) {
return false;
}
return true;
}
Future<String> createFromTemplates(
String flutterBinPath, {
required String name,
bool legacyNameParameter = false,
required String androidLanguage,
required String iosLanguage,
required String outputDirectory,
String? createVersion,
List<String> platforms = const <String>[],
int iterationsAllowed = 5,
}) async {
if (iterationsAllowed <= 0) {
_logger.printError(
'Unable to `flutter create` with the version of flutter at $flutterBinPath');
return outputDirectory;
}
final List<String> cmdArgs = <String>['$flutterBinPath/flutter', 'create'];
if (!legacyNameParameter) {
cmdArgs.add('--project-name=$name');
}
cmdArgs.add('--android-language=$androidLanguage');
cmdArgs.add('--ios-language=$iosLanguage');
if (platforms.isNotEmpty) {
String platformsArg = '--platforms=';
for (int i = 0; i < platforms.length; i++) {
if (i > 0) {
platformsArg += ',';
}
platformsArg += platforms[i];
}
cmdArgs.add(platformsArg);
}
cmdArgs.add('--no-pub');
if (legacyNameParameter) {
cmdArgs.add(name);
} else {
cmdArgs.add(outputDirectory);
}
final ProcessResult result =
await _runCommand(cmdArgs, workingDirectory: outputDirectory);
final String error = result.stderr as String;
if (error.contains('Could not find an option named "platforms".')) {
return createFromTemplates(
flutterBinPath,
name: name,
legacyNameParameter: legacyNameParameter,
androidLanguage: androidLanguage,
iosLanguage: iosLanguage,
outputDirectory: outputDirectory,
iterationsAllowed: iterationsAllowed--,
);
}
if ((result.stderr as String)
.contains('Could not find an option named "project-name".')) {
return createFromTemplates(
flutterBinPath,
name: name,
legacyNameParameter: true,
androidLanguage: androidLanguage,
iosLanguage: iosLanguage,
outputDirectory: outputDirectory,
platforms: platforms,
iterationsAllowed: iterationsAllowed--,
);
}
if (error.contains('Multiple output directories specified.')) {
if (error.contains('Try moving --platforms')) {
return createFromTemplates(
flutterBinPath,
name: name,
legacyNameParameter: legacyNameParameter,
androidLanguage: androidLanguage,
iosLanguage: iosLanguage,
outputDirectory: outputDirectory,
iterationsAllowed: iterationsAllowed--,
);
}
}
checkForErrors(result, commandDescription: cmdArgs.join(' '), silent: true);
if (legacyNameParameter) {
return _fileSystem.path.join(outputDirectory, name);
}
return outputDirectory;
}
Future<MergeResult> gitMergeFile({
required String base,
required String current,
required String target,
required String localPath,
}) async {
final List<String> cmdArgs = <String>[
'git',
'merge-file',
'-p',
current,
base,
target
];
final ProcessResult result = await _runCommand(cmdArgs);
checkForErrors(result,
allowedExitCodes: <int>[-1], commandDescription: cmdArgs.join(' '));
return StringMergeResult(result, localPath);
}
Future<void> gitInit(String workingDirectory) async {
final List<String> cmdArgs = <String>['git', 'init'];
final ProcessResult result =
await _runCommand(cmdArgs, workingDirectory: workingDirectory);
checkForErrors(result, commandDescription: cmdArgs.join(' '));
}
Future<bool> hasUncommittedChanges(String workingDirectory,
{String? migrateStagingDir}) async {
final List<String> cmdArgs = <String>[
'git',
'ls-files',
'--deleted',
'--modified',
'--others',
'--exclude-standard',
'--exclude=${migrateStagingDir ?? kDefaultMigrateStagingDirectoryName}'
];
final ProcessResult result =
await _runCommand(cmdArgs, workingDirectory: workingDirectory);
checkForErrors(result,
allowedExitCodes: <int>[-1], commandDescription: cmdArgs.join(' '));
if ((result.stdout as String).isEmpty) {
return false;
}
return true;
}
Future<bool> isGitRepo(String workingDirectory) async {
final List<String> cmdArgs = <String>[
'git',
'rev-parse',
'--is-inside-work-tree'
];
final ProcessResult result =
await _runCommand(cmdArgs, workingDirectory: workingDirectory);
checkForErrors(result,
allowedExitCodes: <int>[-1], commandDescription: cmdArgs.join(' '));
if (result.exitCode == 0) {
return true;
}
return false;
}
Future<bool> isGitIgnored(String filePath, String workingDirectory) async {
final List<String> cmdArgs = <String>['git', 'check-ignore', filePath];
final ProcessResult result =
await _runCommand(cmdArgs, workingDirectory: workingDirectory);
checkForErrors(result,
allowedExitCodes: <int>[0, 1, 128],
commandDescription: cmdArgs.join(' '));
return result.exitCode == 0;
}
Future<void> flutterPubUpgrade(String workingDirectory) async {
final List<String> cmdArgs = <String>[
'flutter',
'pub',
'upgrade',
'--major-versions'
];
final ProcessResult result =
await _runCommand(cmdArgs, workingDirectory: workingDirectory);
checkForErrors(result, commandDescription: cmdArgs.join(' '));
}
Future<void> gradlewTasks(String workingDirectory) async {
final String baseCommand = isWindows ? 'gradlew.bat' : './gradlew';
final List<String> cmdArgs = <String>[baseCommand, 'tasks'];
final ProcessResult result = await _runCommand(cmdArgs,
workingDirectory: workingDirectory, runInShell: isWindows);
checkForErrors(result, commandDescription: cmdArgs.join(' '));
}
bool checkForErrors(ProcessResult result,
{List<int> allowedExitCodes = const <int>[0],
String? commandDescription,
bool exit = true,
bool silent = false}) {
if (allowedExitCodes.contains(result.exitCode) ||
allowedExitCodes.contains(-1)) {
return true;
}
if (!silent) {
_logger.printError(
'Command encountered an error with exit code ${result.exitCode}.');
if (commandDescription != null) {
_logger.printError('Command:');
_logger.printError(commandDescription, indent: 2);
}
_logger.printError('Stdout:');
_logger.printError(result.stdout as String, indent: 2);
_logger.printError('Stderr:');
_logger.printError(result.stderr as String, indent: 2);
}
if (exit) {
throwToolExit(
'Command failed with exit code ${result.exitCode}: ${result.stderr}\n${result.stdout}',
exitCode: result.exitCode);
}
return false;
}
bool conflictsResolved(String contents) {
final bool hasMarker = contents.contains('>>>>>>>') ||
contents.contains('=======') ||
contents.contains('<<<<<<<');
return !hasMarker;
}
}
Future<bool> gitRepoExists(
String projectDirectory, Logger logger, MigrateUtils migrateUtils) async {
if (await migrateUtils.isGitRepo(projectDirectory)) {
return true;
}
logger.printStatus(
'Project is not a git repo. Please initialize a git repo and try again.');
printCommand('git init', logger);
return false;
}
Future<bool> hasUncommittedChanges(
String projectDirectory, Logger logger, MigrateUtils migrateUtils) async {
if (await migrateUtils.hasUncommittedChanges(projectDirectory)) {
logger.printStatus(
'There are uncommitted changes in your project. Please git commit, abandon, or stash your changes before trying again.');
logger.printStatus('You may commit your changes using');
printCommand('git add .', logger, newlineAfter: false);
printCommand('git commit -m "<message>"', logger);
return true;
}
return false;
}
void printCommand(String command, Logger logger, {bool newlineAfter = true}) {
logger.printStatus(
'\n\$ $command${newlineAfter ? '\n' : ''}',
color: TerminalColor.grey,
indent: 4,
newline: false,
);
}
void printCommandText(String command, Logger logger,
{bool? standalone = true, bool newlineAfter = true}) {
final String prefix = standalone == null
? ''
: (standalone
? 'dart run <flutter_migrate_dir>${Platform.pathSeparator}bin${Platform.pathSeparator}flutter_migrate.dart '
: 'flutter migrate ');
printCommand('$prefix$command', logger, newlineAfter: newlineAfter);
}
enum DiffType {
command,
addition,
deletion,
ignored,
none,
}
class DiffResult {
DiffResult({
required this.diffType,
this.diff,
this.exitCode,
}) : assert(diffType == DiffType.command && exitCode != null ||
diffType != DiffType.command && exitCode == null);
final String? diff;
final DiffType diffType;
final int? exitCode;
}
abstract class MergeResult {
MergeResult(ProcessResult result, this.localPath)
: hasConflict = result.exitCode != 0,
exitCode = result.exitCode;
MergeResult.explicit({
required this.hasConflict,
required this.exitCode,
required this.localPath,
});
bool hasConflict;
int exitCode;
String localPath;
}
class StringMergeResult extends MergeResult {
StringMergeResult(super.result, super.localPath)
: mergedString = result.stdout as String;
StringMergeResult.explicit({
required this.mergedString,
required super.hasConflict,
required super.exitCode,
required super.localPath,
}) : super.explicit();
String mergedString;
}
class BinaryMergeResult extends MergeResult {
BinaryMergeResult(super.result, super.localPath)
: mergedBytes = result.stdout as Uint8List;
BinaryMergeResult.explicit({
required this.mergedBytes,
required super.hasConflict,
required super.exitCode,
required super.localPath,
}) : super.explicit();
Uint8List mergedBytes;
}