Unverified Commit 90a8b056 authored by Gary Qian's avatar Gary Qian Committed by GitHub

[flutter_tools] MigrateUtils and MigrateManifest classes (#101937)

parent 93cce92e
// 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 '../migrate/migrate_utils.dart';
import '../runner/flutter_command.dart';
// TODO(garyq): Add each of these back in as they land.
// import 'migrate_abandon.dart';
// import 'migrate_apply.dart';
// import 'migrate_resolve_conflicts.dart';
// import 'migrate_start.dart';
// import 'migrate_status.dart';
/// Base command for the migration tool.
class MigrateCommand extends FlutterCommand {
MigrateCommand({
// bool verbose = false,
required this.logger,
// TODO(garyq): Add each of these back in as they land.
// required FileSystem fileSystem,
// required Terminal terminal,
// required Platform platform,
// required ProcessManager processManager,
}) {
// TODO(garyq): Add each of these back in as they land.
// addSubcommand(MigrateAbandonCommand(logger: logger, fileSystem: fileSystem, terminal: terminal, platform: platform, processManager: processManager));
// addSubcommand(MigrateApplyCommand(verbose: verbose, logger: logger, fileSystem: fileSystem, terminal: terminal, platform: platform, processManager: processManager));
// addSubcommand(MigrateResolveConflictsCommand(logger: logger, fileSystem: fileSystem, terminal: terminal));
// addSubcommand(MigrateStartCommand(verbose: verbose, logger: logger, fileSystem: fileSystem, platform: platform, processManager: processManager));
// addSubcommand(MigrateStatusCommand(verbose: verbose, logger: logger, fileSystem: fileSystem, platform: platform, processManager: processManager));
}
final Logger logger;
@override
final String name = 'migrate';
@override
final String description = 'Migrates flutter generated project files to the current flutter version';
@override
String get category => FlutterCommandCategory.project;
@override
Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{};
@override
Future<FlutterCommandResult> runCommand() async {
return const FlutterCommandResult(ExitStatus.fail);
}
}
Future<bool> gitRepoExists(String projectDirectory, Logger logger, MigrateUtils migrateUtils) async {
if (await migrateUtils.isGitRepo(projectDirectory)) {
return true;
}
logger.printStatus('Project is not a git repo. Please initialize a git repo and try again.');
printCommandText('git init', logger);
return false;
}
Future<bool> hasUncommittedChanges(String projectDirectory, Logger logger, MigrateUtils migrateUtils) async {
if (await migrateUtils.hasUncommittedChanges(projectDirectory)) {
logger.printStatus('There are uncommitted changes in your project. Please git commit, abandon, or stash your changes before trying again.');
return true;
}
return false;
}
/// Prints a command to logger with appropriate formatting.
void printCommandText(String command, Logger logger) {
logger.printStatus(
'\n\$ $command\n',
color: TerminalColor.grey,
indent: 4,
newline: false,
);
}
// 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:yaml/yaml.dart';
import '../base/file_system.dart';
import '../base/logger.dart';
import '../base/terminal.dart';
import 'migrate_result.dart';
import 'migrate_utils.dart';
const String _kMergedFilesKey = 'merged_files';
const String _kConflictFilesKey = 'conflict_files';
const String _kAddedFilesKey = 'added_files';
const String _kDeletedFilesKey = 'deleted_files';
/// Represents the manifest file that tracks the contents of the current
/// migration working directory.
///
/// This manifest file is created with the MigrateResult of a computeMigration run.
class MigrateManifest {
/// Creates a new manifest from a MigrateResult.
MigrateManifest({
required this.migrateRootDir,
required this.migrateResult,
});
/// Parses an existing migrate manifest.
MigrateManifest.fromFile(File manifestFile) : migrateResult = MigrateResult.empty(), migrateRootDir = manifestFile.parent {
final Object? yamlContents = loadYaml(manifestFile.readAsStringSync());
if (yamlContents is! YamlMap) {
throw Exception('Invalid .migrate_manifest file in the migrate working directory. File is not a Yaml map.');
}
final YamlMap map = yamlContents;
bool valid = map.containsKey(_kMergedFilesKey) && map.containsKey(_kConflictFilesKey) && map.containsKey(_kAddedFilesKey) && map.containsKey(_kDeletedFilesKey);
if (!valid) {
throw Exception('Invalid .migrate_manifest file in the migrate working directory. File is missing an entry.');
}
final Object? mergedFilesYaml = map[_kMergedFilesKey];
final Object? conflictFilesYaml = map[_kConflictFilesKey];
final Object? addedFilesYaml = map[_kAddedFilesKey];
final Object? deletedFilesYaml = map[_kDeletedFilesKey];
valid = valid && (mergedFilesYaml is YamlList || mergedFilesYaml == null);
valid = valid && (conflictFilesYaml is YamlList || conflictFilesYaml == null);
valid = valid && (addedFilesYaml is YamlList || addedFilesYaml == null);
valid = valid && (deletedFilesYaml is YamlList || deletedFilesYaml == null);
if (!valid) {
throw Exception('Invalid .migrate_manifest file in the migrate working directory. Entry is not a Yaml list.');
}
if (mergedFilesYaml != null) {
for (final Object? localPath in mergedFilesYaml as YamlList) {
if (localPath is String) {
// We can fill the maps with partially dummy data as not all properties are used by the manifest.
migrateResult.mergeResults.add(StringMergeResult.explicit(mergedString: '', hasConflict: false, exitCode: 0, localPath: localPath));
}
}
}
if (conflictFilesYaml != null) {
for (final Object? localPath in conflictFilesYaml as YamlList) {
if (localPath is String) {
migrateResult.mergeResults.add(StringMergeResult.explicit(mergedString: '', hasConflict: true, exitCode: 1, localPath: localPath));
}
}
}
if (addedFilesYaml != null) {
for (final Object? localPath in addedFilesYaml as YamlList) {
if (localPath is String) {
migrateResult.addedFiles.add(FilePendingMigration(localPath, migrateRootDir.childFile(localPath)));
}
}
}
if (deletedFilesYaml != null) {
for (final Object? localPath in deletedFilesYaml as YamlList) {
if (localPath is String) {
migrateResult.deletedFiles.add(FilePendingMigration(localPath, migrateRootDir.childFile(localPath)));
}
}
}
}
final Directory migrateRootDir;
final MigrateResult migrateResult;
/// A list of local paths of files that require conflict resolution.
List<String> get conflictFiles {
final List<String> output = <String>[];
for (final MergeResult result in migrateResult.mergeResults) {
if (result.hasConflict) {
output.add(result.localPath);
}
}
return output;
}
/// A list of local paths of files that require conflict resolution.
List<String> remainingConflictFiles(Directory workingDir) {
final List<String> output = <String>[];
for (final String localPath in conflictFiles) {
if (!_conflictsResolved(workingDir.childFile(localPath).readAsStringSync())) {
output.add(localPath);
}
}
return output;
}
// A list of local paths of files that had conflicts and are now fully resolved.
List<String> resolvedConflictFiles(Directory workingDir) {
final List<String> output = <String>[];
for (final String localPath in conflictFiles) {
if (_conflictsResolved(workingDir.childFile(localPath).readAsStringSync())) {
output.add(localPath);
}
}
return output;
}
/// A list of local paths of files that were automatically merged.
List<String> get mergedFiles {
final List<String> output = <String>[];
for (final MergeResult result in migrateResult.mergeResults) {
if (!result.hasConflict) {
output.add(result.localPath);
}
}
return output;
}
/// A list of local paths of files that were newly added.
List<String> get addedFiles {
final List<String> output = <String>[];
for (final FilePendingMigration file in migrateResult.addedFiles) {
output.add(file.localPath);
}
return output;
}
/// A list of local paths of files that are marked for deletion.
List<String> get deletedFiles {
final List<String> output = <String>[];
for (final FilePendingMigration file in migrateResult.deletedFiles) {
output.add(file.localPath);
}
return output;
}
/// Returns the manifest file given a migration workind directory.
static File getManifestFileFromDirectory(Directory workingDir) {
return workingDir.childFile('.migrate_manifest');
}
/// Writes the manifest yaml file in the working directory.
void writeFile() {
final StringBuffer mergedFileManifestContents = StringBuffer();
final StringBuffer conflictFilesManifestContents = StringBuffer();
for (final MergeResult result in migrateResult.mergeResults) {
if (result.hasConflict) {
conflictFilesManifestContents.write(' - ${result.localPath}\n');
} else {
mergedFileManifestContents.write(' - ${result.localPath}\n');
}
}
final StringBuffer newFileManifestContents = StringBuffer();
for (final String localPath in addedFiles) {
newFileManifestContents.write(' - $localPath\n)');
}
final StringBuffer deletedFileManifestContents = StringBuffer();
for (final String localPath in deletedFiles) {
deletedFileManifestContents.write(' - $localPath\n');
}
final String migrateManifestContents = 'merged_files:\n${mergedFileManifestContents.toString()}conflict_files:\n${conflictFilesManifestContents.toString()}added_files:\n${newFileManifestContents.toString()}deleted_files:\n${deletedFileManifestContents.toString()}';
final File migrateManifest = getManifestFileFromDirectory(migrateRootDir);
migrateManifest.createSync(recursive: true);
migrateManifest.writeAsStringSync(migrateManifestContents, flush: true);
}
}
/// Returns true if the file does not contain any git conflict markers.
bool _conflictsResolved(String contents) {
if (contents.contains('>>>>>>>') && contents.contains('=======') && contents.contains('<<<<<<<')) {
return false;
}
return true;
}
/// Returns true if the migration working directory has all conflicts resolved and prints the migration status.
///
/// The migration status printout lists all added, deleted, merged, and conflicted files.
bool checkAndPrintMigrateStatus(MigrateManifest manifest, Directory workingDir, {bool warnConflict = false, Logger? logger}) {
final StringBuffer printout = StringBuffer();
final StringBuffer redPrintout = StringBuffer();
bool result = true;
final List<String> remainingConflicts = <String>[];
final List<String> mergedFiles = <String>[];
for (final String localPath in manifest.conflictFiles) {
if (!_conflictsResolved(workingDir.childFile(localPath).readAsStringSync())) {
remainingConflicts.add(localPath);
} else {
mergedFiles.add(localPath);
}
}
mergedFiles.addAll(manifest.mergedFiles);
if (manifest.addedFiles.isNotEmpty) {
printout.write('Added files:\n');
for (final String localPath in manifest.addedFiles) {
printout.write(' - $localPath\n');
}
}
if (manifest.deletedFiles.isNotEmpty) {
printout.write('Deleted files:\n');
for (final String localPath in manifest.deletedFiles) {
printout.write(' - $localPath\n');
}
}
if (mergedFiles.isNotEmpty) {
printout.write('Modified files:\n');
for (final String localPath in mergedFiles) {
printout.write(' - $localPath\n');
}
}
if (remainingConflicts.isNotEmpty) {
if (warnConflict) {
printout.write('Unable to apply migration. The following files in the migration working directory still have unresolved conflicts:');
} else {
printout.write('Merge conflicted files:');
}
for (final String localPath in remainingConflicts) {
redPrintout.write(' - $localPath\n');
}
result = false;
}
if (logger != null) {
logger.printStatus(printout.toString());
logger.printStatus(redPrintout.toString(), color: TerminalColor.red, newline: false);
}
return result;
}
// 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 'migrate_utils.dart';
/// Data class that holds all results and generated directories from a computeMigration run.
///
/// mergeResults, addedFiles, and deletedFiles includes the sets of files to be migrated while
/// the other members track the temporary sdk and generated app directories created by the tool.
///
/// The compute function does not clean up the temp directories, as the directories may be reused,
/// so this must be done manually afterwards.
class MigrateResult {
/// Explicitly initialize the MigrateResult.
MigrateResult({
required this.mergeResults,
required this.addedFiles,
required this.deletedFiles,
required this.tempDirectories,
required this.sdkDirs,
required this.mergeTypeMap,
required this.diffMap,
this.generatedBaseTemplateDirectory,
this.generatedTargetTemplateDirectory});
/// Creates a MigrateResult with all empty members.
MigrateResult.empty()
: mergeResults = <MergeResult>[],
addedFiles = <FilePendingMigration>[],
deletedFiles = <FilePendingMigration>[],
tempDirectories = <Directory>[],
mergeTypeMap = <String, MergeType>{},
diffMap = <String, DiffResult>{},
sdkDirs = <String, Directory>{};
/// The results of merging existing files with the target files.
final List<MergeResult> mergeResults;
/// Tracks the files that are to be newly added to the project.
final List<FilePendingMigration> addedFiles;
/// Tracks the files that are to be deleted from the project.
final List<FilePendingMigration> deletedFiles;
/// Tracks the temporary directories created during the migrate compute process.
final List<Directory> tempDirectories;
/// Mapping between the local path of a file and the type of merge that should be used.
final Map<String, MergeType> mergeTypeMap;
/// Mapping between the local path of a file and the diff between the base and target
/// versions of the file.
final Map<String, DiffResult> diffMap;
/// The root directory of the base app.
Directory? generatedBaseTemplateDirectory;
/// The root directory of the target app.
Directory? generatedTargetTemplateDirectory;
/// The root directories of the Flutter SDK for each revision.
Map<String, Directory> sdkDirs;
}
/// Defines available merge techniques.
enum MergeType {
/// A standard three-way merge.
threeWay,
/// A two way merge that ignores the base version of the file.
twoWay,
/// A `CustomMerge` manually handles the merge.
custom,
}
/// Stores a file that has been marked for migration and metadata about the file.
class FilePendingMigration {
FilePendingMigration(this.localPath, this.file);
String localPath;
File file;
}
This diff is collapsed.
// 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:file/file.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/process.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/migrate/migrate_utils.dart';
import '../src/common.dart';
void main() {
BufferLogger logger;
FileSystem fileSystem;
Directory projectRoot;
String projectRootPath;
MigrateUtils utils;
ProcessUtils processUtils;
setUpAll(() async {
fileSystem = globals.localFileSystem;
logger = BufferLogger.test();
utils = MigrateUtils(
logger: logger,
fileSystem: fileSystem,
platform: globals.platform,
processManager: globals.processManager,
);
processUtils = ProcessUtils(processManager: globals.processManager, logger: logger);
});
group('git', () {
setUp(() async {
projectRoot = fileSystem.systemTempDirectory.createTempSync('flutter_migrate_utils_test');
projectRoot.createSync(recursive: true);
projectRootPath = projectRoot.path;
});
testWithoutContext('init', () async {
expect(projectRoot.existsSync(), true);
expect(projectRoot.childDirectory('.git').existsSync(), false);
await utils.gitInit(projectRootPath);
expect(projectRoot.childDirectory('.git').existsSync(), true);
});
testWithoutContext('isGitIgnored', () async {
expect(projectRoot.existsSync(), true);
expect(projectRoot.childDirectory('.git').existsSync(), false);
await utils.gitInit(projectRootPath);
expect(projectRoot.childDirectory('.git').existsSync(), true);
projectRoot.childFile('.gitignore')
..createSync()
..writeAsStringSync('ignored_file.dart', flush: true);
expect(await utils.isGitIgnored('ignored_file.dart', projectRootPath), true);
expect(await utils.isGitIgnored('other_file.dart', projectRootPath), false);
});
testWithoutContext('isGitRepo', () async {
expect(projectRoot.existsSync(), true);
expect(projectRoot.childDirectory('.git').existsSync(), false);
expect(await utils.isGitRepo(projectRootPath), false);
await utils.gitInit(projectRootPath);
expect(projectRoot.childDirectory('.git').existsSync(), true);
expect(await utils.isGitRepo(projectRootPath), true);
expect(await utils.isGitRepo(projectRoot.parent.path), false);
});
testWithoutContext('hasUncommittedChanges false on clean repo', () async {
expect(projectRoot.existsSync(), true);
expect(projectRoot.childDirectory('.git').existsSync(), false);
await utils.gitInit(projectRootPath);
expect(projectRoot.childDirectory('.git').existsSync(), true);
projectRoot.childFile('.gitignore')
..createSync()
..writeAsStringSync('ignored_file.dart', flush: true);
await processUtils.run(<String>['git', 'add', '.'], workingDirectory: projectRootPath);
await processUtils.run(<String>['git', 'commit', '-m', 'Initial commit'], workingDirectory: projectRootPath);
expect(await utils.hasUncommittedChanges(projectRootPath), false);
});
testWithoutContext('hasUncommittedChanges true on dirty repo', () async {
expect(projectRoot.existsSync(), true);
expect(projectRoot.childDirectory('.git').existsSync(), false);
await utils.gitInit(projectRootPath);
expect(projectRoot.childDirectory('.git').existsSync(), true);
projectRoot.childFile('some_file.dart')
..createSync()
..writeAsStringSync('void main() {}', flush: true);
expect(await utils.hasUncommittedChanges(projectRootPath), true);
});
testWithoutContext('diffFiles', () async {
expect(projectRoot.existsSync(), true);
expect(projectRoot.childDirectory('.git').existsSync(), false);
await utils.gitInit(projectRootPath);
expect(projectRoot.childDirectory('.git').existsSync(), true);
final File file1 = projectRoot.childFile('some_file.dart')
..createSync()
..writeAsStringSync('void main() {}\n', flush: true);
final File file2 = projectRoot.childFile('some_other_file.dart');
DiffResult result = await utils.diffFiles(file1, file2);
expect(result.diff, null);
expect(result.diffType, DiffType.deletion);
expect(result.exitCode, null);
result = await utils.diffFiles(file2, file1);
expect(result.diff, null);
expect(result.diffType, DiffType.addition);
expect(result.exitCode, null);
file2.createSync();
file2.writeAsStringSync('void main() {}\n', flush: true);
result = await utils.diffFiles(file1, file2);
expect(result.diff, '');
expect(result.diffType, DiffType.command);
expect(result.exitCode, 0);
file2.writeAsStringSync('void main() {}\na second line\na third line\n', flush: true);
result = await utils.diffFiles(file1, file2);
expect(result.diff, contains('@@ -1 +1,3 @@\n void main() {}\n+a second line\n+a third line'));
expect(result.diffType, DiffType.command);
expect(result.exitCode, 1);
});
testWithoutContext('merge', () async {
expect(projectRoot.existsSync(), true);
expect(projectRoot.childDirectory('.git').existsSync(), false);
await utils.gitInit(projectRootPath);
expect(projectRoot.childDirectory('.git').existsSync(), true);
final File file1 = projectRoot.childFile('some_file.dart');
file1.createSync();
file1.writeAsStringSync('void main() {}\n\nline1\nline2\nline3\nline4\nline5\n', flush: true);
final File file2 = projectRoot.childFile('some_other_file.dart');
file2.createSync();
file2.writeAsStringSync('void main() {}\n\nline1\nline2\nline3.0\nline3.5\nline4\nline5\n', flush: true);
final File file3 = projectRoot.childFile('some_other_third_file.dart');
file3.createSync();
file3.writeAsStringSync('void main() {}\n\nline2\nline3\nline4\nline5\n', flush: true);
StringMergeResult result = await utils.gitMergeFile(
base: file1.path,
current: file2.path,
target: file3.path,
localPath: 'some_file.dart',
) as StringMergeResult;
expect(result.mergedString, 'void main() {}\n\nline2\nline3.0\nline3.5\nline4\nline5\n');
expect(result.hasConflict, false);
expect(result.exitCode, 0);
file3.writeAsStringSync('void main() {}\n\nline1\nline2\nline3.1\nline3.5\nline4\nline5\n', flush: true);
result = await utils.gitMergeFile(
base: file1.path,
current: file2.path,
target: file3.path,
localPath: 'some_file.dart',
) as StringMergeResult;
expect(result.mergedString, contains('line3.0\n=======\nline3.1\n>>>>>>>'));
expect(result.hasConflict, true);
expect(result.exitCode, 1);
// Two way merge
result = await utils.gitMergeFile(
base: file1.path,
current: file1.path,
target: file3.path,
localPath: 'some_file.dart',
) as StringMergeResult;
expect(result.mergedString, 'void main() {}\n\nline1\nline2\nline3.1\nline3.5\nline4\nline5\n');
expect(result.hasConflict, false);
expect(result.exitCode, 0);
});
});
group('legacy app creation', () {
testWithoutContext('clone and create', () async {
projectRoot = fileSystem.systemTempDirectory.createTempSync('flutter_sdk_test');
const String revision = '5391447fae6209bb21a89e6a5a6583cac1af9b4b';
expect(await utils.cloneFlutter(revision, projectRoot.path), true);
expect(projectRoot.childFile('README.md').existsSync(), true);
final Directory appDir = fileSystem.systemTempDirectory.createTempSync('flutter_app');
await utils.createFromTemplates(
projectRoot.childDirectory('bin').path,
name: 'testapp',
androidLanguage: 'java',
iosLanguage: 'objc',
outputDirectory: appDir.path,
);
expect(appDir.childFile('pubspec.yaml').existsSync(), true);
expect(appDir.childFile('.metadata').existsSync(), true);
expect(appDir.childFile('.metadata').readAsStringSync(), contains(revision));
expect(appDir.childDirectory('android').existsSync(), true);
expect(appDir.childDirectory('ios').existsSync(), true);
expect(appDir.childDirectory('web').existsSync(), false);
projectRoot.deleteSync(recursive: true);
});
});
}
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