Unverified Commit 696110ba authored by Gary Qian's avatar Gary Qian Committed by GitHub

Migrate apply (#102787)

parent e1aad362
......@@ -11,6 +11,7 @@ import '../base/terminal.dart';
import '../migrate/migrate_utils.dart';
import '../runner/flutter_command.dart';
import 'migrate_abandon.dart';
import 'migrate_apply.dart';
import 'migrate_status.dart';
/// Base command for the migration tool.
......@@ -37,6 +38,14 @@ class MigrateCommand extends FlutterCommand {
platform: platform,
processManager: processManager
));
addSubcommand(MigrateApplyCommand(
verbose: verbose,
logger: logger,
fileSystem: fileSystem,
terminal: terminal,
platform: platform,
processManager: processManager
));
}
final Logger logger;
......
// 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<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{};
@override
Future<FlutterCommandResult> 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<String> allFilesToCopy = <String>[];
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 <file>` to continue '
'working with the migrated files.');
return const FlutterCommandResult(ExitStatus.success);
}
}
// 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 '../base/file_system.dart';
import '../base/logger.dart';
import '../base/terminal.dart';
import '../project.dart';
import 'migrate_utils.dart';
/// Checks if the project uses pubspec dependency locking and prompts if
/// the pub upgrade should be run.
Future<void> updatePubspecDependencies(
FlutterProject flutterProject,
MigrateUtils migrateUtils,
Logger logger,
Terminal terminal
) async {
final File pubspecFile = flutterProject.directory.childFile('pubspec.yaml');
if (!pubspecFile.existsSync()) {
return;
}
if (!pubspecFile.readAsStringSync().contains('# THIS LINE IS AUTOGENERATED')) {
return;
}
logger.printStatus('\nDart dependency locking detected in pubspec.yaml.');
terminal.usesTerminalUi = true;
String selection = 'y';
selection = await terminal.promptForCharInput(
<String>['y', 'n'],
logger: logger,
prompt: 'Do you want the tool to run `flutter pub upgrade --major-versions`? (y)es, (n)o',
defaultChoiceIndex: 1,
);
if (selection == 'y') {
// Runs `flutter pub upgrade --major-versions`
await migrateUtils.flutterPubUpgrade(flutterProject.directory.path);
}
}
/// Checks if gradle dependency locking is used and prompts the developer to
/// remove and back up the gradle dependency lockfile.
Future<void> updateGradleDependencyLocking(
FlutterProject flutterProject,
MigrateUtils migrateUtils,
Logger logger,
Terminal terminal,
bool verbose,
FileSystem fileSystem
) async {
final Directory androidDir = flutterProject.directory.childDirectory('android');
if (!androidDir.existsSync()) {
return;
}
final List<FileSystemEntity> androidFiles = androidDir.listSync();
final List<File> lockfiles = <File>[];
final List<String> backedUpFilePaths = <String>[];
for (final FileSystemEntity entity in androidFiles) {
if (entity is! File) {
continue;
}
final File file = entity.absolute;
// Don't re-handle backed up lockfiles.
if (file.path.contains('_backup_')) {
continue;
}
try {
// lockfiles generated by gradle start with this prefix.
if (file.readAsStringSync().startsWith(
'# This is a Gradle generated file for dependency locking.\n# '
'Manual edits can break the build and are not advised.\n# This '
'file is expected to be part of source control.')) {
lockfiles.add(file);
}
} on FileSystemException {
if (verbose) {
logger.printStatus('Unable to check ${file.path}');
}
}
}
if (lockfiles.isNotEmpty) {
logger.printStatus('\nGradle dependency locking detected.');
logger.printStatus('Flutter can backup the lockfiles and regenerate updated '
'lockfiles.');
terminal.usesTerminalUi = true;
String selection = 'y';
selection = await terminal.promptForCharInput(
<String>['y', 'n'],
logger: logger,
prompt: 'Do you want the tool to update locked dependencies? (y)es, (n)o',
defaultChoiceIndex: 1,
);
if (selection == 'y') {
for (final File file in lockfiles) {
int counter = 0;
while (true) {
final String newPath = '${file.absolute.path}_backup_$counter';
if (!fileSystem.file(newPath).existsSync()) {
file.renameSync(newPath);
backedUpFilePaths.add(newPath);
break;
} else {
counter++;
}
}
}
// Runs `./gradlew tasks`in the project's android directory.
await migrateUtils.gradlewTasks(flutterProject.directory.childDirectory('android').path);
logger.printStatus('Old lockfiles renamed to:');
for (final String path in backedUpFilePaths) {
logger.printStatus(path, color: TerminalColor.grey, indent: 2);
}
}
}
}
// 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.
// @dart = 2.8
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/terminal.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/migrate.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/migrate/migrate_utils.dart';
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/test_flutter_command_runner.dart';
void main() {
FileSystem fileSystem;
BufferLogger logger;
Platform platform;
Terminal terminal;
ProcessManager processManager;
Directory appDir;
setUp(() {
fileSystem = globals.localFileSystem;
appDir = fileSystem.systemTempDirectory.createTempSync('apptestdir');
logger = BufferLogger.test();
platform = FakePlatform();
terminal = Terminal.test();
processManager = globals.processManager;
});
setUpAll(() {
Cache.disableLocking();
});
tearDown(() async {
tryToDelete(appDir);
});
testUsingContext('Apply produces all outputs', () async {
final MigrateCommand command = MigrateCommand(
verbose: true,
logger: logger,
fileSystem: fileSystem,
terminal: terminal,
platform: platform,
processManager: processManager,
);
final Directory workingDir = appDir.childDirectory(kDefaultMigrateStagingDirectoryName);
appDir.childFile('lib/main.dart').createSync(recursive: true);
final File pubspecOriginal = appDir.childFile('pubspec.yaml');
pubspecOriginal.createSync();
pubspecOriginal.writeAsStringSync('''
name: originalname
description: A new Flutter project.
version: 1.0.0+1
environment:
sdk: '>=2.18.0-58.0.dev <3.0.0'
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
uses-material-design: true''', flush: true);
final File gitignore = appDir.childFile('.gitignore');
gitignore.createSync();
gitignore.writeAsStringSync(kDefaultMigrateStagingDirectoryName, flush: true);
logger.clear();
await createTestCommandRunner(command).run(
<String>[
'migrate',
'apply',
'--staging-directory=${workingDir.path}',
'--project-directory=${appDir.path}',
]
);
expect(logger.statusText, contains('Project is not a git repo. Please initialize a git repo and try again.'));
await processManager.run(<String>['git', 'init'], workingDirectory: appDir.path);
logger.clear();
await createTestCommandRunner(command).run(
<String>[
'migrate',
'apply',
'--staging-directory=${workingDir.path}',
'--project-directory=${appDir.path}',
]
);
expect(logger.statusText, contains('No migration in progress'));
final File pubspecModified = workingDir.childFile('pubspec.yaml');
pubspecModified.createSync(recursive: true);
pubspecModified.writeAsStringSync('''
name: newname
description: new description of the test project
version: 1.0.0+1
environment:
sdk: '>=2.18.0-58.0.dev <3.0.0'
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
uses-material-design: false
# EXTRALINE:''', flush: true);
final File addedFile = workingDir.childFile('added.file');
addedFile.createSync(recursive: true);
addedFile.writeAsStringSync('new file contents');
final File manifestFile = workingDir.childFile('.migrate_manifest');
manifestFile.createSync(recursive: true);
manifestFile.writeAsStringSync('''
merged_files:
- pubspec.yaml
conflict_files:
- conflict/conflict.file
added_files:
- added.file
deleted_files:
''');
// Add conflict file
final File conflictFile = workingDir.childDirectory('conflict').childFile('conflict.file');
conflictFile.createSync(recursive: true);
conflictFile.writeAsStringSync('''
line1
<<<<<<< /conflcit/conflict.file
line2
=======
linetwo
>>>>>>> /var/folders/md/gm0zgfcj07vcsj6jkh_mp_wh00ff02/T/flutter_tools.4Xdep8/generatedTargetTemplatetlN44S/conflict/conflict.file
line3
''', flush: true);
final File conflictFileOriginal = appDir.childDirectory('conflict').childFile('conflict.file');
conflictFileOriginal.createSync(recursive: true);
conflictFileOriginal.writeAsStringSync('''
line1
line2
line3
''', flush: true);
logger.clear();
await createTestCommandRunner(command).run(
<String>[
'migrate',
'apply',
'--staging-directory=${workingDir.path}',
'--project-directory=${appDir.path}',
]
);
expect(logger.statusText, contains(r'''
Added files:
- added.file
Modified files:
- pubspec.yaml
Unable to apply migration. The following files in the migration working directory still have unresolved conflicts:
- conflict/conflict.file
Conflicting files found. Resolve these conflicts and try again.
Guided conflict resolution wizard:
$ flutter migrate resolve-conflicts'''));
conflictFile.writeAsStringSync('''
line1
linetwo
line3
''', flush: true);
logger.clear();
await createTestCommandRunner(command).run(
<String>[
'migrate',
'apply',
'--staging-directory=${workingDir.path}',
'--project-directory=${appDir.path}',
]
);
expect(logger.statusText, contains('There are uncommitted changes in your project. Please git commit, abandon, or stash your changes before trying again.'));
await processManager.run(<String>['git', 'add', '.'], workingDirectory: appDir.path);
await processManager.run(<String>['git', 'commit', '-m', 'Initial commit'], workingDirectory: appDir.path);
logger.clear();
await createTestCommandRunner(command).run(
<String>[
'migrate',
'apply',
'--staging-directory=${workingDir.path}',
'--project-directory=${appDir.path}',
]
);
expect(logger.statusText, contains(r'''
Added files:
- added.file
Modified files:
- conflict/conflict.file
- pubspec.yaml
Applying migration.
Modifying 3 files.
Writing pubspec.yaml
Writing conflict/conflict.file
Writing added.file
Updating .migrate_configs
Migration complete. You may use commands like `git status`, `git diff` and `git restore <file>` to continue working with the migrated files.'''));
expect(pubspecOriginal.readAsStringSync(), contains('# EXTRALINE'));
expect(conflictFileOriginal.readAsStringSync(), contains('linetwo'));
expect(appDir.childFile('added.file').existsSync(), true);
expect(appDir.childFile('added.file').readAsStringSync(), contains('new file contents'));
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
Platform: () => platform,
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment