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