// 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 'package:process/process.dart'; import '../base/file_system.dart'; import '../base/logger.dart'; import '../base/platform.dart'; import '../base/terminal.dart'; import '../flutter_project_metadata.dart'; import '../migrate/migrate_manifest.dart'; import '../migrate/migrate_update_locks.dart'; import '../migrate/migrate_utils.dart'; import '../project.dart'; import '../runner/flutter_command.dart'; import '../version.dart'; import 'migrate.dart'; /// Migrate subcommand that checks the migrate working directory for unresolved conflicts and /// applies the staged changes to the project. class MigrateApplyCommand extends FlutterCommand { MigrateApplyCommand({ bool verbose = false, required this.logger, required this.fileSystem, required this.terminal, required Platform platform, required ProcessManager processManager, }) : _verbose = verbose, migrateUtils = MigrateUtils( logger: logger, fileSystem: fileSystem, platform: platform, processManager: processManager, ) { requiresPubspecYaml(); argParser.addOption( 'staging-directory', help: 'Specifies the custom migration working directory used to stage ' 'and edit proposed changes. This path can be absolute or relative ' 'to the flutter project root. This defaults to ' '`$kDefaultMigrateStagingDirectoryName`', valueHelp: 'path', ); argParser.addOption( 'project-directory', help: 'The root directory of the flutter project. This defaults to the ' 'current working directory if omitted.', valueHelp: 'path', ); argParser.addFlag( 'force', abbr: 'f', help: 'Ignore unresolved merge conflicts and uncommitted changes and ' 'apply staged changes by force.', ); argParser.addFlag( 'keep-working-directory', help: 'Do not delete the working directory.', ); } final bool _verbose; final Logger logger; final FileSystem fileSystem; final Terminal terminal; final MigrateUtils migrateUtils; @override final String name = 'apply'; @override final String description = r'Accepts the changes produced by `$ flutter ' 'migrate start` and copies the changed files into ' 'your project files. All merge conflicts should ' 'be resolved before apply will complete ' 'successfully. If conflicts still exist, this ' 'command will print the remaining conflicted files.'; @override String get category => FlutterCommandCategory.project; @override Future> get requiredArtifacts async => const {}; @override Future runCommand() async { final String? projectDirectory = stringArg('project-directory'); final FlutterProjectFactory flutterProjectFactory = FlutterProjectFactory(logger: logger, fileSystem: fileSystem); final FlutterProject project = projectDirectory == null ? FlutterProject.current() : flutterProjectFactory.fromDirectory(fileSystem.directory(projectDirectory)); if (!await gitRepoExists(project.directory.path, logger, migrateUtils)) { logger.printStatus('No git repo found. Please run in a project with an ' 'initialized git repo or initialize one with:'); printCommandText('git init', logger); return const FlutterCommandResult(ExitStatus.fail); } final bool force = boolArg('force') ?? false; 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('No migration in progress at $stagingDirectory. Please run:'); printCommandText('flutter migrate start', logger); return const FlutterCommandResult(ExitStatus.fail); } final File manifestFile = MigrateManifest.getManifestFileFromDirectory(stagingDirectory); final MigrateManifest manifest = MigrateManifest.fromFile(manifestFile); if (!checkAndPrintMigrateStatus(manifest, stagingDirectory, warnConflict: true, logger: logger) && !force) { logger.printStatus('Conflicting files found. Resolve these conflicts and try again.'); logger.printStatus('Guided conflict resolution wizard:'); printCommandText('flutter migrate resolve-conflicts', logger); return const FlutterCommandResult(ExitStatus.fail); } if (await hasUncommittedChanges(project.directory.path, logger, migrateUtils) && !force) { return const FlutterCommandResult(ExitStatus.fail); } logger.printStatus('Applying migration.'); // Copy files from working directory to project root final List allFilesToCopy = []; allFilesToCopy.addAll(manifest.mergedFiles); allFilesToCopy.addAll(manifest.conflictFiles); allFilesToCopy.addAll(manifest.addedFiles); if (allFilesToCopy.isNotEmpty && _verbose) { logger.printStatus('Modifying ${allFilesToCopy.length} files.', indent: 2); } for (final String localPath in allFilesToCopy) { if (_verbose) { logger.printStatus('Writing $localPath'); } final File workingFile = stagingDirectory.childFile(localPath); final File targetFile = project.directory.childFile(localPath); if (!workingFile.existsSync()) { continue; } if (!targetFile.existsSync()) { targetFile.createSync(recursive: true); } try { targetFile.writeAsStringSync(workingFile.readAsStringSync(), flush: true); } on FileSystemException { targetFile.writeAsBytesSync(workingFile.readAsBytesSync(), flush: true); } } // Delete files slated for deletion. if (manifest.deletedFiles.isNotEmpty) { logger.printStatus('Deleting ${manifest.deletedFiles.length} files.', indent: 2); } for (final String localPath in manifest.deletedFiles) { final File targetFile = FlutterProject.current().directory.childFile(localPath); targetFile.deleteSync(); } // Update the migrate config files to reflect latest migration. if (_verbose) { logger.printStatus('Updating .migrate_configs'); } final FlutterProjectMetadata metadata = FlutterProjectMetadata(project.directory.childFile('.metadata'), logger); final FlutterVersion version = FlutterVersion(workingDirectory: project.directory.absolute.path); final String currentGitHash = version.frameworkRevision; metadata.migrateConfig.populate( projectDirectory: project.directory, currentRevision: currentGitHash, logger: logger, ); // Clean up the working directory final bool keepWorkingDirectory = boolArg('keep-working-directory') ?? false; if (!keepWorkingDirectory) { stagingDirectory.deleteSync(recursive: true); } // Detect pub dependency locking. Run flutter pub upgrade --major-versions await updatePubspecDependencies(project, migrateUtils, logger, terminal); // Detect gradle lockfiles in android directory. Delete lockfiles and regenerate with ./gradlew tasks (any gradle task that requires a build). await updateGradleDependencyLocking(project, migrateUtils, logger, terminal, _verbose, fileSystem); logger.printStatus('Migration complete. You may use commands like `git ' 'status`, `git diff` and `git restore ` to continue ' 'working with the migrated files.'); return const FlutterCommandResult(ExitStatus.success); } }