Unverified Commit 7ab2bf8f authored by Gary Qian's avatar Gary Qian Committed by GitHub

Delete flutter_migrate code (#114253)

parent 0c7ee586
// 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';
import 'migrate_abandon.dart';
import 'migrate_apply.dart';
import 'migrate_status.dart';
/// Base command for the migration tool.
class MigrateCommand extends FlutterCommand {
MigrateCommand({
required bool verbose,
required this.logger,
required FileSystem fileSystem,
required Terminal terminal,
required Platform platform,
required ProcessManager processManager,
}) {
addSubcommand(MigrateStatusCommand(
verbose: verbose,
logger: logger,
fileSystem: fileSystem,
platform: platform,
processManager: processManager
));
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
));
}
final Logger logger;
@override
final String name = 'migrate';
@override
final String description = 'Migrates flutter generated project files to the current flutter version';
@override
String get category => FlutterCommandCategory.project;
@override
Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{};
@override
Future<FlutterCommandResult> runCommand() async {
return const FlutterCommandResult(ExitStatus.fail);
}
}
Future<bool> gitRepoExists(String projectDirectory, Logger logger, MigrateUtils migrateUtils) async {
if (await migrateUtils.isGitRepo(projectDirectory)) {
return true;
}
logger.printStatus('Project is not a git repo. Please initialize a git repo and try again.');
printCommandText('git init', logger);
return false;
}
Future<bool> hasUncommittedChanges(String projectDirectory, Logger logger, MigrateUtils migrateUtils) async {
if (await migrateUtils.hasUncommittedChanges(projectDirectory)) {
logger.printStatus('There are uncommitted changes in your project. Please git commit, abandon, or stash your changes before trying again.');
return true;
}
return false;
}
/// Prints a command to logger with appropriate formatting.
void printCommandText(String command, Logger logger) {
logger.printStatus(
'\n\$ $command\n',
color: TerminalColor.grey,
indent: 4,
newline: false,
);
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package: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 '../project.dart';
import '../runner/flutter_command.dart';
import 'migrate.dart';
/// Abandons the existing migration by deleting the migrate working directory.
class MigrateAbandonCommand extends FlutterCommand {
MigrateAbandonCommand({
required this.logger,
required this.fileSystem,
required this.terminal,
required Platform platform,
required ProcessManager processManager,
}) : migrateUtils = MigrateUtils(
logger: logger,
fileSystem: fileSystem,
platform: platform,
processManager: processManager,
) {
requiresPubspecYaml();
argParser.addOption(
'staging-directory',
help: 'Specifies the custom migration working directory used to stage '
'and edit proposed changes. This path can be absolute or relative '
'to the flutter project root. This defaults to '
'`$kDefaultMigrateStagingDirectoryName`',
valueHelp: 'path',
);
argParser.addOption(
'project-directory',
help: 'The root directory of the flutter project. This defaults to the '
'current working directory if omitted.',
valueHelp: 'path',
);
argParser.addFlag(
'force',
abbr: 'f',
help: 'Delete the migrate working directory without asking for confirmation.',
);
}
final Logger logger;
final FileSystem fileSystem;
final Terminal terminal;
final MigrateUtils migrateUtils;
@override
final String name = 'abandon';
@override
final String description = 'Deletes the current active migration working directory.';
@override
String get category => FlutterCommandCategory.project;
@override
Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{};
@override
Future<FlutterCommandResult> runCommand() async {
final String? projectDirectory = stringArg('project-directory');
final FlutterProjectFactory flutterProjectFactory = FlutterProjectFactory(logger: logger, fileSystem: fileSystem);
final FlutterProject project = projectDirectory == null
? FlutterProject.current()
: flutterProjectFactory.fromDirectory(fileSystem.directory(projectDirectory));
Directory stagingDirectory = project.directory.childDirectory(kDefaultMigrateStagingDirectoryName);
final String? customStagingDirectoryPath = stringArg('staging-directory');
if (customStagingDirectoryPath != null) {
if (fileSystem.path.isAbsolute(customStagingDirectoryPath)) {
stagingDirectory = fileSystem.directory(customStagingDirectoryPath);
} else {
stagingDirectory = project.directory.childDirectory(customStagingDirectoryPath);
}
if (!stagingDirectory.existsSync()) {
logger.printError('Provided staging directory `$customStagingDirectoryPath` '
'does not exist or is not valid.');
return const FlutterCommandResult(ExitStatus.fail);
}
}
if (!stagingDirectory.existsSync()) {
logger.printStatus('No migration in progress. Start a new migration with:');
printCommandText('flutter migrate start', logger);
return const FlutterCommandResult(ExitStatus.fail);
}
logger.printStatus('\nAbandoning the existing migration will delete the '
'migration staging directory at ${stagingDirectory.path}');
final bool force = boolArg('force') ?? false;
if (!force) {
String selection = 'y';
terminal.usesTerminalUi = true;
try {
selection = await terminal.promptForCharInput(
<String>['y', 'n'],
logger: logger,
prompt: 'Are you sure you wish to continue with abandoning? (y)es, (N)o',
defaultChoiceIndex: 1,
);
} on StateError catch(e) {
logger.printError(
e.message,
indent: 0,
);
}
if (selection != 'y') {
return const FlutterCommandResult(ExitStatus.success);
}
}
try {
stagingDirectory.deleteSync(recursive: true);
} on FileSystemException catch (e) {
logger.printError('Deletion failed with: $e');
logger.printError('Please manually delete the staging directory at `${stagingDirectory.path}`');
}
logger.printStatus('\nAbandon complete. Start a new migration with:');
printCommandText('flutter migrate start', logger);
return const FlutterCommandResult(ExitStatus.success);
}
}
// 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 '../flutter_project_metadata.dart';
import '../migrate/migrate_manifest.dart';
import '../migrate/migrate_update_locks.dart';
import '../migrate/migrate_utils.dart';
import '../project.dart';
import '../runner/flutter_command.dart';
import '../version.dart';
import 'migrate.dart';
/// Migrate subcommand that checks the migrate working directory for unresolved conflicts and
/// applies the staged changes to the project.
class MigrateApplyCommand extends FlutterCommand {
MigrateApplyCommand({
bool verbose = false,
required this.logger,
required this.fileSystem,
required this.terminal,
required Platform platform,
required ProcessManager processManager,
}) : _verbose = verbose,
migrateUtils = MigrateUtils(
logger: logger,
fileSystem: fileSystem,
platform: platform,
processManager: processManager,
) {
requiresPubspecYaml();
argParser.addOption(
'staging-directory',
help: 'Specifies the custom migration working directory used to stage '
'and edit proposed changes. This path can be absolute or relative '
'to the flutter project root. This defaults to '
'`$kDefaultMigrateStagingDirectoryName`',
valueHelp: 'path',
);
argParser.addOption(
'project-directory',
help: 'The root directory of the flutter project. This defaults to the '
'current working directory if omitted.',
valueHelp: 'path',
);
argParser.addFlag(
'force',
abbr: 'f',
help: 'Ignore unresolved merge conflicts and uncommitted changes and '
'apply staged changes by force.',
);
argParser.addFlag(
'keep-working-directory',
help: 'Do not delete the working directory.',
);
}
final bool _verbose;
final Logger logger;
final FileSystem fileSystem;
final Terminal terminal;
final MigrateUtils migrateUtils;
@override
final String name = 'apply';
@override
final String description = r'Accepts the changes produced by `$ flutter '
'migrate start` and copies the changed files into '
'your project files. All merge conflicts should '
'be resolved before apply will complete '
'successfully. If conflicts still exist, this '
'command will print the remaining conflicted files.';
@override
String get category => FlutterCommandCategory.project;
@override
Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{};
@override
Future<FlutterCommandResult> runCommand() async {
final String? projectDirectory = stringArg('project-directory');
final FlutterProjectFactory flutterProjectFactory = FlutterProjectFactory(logger: logger, fileSystem: fileSystem);
final FlutterProject project = projectDirectory == null
? FlutterProject.current()
: flutterProjectFactory.fromDirectory(fileSystem.directory(projectDirectory));
if (!await gitRepoExists(project.directory.path, logger, migrateUtils)) {
logger.printStatus('No git repo found. Please run in a project with an '
'initialized git repo or initialize one with:');
printCommandText('git init', logger);
return const FlutterCommandResult(ExitStatus.fail);
}
final bool force = boolArg('force') ?? false;
Directory stagingDirectory = project.directory.childDirectory(kDefaultMigrateStagingDirectoryName);
final String? customStagingDirectoryPath = stringArg('staging-directory');
if (customStagingDirectoryPath != null) {
if (fileSystem.path.isAbsolute(customStagingDirectoryPath)) {
stagingDirectory = fileSystem.directory(customStagingDirectoryPath);
} else {
stagingDirectory = project.directory.childDirectory(customStagingDirectoryPath);
}
}
if (!stagingDirectory.existsSync()) {
logger.printStatus('No migration in progress at $stagingDirectory. Please run:');
printCommandText('flutter migrate start', logger);
return const FlutterCommandResult(ExitStatus.fail);
}
final File manifestFile = MigrateManifest.getManifestFileFromDirectory(stagingDirectory);
final MigrateManifest manifest = MigrateManifest.fromFile(manifestFile);
if (!checkAndPrintMigrateStatus(manifest, stagingDirectory, warnConflict: true, logger: logger) && !force) {
logger.printStatus('Conflicting files found. Resolve these conflicts and try again.');
logger.printStatus('Guided conflict resolution wizard:');
printCommandText('flutter migrate resolve-conflicts', logger);
return const FlutterCommandResult(ExitStatus.fail);
}
if (await hasUncommittedChanges(project.directory.path, logger, migrateUtils) && !force) {
return const FlutterCommandResult(ExitStatus.fail);
}
logger.printStatus('Applying migration.');
// Copy files from working directory to project root
final List<String> allFilesToCopy = <String>[];
allFilesToCopy.addAll(manifest.mergedFiles);
allFilesToCopy.addAll(manifest.conflictFiles);
allFilesToCopy.addAll(manifest.addedFiles);
if (allFilesToCopy.isNotEmpty && _verbose) {
logger.printStatus('Modifying ${allFilesToCopy.length} files.', indent: 2);
}
for (final String localPath in allFilesToCopy) {
if (_verbose) {
logger.printStatus('Writing $localPath');
}
final File workingFile = stagingDirectory.childFile(localPath);
final File targetFile = project.directory.childFile(localPath);
if (!workingFile.existsSync()) {
continue;
}
if (!targetFile.existsSync()) {
targetFile.createSync(recursive: true);
}
try {
targetFile.writeAsStringSync(workingFile.readAsStringSync(), flush: true);
} on FileSystemException {
targetFile.writeAsBytesSync(workingFile.readAsBytesSync(), flush: true);
}
}
// Delete files slated for deletion.
if (manifest.deletedFiles.isNotEmpty) {
logger.printStatus('Deleting ${manifest.deletedFiles.length} files.', indent: 2);
}
for (final String localPath in manifest.deletedFiles) {
final File targetFile = FlutterProject.current().directory.childFile(localPath);
targetFile.deleteSync();
}
// Update the migrate config files to reflect latest migration.
if (_verbose) {
logger.printStatus('Updating .migrate_configs');
}
final FlutterProjectMetadata metadata = FlutterProjectMetadata(project.directory.childFile('.metadata'), logger);
final FlutterVersion version = FlutterVersion(workingDirectory: project.directory.absolute.path);
final String currentGitHash = version.frameworkRevision;
metadata.migrateConfig.populate(
projectDirectory: project.directory,
currentRevision: currentGitHash,
logger: logger,
);
// Clean up the working directory
final bool keepWorkingDirectory = boolArg('keep-working-directory') ?? false;
if (!keepWorkingDirectory) {
stagingDirectory.deleteSync(recursive: true);
}
// Detect pub dependency locking. Run flutter pub upgrade --major-versions
await updatePubspecDependencies(project, migrateUtils, logger, terminal);
// Detect gradle lockfiles in android directory. Delete lockfiles and regenerate with ./gradlew tasks (any gradle task that requires a build).
await updateGradleDependencyLocking(project, migrateUtils, logger, terminal, _verbose, fileSystem);
logger.printStatus('Migration complete. You may use commands like `git '
'status`, `git diff` and `git restore <file>` to continue '
'working with the migrated files.');
return const FlutterCommandResult(ExitStatus.success);
}
}
// 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_manifest.dart';
import '../migrate/migrate_utils.dart';
import '../project.dart';
import '../runner/flutter_command.dart';
import 'migrate.dart';
/// Flutter migrate subcommand to check the migration status of the project.
class MigrateStatusCommand extends FlutterCommand {
MigrateStatusCommand({
bool verbose = false,
required this.logger,
required this.fileSystem,
required Platform platform,
required ProcessManager processManager,
}) : _verbose = verbose,
migrateUtils = MigrateUtils(
logger: logger,
fileSystem: fileSystem,
platform: platform,
processManager: processManager,
) {
requiresPubspecYaml();
argParser.addOption(
'staging-directory',
help: 'Specifies the custom migration working directory used to stage '
'and edit proposed changes. This path can be absolute or relative '
'to the flutter project root. This defaults to '
'`$kDefaultMigrateStagingDirectoryName`',
valueHelp: 'path',
);
argParser.addOption(
'project-directory',
help: 'The root directory of the flutter project. This defaults to the '
'current working directory if omitted.',
valueHelp: 'path',
);
argParser.addFlag(
'diff',
defaultsTo: true,
help: 'Shows the diff output when enabled. Enabled by default.',
);
argParser.addFlag(
'show-added-files',
help: 'Shows the contents of added files. Disabled by default.',
);
}
final bool _verbose;
final Logger logger;
final FileSystem fileSystem;
final MigrateUtils migrateUtils;
@override
final String name = 'status';
@override
final String description = 'Prints the current status of the in progress migration.';
@override
String get category => FlutterCommandCategory.project;
@override
Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{};
/// Manually marks the lines in a diff that should be printed unformatted for visbility.
///
/// This is used to ensure the initial lines that display the files being diffed and the
/// git revisions are printed and never skipped.
final Set<int> _initialDiffLines = <int>{0, 1};
@override
Future<FlutterCommandResult> runCommand() async {
final String? projectDirectory = stringArg('project-directory');
final FlutterProjectFactory flutterProjectFactory = FlutterProjectFactory(logger: logger, fileSystem: fileSystem);
final FlutterProject project = projectDirectory == null
? FlutterProject.current()
: flutterProjectFactory.fromDirectory(fileSystem.directory(projectDirectory));
Directory stagingDirectory = project.directory.childDirectory(kDefaultMigrateStagingDirectoryName);
final String? customStagingDirectoryPath = stringArg('staging-directory');
if (customStagingDirectoryPath != null) {
if (fileSystem.path.isAbsolute(customStagingDirectoryPath)) {
stagingDirectory = fileSystem.directory(customStagingDirectoryPath);
} else {
stagingDirectory = project.directory.childDirectory(customStagingDirectoryPath);
}
}
if (!stagingDirectory.existsSync()) {
logger.printStatus('No migration in progress in $stagingDirectory. Start a new migration with:');
printCommandText('flutter migrate start', logger);
return const FlutterCommandResult(ExitStatus.fail);
}
final File manifestFile = MigrateManifest.getManifestFileFromDirectory(stagingDirectory);
if (!manifestFile.existsSync()) {
logger.printError('No migrate manifest in the migrate working directory '
'at ${stagingDirectory.path}. Fix the working directory '
'or abandon and restart the migration.');
return const FlutterCommandResult(ExitStatus.fail);
}
final MigrateManifest manifest = MigrateManifest.fromFile(manifestFile);
final bool showDiff = boolArg('diff') ?? true;
final bool showAddedFiles = boolArg('show-added-files') ?? true;
if (showDiff || _verbose) {
if (showAddedFiles || _verbose) {
for (final String localPath in manifest.addedFiles) {
logger.printStatus('Newly added file at $localPath:\n');
try {
logger.printStatus(stagingDirectory.childFile(localPath).readAsStringSync(), color: TerminalColor.green);
} on FileSystemException {
logger.printStatus('Contents are byte data\n', color: TerminalColor.grey);
}
}
}
final List<String> files = <String>[];
files.addAll(manifest.mergedFiles);
files.addAll(manifest.resolvedConflictFiles(stagingDirectory));
files.addAll(manifest.remainingConflictFiles(stagingDirectory));
for (final String localPath in files) {
final DiffResult result = await migrateUtils.diffFiles(project.directory.childFile(localPath), stagingDirectory.childFile(localPath));
if (result.diff != '' && result.diff != null) {
// Print with different colors for better visibility.
int lineNumber = -1;
for (final String line in result.diff!.split('\n')) {
lineNumber++;
if (line.startsWith('---') || line.startsWith('+++') || line.startsWith('&&') || _initialDiffLines.contains(lineNumber)) {
logger.printStatus(line);
continue;
}
if (line.startsWith('-')) {
logger.printStatus(line, color: TerminalColor.red);
continue;
}
if (line.startsWith('+')) {
logger.printStatus(line, color: TerminalColor.green);
continue;
}
logger.printStatus(line, color: TerminalColor.grey);
}
}
}
}
logger.printBox('Working directory at `${stagingDirectory.path}`');
checkAndPrintMigrateStatus(manifest, stagingDirectory, logger: logger);
final bool readyToApply = manifest.remainingConflictFiles(stagingDirectory).isEmpty;
if (!readyToApply) {
logger.printStatus('Guided conflict resolution wizard:');
printCommandText('flutter migrate resolve-conflicts', logger);
logger.printStatus('Resolve conflicts and accept changes with:');
} else {
logger.printStatus('All conflicts resolved. Review changes above and '
'apply the migration with:',
color: TerminalColor.green);
}
printCommandText('flutter migrate apply', logger);
return const FlutterCommandResult(ExitStatus.success);
}
}
// 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}conflict_files:\n${conflictFilesManifestContents}added_files:\n${newFileManifestContents}deleted_files:\n$deletedFileManifestContents';
final File migrateManifest = getManifestFileFromDirectory(migrateRootDir);
migrateManifest.createSync(recursive: true);
migrateManifest.writeAsStringSync(migrateManifestContents, flush: true);
}
}
/// Returns true if the file does not contain any git conflict markers.
bool _conflictsResolved(String contents) {
if (contents.contains('>>>>>>>') && contents.contains('=======') && contents.contains('<<<<<<<')) {
return false;
}
return true;
}
/// Returns true if the migration working directory has all conflicts resolved and prints the migration status.
///
/// The migration status printout lists all added, deleted, merged, and conflicted files.
bool checkAndPrintMigrateStatus(MigrateManifest manifest, Directory workingDir, {bool warnConflict = false, Logger? logger}) {
final StringBuffer printout = StringBuffer();
final StringBuffer redPrintout = StringBuffer();
bool result = true;
final List<String> remainingConflicts = <String>[];
final List<String> mergedFiles = <String>[];
for (final String localPath in manifest.conflictFiles) {
if (!_conflictsResolved(workingDir.childFile(localPath).readAsStringSync())) {
remainingConflicts.add(localPath);
} else {
mergedFiles.add(localPath);
}
}
mergedFiles.addAll(manifest.mergedFiles);
if (manifest.addedFiles.isNotEmpty) {
printout.write('Added files:\n');
for (final String localPath in manifest.addedFiles) {
printout.write(' - $localPath\n');
}
}
if (manifest.deletedFiles.isNotEmpty) {
printout.write('Deleted files:\n');
for (final String localPath in manifest.deletedFiles) {
printout.write(' - $localPath\n');
}
}
if (mergedFiles.isNotEmpty) {
printout.write('Modified files:\n');
for (final String localPath in mergedFiles) {
printout.write(' - $localPath\n');
}
}
if (remainingConflicts.isNotEmpty) {
if (warnConflict) {
printout.write('Unable to apply migration. The following files in the migration working directory still have unresolved conflicts:');
} else {
printout.write('Merge conflicted files:');
}
for (final String localPath in remainingConflicts) {
redPrintout.write(' - $localPath\n');
}
result = false;
}
if (logger != null) {
logger.printStatus(printout.toString());
logger.printStatus(redPrintout.toString(), color: TerminalColor.red, newline: false);
}
return result;
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '../base/file_system.dart';
import 'migrate_utils.dart';
/// Data class that holds all results and generated directories from a computeMigration run.
///
/// mergeResults, addedFiles, and deletedFiles includes the sets of files to be migrated while
/// the other members track the temporary sdk and generated app directories created by the tool.
///
/// The compute function does not clean up the temp directories, as the directories may be reused,
/// so this must be done manually afterwards.
class MigrateResult {
/// Explicitly initialize the MigrateResult.
MigrateResult({
required this.mergeResults,
required this.addedFiles,
required this.deletedFiles,
required this.tempDirectories,
required this.sdkDirs,
required this.mergeTypeMap,
required this.diffMap,
this.generatedBaseTemplateDirectory,
this.generatedTargetTemplateDirectory});
/// Creates a MigrateResult with all empty members.
MigrateResult.empty()
: mergeResults = <MergeResult>[],
addedFiles = <FilePendingMigration>[],
deletedFiles = <FilePendingMigration>[],
tempDirectories = <Directory>[],
mergeTypeMap = <String, MergeType>{},
diffMap = <String, DiffResult>{},
sdkDirs = <String, Directory>{};
/// The results of merging existing files with the target files.
final List<MergeResult> mergeResults;
/// Tracks the files that are to be newly added to the project.
final List<FilePendingMigration> addedFiles;
/// Tracks the files that are to be deleted from the project.
final List<FilePendingMigration> deletedFiles;
/// Tracks the temporary directories created during the migrate compute process.
final List<Directory> tempDirectories;
/// Mapping between the local path of a file and the type of merge that should be used.
final Map<String, MergeType> mergeTypeMap;
/// Mapping between the local path of a file and the diff between the base and target
/// versions of the file.
final Map<String, DiffResult> diffMap;
/// The root directory of the base app.
Directory? generatedBaseTemplateDirectory;
/// The root directory of the target app.
Directory? generatedTargetTemplateDirectory;
/// The root directories of the Flutter SDK for each revision.
Map<String, Directory> sdkDirs;
}
/// Defines available merge techniques.
enum MergeType {
/// A standard three-way merge.
threeWay,
/// A two way merge that ignores the base version of the file.
twoWay,
/// A `CustomMerge` manually handles the merge.
custom,
}
/// Stores a file that has been marked for migration and metadata about the file.
class FilePendingMigration {
FilePendingMigration(this.localPath, this.file);
String localPath;
File file;
}
// 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 '../base/logger.dart';
import '../base/terminal.dart';
import '../project.dart';
import 'migrate_utils.dart';
/// Checks if the project uses pubspec dependency locking and prompts if
/// the pub upgrade should be run.
Future<void> updatePubspecDependencies(
FlutterProject flutterProject,
MigrateUtils migrateUtils,
Logger logger,
Terminal terminal
) async {
final File pubspecFile = flutterProject.directory.childFile('pubspec.yaml');
if (!pubspecFile.existsSync()) {
return;
}
if (!pubspecFile.readAsStringSync().contains('# THIS LINE IS AUTOGENERATED')) {
return;
}
logger.printStatus('\nDart dependency locking detected in pubspec.yaml.');
terminal.usesTerminalUi = true;
String selection = 'y';
selection = await terminal.promptForCharInput(
<String>['y', 'n'],
logger: logger,
prompt: 'Do you want the tool to run `flutter pub upgrade --major-versions`? (y)es, (n)o',
defaultChoiceIndex: 1,
);
if (selection == 'y') {
// Runs `flutter pub upgrade --major-versions`
await migrateUtils.flutterPubUpgrade(flutterProject.directory.path);
}
}
/// Checks if gradle dependency locking is used and prompts the developer to
/// remove and back up the gradle dependency lockfile.
Future<void> updateGradleDependencyLocking(
FlutterProject flutterProject,
MigrateUtils migrateUtils,
Logger logger,
Terminal terminal,
bool verbose,
FileSystem fileSystem
) async {
final Directory androidDir = flutterProject.directory.childDirectory('android');
if (!androidDir.existsSync()) {
return;
}
final List<FileSystemEntity> androidFiles = androidDir.listSync();
final List<File> lockfiles = <File>[];
final List<String> backedUpFilePaths = <String>[];
for (final FileSystemEntity entity in androidFiles) {
if (entity is! File) {
continue;
}
final File file = entity.absolute;
// Don't re-handle backed up lockfiles.
if (file.path.contains('_backup_')) {
continue;
}
try {
// lockfiles generated by gradle start with this prefix.
if (file.readAsStringSync().startsWith(
'# This is a Gradle generated file for dependency locking.\n# '
'Manual edits can break the build and are not advised.\n# This '
'file is expected to be part of source control.')) {
lockfiles.add(file);
}
} on FileSystemException {
if (verbose) {
logger.printStatus('Unable to check ${file.path}');
}
}
}
if (lockfiles.isNotEmpty) {
logger.printStatus('\nGradle dependency locking detected.');
logger.printStatus('Flutter can backup the lockfiles and regenerate updated '
'lockfiles.');
terminal.usesTerminalUi = true;
String selection = 'y';
selection = await terminal.promptForCharInput(
<String>['y', 'n'],
logger: logger,
prompt: 'Do you want the tool to update locked dependencies? (y)es, (n)o',
defaultChoiceIndex: 1,
);
if (selection == 'y') {
for (final File file in lockfiles) {
int counter = 0;
while (true) {
final String newPath = '${file.absolute.path}_backup_$counter';
if (!fileSystem.file(newPath).existsSync()) {
file.renameSync(newPath);
backedUpFilePaths.add(newPath);
break;
} else {
counter++;
}
}
}
// Runs `./gradlew tasks`in the project's android directory.
await migrateUtils.gradlewTasks(flutterProject.directory.childDirectory('android').path);
logger.printStatus('Old lockfiles renamed to:');
for (final String path in backedUpFilePaths) {
logger.printStatus(path, color: TerminalColor.grey, indent: 2);
}
}
}
}
// 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 kDefaultMigrateStagingDirectoryName = 'migrate_staging_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? migrateStagingDir}) async {
final List<String> cmdArgs = <String>[
'git',
'ls-files',
'--deleted',
'--modified',
'--others',
'--exclude=${migrateStagingDir ?? kDefaultMigrateStagingDirectoryName}'
];
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)) {
return true;
}
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;
}
/// Returns true if the file does not contain any git conflit markers.
bool conflictsResolved(String contents) {
final bool hasMarker = contents.contains('>>>>>>>') ||
contents.contains('=======') ||
contents.contains('<<<<<<<');
return !hasMarker;
}
}
/// 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;
}
// 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:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/terminal.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/migrate.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/migrate/migrate_utils.dart';
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/test_flutter_command_runner.dart';
void main() {
late FileSystem fileSystem;
late BufferLogger logger;
late Platform platform;
late Terminal terminal;
late ProcessManager processManager;
late Directory appDir;
setUp(() {
fileSystem = globals.localFileSystem;
appDir = fileSystem.systemTempDirectory.createTempSync('apptestdir');
logger = BufferLogger.test();
platform = FakePlatform();
terminal = Terminal.test();
processManager = globals.processManager;
});
setUpAll(() {
Cache.disableLocking();
});
tearDown(() async {
tryToDelete(appDir);
});
testUsingContext('abandon deletes staging directory', () async {
final MigrateCommand command = MigrateCommand(
verbose: true,
logger: logger,
fileSystem: fileSystem,
terminal: terminal,
platform: platform,
processManager: processManager,
);
final Directory stagingDir = appDir.childDirectory(kDefaultMigrateStagingDirectoryName);
appDir.childFile('lib/main.dart').createSync(recursive: true);
final File pubspecOriginal = appDir.childFile('pubspec.yaml');
pubspecOriginal.createSync();
pubspecOriginal.writeAsStringSync('''
name: originalname
description: A new Flutter project.
version: 1.0.0+1
environment:
sdk: '>=2.18.0-58.0.dev <3.0.0'
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
uses-material-design: true''', flush: true);
expect(stagingDir.existsSync(), false);
await createTestCommandRunner(command).run(
<String>[
'migrate',
'abandon',
'--staging-directory=${stagingDir.path}',
'--project-directory=${appDir.path}',
]
);
expect(logger.errorText, contains('Provided staging directory'));
expect(logger.errorText, contains('migrate_staging_dir` does not exist or is not valid.'));
logger.clear();
await createTestCommandRunner(command).run(
<String>[
'migrate',
'abandon',
'--project-directory=${appDir.path}',
]
);
expect(logger.statusText, contains('No migration in progress. Start a new migration with:'));
final File pubspecModified = stagingDir.childFile('pubspec.yaml');
pubspecModified.createSync(recursive: true);
pubspecModified.writeAsStringSync('''
name: newname
description: new description of the test project
version: 1.0.0+1
environment:
sdk: '>=2.18.0-58.0.dev <3.0.0'
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
uses-material-design: false
EXTRALINE''', flush: true);
final File addedFile = stagingDir.childFile('added.file');
addedFile.createSync(recursive: true);
addedFile.writeAsStringSync('new file contents');
final File manifestFile = stagingDir.childFile('.migrate_manifest');
manifestFile.createSync(recursive: true);
manifestFile.writeAsStringSync('''
merged_files:
- pubspec.yaml
conflict_files:
added_files:
- added.file
deleted_files:
''');
expect(appDir.childFile('lib/main.dart').existsSync(), true);
expect(stagingDir.existsSync(), true);
logger.clear();
await createTestCommandRunner(command).run(
<String>[
'migrate',
'abandon',
'--staging-directory=${stagingDir.path}',
'--project-directory=${appDir.path}',
'--force',
]
);
expect(logger.statusText, contains('Abandon complete. Start a new migration with:'));
expect(stagingDir.existsSync(), false);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
Platform: () => platform,
});
}
// 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:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/terminal.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/migrate.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/migrate/migrate_utils.dart';
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/test_flutter_command_runner.dart';
void main() {
late FileSystem fileSystem;
late BufferLogger logger;
late Platform platform;
late Terminal terminal;
late ProcessManager processManager;
late Directory appDir;
setUp(() {
fileSystem = globals.localFileSystem;
appDir = fileSystem.systemTempDirectory.createTempSync('apptestdir');
logger = BufferLogger.test();
platform = FakePlatform();
terminal = Terminal.test();
processManager = globals.processManager;
});
setUpAll(() {
Cache.disableLocking();
});
tearDown(() async {
tryToDelete(appDir);
});
testUsingContext('Apply produces all outputs', () async {
final MigrateCommand command = MigrateCommand(
verbose: true,
logger: logger,
fileSystem: fileSystem,
terminal: terminal,
platform: platform,
processManager: processManager,
);
final Directory workingDir = appDir.childDirectory(kDefaultMigrateStagingDirectoryName);
appDir.childFile('lib/main.dart').createSync(recursive: true);
final File pubspecOriginal = appDir.childFile('pubspec.yaml');
pubspecOriginal.createSync();
pubspecOriginal.writeAsStringSync('''
name: originalname
description: A new Flutter project.
version: 1.0.0+1
environment:
sdk: '>=2.18.0-58.0.dev <3.0.0'
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
uses-material-design: true''', flush: true);
final File gitignore = appDir.childFile('.gitignore');
gitignore.createSync();
gitignore.writeAsStringSync(kDefaultMigrateStagingDirectoryName, flush: true);
logger.clear();
await createTestCommandRunner(command).run(
<String>[
'migrate',
'apply',
'--staging-directory=${workingDir.path}',
'--project-directory=${appDir.path}',
]
);
expect(logger.statusText, contains('Project is not a git repo. Please initialize a git repo and try again.'));
await processManager.run(<String>['git', 'init'], workingDirectory: appDir.path);
logger.clear();
await createTestCommandRunner(command).run(
<String>[
'migrate',
'apply',
'--staging-directory=${workingDir.path}',
'--project-directory=${appDir.path}',
]
);
expect(logger.statusText, contains('No migration in progress'));
final File pubspecModified = workingDir.childFile('pubspec.yaml');
pubspecModified.createSync(recursive: true);
pubspecModified.writeAsStringSync('''
name: newname
description: new description of the test project
version: 1.0.0+1
environment:
sdk: '>=2.18.0-58.0.dev <3.0.0'
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
uses-material-design: false
# EXTRALINE:''', flush: true);
final File addedFile = workingDir.childFile('added.file');
addedFile.createSync(recursive: true);
addedFile.writeAsStringSync('new file contents');
final File manifestFile = workingDir.childFile('.migrate_manifest');
manifestFile.createSync(recursive: true);
manifestFile.writeAsStringSync('''
merged_files:
- pubspec.yaml
conflict_files:
- conflict/conflict.file
added_files:
- added.file
deleted_files:
''');
// Add conflict file
final File conflictFile = workingDir.childDirectory('conflict').childFile('conflict.file');
conflictFile.createSync(recursive: true);
conflictFile.writeAsStringSync('''
line1
<<<<<<< /conflcit/conflict.file
line2
=======
linetwo
>>>>>>> /var/folders/md/gm0zgfcj07vcsj6jkh_mp_wh00ff02/T/flutter_tools.4Xdep8/generatedTargetTemplatetlN44S/conflict/conflict.file
line3
''', flush: true);
final File conflictFileOriginal = appDir.childDirectory('conflict').childFile('conflict.file');
conflictFileOriginal.createSync(recursive: true);
conflictFileOriginal.writeAsStringSync('''
line1
line2
line3
''', flush: true);
logger.clear();
await createTestCommandRunner(command).run(
<String>[
'migrate',
'apply',
'--staging-directory=${workingDir.path}',
'--project-directory=${appDir.path}',
]
);
expect(logger.statusText, contains(r'''
Added files:
- added.file
Modified files:
- pubspec.yaml
Unable to apply migration. The following files in the migration working directory still have unresolved conflicts:
- conflict/conflict.file
Conflicting files found. Resolve these conflicts and try again.
Guided conflict resolution wizard:
$ flutter migrate resolve-conflicts'''));
conflictFile.writeAsStringSync('''
line1
linetwo
line3
''', flush: true);
logger.clear();
await createTestCommandRunner(command).run(
<String>[
'migrate',
'apply',
'--staging-directory=${workingDir.path}',
'--project-directory=${appDir.path}',
]
);
expect(logger.statusText, contains('There are uncommitted changes in your project. Please git commit, abandon, or stash your changes before trying again.'));
await processManager.run(<String>['git', 'add', '.'], workingDirectory: appDir.path);
await processManager.run(<String>['git', 'commit', '-m', 'Initial commit'], workingDirectory: appDir.path);
logger.clear();
await createTestCommandRunner(command).run(
<String>[
'migrate',
'apply',
'--staging-directory=${workingDir.path}',
'--project-directory=${appDir.path}',
]
);
expect(logger.statusText, contains(r'''
Added files:
- added.file
Modified files:
- conflict/conflict.file
- pubspec.yaml
Applying migration.
Modifying 3 files.
Writing pubspec.yaml
Writing conflict/conflict.file
Writing added.file
Updating .migrate_configs
Migration complete. You may use commands like `git status`, `git diff` and `git restore <file>` to continue working with the migrated files.'''));
expect(pubspecOriginal.readAsStringSync(), contains('# EXTRALINE'));
expect(conflictFileOriginal.readAsStringSync(), contains('linetwo'));
expect(appDir.childFile('added.file').existsSync(), true);
expect(appDir.childFile('added.file').readAsStringSync(), contains('new file contents'));
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
Platform: () => platform,
});
}
// 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:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/terminal.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/migrate.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/migrate/migrate_utils.dart';
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/test_flutter_command_runner.dart';
void main() {
late FileSystem fileSystem;
late BufferLogger logger;
late Platform platform;
late Terminal terminal;
late ProcessManager processManager;
late Directory appDir;
setUp(() {
fileSystem = globals.localFileSystem;
appDir = fileSystem.systemTempDirectory.createTempSync('apptestdir');
logger = BufferLogger.test();
platform = FakePlatform();
terminal = Terminal.test();
processManager = globals.processManager;
});
setUpAll(() {
Cache.disableLocking();
});
tearDown(() async {
tryToDelete(appDir);
});
testUsingContext('Status produces all outputs', () async {
final MigrateCommand command = MigrateCommand(
verbose: true,
logger: logger,
fileSystem: fileSystem,
terminal: terminal,
platform: platform,
processManager: processManager,
);
final Directory stagingDir = appDir.childDirectory(kDefaultMigrateStagingDirectoryName);
final File pubspecOriginal = appDir.childFile('pubspec.yaml');
pubspecOriginal.createSync();
pubspecOriginal.writeAsStringSync('''
name: originalname
description: A new Flutter project.
version: 1.0.0+1
environment:
sdk: '>=2.18.0-58.0.dev <3.0.0'
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
uses-material-design: true''', flush: true);
final File pubspecModified = stagingDir.childFile('pubspec.yaml');
pubspecModified.createSync(recursive: true);
pubspecModified.writeAsStringSync('''
name: newname
description: new description of the test project
version: 1.0.0+1
environment:
sdk: '>=2.18.0-58.0.dev <3.0.0'
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
uses-material-design: false
EXTRALINE''', flush: true);
final File addedFile = stagingDir.childFile('added.file');
addedFile.createSync(recursive: true);
addedFile.writeAsStringSync('new file contents');
final File manifestFile = stagingDir.childFile('.migrate_manifest');
manifestFile.createSync(recursive: true);
manifestFile.writeAsStringSync('''
merged_files:
- pubspec.yaml
conflict_files:
added_files:
- added.file
deleted_files:
''');
await createTestCommandRunner(command).run(
<String>[
'migrate',
'status',
'--staging-directory=${stagingDir.path}',
'--project-directory=${appDir.path}',
]
);
expect(logger.statusText, contains('''
Newly added file at added.file:
new file contents'''));
expect(logger.statusText, contains(r'''
Added files:
- added.file
Modified files:
- pubspec.yaml
All conflicts resolved. Review changes above and apply the migration with:
$ flutter migrate apply
'''));
expect(logger.statusText, contains(r'''
@@ -1,5 +1,5 @@
-name: originalname
-description: A new Flutter project.
+name: newname
+description: new description of the test project
version: 1.0.0+1
environment:
sdk: '>=2.18.0-58.0.dev <3.0.0'
@@ -10,4 +10,5 @@ dev_dependencies:
flutter_test:
sdk: flutter
flutter:
- uses-material-design: true
\ No newline at end of file
+ uses-material-design: false
+ EXTRALINE'''));
// Add conflict file
final File conflictFile = stagingDir.childDirectory('conflict').childFile('conflict.file');
conflictFile.createSync(recursive: true);
conflictFile.writeAsStringSync('''
line1
<<<<<<< /conflcit/conflict.file
line2
=======
linetwo
>>>>>>> /var/folders/md/gm0zgfcj07vcsj6jkh_mp_wh00ff02/T/flutter_tools.4Xdep8/generatedTargetTemplatetlN44S/conflict/conflict.file
line3
''', flush: true);
final File conflictFileOriginal = appDir.childDirectory('conflict').childFile('conflict.file');
conflictFileOriginal.createSync(recursive: true);
conflictFileOriginal.writeAsStringSync('''
line1
line2
line3
''', flush: true);
manifestFile.writeAsStringSync('''
merged_files:
- pubspec.yaml
conflict_files:
- conflict/conflict.file
added_files:
- added.file
deleted_files:
''');
logger.clear();
await createTestCommandRunner(command).run(
<String>[
'migrate',
'status',
'--staging-directory=${stagingDir.path}',
'--project-directory=${appDir.path}',
]
);
expect(logger.statusText, contains('''
@@ -1,3 +1,7 @@
line1
+<<<<<<< /conflcit/conflict.file
line2
+=======
+linetwo
+>>>>>>>'''));
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
Platform: () => platform,
});
}
// 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/base/logger.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;
late BufferLogger logger;
setUpAll(() {
fileSystem = MemoryFileSystem.test();
logger = BufferLogger.test();
manifestFile = fileSystem.file('.migrate_manifest');
});
group('checkAndPrintMigrateStatus', () {
testWithoutContext('empty MigrateResult produces empty output', () async {
final Directory workingDir = fileSystem.directory('migrate_working_dir');
workingDir.createSync(recursive: true);
final MigrateManifest manifest = MigrateManifest(migrateRootDir: workingDir, migrateResult: MigrateResult(
mergeResults: <MergeResult>[],
addedFiles: <FilePendingMigration>[],
deletedFiles: <FilePendingMigration>[],
mergeTypeMap: <String, MergeType>{},
diffMap: <String, DiffResult>{},
tempDirectories: <Directory>[],
sdkDirs: <String, Directory>{},
));
checkAndPrintMigrateStatus(manifest, workingDir, warnConflict: true, logger: logger);
expect(logger.statusText, contains('\n'));
});
testWithoutContext('populated MigrateResult produces correct output', () async {
final Directory workingDir = fileSystem.directory('migrate_working_dir');
workingDir.createSync(recursive: true);
final MigrateManifest manifest = MigrateManifest(migrateRootDir: workingDir, migrateResult: MigrateResult(
mergeResults: <MergeResult>[
StringMergeResult.explicit(
localPath: 'merged_file',
mergedString: 'str',
hasConflict: false,
exitCode: 0,
),
StringMergeResult.explicit(
localPath: 'conflict_file',
mergedString: 'hello\nwow a bunch of lines\n<<<<<<<\n=======\n<<<<<<<\nhi\n',
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>{},
));
final File conflictFile = workingDir.childFile('conflict_file');
conflictFile.writeAsStringSync('hello\nwow a bunch of lines\n<<<<<<<\n=======\n<<<<<<<\nhi\n', flush: true);
checkAndPrintMigrateStatus(manifest, workingDir, warnConflict: true, logger: logger);
expect(logger.statusText, contains('''
Added files:
- added_file
Deleted files:
- deleted_file
Modified files:
- conflict_file
- merged_file
'''));
});
testWithoutContext('populated MigrateResult detects fixed conflict', () async {
final Directory workingDir = fileSystem.directory('migrate_working_dir');
workingDir.createSync(recursive: true);
final MigrateManifest manifest = MigrateManifest(migrateRootDir: workingDir, migrateResult: MigrateResult(
mergeResults: <MergeResult>[
StringMergeResult.explicit(
localPath: 'merged_file',
mergedString: 'str',
hasConflict: false,
exitCode: 0,
),
StringMergeResult.explicit(
localPath: 'conflict_file',
mergedString: 'hello\nwow a bunch of lines\n<<<<<<<\n=======\n<<<<<<<\nhi\n',
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>{},
));
final File conflictFile = workingDir.childFile('conflict_file');
conflictFile.writeAsStringSync('hello\nwow a bunch of lines\nhi\n', flush: true);
checkAndPrintMigrateStatus(manifest, workingDir, warnConflict: true, logger: logger);
expect(logger.statusText, contains('''
Added files:
- added_file
Deleted files:
- deleted_file
Modified files:
- conflict_file
- merged_file
'''));
});
});
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
''');
});
});
}
// 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/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/commands/migrate.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() {
late BufferLogger logger;
late FileSystem fileSystem;
late Directory projectRoot;
late String projectRootPath;
late ProcessUtils processUtils;
late MigrateUtils utils;
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_command_test');
projectRoot.createSync(recursive: true);
projectRootPath = projectRoot.path;
});
tearDown(() async {
tryToDelete(projectRoot);
});
testWithoutContext('isGitRepo', () async {
expect(projectRoot.existsSync(), true);
expect(projectRoot.childDirectory('.git').existsSync(), false);
expect(await gitRepoExists(projectRootPath, logger, utils), false);
expect(logger.statusText, contains('Project is not a git repo. Please initialize a git repo and try again.'));
await utils.gitInit(projectRootPath);
expect(projectRoot.childDirectory('.git').existsSync(), true);
expect(await gitRepoExists(projectRootPath, logger, utils), true);
});
testWithoutContext('printCommandText produces formatted output', () async {
printCommandText('some command --help', logger);
expect(logger.statusText, contains(r' $ some command --help'));
});
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 hasUncommittedChanges(projectRootPath, logger, utils), 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 hasUncommittedChanges(projectRootPath, logger, utils), true);
});
});
}
// 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/file.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/flutter_project_metadata.dart';
import 'package:flutter_tools/src/project.dart';
import '../src/common.dart';
import '../src/context.dart';
import 'test_data/migrate_project.dart';
import 'test_driver.dart';
import 'test_utils.dart';
void main() {
late Directory tempDir;
late FlutterRunTestDriver flutter;
late Logger logger;
setUp(() async {
tempDir = createResolvedTempDirectorySync('run_test.');
flutter = FlutterRunTestDriver(tempDir);
logger = BufferLogger.test();
});
tearDown(() async {
await flutter.stop();
tryToDelete(tempDir);
});
testWithoutContext('parse simple config file', () async {
final File metadataFile = tempDir.childFile('.metadata');
metadataFile.createSync(recursive: true);
metadataFile.writeAsStringSync('''
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled.
version:
revision: fj19vkla9vnlka9vni3n808v3nch8cd
channel: stable
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: fj19vkla9vnlka9vni3n808v3nch8cd
base_revision: 93kf9v3njfa90vnidfjvn39nvi3vnie
- platform: android
create_revision: abfj19vkla9vnlka9vni3n808v3nch8cd
base_revision: ab93kf9v3njfa90vnidfjvn39nvi3vnie
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- lib/main.dart
- ios/Runner.xcodeproj/project.pbxproj
- lib/file1/etc.dart
- android/my_file.java
''', flush: true);
FlutterProjectMetadata metadata = FlutterProjectMetadata(metadataFile, logger);
expect(metadata.migrateConfig.platformConfigs[SupportedPlatform.root]!.createRevision, equals('fj19vkla9vnlka9vni3n808v3nch8cd'));
expect(metadata.migrateConfig.platformConfigs[SupportedPlatform.root]!.baseRevision, equals('93kf9v3njfa90vnidfjvn39nvi3vnie'));
expect(metadata.migrateConfig.platformConfigs[SupportedPlatform.android]!.createRevision, equals('abfj19vkla9vnlka9vni3n808v3nch8cd'));
expect(metadata.migrateConfig.platformConfigs[SupportedPlatform.android]!.baseRevision, equals('ab93kf9v3njfa90vnidfjvn39nvi3vnie'));
expect(metadata.migrateConfig.unmanagedFiles[0], equals('lib/main.dart'));
expect(metadata.migrateConfig.unmanagedFiles[1], equals('ios/Runner.xcodeproj/project.pbxproj'));
expect(metadata.migrateConfig.unmanagedFiles[2], equals('lib/file1/etc.dart'));
expect(metadata.migrateConfig.unmanagedFiles[3], equals('android/my_file.java'));
metadataFile.writeAsStringSync('''
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled.
version:
revision: fj19vkla9vnlka9vni3n808v3nch8cd
channel: stable
project_type: app
''', flush: true);
metadata = FlutterProjectMetadata(metadataFile, logger);
expect(metadata.migrateConfig.isEmpty, equals(true));
expect(metadata.versionRevision, equals('fj19vkla9vnlka9vni3n808v3nch8cd'));
expect(metadata.versionChannel, equals('stable'));
});
testUsingContext('write simple config file', () async {
const String testCreateRevision = 'testmc9skl32nlnf23lnakcs9njr3';
const String testBaseRevision = 'testanas9anlnq9ba7bjhavan3kma';
MigrateConfig config = MigrateConfig(
platformConfigs: <SupportedPlatform, MigratePlatformConfig>{
SupportedPlatform.android: MigratePlatformConfig(platform: SupportedPlatform.android, createRevision: testCreateRevision, baseRevision: testBaseRevision),
SupportedPlatform.ios: MigratePlatformConfig(platform: SupportedPlatform.ios, createRevision: testCreateRevision, baseRevision: testBaseRevision),
SupportedPlatform.root: MigratePlatformConfig(platform: SupportedPlatform.root, createRevision: testCreateRevision, baseRevision: testBaseRevision),
SupportedPlatform.windows: MigratePlatformConfig(platform: SupportedPlatform.windows, createRevision: testCreateRevision, baseRevision: testBaseRevision),
},
unmanagedFiles: <String>[
'lib/main.dart',
'ios/Runner.xcodeproj/project.pbxproj',
'lib/file1/etc.dart',
],
);
String outputString = config.getOutputFileString();
expect(outputString, equals('''
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: android
create_revision: $testCreateRevision
base_revision: $testBaseRevision
- platform: ios
create_revision: $testCreateRevision
base_revision: $testBaseRevision
- platform: root
create_revision: $testCreateRevision
base_revision: $testBaseRevision
- platform: windows
create_revision: $testCreateRevision
base_revision: $testBaseRevision
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
- 'lib/file1/etc.dart'
'''));
config = MigrateConfig();
outputString = config.getOutputFileString();
expect(outputString, equals(''));
});
testUsingContext('populate migrate config', () async {
// Flutter Stable 1.22.6 hash: 9b2d32b605630f28625709ebd9d78ab3016b2bf6
final MigrateProject project = MigrateProject('version:1.22.6_stable');
await project.setUpIn(tempDir);
final File metadataFile = tempDir.childFile('.metadata');
const String currentRevision = 'test_base_revision';
const String createRevision = 'test_create_revision';
final FlutterProjectMetadata metadata = FlutterProjectMetadata(metadataFile, logger);
metadata.migrateConfig.populate(
projectDirectory: tempDir,
currentRevision: currentRevision,
createRevision: createRevision,
logger: logger,
);
expect(metadata.migrateConfig.platformConfigs.length, equals(3));
final List<SupportedPlatform> keyList = List<SupportedPlatform>.from(metadata.migrateConfig.platformConfigs.keys);
expect(keyList[0], equals(SupportedPlatform.root));
expect(metadata.migrateConfig.platformConfigs[SupportedPlatform.root]!.baseRevision, equals(currentRevision));
expect(metadata.migrateConfig.platformConfigs[SupportedPlatform.root]!.createRevision, equals(createRevision));
expect(keyList[1], equals(SupportedPlatform.android));
expect(metadata.migrateConfig.platformConfigs[SupportedPlatform.android]!.baseRevision, equals(currentRevision));
expect(metadata.migrateConfig.platformConfigs[SupportedPlatform.android]!.createRevision, equals(createRevision));
expect(keyList[2], equals(SupportedPlatform.ios));
expect(metadata.migrateConfig.platformConfigs[SupportedPlatform.ios]!.baseRevision, equals(currentRevision));
expect(metadata.migrateConfig.platformConfigs[SupportedPlatform.ios]!.createRevision, equals(createRevision));
final File metadataFileOutput = tempDir.childFile('.metadata_output');
metadata.writeFile(outputFile: metadataFileOutput);
expect(metadataFileOutput.readAsStringSync(), equals('''
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled.
version:
revision: 9b2d32b605630f28625709ebd9d78ab3016b2bf6
channel: unknown
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: $createRevision
base_revision: $currentRevision
- platform: android
create_revision: $createRevision
base_revision: $currentRevision
- platform: ios
create_revision: $createRevision
base_revision: $currentRevision
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
'''));
});
testUsingContext('equality compares platform', () async {
const String testCreateRevision = 'testmc9skl32nlnf23lnakcs9njr3';
const String testBaseRevision = 'testanas9anlnq9ba7bjhavan3kma';
final MigratePlatformConfig configAndroid = MigratePlatformConfig(platform: SupportedPlatform.android, createRevision: testCreateRevision, baseRevision: testBaseRevision);
final MigratePlatformConfig configIos = MigratePlatformConfig(platform: SupportedPlatform.ios, createRevision: testCreateRevision, baseRevision: testBaseRevision);
expect(configAndroid.equals(configIos), false);
expect(configAndroid.equals(configAndroid), true);
expect(configIos.equals(configIos), true);
});
}
// 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/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() {
late BufferLogger logger;
late FileSystem fileSystem;
late Directory projectRoot;
late String projectRootPath;
late MigrateUtils utils;
late 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;
});
tearDown(() async {
tryToDelete(projectRoot);
});
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);
});
});
testWithoutContext('conflictsResolved', () async {
expect(utils.conflictsResolved(''), true);
expect(utils.conflictsResolved('hello'), true);
expect(utils.conflictsResolved('hello\n'), true);
expect(utils.conflictsResolved('hello\nwow a bunch of lines\n\nhi\n'), true);
expect(utils.conflictsResolved('hello\nwow a bunch of lines\n>>>>>>>\nhi\n'), false);
expect(utils.conflictsResolved('hello\nwow a bunch of lines\n=======\nhi\n'), false);
expect(utils.conflictsResolved('hello\nwow a bunch of lines\n<<<<<<<\nhi\n'), false);
expect(utils.conflictsResolved('hello\nwow a bunch of lines\n<<<<<<<\n=======\n<<<<<<<\nhi\n'), false);
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment