mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
[flutter_tools] MigrateUtils and MigrateManifest classes (#101937)
This commit is contained in:
parent
93cce92ea8
commit
90a8b0561d
84
packages/flutter_tools/lib/src/commands/migrate.dart
Normal file
84
packages/flutter_tools/lib/src/commands/migrate.dart
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
// 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,
|
||||||
|
);
|
||||||
|
}
|
241
packages/flutter_tools/lib/src/migrate/migrate_manifest.dart
Normal file
241
packages/flutter_tools/lib/src/migrate/migrate_manifest.dart
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
// 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;
|
||||||
|
}
|
82
packages/flutter_tools/lib/src/migrate/migrate_result.dart
Normal file
82
packages/flutter_tools/lib/src/migrate/migrate_result.dart
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
// 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;
|
||||||
|
}
|
359
packages/flutter_tools/lib/src/migrate/migrate_utils.dart
Normal file
359
packages/flutter_tools/lib/src/migrate/migrate_utils.dart
Normal file
@ -0,0 +1,359 @@
|
|||||||
|
// 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 'dart:async';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:process/process.dart';
|
||||||
|
|
||||||
|
import '../base/common.dart';
|
||||||
|
import '../base/file_system.dart';
|
||||||
|
import '../base/logger.dart';
|
||||||
|
import '../base/platform.dart';
|
||||||
|
import '../base/process.dart';
|
||||||
|
|
||||||
|
/// The default name of the migrate working directory used to stage proposed changes.
|
||||||
|
const String kDefaultMigrateWorkingDirectoryName = 'migrate_working_dir';
|
||||||
|
|
||||||
|
/// Utility class that contains methods that wrap git and other shell commands.
|
||||||
|
class MigrateUtils {
|
||||||
|
MigrateUtils({
|
||||||
|
required Logger logger,
|
||||||
|
required FileSystem fileSystem,
|
||||||
|
required Platform platform,
|
||||||
|
required ProcessManager processManager,
|
||||||
|
}) :
|
||||||
|
_processUtils = ProcessUtils(processManager: processManager, logger: logger),
|
||||||
|
_logger = logger,
|
||||||
|
_fileSystem = fileSystem,
|
||||||
|
_platform = platform;
|
||||||
|
|
||||||
|
final Logger _logger;
|
||||||
|
final FileSystem _fileSystem;
|
||||||
|
final Platform _platform;
|
||||||
|
final ProcessUtils _processUtils;
|
||||||
|
|
||||||
|
/// Calls `git diff` on two files and returns the diff as a DiffResult.
|
||||||
|
Future<DiffResult> diffFiles(File one, File two) async {
|
||||||
|
if (one.existsSync() && !two.existsSync()) {
|
||||||
|
return DiffResult(diffType: DiffType.deletion);
|
||||||
|
}
|
||||||
|
if (!one.existsSync() && two.existsSync()) {
|
||||||
|
return DiffResult(diffType: DiffType.addition);
|
||||||
|
}
|
||||||
|
final List<String> cmdArgs = <String>['git', 'diff', '--no-index', one.absolute.path, two.absolute.path];
|
||||||
|
final RunResult result = await _processUtils.run(cmdArgs);
|
||||||
|
|
||||||
|
// diff exits with 1 if diffs are found.
|
||||||
|
checkForErrors(result, allowedExitCodes: <int>[0, 1], commandDescription: 'git ${cmdArgs.join(' ')}');
|
||||||
|
return DiffResult(diffType: DiffType.command, diff: result.stdout, exitCode: result.exitCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clones a copy of the flutter repo into the destination directory. Returns false if unsuccessful.
|
||||||
|
Future<bool> cloneFlutter(String revision, String destination) async {
|
||||||
|
// Use https url instead of ssh to avoid need to setup ssh on git.
|
||||||
|
List<String> cmdArgs = <String>['git', 'clone', '--filter=blob:none', 'https://github.com/flutter/flutter.git', destination];
|
||||||
|
RunResult result = await _processUtils.run(cmdArgs);
|
||||||
|
checkForErrors(result, commandDescription: cmdArgs.join(' '));
|
||||||
|
|
||||||
|
cmdArgs.clear();
|
||||||
|
cmdArgs = <String>['git', 'reset', '--hard', revision];
|
||||||
|
result = await _processUtils.run(cmdArgs, workingDirectory: destination);
|
||||||
|
if (!checkForErrors(result, commandDescription: cmdArgs.join(' '), exit: false)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calls `flutter create` as a re-entrant command.
|
||||||
|
Future<String> createFromTemplates(String flutterBinPath, {
|
||||||
|
required String name,
|
||||||
|
bool legacyNameParameter = false,
|
||||||
|
required String androidLanguage,
|
||||||
|
required String iosLanguage,
|
||||||
|
required String outputDirectory,
|
||||||
|
String? createVersion,
|
||||||
|
List<String> platforms = const <String>[],
|
||||||
|
int iterationsAllowed = 5,
|
||||||
|
}) async {
|
||||||
|
// Limit the number of iterations this command is allowed to attempt to prevent infinite looping.
|
||||||
|
if (iterationsAllowed <= 0) {
|
||||||
|
_logger.printError('Unable to `flutter create` with the version of flutter at $flutterBinPath');
|
||||||
|
return outputDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<String> cmdArgs = <String>['$flutterBinPath/flutter', 'create'];
|
||||||
|
if (!legacyNameParameter) {
|
||||||
|
cmdArgs.add('--project-name=$name');
|
||||||
|
}
|
||||||
|
cmdArgs.add('--android-language=$androidLanguage');
|
||||||
|
cmdArgs.add('--ios-language=$iosLanguage');
|
||||||
|
if (platforms.isNotEmpty) {
|
||||||
|
String platformsArg = '--platforms=';
|
||||||
|
for (int i = 0; i < platforms.length; i++) {
|
||||||
|
if (i > 0) {
|
||||||
|
platformsArg += ',';
|
||||||
|
}
|
||||||
|
platformsArg += platforms[i];
|
||||||
|
}
|
||||||
|
cmdArgs.add(platformsArg);
|
||||||
|
}
|
||||||
|
cmdArgs.add('--no-pub');
|
||||||
|
if (legacyNameParameter) {
|
||||||
|
cmdArgs.add(name);
|
||||||
|
} else {
|
||||||
|
cmdArgs.add(outputDirectory);
|
||||||
|
}
|
||||||
|
final RunResult result = await _processUtils.run(cmdArgs, workingDirectory: outputDirectory, allowReentrantFlutter: true);
|
||||||
|
final String error = result.stderr;
|
||||||
|
|
||||||
|
// Catch errors due to parameters not existing.
|
||||||
|
|
||||||
|
// Old versions of the tool does not include the platforms option.
|
||||||
|
if (error.contains('Could not find an option named "platforms".')) {
|
||||||
|
return createFromTemplates(
|
||||||
|
flutterBinPath,
|
||||||
|
name: name,
|
||||||
|
legacyNameParameter: legacyNameParameter,
|
||||||
|
androidLanguage: androidLanguage,
|
||||||
|
iosLanguage: iosLanguage,
|
||||||
|
outputDirectory: outputDirectory,
|
||||||
|
iterationsAllowed: iterationsAllowed--,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Old versions of the tool does not include the project-name option.
|
||||||
|
if ((result.stderr).contains('Could not find an option named "project-name".')) {
|
||||||
|
return createFromTemplates(
|
||||||
|
flutterBinPath,
|
||||||
|
name: name,
|
||||||
|
legacyNameParameter: true,
|
||||||
|
androidLanguage: androidLanguage,
|
||||||
|
iosLanguage: iosLanguage,
|
||||||
|
outputDirectory: outputDirectory,
|
||||||
|
platforms: platforms,
|
||||||
|
iterationsAllowed: iterationsAllowed--,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (error.contains('Multiple output directories specified.')) {
|
||||||
|
if (error.contains('Try moving --platforms')) {
|
||||||
|
return createFromTemplates(
|
||||||
|
flutterBinPath,
|
||||||
|
name: name,
|
||||||
|
legacyNameParameter: legacyNameParameter,
|
||||||
|
androidLanguage: androidLanguage,
|
||||||
|
iosLanguage: iosLanguage,
|
||||||
|
outputDirectory: outputDirectory,
|
||||||
|
iterationsAllowed: iterationsAllowed--,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checkForErrors(result, commandDescription: cmdArgs.join(' '), silent: true);
|
||||||
|
|
||||||
|
if (legacyNameParameter) {
|
||||||
|
return _fileSystem.path.join(outputDirectory, name);
|
||||||
|
}
|
||||||
|
return outputDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs the git 3-way merge on three files and returns the results as a MergeResult.
|
||||||
|
///
|
||||||
|
/// Passing the same path for base and current will perform a two-way fast forward merge.
|
||||||
|
Future<MergeResult> gitMergeFile({
|
||||||
|
required String base,
|
||||||
|
required String current,
|
||||||
|
required String target,
|
||||||
|
required String localPath,
|
||||||
|
}) async {
|
||||||
|
final List<String> cmdArgs = <String>['git', 'merge-file', '-p', current, base, target];
|
||||||
|
final RunResult result = await _processUtils.run(cmdArgs);
|
||||||
|
checkForErrors(result, allowedExitCodes: <int>[-1], commandDescription: cmdArgs.join(' '));
|
||||||
|
return StringMergeResult(result, localPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calls `git init` on the workingDirectory.
|
||||||
|
Future<void> gitInit(String workingDirectory) async {
|
||||||
|
final List<String> cmdArgs = <String>['git', 'init'];
|
||||||
|
final RunResult result = await _processUtils.run(cmdArgs, workingDirectory: workingDirectory);
|
||||||
|
checkForErrors(result, commandDescription: cmdArgs.join(' '));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the workingDirectory git repo has any uncommited changes.
|
||||||
|
Future<bool> hasUncommittedChanges(String workingDirectory, {String? migrateWorkingDir}) async {
|
||||||
|
final List<String> cmdArgs = <String>[
|
||||||
|
'git',
|
||||||
|
'ls-files',
|
||||||
|
'--deleted',
|
||||||
|
'--modified',
|
||||||
|
'--others',
|
||||||
|
'--exclude=${migrateWorkingDir ?? kDefaultMigrateWorkingDirectoryName}'
|
||||||
|
];
|
||||||
|
final RunResult result = await _processUtils.run(cmdArgs, workingDirectory: workingDirectory);
|
||||||
|
checkForErrors(result, allowedExitCodes: <int>[-1], commandDescription: cmdArgs.join(' '));
|
||||||
|
if (result.stdout.isEmpty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the workingDirectory is a git repo.
|
||||||
|
Future<bool> isGitRepo(String workingDirectory) async {
|
||||||
|
final List<String> cmdArgs = <String>['git', 'rev-parse', '--is-inside-work-tree'];
|
||||||
|
final RunResult result = await _processUtils.run(cmdArgs, workingDirectory: workingDirectory);
|
||||||
|
checkForErrors(result, allowedExitCodes: <int>[-1], commandDescription: cmdArgs.join(' '));
|
||||||
|
if (result.exitCode == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the file at `filePath` is covered by the `.gitignore`
|
||||||
|
Future<bool> isGitIgnored(String filePath, String workingDirectory) async {
|
||||||
|
final List<String> cmdArgs = <String>['git', 'check-ignore', filePath];
|
||||||
|
final RunResult result = await _processUtils.run(cmdArgs, workingDirectory: workingDirectory);
|
||||||
|
checkForErrors(result, allowedExitCodes: <int>[0, 1, 128], commandDescription: cmdArgs.join(' '));
|
||||||
|
return result.exitCode == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs `flutter pub upgrade --major-revisions`.
|
||||||
|
Future<void> flutterPubUpgrade(String workingDirectory) async {
|
||||||
|
final List<String> cmdArgs = <String>['flutter', 'pub', 'upgrade', '--major-versions'];
|
||||||
|
final RunResult result = await _processUtils.run(cmdArgs, workingDirectory: workingDirectory, allowReentrantFlutter: true);
|
||||||
|
checkForErrors(result, commandDescription: cmdArgs.join(' '));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs `./gradlew tasks` in the android directory of a flutter project.
|
||||||
|
Future<void> gradlewTasks(String workingDirectory) async {
|
||||||
|
final String baseCommand = _platform.isWindows ? 'gradlew.bat' : './gradlew';
|
||||||
|
final List<String> cmdArgs = <String>[baseCommand, 'tasks'];
|
||||||
|
final RunResult result = await _processUtils.run(cmdArgs, workingDirectory: workingDirectory);
|
||||||
|
checkForErrors(result, commandDescription: cmdArgs.join(' '));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verifies that the RunResult does not contain an error.
|
||||||
|
///
|
||||||
|
/// If an error is detected, the error can be optionally logged or exit the tool.
|
||||||
|
///
|
||||||
|
/// Passing -1 in allowedExitCodes means all exit codes are valid.
|
||||||
|
bool checkForErrors(
|
||||||
|
RunResult result, {
|
||||||
|
List<int> allowedExitCodes = const <int>[0],
|
||||||
|
String? commandDescription,
|
||||||
|
bool exit = true,
|
||||||
|
bool silent = false
|
||||||
|
}) {
|
||||||
|
if (!allowedExitCodes.contains(result.exitCode) && !allowedExitCodes.contains(-1)) {
|
||||||
|
if (!silent) {
|
||||||
|
_logger.printError('Command encountered an error with exit code ${result.exitCode}.');
|
||||||
|
if (commandDescription != null) {
|
||||||
|
_logger.printError('Command:');
|
||||||
|
_logger.printError(commandDescription, indent: 2);
|
||||||
|
}
|
||||||
|
_logger.printError('Stdout:');
|
||||||
|
_logger.printError(result.stdout, indent: 2);
|
||||||
|
_logger.printError('Stderr:');
|
||||||
|
_logger.printError(result.stderr, indent: 2);
|
||||||
|
}
|
||||||
|
if (exit) {
|
||||||
|
throwToolExit('Command failed with exit code ${result.exitCode}', exitCode: result.exitCode);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the file does not contain any git conflit markers.
|
||||||
|
bool conflictsResolved(String contents) {
|
||||||
|
if (contents.contains('>>>>>>>') && contents.contains('=======') && contents.contains('<<<<<<<')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Defines the classification of difference between files.
|
||||||
|
enum DiffType {
|
||||||
|
command,
|
||||||
|
addition,
|
||||||
|
deletion,
|
||||||
|
ignored,
|
||||||
|
none,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tracks the output of a git diff command or any special cases such as addition of a new
|
||||||
|
/// file or deletion of an existing file.
|
||||||
|
class DiffResult {
|
||||||
|
DiffResult({
|
||||||
|
required this.diffType,
|
||||||
|
this.diff,
|
||||||
|
this.exitCode,
|
||||||
|
}) : assert(diffType == DiffType.command && exitCode != null || diffType != DiffType.command && exitCode == null);
|
||||||
|
|
||||||
|
/// The diff string output by git.
|
||||||
|
final String? diff;
|
||||||
|
|
||||||
|
final DiffType diffType;
|
||||||
|
|
||||||
|
/// The exit code of the command. This is zero when no diffs are found.
|
||||||
|
///
|
||||||
|
/// The exitCode is null when the diffType is not `command`.
|
||||||
|
final int? exitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Data class to hold the results of a merge.
|
||||||
|
abstract class MergeResult {
|
||||||
|
/// Initializes a MergeResult based off of a RunResult.
|
||||||
|
MergeResult(RunResult result, this.localPath) :
|
||||||
|
hasConflict = result.exitCode != 0,
|
||||||
|
exitCode = result.exitCode;
|
||||||
|
|
||||||
|
/// Manually initializes a MergeResult with explicit values.
|
||||||
|
MergeResult.explicit({
|
||||||
|
required this.hasConflict,
|
||||||
|
required this.exitCode,
|
||||||
|
required this.localPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// True when there is a merge conflict.
|
||||||
|
bool hasConflict;
|
||||||
|
|
||||||
|
/// The exitcode of the merge command.
|
||||||
|
int exitCode;
|
||||||
|
|
||||||
|
/// The local path relative to the project root of the file.
|
||||||
|
String localPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The results of a string merge.
|
||||||
|
class StringMergeResult extends MergeResult {
|
||||||
|
/// Initializes a BinaryMergeResult based off of a RunResult.
|
||||||
|
StringMergeResult(super.result, super.localPath) :
|
||||||
|
mergedString = result.stdout;
|
||||||
|
|
||||||
|
/// Manually initializes a StringMergeResult with explicit values.
|
||||||
|
StringMergeResult.explicit({
|
||||||
|
required this.mergedString,
|
||||||
|
required super.hasConflict,
|
||||||
|
required super.exitCode,
|
||||||
|
required super.localPath,
|
||||||
|
}) : super.explicit();
|
||||||
|
/// The final merged string.
|
||||||
|
String mergedString;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The results of a binary merge.
|
||||||
|
class BinaryMergeResult extends MergeResult {
|
||||||
|
/// Initializes a BinaryMergeResult based off of a RunResult.
|
||||||
|
BinaryMergeResult(super.result, super.localPath) :
|
||||||
|
mergedBytes = result.stdout as Uint8List;
|
||||||
|
|
||||||
|
/// Manually initializes a BinaryMergeResult with explicit values.
|
||||||
|
BinaryMergeResult.explicit({
|
||||||
|
required this.mergedBytes,
|
||||||
|
required super.hasConflict,
|
||||||
|
required super.exitCode,
|
||||||
|
required super.localPath,
|
||||||
|
}) : super.explicit();
|
||||||
|
/// The final merged bytes.
|
||||||
|
Uint8List mergedBytes;
|
||||||
|
}
|
@ -0,0 +1,292 @@
|
|||||||
|
// 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:file/memory.dart';
|
||||||
|
import 'package:flutter_tools/src/base/file_system.dart';
|
||||||
|
import 'package:flutter_tools/src/migrate/migrate_manifest.dart';
|
||||||
|
import 'package:flutter_tools/src/migrate/migrate_result.dart';
|
||||||
|
import 'package:flutter_tools/src/migrate/migrate_utils.dart';
|
||||||
|
|
||||||
|
import '../../src/common.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late FileSystem fileSystem;
|
||||||
|
late File manifestFile;
|
||||||
|
|
||||||
|
setUpAll(() {
|
||||||
|
fileSystem = MemoryFileSystem.test();
|
||||||
|
manifestFile = fileSystem.file('.migrate_manifest');
|
||||||
|
});
|
||||||
|
|
||||||
|
group('manifest file parsing', () {
|
||||||
|
testWithoutContext('empty fails', () async {
|
||||||
|
manifestFile.writeAsStringSync('');
|
||||||
|
bool exceptionFound = false;
|
||||||
|
try {
|
||||||
|
MigrateManifest.fromFile(manifestFile);
|
||||||
|
} on Exception catch (e) {
|
||||||
|
exceptionFound = true;
|
||||||
|
expect(e.toString(), 'Exception: Invalid .migrate_manifest file in the migrate working directory. File is not a Yaml map.');
|
||||||
|
}
|
||||||
|
expect(exceptionFound, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('invalid name fails', () async {
|
||||||
|
manifestFile.writeAsStringSync('''
|
||||||
|
merged_files:
|
||||||
|
conflict_files:
|
||||||
|
added_filessssss:
|
||||||
|
deleted_files:
|
||||||
|
''');
|
||||||
|
bool exceptionFound = false;
|
||||||
|
try {
|
||||||
|
MigrateManifest.fromFile(manifestFile);
|
||||||
|
} on Exception catch (e) {
|
||||||
|
exceptionFound = true;
|
||||||
|
expect(e.toString(), 'Exception: Invalid .migrate_manifest file in the migrate working directory. File is missing an entry.');
|
||||||
|
}
|
||||||
|
expect(exceptionFound, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('missing name fails', () async {
|
||||||
|
manifestFile.writeAsStringSync('''
|
||||||
|
merged_files:
|
||||||
|
conflict_files:
|
||||||
|
deleted_files:
|
||||||
|
''');
|
||||||
|
bool exceptionFound = false;
|
||||||
|
try {
|
||||||
|
MigrateManifest.fromFile(manifestFile);
|
||||||
|
} on Exception catch (e) {
|
||||||
|
exceptionFound = true;
|
||||||
|
expect(e.toString(), 'Exception: Invalid .migrate_manifest file in the migrate working directory. File is missing an entry.');
|
||||||
|
}
|
||||||
|
expect(exceptionFound, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('wrong entry type fails', () async {
|
||||||
|
manifestFile.writeAsStringSync('''
|
||||||
|
merged_files:
|
||||||
|
conflict_files:
|
||||||
|
other_key:
|
||||||
|
added_files:
|
||||||
|
deleted_files:
|
||||||
|
''');
|
||||||
|
bool exceptionFound = false;
|
||||||
|
try {
|
||||||
|
MigrateManifest.fromFile(manifestFile);
|
||||||
|
} on Exception catch (e) {
|
||||||
|
exceptionFound = true;
|
||||||
|
expect(e.toString(), 'Exception: Invalid .migrate_manifest file in the migrate working directory. Entry is not a Yaml list.');
|
||||||
|
}
|
||||||
|
expect(exceptionFound, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('unpopulated succeeds', () async {
|
||||||
|
manifestFile.writeAsStringSync('''
|
||||||
|
merged_files:
|
||||||
|
conflict_files:
|
||||||
|
added_files:
|
||||||
|
deleted_files:
|
||||||
|
''');
|
||||||
|
final MigrateManifest manifest = MigrateManifest.fromFile(manifestFile);
|
||||||
|
expect(manifest.mergedFiles.isEmpty, true);
|
||||||
|
expect(manifest.conflictFiles.isEmpty, true);
|
||||||
|
expect(manifest.addedFiles.isEmpty, true);
|
||||||
|
expect(manifest.deletedFiles.isEmpty, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('order does not matter', () async {
|
||||||
|
manifestFile.writeAsStringSync('''
|
||||||
|
added_files:
|
||||||
|
merged_files:
|
||||||
|
deleted_files:
|
||||||
|
conflict_files:
|
||||||
|
''');
|
||||||
|
final MigrateManifest manifest = MigrateManifest.fromFile(manifestFile);
|
||||||
|
expect(manifest.mergedFiles.isEmpty, true);
|
||||||
|
expect(manifest.conflictFiles.isEmpty, true);
|
||||||
|
expect(manifest.addedFiles.isEmpty, true);
|
||||||
|
expect(manifest.deletedFiles.isEmpty, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('basic succeeds', () async {
|
||||||
|
manifestFile.writeAsStringSync('''
|
||||||
|
merged_files:
|
||||||
|
- file1
|
||||||
|
conflict_files:
|
||||||
|
- file2
|
||||||
|
added_files:
|
||||||
|
- file3
|
||||||
|
deleted_files:
|
||||||
|
- file4
|
||||||
|
''');
|
||||||
|
final MigrateManifest manifest = MigrateManifest.fromFile(manifestFile);
|
||||||
|
expect(manifest.mergedFiles.isEmpty, false);
|
||||||
|
expect(manifest.conflictFiles.isEmpty, false);
|
||||||
|
expect(manifest.addedFiles.isEmpty, false);
|
||||||
|
expect(manifest.deletedFiles.isEmpty, false);
|
||||||
|
|
||||||
|
expect(manifest.mergedFiles.length, 1);
|
||||||
|
expect(manifest.conflictFiles.length, 1);
|
||||||
|
expect(manifest.addedFiles.length, 1);
|
||||||
|
expect(manifest.deletedFiles.length, 1);
|
||||||
|
|
||||||
|
expect(manifest.mergedFiles[0], 'file1');
|
||||||
|
expect(manifest.conflictFiles[0], 'file2');
|
||||||
|
expect(manifest.addedFiles[0], 'file3');
|
||||||
|
expect(manifest.deletedFiles[0], 'file4');
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('basic multi-list succeeds', () async {
|
||||||
|
manifestFile.writeAsStringSync('''
|
||||||
|
merged_files:
|
||||||
|
- file1
|
||||||
|
- file2
|
||||||
|
conflict_files:
|
||||||
|
added_files:
|
||||||
|
deleted_files:
|
||||||
|
- file3
|
||||||
|
- file4
|
||||||
|
''');
|
||||||
|
final MigrateManifest manifest = MigrateManifest.fromFile(manifestFile);
|
||||||
|
expect(manifest.mergedFiles.isEmpty, false);
|
||||||
|
expect(manifest.conflictFiles.isEmpty, true);
|
||||||
|
expect(manifest.addedFiles.isEmpty, true);
|
||||||
|
expect(manifest.deletedFiles.isEmpty, false);
|
||||||
|
|
||||||
|
expect(manifest.mergedFiles.length, 2);
|
||||||
|
expect(manifest.conflictFiles.length, 0);
|
||||||
|
expect(manifest.addedFiles.length, 0);
|
||||||
|
expect(manifest.deletedFiles.length, 2);
|
||||||
|
|
||||||
|
expect(manifest.mergedFiles[0], 'file1');
|
||||||
|
expect(manifest.mergedFiles[1], 'file2');
|
||||||
|
expect(manifest.deletedFiles[0], 'file3');
|
||||||
|
expect(manifest.deletedFiles[1], 'file4');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('manifest MigrateResult creation', () {
|
||||||
|
testWithoutContext('empty MigrateResult', () async {
|
||||||
|
final MigrateManifest manifest = MigrateManifest(migrateRootDir: fileSystem.directory('root'), migrateResult: MigrateResult(
|
||||||
|
mergeResults: <MergeResult>[],
|
||||||
|
addedFiles: <FilePendingMigration>[],
|
||||||
|
deletedFiles: <FilePendingMigration>[],
|
||||||
|
mergeTypeMap: <String, MergeType>{},
|
||||||
|
diffMap: <String, DiffResult>{},
|
||||||
|
tempDirectories: <Directory>[],
|
||||||
|
sdkDirs: <String, Directory>{},
|
||||||
|
));
|
||||||
|
expect(manifest.mergedFiles.isEmpty, true);
|
||||||
|
expect(manifest.conflictFiles.isEmpty, true);
|
||||||
|
expect(manifest.addedFiles.isEmpty, true);
|
||||||
|
expect(manifest.deletedFiles.isEmpty, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('simple MigrateResult', () async {
|
||||||
|
final MigrateManifest manifest = MigrateManifest(migrateRootDir: fileSystem.directory('root'), migrateResult: MigrateResult(
|
||||||
|
mergeResults: <MergeResult>[
|
||||||
|
StringMergeResult.explicit(
|
||||||
|
localPath: 'merged_file',
|
||||||
|
mergedString: 'str',
|
||||||
|
hasConflict: false,
|
||||||
|
exitCode: 0,
|
||||||
|
),
|
||||||
|
StringMergeResult.explicit(
|
||||||
|
localPath: 'conflict_file',
|
||||||
|
mergedString: '<<<<<<<<<<<',
|
||||||
|
hasConflict: true,
|
||||||
|
exitCode: 1,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
addedFiles: <FilePendingMigration>[FilePendingMigration('added_file', fileSystem.file('added_file'))],
|
||||||
|
deletedFiles: <FilePendingMigration>[FilePendingMigration('deleted_file', fileSystem.file('deleted_file'))],
|
||||||
|
// The following are ignored by the manifest.
|
||||||
|
mergeTypeMap: <String, MergeType>{'test': MergeType.threeWay},
|
||||||
|
diffMap: <String, DiffResult>{},
|
||||||
|
tempDirectories: <Directory>[],
|
||||||
|
sdkDirs: <String, Directory>{},
|
||||||
|
));
|
||||||
|
expect(manifest.mergedFiles.isEmpty, false);
|
||||||
|
expect(manifest.conflictFiles.isEmpty, false);
|
||||||
|
expect(manifest.addedFiles.isEmpty, false);
|
||||||
|
expect(manifest.deletedFiles.isEmpty, false);
|
||||||
|
|
||||||
|
expect(manifest.mergedFiles.length, 1);
|
||||||
|
expect(manifest.conflictFiles.length, 1);
|
||||||
|
expect(manifest.addedFiles.length, 1);
|
||||||
|
expect(manifest.deletedFiles.length, 1);
|
||||||
|
|
||||||
|
expect(manifest.mergedFiles[0], 'merged_file');
|
||||||
|
expect(manifest.conflictFiles[0], 'conflict_file');
|
||||||
|
expect(manifest.addedFiles[0], 'added_file');
|
||||||
|
expect(manifest.deletedFiles[0], 'deleted_file');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('manifest write', () {
|
||||||
|
testWithoutContext('empty', () async {
|
||||||
|
manifestFile.writeAsStringSync('''
|
||||||
|
merged_files:
|
||||||
|
conflict_files:
|
||||||
|
added_files:
|
||||||
|
deleted_files:
|
||||||
|
''');
|
||||||
|
final MigrateManifest manifest = MigrateManifest.fromFile(manifestFile);
|
||||||
|
expect(manifest.mergedFiles.isEmpty, true);
|
||||||
|
expect(manifest.conflictFiles.isEmpty, true);
|
||||||
|
expect(manifest.addedFiles.isEmpty, true);
|
||||||
|
expect(manifest.deletedFiles.isEmpty, true);
|
||||||
|
|
||||||
|
manifest.writeFile();
|
||||||
|
expect(manifestFile.readAsStringSync(), '''
|
||||||
|
merged_files:
|
||||||
|
conflict_files:
|
||||||
|
added_files:
|
||||||
|
deleted_files:
|
||||||
|
''');
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('basic multi-list', () async {
|
||||||
|
manifestFile.writeAsStringSync('''
|
||||||
|
merged_files:
|
||||||
|
- file1
|
||||||
|
- file2
|
||||||
|
conflict_files:
|
||||||
|
added_files:
|
||||||
|
deleted_files:
|
||||||
|
- file3
|
||||||
|
- file4
|
||||||
|
''');
|
||||||
|
final MigrateManifest manifest = MigrateManifest.fromFile(manifestFile);
|
||||||
|
expect(manifest.mergedFiles.isEmpty, false);
|
||||||
|
expect(manifest.conflictFiles.isEmpty, true);
|
||||||
|
expect(manifest.addedFiles.isEmpty, true);
|
||||||
|
expect(manifest.deletedFiles.isEmpty, false);
|
||||||
|
|
||||||
|
expect(manifest.mergedFiles.length, 2);
|
||||||
|
expect(manifest.conflictFiles.length, 0);
|
||||||
|
expect(manifest.addedFiles.length, 0);
|
||||||
|
expect(manifest.deletedFiles.length, 2);
|
||||||
|
|
||||||
|
expect(manifest.mergedFiles[0], 'file1');
|
||||||
|
expect(manifest.mergedFiles[1], 'file2');
|
||||||
|
expect(manifest.deletedFiles[0], 'file3');
|
||||||
|
expect(manifest.deletedFiles[1], 'file4');
|
||||||
|
|
||||||
|
manifest.writeFile();
|
||||||
|
expect(manifestFile.readAsStringSync(), '''
|
||||||
|
merged_files:
|
||||||
|
- file1
|
||||||
|
- file2
|
||||||
|
conflict_files:
|
||||||
|
added_files:
|
||||||
|
deleted_files:
|
||||||
|
- file3
|
||||||
|
- file4
|
||||||
|
''');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,223 @@
|
|||||||
|
// 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user