blob: 34c4b5dc642e4659fa6043e9fad776a4c0918181 [file] [log] [blame]
// Copyright 2013 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 'package:process/process.dart';
import '../base/command.dart';
import '../base/file_system.dart';
import '../base/logger.dart';
import '../base/project.dart';
import '../compute.dart';
import '../environment.dart';
import '../manifest.dart';
import '../result.dart';
import '../utils.dart';
class MigrateStartCommand extends MigrateCommand {
MigrateStartCommand({
bool verbose = false,
required this.logger,
required this.fileSystem,
required this.processManager,
this.standalone = false,
}) : _verbose = verbose,
migrateUtils = MigrateUtils(
logger: logger,
fileSystem: fileSystem,
processManager: processManager,
) {
argParser.addOption(
'staging-directory',
help:
'Specifies the custom migration staging directory used to stage and edit proposed changes. '
'This path can be absolute or relative to the flutter project root.',
valueHelp: 'path',
);
argParser.addOption(
'project-directory',
help: 'The root directory of the flutter project.',
valueHelp: 'path',
);
argParser.addOption(
'platforms',
help:
'Restrict the tool to only migrate the listed platforms. By default all platforms generated by '
'flutter create will be migrated. To indicate the project root, use the `root` platform',
valueHelp: 'root,android,ios,windows...',
);
argParser.addFlag(
'delete-temp-directories',
help:
'Indicates if the temporary directories created by the migrate tool will be deleted.',
);
argParser.addOption(
'base-app-directory',
help:
'The directory containing the base reference app. This is used as the common ancestor in a 3 way merge. '
'Providing this directory will prevent the tool from generating its own. This is primarily used '
'in testing and CI.',
valueHelp: 'path',
hide: !verbose,
);
argParser.addOption(
'target-app-directory',
help:
'The directory containing the target reference app. This is used as the target app in 3 way merge. '
'Providing this directory will prevent the tool from generating its own. This is primarily used '
'in testing and CI.',
valueHelp: 'path',
hide: !verbose,
);
argParser.addFlag(
'allow-fallback-base-revision',
help:
'If a base revision cannot be determined, this flag enables using flutter 1.0.0 as a fallback base revision. '
'Using this fallback will typically produce worse quality migrations and possibly more conflicts.',
);
argParser.addOption(
'base-revision',
help:
'Manually sets the base revision to generate the base ancestor reference app with. This may be used '
'if the tool is unable to determine an appropriate base revision.',
valueHelp: 'git revision hash',
);
argParser.addOption(
'target-revision',
help:
'Manually sets the target revision to generate the target reference app with. Passing this indicates '
'that the current flutter sdk version is not the version that should be migrated to.',
valueHelp: 'git revision hash',
);
argParser.addFlag(
'prefer-two-way-merge',
negatable: false,
help:
'Avoid three way merges when possible. Enabling this effectively ignores the base ancestor reference '
'files when a merge is required, opting for a simpler two way merge instead. In some edge cases typically '
'involving using a fallback or incorrect base revision, the default three way merge algorithm may produce '
'incorrect merges. Two way merges are more conflict prone, but less likely to produce incorrect results '
'silently.',
);
argParser.addFlag(
'flutter-subcommand',
help:
'Enable when using the flutter tool as a subcommand. This changes the '
'wording of log messages to indicate the correct suggested commands to use.',
);
}
final bool _verbose;
final Logger logger;
final FileSystem fileSystem;
final MigrateUtils migrateUtils;
final ProcessManager processManager;
final bool standalone;
@override
final String name = 'start';
@override
final String description =
r'Begins a new migration. Computes the changes needed to migrate the project from the base revision of Flutter to the current revision of Flutter and outputs the results in a working directory. Use `$ flutter migrate apply` accept and apply the changes.';
@override
Future<CommandResult> runCommand() async {
final FlutterToolsEnvironment environment =
await FlutterToolsEnvironment.initializeFlutterToolsEnvironment(
processManager, logger);
if (!_validateEnvironment(environment)) {
return const CommandResult(ExitStatus.fail);
}
final String? projectRootDirPath = stringArg('project-directory') ??
environment.getString('FlutterProject.directory');
final Directory projectRootDir = fileSystem.directory(projectRootDirPath);
final FlutterProjectFactory flutterProjectFactory = FlutterProjectFactory();
final FlutterProject project = projectRootDirPath == null
? FlutterProject.current(fileSystem)
: flutterProjectFactory
.fromDirectory(fileSystem.directory(projectRootDirPath));
if (!validateWorkingDirectory(project, logger)) {
return CommandResult.fail();
}
final bool isModule =
environment.getBool('FlutterProject.isModule') ?? false;
final bool isPlugin =
environment.getBool('FlutterProject.isPlugin') ?? false;
if (isModule || isPlugin) {
logger.printError(
'Migrate tool only supports app projects. This project is a ${isModule ? 'module' : 'plugin'}');
return const CommandResult(ExitStatus.fail);
}
final bool isSubcommand = boolArg('flutter-subcommand') ?? !standalone;
if (!await gitRepoExists(project.directory.path, logger, migrateUtils)) {
return const CommandResult(ExitStatus.fail);
}
Directory stagingDirectory =
project.directory.childDirectory(kDefaultMigrateStagingDirectoryName);
final String? customStagingDirectoryPath = stringArg('staging-directory');
if (customStagingDirectoryPath != null) {
if (fileSystem.path.isAbsolute(customStagingDirectoryPath)) {
stagingDirectory = fileSystem.directory(customStagingDirectoryPath);
} else {
stagingDirectory =
project.directory.childDirectory(customStagingDirectoryPath);
}
}
if (stagingDirectory.existsSync()) {
logger.printStatus('Old migration already in progress.', emphasis: true);
logger.printStatus(
'Pending migration files exist in `${stagingDirectory.path}/$kDefaultMigrateStagingDirectoryName`');
logger.printStatus(
'Resolve merge conflicts and accept changes with by running:');
printCommandText('apply', logger, standalone: !isSubcommand);
logger.printStatus(
'You may also abandon the existing migration and start a new one with:');
printCommandText('abandon', logger, standalone: !isSubcommand);
return const CommandResult(ExitStatus.fail);
}
if (await hasUncommittedChanges(
project.directory.path, logger, migrateUtils)) {
return const CommandResult(ExitStatus.fail);
}
List<SupportedPlatform>? platforms;
if (stringArg('platforms') != null) {
platforms = <SupportedPlatform>[];
for (String platformString in stringArg('platforms')!.split(',')) {
platformString = platformString.trim();
platforms.add(SupportedPlatform.values.firstWhere(
(SupportedPlatform val) =>
val.toString() == 'SupportedPlatform.$platformString'));
}
}
final MigrateCommandParameters commandParameters = MigrateCommandParameters(
verbose: _verbose,
baseAppPath: stringArg('base-app-directory'),
targetAppPath: stringArg('target-app-directory'),
baseRevision: stringArg('base-revision'),
targetRevision: stringArg('target-revision'),
deleteTempDirectories: boolArg('delete-temp-directories') ?? true,
platforms: platforms,
preferTwoWayMerge: boolArg('prefer-two-way-merge') ?? false,
allowFallbackBaseRevision:
boolArg('allow-fallback-base-revision') ?? false,
);
final MigrateResult? migrateResult = await computeMigration(
flutterProject: project,
commandParameters: commandParameters,
fileSystem: fileSystem,
logger: logger,
migrateUtils: migrateUtils,
environment: environment,
);
if (migrateResult == null) {
return const CommandResult(ExitStatus.fail);
}
await writeStagingDir(migrateResult, logger,
verbose: _verbose, projectRootDir: projectRootDir);
_deleteTempDirectories(
paths: <String>[],
directories: migrateResult.tempDirectories,
);
logger.printStatus(
'The migrate tool has staged proposed changes in the migrate staging directory.\n');
logger.printStatus('Guided conflict resolution wizard:');
printCommandText('resolve-conflicts', logger, standalone: !isSubcommand);
logger.printStatus('Check the status and diffs of the migration with:');
printCommandText('status', logger, standalone: !isSubcommand);
logger.printStatus('Abandon the proposed migration with:');
printCommandText('abandon', logger, standalone: !isSubcommand);
logger.printStatus(
'Accept staged changes after resolving any merge conflicts with:');
printCommandText('apply', logger, standalone: !isSubcommand);
return const CommandResult(ExitStatus.success);
}
/// Deletes the files or directories at the provided paths.
void _deleteTempDirectories(
{List<String> paths = const <String>[],
List<Directory> directories = const <Directory>[]}) {
for (final Directory d in directories) {
try {
d.deleteSync(recursive: true);
} on FileSystemException catch (e) {
logger.printError(
'Unabled to delete ${d.path} due to ${e.message}, please clean up manually.');
}
}
for (final String p in paths) {
try {
fileSystem.directory(p).deleteSync(recursive: true);
} on FileSystemException catch (e) {
logger.printError(
'Unabled to delete $p due to ${e.message}, please clean up manually.');
}
}
}
bool _validateEnvironment(FlutterToolsEnvironment environment) {
if (environment.getString('FlutterProject.directory') == null) {
logger.printError(
'No valid flutter project found. This command must be run from a flutter project directory');
return false;
}
if (environment.getString('FlutterProject.manifest.appname') == null) {
logger.printError('No app name found in project pubspec.yaml');
return false;
}
if (!(environment.getBool('FlutterProject.android.exists') ?? false) &&
environment['FlutterProject.android.isKotlin'] == null) {
logger.printError(
'Could not detect if android project uses kotlin or java');
return false;
}
if (!(environment.getBool('FlutterProject.ios.exists') ?? false) &&
environment['FlutterProject.ios.isSwift'] == null) {
logger.printError(
'Could not detect if iosProject uses swift or objective-c');
return false;
}
return true;
}
/// Writes the files into the working directory for the developer to review and resolve any conflicts.
Future<void> writeStagingDir(MigrateResult migrateResult, Logger logger,
{bool verbose = false, required Directory projectRootDir}) async {
final Directory stagingDir =
projectRootDir.childDirectory(kDefaultMigrateStagingDirectoryName);
if (verbose) {
logger.printStatus(
'Writing migrate staging directory at `${stagingDir.path}`');
}
// Write files in working dir
for (final MergeResult result in migrateResult.mergeResults) {
final File file = stagingDir.childFile(result.localPath);
file.createSync(recursive: true);
if (result is StringMergeResult) {
file.writeAsStringSync(result.mergedString, flush: true);
} else {
file.writeAsBytesSync((result as BinaryMergeResult).mergedBytes,
flush: true);
}
}
// Write all files that are newly added in target
for (final FilePendingMigration addedFile in migrateResult.addedFiles) {
final File file = stagingDir.childFile(addedFile.localPath);
file.createSync(recursive: true);
try {
file.writeAsStringSync(addedFile.file.readAsStringSync(), flush: true);
} on FileSystemException {
file.writeAsBytesSync(addedFile.file.readAsBytesSync(), flush: true);
}
}
// Write the MigrateManifest.
final MigrateManifest manifest = MigrateManifest(
migrateRootDir: stagingDir,
migrateResult: migrateResult,
);
manifest.writeFile();
// output the manifest contents.
checkAndPrintMigrateStatus(manifest, stagingDir, logger: logger);
logger.printBox('Staging directory created at `${stagingDir.path}`');
}
}