Skip to content

Commit 696110b

Browse files
authored
Migrate apply (#102787)
1 parent e1aad36 commit 696110b

File tree

4 files changed

+560
-0
lines changed

4 files changed

+560
-0
lines changed

packages/flutter_tools/lib/src/commands/migrate.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import '../base/terminal.dart';
1111
import '../migrate/migrate_utils.dart';
1212
import '../runner/flutter_command.dart';
1313
import 'migrate_abandon.dart';
14+
import 'migrate_apply.dart';
1415
import 'migrate_status.dart';
1516

1617
/// Base command for the migration tool.
@@ -37,6 +38,14 @@ class MigrateCommand extends FlutterCommand {
3738
platform: platform,
3839
processManager: processManager
3940
));
41+
addSubcommand(MigrateApplyCommand(
42+
verbose: verbose,
43+
logger: logger,
44+
fileSystem: fileSystem,
45+
terminal: terminal,
46+
platform: platform,
47+
processManager: processManager
48+
));
4049
}
4150

4251
final Logger logger;
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:process/process.dart';
6+
7+
import '../base/file_system.dart';
8+
import '../base/logger.dart';
9+
import '../base/platform.dart';
10+
import '../base/terminal.dart';
11+
import '../flutter_project_metadata.dart';
12+
import '../migrate/migrate_manifest.dart';
13+
import '../migrate/migrate_update_locks.dart';
14+
import '../migrate/migrate_utils.dart';
15+
import '../project.dart';
16+
import '../runner/flutter_command.dart';
17+
import '../version.dart';
18+
import 'migrate.dart';
19+
20+
/// Migrate subcommand that checks the migrate working directory for unresolved conflicts and
21+
/// applies the staged changes to the project.
22+
class MigrateApplyCommand extends FlutterCommand {
23+
MigrateApplyCommand({
24+
bool verbose = false,
25+
required this.logger,
26+
required this.fileSystem,
27+
required this.terminal,
28+
required Platform platform,
29+
required ProcessManager processManager,
30+
}) : _verbose = verbose,
31+
migrateUtils = MigrateUtils(
32+
logger: logger,
33+
fileSystem: fileSystem,
34+
platform: platform,
35+
processManager: processManager,
36+
) {
37+
requiresPubspecYaml();
38+
argParser.addOption(
39+
'staging-directory',
40+
help: 'Specifies the custom migration working directory used to stage '
41+
'and edit proposed changes. This path can be absolute or relative '
42+
'to the flutter project root. This defaults to '
43+
'`$kDefaultMigrateStagingDirectoryName`',
44+
valueHelp: 'path',
45+
);
46+
argParser.addOption(
47+
'project-directory',
48+
help: 'The root directory of the flutter project. This defaults to the '
49+
'current working directory if omitted.',
50+
valueHelp: 'path',
51+
);
52+
argParser.addFlag(
53+
'force',
54+
abbr: 'f',
55+
help: 'Ignore unresolved merge conflicts and uncommitted changes and '
56+
'apply staged changes by force.',
57+
);
58+
argParser.addFlag(
59+
'keep-working-directory',
60+
help: 'Do not delete the working directory.',
61+
);
62+
}
63+
64+
final bool _verbose;
65+
66+
final Logger logger;
67+
68+
final FileSystem fileSystem;
69+
70+
final Terminal terminal;
71+
72+
final MigrateUtils migrateUtils;
73+
74+
@override
75+
final String name = 'apply';
76+
77+
@override
78+
final String description = r'Accepts the changes produced by `$ flutter '
79+
'migrate start` and copies the changed files into '
80+
'your project files. All merge conflicts should '
81+
'be resolved before apply will complete '
82+
'successfully. If conflicts still exist, this '
83+
'command will print the remaining conflicted files.';
84+
85+
@override
86+
String get category => FlutterCommandCategory.project;
87+
88+
@override
89+
Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{};
90+
91+
@override
92+
Future<FlutterCommandResult> runCommand() async {
93+
final String? projectDirectory = stringArg('project-directory');
94+
final FlutterProjectFactory flutterProjectFactory = FlutterProjectFactory(logger: logger, fileSystem: fileSystem);
95+
final FlutterProject project = projectDirectory == null
96+
? FlutterProject.current()
97+
: flutterProjectFactory.fromDirectory(fileSystem.directory(projectDirectory));
98+
99+
if (!await gitRepoExists(project.directory.path, logger, migrateUtils)) {
100+
logger.printStatus('No git repo found. Please run in a project with an '
101+
'initialized git repo or initialize one with:');
102+
printCommandText('git init', logger);
103+
return const FlutterCommandResult(ExitStatus.fail);
104+
}
105+
106+
final bool force = boolArg('force') ?? false;
107+
108+
Directory stagingDirectory = project.directory.childDirectory(kDefaultMigrateStagingDirectoryName);
109+
final String? customStagingDirectoryPath = stringArg('staging-directory');
110+
if (customStagingDirectoryPath != null) {
111+
if (fileSystem.path.isAbsolute(customStagingDirectoryPath)) {
112+
stagingDirectory = fileSystem.directory(customStagingDirectoryPath);
113+
} else {
114+
stagingDirectory = project.directory.childDirectory(customStagingDirectoryPath);
115+
}
116+
}
117+
if (!stagingDirectory.existsSync()) {
118+
logger.printStatus('No migration in progress at $stagingDirectory. Please run:');
119+
printCommandText('flutter migrate start', logger);
120+
return const FlutterCommandResult(ExitStatus.fail);
121+
}
122+
123+
final File manifestFile = MigrateManifest.getManifestFileFromDirectory(stagingDirectory);
124+
final MigrateManifest manifest = MigrateManifest.fromFile(manifestFile);
125+
if (!checkAndPrintMigrateStatus(manifest, stagingDirectory, warnConflict: true, logger: logger) && !force) {
126+
logger.printStatus('Conflicting files found. Resolve these conflicts and try again.');
127+
logger.printStatus('Guided conflict resolution wizard:');
128+
printCommandText('flutter migrate resolve-conflicts', logger);
129+
return const FlutterCommandResult(ExitStatus.fail);
130+
}
131+
132+
if (await hasUncommittedChanges(project.directory.path, logger, migrateUtils) && !force) {
133+
return const FlutterCommandResult(ExitStatus.fail);
134+
}
135+
136+
logger.printStatus('Applying migration.');
137+
// Copy files from working directory to project root
138+
final List<String> allFilesToCopy = <String>[];
139+
allFilesToCopy.addAll(manifest.mergedFiles);
140+
allFilesToCopy.addAll(manifest.conflictFiles);
141+
allFilesToCopy.addAll(manifest.addedFiles);
142+
if (allFilesToCopy.isNotEmpty && _verbose) {
143+
logger.printStatus('Modifying ${allFilesToCopy.length} files.', indent: 2);
144+
}
145+
for (final String localPath in allFilesToCopy) {
146+
if (_verbose) {
147+
logger.printStatus('Writing $localPath');
148+
}
149+
final File workingFile = stagingDirectory.childFile(localPath);
150+
final File targetFile = project.directory.childFile(localPath);
151+
if (!workingFile.existsSync()) {
152+
continue;
153+
}
154+
155+
if (!targetFile.existsSync()) {
156+
targetFile.createSync(recursive: true);
157+
}
158+
try {
159+
targetFile.writeAsStringSync(workingFile.readAsStringSync(), flush: true);
160+
} on FileSystemException {
161+
targetFile.writeAsBytesSync(workingFile.readAsBytesSync(), flush: true);
162+
}
163+
}
164+
// Delete files slated for deletion.
165+
if (manifest.deletedFiles.isNotEmpty) {
166+
logger.printStatus('Deleting ${manifest.deletedFiles.length} files.', indent: 2);
167+
}
168+
for (final String localPath in manifest.deletedFiles) {
169+
final File targetFile = FlutterProject.current().directory.childFile(localPath);
170+
targetFile.deleteSync();
171+
}
172+
173+
// Update the migrate config files to reflect latest migration.
174+
if (_verbose) {
175+
logger.printStatus('Updating .migrate_configs');
176+
}
177+
final FlutterProjectMetadata metadata = FlutterProjectMetadata(project.directory.childFile('.metadata'), logger);
178+
final FlutterVersion version = FlutterVersion(workingDirectory: project.directory.absolute.path);
179+
180+
final String currentGitHash = version.frameworkRevision;
181+
metadata.migrateConfig.populate(
182+
projectDirectory: project.directory,
183+
currentRevision: currentGitHash,
184+
logger: logger,
185+
);
186+
187+
// Clean up the working directory
188+
final bool keepWorkingDirectory = boolArg('keep-working-directory') ?? false;
189+
if (!keepWorkingDirectory) {
190+
stagingDirectory.deleteSync(recursive: true);
191+
}
192+
193+
// Detect pub dependency locking. Run flutter pub upgrade --major-versions
194+
await updatePubspecDependencies(project, migrateUtils, logger, terminal);
195+
196+
// Detect gradle lockfiles in android directory. Delete lockfiles and regenerate with ./gradlew tasks (any gradle task that requires a build).
197+
await updateGradleDependencyLocking(project, migrateUtils, logger, terminal, _verbose, fileSystem);
198+
199+
logger.printStatus('Migration complete. You may use commands like `git '
200+
'status`, `git diff` and `git restore <file>` to continue '
201+
'working with the migrated files.');
202+
return const FlutterCommandResult(ExitStatus.success);
203+
}
204+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
6+
import '../base/file_system.dart';
7+
import '../base/logger.dart';
8+
import '../base/terminal.dart';
9+
import '../project.dart';
10+
import 'migrate_utils.dart';
11+
12+
/// Checks if the project uses pubspec dependency locking and prompts if
13+
/// the pub upgrade should be run.
14+
Future<void> updatePubspecDependencies(
15+
FlutterProject flutterProject,
16+
MigrateUtils migrateUtils,
17+
Logger logger,
18+
Terminal terminal
19+
) async {
20+
final File pubspecFile = flutterProject.directory.childFile('pubspec.yaml');
21+
if (!pubspecFile.existsSync()) {
22+
return;
23+
}
24+
if (!pubspecFile.readAsStringSync().contains('# THIS LINE IS AUTOGENERATED')) {
25+
return;
26+
}
27+
logger.printStatus('\nDart dependency locking detected in pubspec.yaml.');
28+
terminal.usesTerminalUi = true;
29+
String selection = 'y';
30+
selection = await terminal.promptForCharInput(
31+
<String>['y', 'n'],
32+
logger: logger,
33+
prompt: 'Do you want the tool to run `flutter pub upgrade --major-versions`? (y)es, (n)o',
34+
defaultChoiceIndex: 1,
35+
);
36+
if (selection == 'y') {
37+
// Runs `flutter pub upgrade --major-versions`
38+
await migrateUtils.flutterPubUpgrade(flutterProject.directory.path);
39+
}
40+
}
41+
42+
/// Checks if gradle dependency locking is used and prompts the developer to
43+
/// remove and back up the gradle dependency lockfile.
44+
Future<void> updateGradleDependencyLocking(
45+
FlutterProject flutterProject,
46+
MigrateUtils migrateUtils,
47+
Logger logger,
48+
Terminal terminal,
49+
bool verbose,
50+
FileSystem fileSystem
51+
) async {
52+
final Directory androidDir = flutterProject.directory.childDirectory('android');
53+
if (!androidDir.existsSync()) {
54+
return;
55+
}
56+
final List<FileSystemEntity> androidFiles = androidDir.listSync();
57+
final List<File> lockfiles = <File>[];
58+
final List<String> backedUpFilePaths = <String>[];
59+
for (final FileSystemEntity entity in androidFiles) {
60+
if (entity is! File) {
61+
continue;
62+
}
63+
final File file = entity.absolute;
64+
// Don't re-handle backed up lockfiles.
65+
if (file.path.contains('_backup_')) {
66+
continue;
67+
}
68+
try {
69+
// lockfiles generated by gradle start with this prefix.
70+
if (file.readAsStringSync().startsWith(
71+
'# This is a Gradle generated file for dependency locking.\n# '
72+
'Manual edits can break the build and are not advised.\n# This '
73+
'file is expected to be part of source control.')) {
74+
lockfiles.add(file);
75+
}
76+
} on FileSystemException {
77+
if (verbose) {
78+
logger.printStatus('Unable to check ${file.path}');
79+
}
80+
}
81+
}
82+
if (lockfiles.isNotEmpty) {
83+
logger.printStatus('\nGradle dependency locking detected.');
84+
logger.printStatus('Flutter can backup the lockfiles and regenerate updated '
85+
'lockfiles.');
86+
terminal.usesTerminalUi = true;
87+
String selection = 'y';
88+
selection = await terminal.promptForCharInput(
89+
<String>['y', 'n'],
90+
logger: logger,
91+
prompt: 'Do you want the tool to update locked dependencies? (y)es, (n)o',
92+
defaultChoiceIndex: 1,
93+
);
94+
if (selection == 'y') {
95+
for (final File file in lockfiles) {
96+
int counter = 0;
97+
while (true) {
98+
final String newPath = '${file.absolute.path}_backup_$counter';
99+
if (!fileSystem.file(newPath).existsSync()) {
100+
file.renameSync(newPath);
101+
backedUpFilePaths.add(newPath);
102+
break;
103+
} else {
104+
counter++;
105+
}
106+
}
107+
}
108+
// Runs `./gradlew tasks`in the project's android directory.
109+
await migrateUtils.gradlewTasks(flutterProject.directory.childDirectory('android').path);
110+
logger.printStatus('Old lockfiles renamed to:');
111+
for (final String path in backedUpFilePaths) {
112+
logger.printStatus(path, color: TerminalColor.grey, indent: 2);
113+
}
114+
}
115+
}
116+
}

0 commit comments

Comments
 (0)