Skip to content

Commit 90a8b05

Browse files
authored
[flutter_tools] MigrateUtils and MigrateManifest classes (#101937)
1 parent 93cce92 commit 90a8b05

File tree

6 files changed

+1281
-0
lines changed

6 files changed

+1281
-0
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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 '../migrate/migrate_utils.dart';
12+
import '../runner/flutter_command.dart';
13+
// TODO(garyq): Add each of these back in as they land.
14+
// import 'migrate_abandon.dart';
15+
// import 'migrate_apply.dart';
16+
// import 'migrate_resolve_conflicts.dart';
17+
// import 'migrate_start.dart';
18+
// import 'migrate_status.dart';
19+
20+
/// Base command for the migration tool.
21+
class MigrateCommand extends FlutterCommand {
22+
MigrateCommand({
23+
// bool verbose = false,
24+
required this.logger,
25+
// TODO(garyq): Add each of these back in as they land.
26+
// required FileSystem fileSystem,
27+
// required Terminal terminal,
28+
// required Platform platform,
29+
// required ProcessManager processManager,
30+
}) {
31+
// TODO(garyq): Add each of these back in as they land.
32+
// addSubcommand(MigrateAbandonCommand(logger: logger, fileSystem: fileSystem, terminal: terminal, platform: platform, processManager: processManager));
33+
// addSubcommand(MigrateApplyCommand(verbose: verbose, logger: logger, fileSystem: fileSystem, terminal: terminal, platform: platform, processManager: processManager));
34+
// addSubcommand(MigrateResolveConflictsCommand(logger: logger, fileSystem: fileSystem, terminal: terminal));
35+
// addSubcommand(MigrateStartCommand(verbose: verbose, logger: logger, fileSystem: fileSystem, platform: platform, processManager: processManager));
36+
// addSubcommand(MigrateStatusCommand(verbose: verbose, logger: logger, fileSystem: fileSystem, platform: platform, processManager: processManager));
37+
}
38+
39+
final Logger logger;
40+
41+
@override
42+
final String name = 'migrate';
43+
44+
@override
45+
final String description = 'Migrates flutter generated project files to the current flutter version';
46+
47+
@override
48+
String get category => FlutterCommandCategory.project;
49+
50+
@override
51+
Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{};
52+
53+
@override
54+
Future<FlutterCommandResult> runCommand() async {
55+
return const FlutterCommandResult(ExitStatus.fail);
56+
}
57+
}
58+
59+
Future<bool> gitRepoExists(String projectDirectory, Logger logger, MigrateUtils migrateUtils) async {
60+
if (await migrateUtils.isGitRepo(projectDirectory)) {
61+
return true;
62+
}
63+
logger.printStatus('Project is not a git repo. Please initialize a git repo and try again.');
64+
printCommandText('git init', logger);
65+
return false;
66+
}
67+
68+
Future<bool> hasUncommittedChanges(String projectDirectory, Logger logger, MigrateUtils migrateUtils) async {
69+
if (await migrateUtils.hasUncommittedChanges(projectDirectory)) {
70+
logger.printStatus('There are uncommitted changes in your project. Please git commit, abandon, or stash your changes before trying again.');
71+
return true;
72+
}
73+
return false;
74+
}
75+
76+
/// Prints a command to logger with appropriate formatting.
77+
void printCommandText(String command, Logger logger) {
78+
logger.printStatus(
79+
'\n\$ $command\n',
80+
color: TerminalColor.grey,
81+
indent: 4,
82+
newline: false,
83+
);
84+
}
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
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:yaml/yaml.dart';
6+
7+
import '../base/file_system.dart';
8+
import '../base/logger.dart';
9+
import '../base/terminal.dart';
10+
import 'migrate_result.dart';
11+
import 'migrate_utils.dart';
12+
13+
const String _kMergedFilesKey = 'merged_files';
14+
const String _kConflictFilesKey = 'conflict_files';
15+
const String _kAddedFilesKey = 'added_files';
16+
const String _kDeletedFilesKey = 'deleted_files';
17+
18+
/// Represents the manifest file that tracks the contents of the current
19+
/// migration working directory.
20+
///
21+
/// This manifest file is created with the MigrateResult of a computeMigration run.
22+
class MigrateManifest {
23+
/// Creates a new manifest from a MigrateResult.
24+
MigrateManifest({
25+
required this.migrateRootDir,
26+
required this.migrateResult,
27+
});
28+
29+
/// Parses an existing migrate manifest.
30+
MigrateManifest.fromFile(File manifestFile) : migrateResult = MigrateResult.empty(), migrateRootDir = manifestFile.parent {
31+
final Object? yamlContents = loadYaml(manifestFile.readAsStringSync());
32+
if (yamlContents is! YamlMap) {
33+
throw Exception('Invalid .migrate_manifest file in the migrate working directory. File is not a Yaml map.');
34+
}
35+
final YamlMap map = yamlContents;
36+
bool valid = map.containsKey(_kMergedFilesKey) && map.containsKey(_kConflictFilesKey) && map.containsKey(_kAddedFilesKey) && map.containsKey(_kDeletedFilesKey);
37+
if (!valid) {
38+
throw Exception('Invalid .migrate_manifest file in the migrate working directory. File is missing an entry.');
39+
}
40+
final Object? mergedFilesYaml = map[_kMergedFilesKey];
41+
final Object? conflictFilesYaml = map[_kConflictFilesKey];
42+
final Object? addedFilesYaml = map[_kAddedFilesKey];
43+
final Object? deletedFilesYaml = map[_kDeletedFilesKey];
44+
valid = valid && (mergedFilesYaml is YamlList || mergedFilesYaml == null);
45+
valid = valid && (conflictFilesYaml is YamlList || conflictFilesYaml == null);
46+
valid = valid && (addedFilesYaml is YamlList || addedFilesYaml == null);
47+
valid = valid && (deletedFilesYaml is YamlList || deletedFilesYaml == null);
48+
if (!valid) {
49+
throw Exception('Invalid .migrate_manifest file in the migrate working directory. Entry is not a Yaml list.');
50+
}
51+
if (mergedFilesYaml != null) {
52+
for (final Object? localPath in mergedFilesYaml as YamlList) {
53+
if (localPath is String) {
54+
// We can fill the maps with partially dummy data as not all properties are used by the manifest.
55+
migrateResult.mergeResults.add(StringMergeResult.explicit(mergedString: '', hasConflict: false, exitCode: 0, localPath: localPath));
56+
}
57+
}
58+
}
59+
if (conflictFilesYaml != null) {
60+
for (final Object? localPath in conflictFilesYaml as YamlList) {
61+
if (localPath is String) {
62+
migrateResult.mergeResults.add(StringMergeResult.explicit(mergedString: '', hasConflict: true, exitCode: 1, localPath: localPath));
63+
}
64+
}
65+
}
66+
if (addedFilesYaml != null) {
67+
for (final Object? localPath in addedFilesYaml as YamlList) {
68+
if (localPath is String) {
69+
migrateResult.addedFiles.add(FilePendingMigration(localPath, migrateRootDir.childFile(localPath)));
70+
}
71+
}
72+
}
73+
if (deletedFilesYaml != null) {
74+
for (final Object? localPath in deletedFilesYaml as YamlList) {
75+
if (localPath is String) {
76+
migrateResult.deletedFiles.add(FilePendingMigration(localPath, migrateRootDir.childFile(localPath)));
77+
}
78+
}
79+
}
80+
}
81+
82+
final Directory migrateRootDir;
83+
final MigrateResult migrateResult;
84+
85+
/// A list of local paths of files that require conflict resolution.
86+
List<String> get conflictFiles {
87+
final List<String> output = <String>[];
88+
for (final MergeResult result in migrateResult.mergeResults) {
89+
if (result.hasConflict) {
90+
output.add(result.localPath);
91+
}
92+
}
93+
return output;
94+
}
95+
96+
/// A list of local paths of files that require conflict resolution.
97+
List<String> remainingConflictFiles(Directory workingDir) {
98+
final List<String> output = <String>[];
99+
for (final String localPath in conflictFiles) {
100+
if (!_conflictsResolved(workingDir.childFile(localPath).readAsStringSync())) {
101+
output.add(localPath);
102+
}
103+
}
104+
return output;
105+
}
106+
107+
// A list of local paths of files that had conflicts and are now fully resolved.
108+
List<String> resolvedConflictFiles(Directory workingDir) {
109+
final List<String> output = <String>[];
110+
for (final String localPath in conflictFiles) {
111+
if (_conflictsResolved(workingDir.childFile(localPath).readAsStringSync())) {
112+
output.add(localPath);
113+
}
114+
}
115+
return output;
116+
}
117+
118+
/// A list of local paths of files that were automatically merged.
119+
List<String> get mergedFiles {
120+
final List<String> output = <String>[];
121+
for (final MergeResult result in migrateResult.mergeResults) {
122+
if (!result.hasConflict) {
123+
output.add(result.localPath);
124+
}
125+
}
126+
return output;
127+
}
128+
129+
/// A list of local paths of files that were newly added.
130+
List<String> get addedFiles {
131+
final List<String> output = <String>[];
132+
for (final FilePendingMigration file in migrateResult.addedFiles) {
133+
output.add(file.localPath);
134+
}
135+
return output;
136+
}
137+
138+
/// A list of local paths of files that are marked for deletion.
139+
List<String> get deletedFiles {
140+
final List<String> output = <String>[];
141+
for (final FilePendingMigration file in migrateResult.deletedFiles) {
142+
output.add(file.localPath);
143+
}
144+
return output;
145+
}
146+
147+
/// Returns the manifest file given a migration workind directory.
148+
static File getManifestFileFromDirectory(Directory workingDir) {
149+
return workingDir.childFile('.migrate_manifest');
150+
}
151+
152+
/// Writes the manifest yaml file in the working directory.
153+
void writeFile() {
154+
final StringBuffer mergedFileManifestContents = StringBuffer();
155+
final StringBuffer conflictFilesManifestContents = StringBuffer();
156+
for (final MergeResult result in migrateResult.mergeResults) {
157+
if (result.hasConflict) {
158+
conflictFilesManifestContents.write(' - ${result.localPath}\n');
159+
} else {
160+
mergedFileManifestContents.write(' - ${result.localPath}\n');
161+
}
162+
}
163+
164+
final StringBuffer newFileManifestContents = StringBuffer();
165+
for (final String localPath in addedFiles) {
166+
newFileManifestContents.write(' - $localPath\n)');
167+
}
168+
169+
final StringBuffer deletedFileManifestContents = StringBuffer();
170+
for (final String localPath in deletedFiles) {
171+
deletedFileManifestContents.write(' - $localPath\n');
172+
}
173+
174+
final String migrateManifestContents = 'merged_files:\n${mergedFileManifestContents.toString()}conflict_files:\n${conflictFilesManifestContents.toString()}added_files:\n${newFileManifestContents.toString()}deleted_files:\n${deletedFileManifestContents.toString()}';
175+
final File migrateManifest = getManifestFileFromDirectory(migrateRootDir);
176+
migrateManifest.createSync(recursive: true);
177+
migrateManifest.writeAsStringSync(migrateManifestContents, flush: true);
178+
}
179+
}
180+
181+
/// Returns true if the file does not contain any git conflict markers.
182+
bool _conflictsResolved(String contents) {
183+
if (contents.contains('>>>>>>>') && contents.contains('=======') && contents.contains('<<<<<<<')) {
184+
return false;
185+
}
186+
return true;
187+
}
188+
189+
/// Returns true if the migration working directory has all conflicts resolved and prints the migration status.
190+
///
191+
/// The migration status printout lists all added, deleted, merged, and conflicted files.
192+
bool checkAndPrintMigrateStatus(MigrateManifest manifest, Directory workingDir, {bool warnConflict = false, Logger? logger}) {
193+
final StringBuffer printout = StringBuffer();
194+
final StringBuffer redPrintout = StringBuffer();
195+
bool result = true;
196+
final List<String> remainingConflicts = <String>[];
197+
final List<String> mergedFiles = <String>[];
198+
for (final String localPath in manifest.conflictFiles) {
199+
if (!_conflictsResolved(workingDir.childFile(localPath).readAsStringSync())) {
200+
remainingConflicts.add(localPath);
201+
} else {
202+
mergedFiles.add(localPath);
203+
}
204+
}
205+
206+
mergedFiles.addAll(manifest.mergedFiles);
207+
if (manifest.addedFiles.isNotEmpty) {
208+
printout.write('Added files:\n');
209+
for (final String localPath in manifest.addedFiles) {
210+
printout.write(' - $localPath\n');
211+
}
212+
}
213+
if (manifest.deletedFiles.isNotEmpty) {
214+
printout.write('Deleted files:\n');
215+
for (final String localPath in manifest.deletedFiles) {
216+
printout.write(' - $localPath\n');
217+
}
218+
}
219+
if (mergedFiles.isNotEmpty) {
220+
printout.write('Modified files:\n');
221+
for (final String localPath in mergedFiles) {
222+
printout.write(' - $localPath\n');
223+
}
224+
}
225+
if (remainingConflicts.isNotEmpty) {
226+
if (warnConflict) {
227+
printout.write('Unable to apply migration. The following files in the migration working directory still have unresolved conflicts:');
228+
} else {
229+
printout.write('Merge conflicted files:');
230+
}
231+
for (final String localPath in remainingConflicts) {
232+
redPrintout.write(' - $localPath\n');
233+
}
234+
result = false;
235+
}
236+
if (logger != null) {
237+
logger.printStatus(printout.toString());
238+
logger.printStatus(redPrintout.toString(), color: TerminalColor.red, newline: false);
239+
}
240+
return result;
241+
}

0 commit comments

Comments
 (0)