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

[flutter_tools] MigrateUtils and MigrateManifest classes (#101937)

parent 93cce92e
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// import 'package:process/process.dart';
// import '../base/file_system.dart';
import '../base/logger.dart';
// import '../base/platform.dart';
import '../base/terminal.dart';
import '../migrate/migrate_utils.dart';
import '../runner/flutter_command.dart';
// TODO(garyq): Add each of these back in as they land.
// import 'migrate_abandon.dart';
// import 'migrate_apply.dart';
// import 'migrate_resolve_conflicts.dart';
// import 'migrate_start.dart';
// import 'migrate_status.dart';
/// Base command for the migration tool.
class MigrateCommand extends FlutterCommand {
MigrateCommand({
// bool verbose = false,
required this.logger,
// TODO(garyq): Add each of these back in as they land.
// required FileSystem fileSystem,
// required Terminal terminal,
// required Platform platform,
// required ProcessManager processManager,
}) {
// TODO(garyq): Add each of these back in as they land.
// addSubcommand(MigrateAbandonCommand(logger: logger, fileSystem: fileSystem, terminal: terminal, platform: platform, processManager: processManager));
// addSubcommand(MigrateApplyCommand(verbose: verbose, logger: logger, fileSystem: fileSystem, terminal: terminal, platform: platform, processManager: processManager));
// addSubcommand(MigrateResolveConflictsCommand(logger: logger, fileSystem: fileSystem, terminal: terminal));
// addSubcommand(MigrateStartCommand(verbose: verbose, logger: logger, fileSystem: fileSystem, platform: platform, processManager: processManager));
// addSubcommand(MigrateStatusCommand(verbose: verbose, logger: logger, fileSystem: fileSystem, platform: platform, processManager: processManager));
}
final Logger logger;
@override
final String name = 'migrate';
@override
final String description = 'Migrates flutter generated project files to the current flutter version';
@override
String get category => FlutterCommandCategory.project;
@override
Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{};
@override
Future<FlutterCommandResult> runCommand() async {
return const FlutterCommandResult(ExitStatus.fail);
}
}
Future<bool> gitRepoExists(String projectDirectory, Logger logger, MigrateUtils migrateUtils) async {
if (await migrateUtils.isGitRepo(projectDirectory)) {
return true;
}
logger.printStatus('Project is not a git repo. Please initialize a git repo and try again.');
printCommandText('git init', logger);
return false;
}
Future<bool> hasUncommittedChanges(String projectDirectory, Logger logger, MigrateUtils migrateUtils) async {
if (await migrateUtils.hasUncommittedChanges(projectDirectory)) {
logger.printStatus('There are uncommitted changes in your project. Please git commit, abandon, or stash your changes before trying again.');
return true;
}
return false;
}
/// Prints a command to logger with appropriate formatting.
void printCommandText(String command, Logger logger) {
logger.printStatus(
'\n\$ $command\n',
color: TerminalColor.grey,
indent: 4,
newline: false,
);
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:yaml/yaml.dart';
import '../base/file_system.dart';
import '../base/logger.dart';
import '../base/terminal.dart';
import 'migrate_result.dart';
import 'migrate_utils.dart';
const String _kMergedFilesKey = 'merged_files';
const String _kConflictFilesKey = 'conflict_files';
const String _kAddedFilesKey = 'added_files';
const String _kDeletedFilesKey = 'deleted_files';
/// Represents the manifest file that tracks the contents of the current
/// migration working directory.
///
/// This manifest file is created with the MigrateResult of a computeMigration run.
class MigrateManifest {
/// Creates a new manifest from a MigrateResult.
MigrateManifest({
required this.migrateRootDir,
required this.migrateResult,
});
/// Parses an existing migrate manifest.
MigrateManifest.fromFile(File manifestFile) : migrateResult = MigrateResult.empty(), migrateRootDir = manifestFile.parent {
final Object? yamlContents = loadYaml(manifestFile.readAsStringSync());
if (yamlContents is! YamlMap) {
throw Exception('Invalid .migrate_manifest file in the migrate working directory. File is not a Yaml map.');
}
final YamlMap map = yamlContents;
bool valid = map.containsKey(_kMergedFilesKey) && map.containsKey(_kConflictFilesKey) && map.containsKey(_kAddedFilesKey) && map.containsKey(_kDeletedFilesKey);
if (!valid) {
throw Exception('Invalid .migrate_manifest file in the migrate working directory. File is missing an entry.');
}
final Object? mergedFilesYaml = map[_kMergedFilesKey];
final Object? conflictFilesYaml = map[_kConflictFilesKey];
final Object? addedFilesYaml = map[_kAddedFilesKey];
final Object? deletedFilesYaml = map[_kDeletedFilesKey];
valid = valid && (mergedFilesYaml is YamlList || mergedFilesYaml == null);
valid = valid && (conflictFilesYaml is YamlList || conflictFilesYaml == null);
valid = valid && (addedFilesYaml is YamlList || addedFilesYaml == null);
valid = valid && (deletedFilesYaml is YamlList || deletedFilesYaml == null);
if (!valid) {
throw Exception('Invalid .migrate_manifest file in the migrate working directory. Entry is not a Yaml list.');
}
if (mergedFilesYaml != null) {
for (final Object? localPath in mergedFilesYaml as YamlList) {
if (localPath is String) {
// We can fill the maps with partially dummy data as not all properties are used by the manifest.
migrateResult.mergeResults.add(StringMergeResult.explicit(mergedString: '', hasConflict: false, exitCode: 0, localPath: localPath));
}
}
}
if (conflictFilesYaml != null) {
for (final Object? localPath in conflictFilesYaml as YamlList) {
if (localPath is String) {
migrateResult.mergeResults.add(StringMergeResult.explicit(mergedString: '', hasConflict: true, exitCode: 1, localPath: localPath));
}
}
}
if (addedFilesYaml != null) {
for (final Object? localPath in addedFilesYaml as YamlList) {
if (localPath is String) {
migrateResult.addedFiles.add(FilePendingMigration(localPath, migrateRootDir.childFile(localPath)));
}
}
}
if (deletedFilesYaml != null) {
for (final Object? localPath in deletedFilesYaml as YamlList) {
if (localPath is String) {
migrateResult.deletedFiles.add(FilePendingMigration(localPath, migrateRootDir.childFile(localPath)));
}
}
}
}
final Directory migrateRootDir;
final MigrateResult migrateResult;
/// A list of local paths of files that require conflict resolution.
List<String> get conflictFiles {
final List<String> output = <String>[];
for (final MergeResult result in migrateResult.mergeResults) {
if (result.hasConflict) {
output.add(result.localPath);
}
}
return output;
}
/// A list of local paths of files that require conflict resolution.
List<String> remainingConflictFiles(Directory workingDir) {
final List<String> output = <String>[];
for (final String localPath in conflictFiles) {
if (!_conflictsResolved(workingDir.childFile(localPath).readAsStringSync())) {
output.add(localPath);
}
}
return output;
}
// A list of local paths of files that had conflicts and are now fully resolved.
List<String> resolvedConflictFiles(Directory workingDir) {
final List<String> output = <String>[];
for (final String localPath in conflictFiles) {
if (_conflictsResolved(workingDir.childFile(localPath).readAsStringSync())) {
output.add(localPath);
}
}
return output;
}
/// A list of local paths of files that were automatically merged.
List<String> get mergedFiles {
final List<String> output = <String>[];
for (final MergeResult result in migrateResult.mergeResults) {
if (!result.hasConflict) {
output.add(result.localPath);
}
}
return output;
}
/// A list of local paths of files that were newly added.
List<String> get addedFiles {
final List<String> output = <String>[];
for (final FilePendingMigration file in migrateResult.addedFiles) {
output.add(file.localPath);
}
return output;
}
/// A list of local paths of files that are marked for deletion.
List<String> get deletedFiles {
final List<String> output = <String>[];
for (final FilePendingMigration file in migrateResult.deletedFiles) {
output.add(file.localPath);
}
return output;
}
/// Returns the manifest file given a migration workind directory.
static File getManifestFileFromDirectory(Directory workingDir) {
return workingDir.childFile('.migrate_manifest');
}
/// Writes the manifest yaml file in the working directory.
void writeFile() {
final StringBuffer mergedFileManifestContents = StringBuffer();
final StringBuffer conflictFilesManifestContents = StringBuffer();
for (final MergeResult result in migrateResult.mergeResults) {
if (result.hasConflict) {
conflictFilesManifestContents.write(' - ${result.localPath}\n');
} else {
mergedFileManifestContents.write(' - ${result.localPath}\n');
}
}
final StringBuffer newFileManifestContents = StringBuffer();
for (final String localPath in addedFiles) {
newFileManifestContents.write(' - $localPath\n)');
}
final StringBuffer deletedFileManifestContents = StringBuffer();
for (final String localPath in deletedFiles) {
deletedFileManifestContents.write(' - $localPath\n');
}
final String migrateManifestContents = 'merged_files:\n${mergedFileManifestContents.toString()}conflict_files:\n${conflictFilesManifestContents.toString()}added_files:\n${newFileManifestContents.toString()}deleted_files:\n${deletedFileManifestContents.toString()}';
final File migrateManifest = getManifestFileFromDirectory(migrateRootDir);
migrateManifest.createSync(recursive: true);
migrateManifest.writeAsStringSync(migrateManifestContents, flush: true);
}
}
/// Returns true if the file does not contain any git conflict markers.
bool _conflictsResolved(String contents) {
if (contents.contains('>>>>>>>') && contents.contains('=======') && contents.contains('<<<<<<<')) {
return false;
}
return true;
}
/// Returns true if the migration working directory has all conflicts resolved and prints the migration status.
///
/// The migration status printout lists all added, deleted, merged, and conflicted files.
bool checkAndPrintMigrateStatus(MigrateManifest manifest, Directory workingDir, {bool warnConflict = false, Logger? logger}) {
final StringBuffer printout = StringBuffer();
final StringBuffer redPrintout = StringBuffer();
bool result = true;
final List<String> remainingConflicts = <String>[];
final List<String> mergedFiles = <String>[];
for (final String localPath in manifest.conflictFiles) {
if (!_conflictsResolved(workingDir.childFile(localPath).readAsStringSync())) {
remainingConflicts.add(localPath);
} else {
mergedFiles.add(localPath);
}
}
mergedFiles.addAll(manifest.mergedFiles);
if (manifest.addedFiles.isNotEmpty) {
printout.write('Added files:\n');
for (final String localPath in manifest.addedFiles) {
printout.write(' - $localPath\n');
}
}
if (manifest.deletedFiles.isNotEmpty) {
printout.write('Deleted files:\n');
for (final String localPath in manifest.deletedFiles) {
printout.write(' - $localPath\n');
}
}
if (mergedFiles.isNotEmpty) {
printout.write('Modified files:\n');
for (final String localPath in mergedFiles) {
printout.write(' - $localPath\n');
}
}
if (remainingConflicts.isNotEmpty) {
if (warnConflict) {
printout.write('Unable to apply migration. The following files in the migration working directory still have unresolved conflicts:');
} else {
printout.write('Merge conflicted files:');
}
for (final String localPath in remainingConflicts) {
redPrintout.write(' - $localPath\n');
}
result = false;
}
if (logger != null) {
logger.printStatus(printout.toString());
logger.printStatus(redPrintout.toString(), color: TerminalColor.red, newline: false);
}
return result;
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '../base/file_system.dart';
import 'migrate_utils.dart';
/// Data class that holds all results and generated directories from a computeMigration run.
///
/// mergeResults, addedFiles, and deletedFiles includes the sets of files to be migrated while
/// the other members track the temporary sdk and generated app directories created by the tool.
///
/// The compute function does not clean up the temp directories, as the directories may be reused,
/// so this must be done manually afterwards.
class MigrateResult {
/// Explicitly initialize the MigrateResult.
MigrateResult({
required this.mergeResults,
required this.addedFiles,
required this.deletedFiles,
required this.tempDirectories,
required this.sdkDirs,
required this.mergeTypeMap,
required this.diffMap,
this.generatedBaseTemplateDirectory,
this.generatedTargetTemplateDirectory});
/// Creates a MigrateResult with all empty members.
MigrateResult.empty()
: mergeResults = <MergeResult>[],
addedFiles = <FilePendingMigration>[],
deletedFiles = <FilePendingMigration>[],
tempDirectories = <Directory>[],
mergeTypeMap = <String, MergeType>{},
diffMap = <String, DiffResult>{},
sdkDirs = <String, Directory>{};
/// The results of merging existing files with the target files.
final List<MergeResult> mergeResults;
/// Tracks the files that are to be newly added to the project.
final List<FilePendingMigration> addedFiles;
/// Tracks the files that are to be deleted from the project.
final List<FilePendingMigration> deletedFiles;
/// Tracks the temporary directories created during the migrate compute process.
final List<Directory> tempDirectories;
/// Mapping between the local path of a file and the type of merge that should be used.
final Map<String, MergeType> mergeTypeMap;
/// Mapping between the local path of a file and the diff between the base and target
/// versions of the file.
final Map<String, DiffResult> diffMap;
/// The root directory of the base app.
Directory? generatedBaseTemplateDirectory;
/// The root directory of the target app.
Directory? generatedTargetTemplateDirectory;
/// The root directories of the Flutter SDK for each revision.
Map<String, Directory> sdkDirs;
}
/// Defines available merge techniques.
enum MergeType {
/// A standard three-way merge.
threeWay,
/// A two way merge that ignores the base version of the file.
twoWay,
/// A `CustomMerge` manually handles the merge.
custom,
}
/// Stores a file that has been marked for migration and metadata about the file.
class FilePendingMigration {
FilePendingMigration(this.localPath, this.file);
String localPath;
File file;
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:typed_data';
import 'package:process/process.dart';
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/logger.dart';
import '../base/platform.dart';
import '../base/process.dart';
/// The default name of the migrate working directory used to stage proposed changes.
const String kDefaultMigrateWorkingDirectoryName = 'migrate_working_dir';
/// Utility class that contains methods that wrap git and other shell commands.
class MigrateUtils {
MigrateUtils({
required Logger logger,
required FileSystem fileSystem,
required Platform platform,
required ProcessManager processManager,
}) :
_processUtils = ProcessUtils(processManager: processManager, logger: logger),
_logger = logger,
_fileSystem = fileSystem,
_platform = platform;
final Logger _logger;
final FileSystem _fileSystem;
final Platform _platform;
final ProcessUtils _processUtils;
/// Calls `git diff` on two files and returns the diff as a DiffResult.
Future<DiffResult> diffFiles(File one, File two) async {
if (one.existsSync() && !two.existsSync()) {
return DiffResult(diffType: DiffType.deletion);
}
if (!one.existsSync() && two.existsSync()) {
return DiffResult(diffType: DiffType.addition);
}
final List<String> cmdArgs = <String>['git', 'diff', '--no-index', one.absolute.path, two.absolute.path];
final RunResult result = await _processUtils.run(cmdArgs);
// diff exits with 1 if diffs are found.
checkForErrors(result, allowedExitCodes: <int>[0, 1], commandDescription: 'git ${cmdArgs.join(' ')}');
return DiffResult(diffType: DiffType.command, diff: result.stdout, exitCode: result.exitCode);
}
/// Clones a copy of the flutter repo into the destination directory. Returns false if unsuccessful.
Future<bool> cloneFlutter(String revision, String destination) async {
// Use https url instead of ssh to avoid need to setup ssh on git.
List<String> cmdArgs = <String>['git', 'clone', '--filter=blob:none', 'https://github.com/flutter/flutter.git', destination];
RunResult result = await _processUtils.run(cmdArgs);
checkForErrors(result, commandDescription: cmdArgs.join(' '));
cmdArgs.clear();
cmdArgs = <String>['git', 'reset', '--hard', revision];
result = await _processUtils.run(cmdArgs, workingDirectory: destination);
if (!checkForErrors(result, commandDescription: cmdArgs.join(' '), exit: false)) {
return false;
}
return true;
}
/// Calls `flutter create` as a re-entrant command.
Future<String> createFromTemplates(String flutterBinPath, {
required String name,
bool legacyNameParameter = false,
required String androidLanguage,
required String iosLanguage,
required String outputDirectory,
String? createVersion,
List<String> platforms = const <String>[],
int iterationsAllowed = 5,
}) async {
// Limit the number of iterations this command is allowed to attempt to prevent infinite looping.
if (iterationsAllowed <= 0) {
_logger.printError('Unable to `flutter create` with the version of flutter at $flutterBinPath');
return outputDirectory;
}
final List<String> cmdArgs = <String>['$flutterBinPath/flutter', 'create'];
if (!legacyNameParameter) {
cmdArgs.add('--project-name=$name');
}
cmdArgs.add('--android-language=$androidLanguage');
cmdArgs.add('--ios-language=$iosLanguage');
if (platforms.isNotEmpty) {
String platformsArg = '--platforms=';
for (int i = 0; i < platforms.length; i++) {
if (i > 0) {
platformsArg += ',';
}
platformsArg += platforms[i];
}
cmdArgs.add(platformsArg);
}
cmdArgs.add('--no-pub');
if (legacyNameParameter) {
cmdArgs.add(name);
} else {
cmdArgs.add(outputDirectory);
}
final RunResult result = await _processUtils.run(cmdArgs, workingDirectory: outputDirectory, allowReentrantFlutter: true);
final String error = result.stderr;
// Catch errors due to parameters not existing.
// Old versions of the tool does not include the platforms option.
if (error.contains('Could not find an option named "platforms".')) {
return createFromTemplates(
flutterBinPath,
name: name,
legacyNameParameter: legacyNameParameter,
androidLanguage: androidLanguage,
iosLanguage: iosLanguage,
outputDirectory: outputDirectory,
iterationsAllowed: iterationsAllowed--,
);
}
// Old versions of the tool does not include the project-name option.
if ((result.stderr).contains('Could not find an option named "project-name".')) {
return createFromTemplates(
flutterBinPath,
name: name,
legacyNameParameter: true,
androidLanguage: androidLanguage,
iosLanguage: iosLanguage,
outputDirectory: outputDirectory,
platforms: platforms,
iterationsAllowed: iterationsAllowed--,
);
}
if (error.contains('Multiple output directories specified.')) {
if (error.contains('Try moving --platforms')) {
return createFromTemplates(
flutterBinPath,
name: name,
legacyNameParameter: legacyNameParameter,
androidLanguage: androidLanguage,
iosLanguage: iosLanguage,
outputDirectory: outputDirectory,
iterationsAllowed: iterationsAllowed--,
);
}
}
checkForErrors(result, commandDescription: cmdArgs.join(' '), silent: true);
if (legacyNameParameter) {
return _fileSystem.path.join(outputDirectory, name);
}
return outputDirectory;
}
/// Runs the git 3-way merge on three files and returns the results as a MergeResult.
///
/// Passing the same path for base and current will perform a two-way fast forward merge.
Future<MergeResult> gitMergeFile({
required String base,
required String current,
required String target,
required String localPath,
}) async {
final List<String> cmdArgs = <String>['git', 'merge-file', '-p', current, base, target];
final RunResult result = await _processUtils.run(cmdArgs);
checkForErrors(result, allowedExitCodes: <int>[-1], commandDescription: cmdArgs.join(' '));
return StringMergeResult(result, localPath);
}
/// Calls `git init` on the workingDirectory.
Future<void> gitInit(String workingDirectory) async {
final List<String> cmdArgs = <String>['git', 'init'];
final RunResult result = await _processUtils.run(cmdArgs, workingDirectory: workingDirectory);
checkForErrors(result, commandDescription: cmdArgs.join(' '));
}
/// Returns true if the workingDirectory git repo has any uncommited changes.
Future<bool> hasUncommittedChanges(String workingDirectory, {String? migrateWorkingDir}) async {
final List<String> cmdArgs = <String>[
'git',
'ls-files',
'--deleted',
'--modified',
'--others',
'--exclude=${migrateWorkingDir ?? kDefaultMigrateWorkingDirectoryName}'
];
final RunResult result = await _processUtils.run(cmdArgs, workingDirectory: workingDirectory);
checkForErrors(result, allowedExitCodes: <int>[-1], commandDescription: cmdArgs.join(' '));
if (result.stdout.isEmpty) {
return false;
}
return true;
}
/// Returns true if the workingDirectory is a git repo.
Future<bool> isGitRepo(String workingDirectory) async {
final List<String> cmdArgs = <String>['git', 'rev-parse', '--is-inside-work-tree'];
final RunResult result = await _processUtils.run(cmdArgs, workingDirectory: workingDirectory);
checkForErrors(result, allowedExitCodes: <int>[-1], commandDescription: cmdArgs.join(' '));
if (result.exitCode == 0) {
return true;
}
return false;
}
/// Returns true if the file at `filePath` is covered by the `.gitignore`
Future<bool> isGitIgnored(String filePath, String workingDirectory) async {
final List<String> cmdArgs = <String>['git', 'check-ignore', filePath];
final RunResult result = await _processUtils.run(cmdArgs, workingDirectory: workingDirectory);
checkForErrors(result, allowedExitCodes: <int>[0, 1, 128], commandDescription: cmdArgs.join(' '));
return result.exitCode == 0;
}
/// Runs `flutter pub upgrade --major-revisions`.
Future<void> flutterPubUpgrade(String workingDirectory) async {
final List<String> cmdArgs = <String>['flutter', 'pub', 'upgrade', '--major-versions'];
final RunResult result = await _processUtils.run(cmdArgs, workingDirectory: workingDirectory, allowReentrantFlutter: true);
checkForErrors(result, commandDescription: cmdArgs.join(' '));
}
/// Runs `./gradlew tasks` in the android directory of a flutter project.
Future<void> gradlewTasks(String workingDirectory) async {
final String baseCommand = _platform.isWindows ? 'gradlew.bat' : './gradlew';
final List<String> cmdArgs = <String>[baseCommand, 'tasks'];
final RunResult result = await _processUtils.run(cmdArgs, workingDirectory: workingDirectory);
checkForErrors(result, commandDescription: cmdArgs.join(' '));
}
/// Verifies that the RunResult does not contain an error.
///
/// If an error is detected, the error can be optionally logged or exit the tool.
///
/// Passing -1 in allowedExitCodes means all exit codes are valid.
bool checkForErrors(
RunResult result, {
List<int> allowedExitCodes = const <int>[0],
String? commandDescription,
bool exit = true,
bool silent = false
}) {
if (!allowedExitCodes.contains(result.exitCode) && !allowedExitCodes.contains(-1)) {
if (!silent) {
_logger.printError('Command encountered an error with exit code ${result.exitCode}.');
if (commandDescription != null) {
_logger.printError('Command:');
_logger.printError(commandDescription, indent: 2);
}
_logger.printError('Stdout:');
_logger.printError(result.stdout, indent: 2);
_logger.printError('Stderr:');
_logger.printError(result.stderr, indent: 2);
}
if (exit) {
throwToolExit('Command failed with exit code ${result.exitCode}', exitCode: result.exitCode);
}
return false;
}
return true;
}
/// Returns true if the file does not contain any git conflit markers.
bool conflictsResolved(String contents) {
if (contents.contains('>>>>>>>') && contents.contains('=======') && contents.contains('<<<<<<<')) {
return false;
}
return true;
}
}
/// Defines the classification of difference between files.
enum DiffType {
command,
addition,
deletion,
ignored,
none,
}
/// Tracks the output of a git diff command or any special cases such as addition of a new
/// file or deletion of an existing file.
class DiffResult {
DiffResult({
required this.diffType,
this.diff,
this.exitCode,
}) : assert(diffType == DiffType.command && exitCode != null || diffType != DiffType.command && exitCode == null);
/// The diff string output by git.
final String? diff;
final DiffType diffType;
/// The exit code of the command. This is zero when no diffs are found.
///
/// The exitCode is null when the diffType is not `command`.
final int? exitCode;
}
/// Data class to hold the results of a merge.
abstract class MergeResult {
/// Initializes a MergeResult based off of a RunResult.
MergeResult(RunResult result, this.localPath) :
hasConflict = result.exitCode != 0,
exitCode = result.exitCode;
/// Manually initializes a MergeResult with explicit values.
MergeResult.explicit({
required this.hasConflict,
required this.exitCode,
required this.localPath,
});
/// True when there is a merge conflict.
bool hasConflict;
/// The exitcode of the merge command.
int exitCode;
/// The local path relative to the project root of the file.
String localPath;
}
/// The results of a string merge.
class StringMergeResult extends MergeResult {
/// Initializes a BinaryMergeResult based off of a RunResult.
StringMergeResult(super.result, super.localPath) :
mergedString = result.stdout;
/// Manually initializes a StringMergeResult with explicit values.
StringMergeResult.explicit({
required this.mergedString,
required super.hasConflict,
required super.exitCode,
required super.localPath,
}) : super.explicit();
/// The final merged string.
String mergedString;
}
/// The results of a binary merge.
class BinaryMergeResult extends MergeResult {
/// Initializes a BinaryMergeResult based off of a RunResult.
BinaryMergeResult(super.result, super.localPath) :
mergedBytes = result.stdout as Uint8List;
/// Manually initializes a BinaryMergeResult with explicit values.
BinaryMergeResult.explicit({
required this.mergedBytes,
required super.hasConflict,
required super.exitCode,
required super.localPath,
}) : super.explicit();
/// The final merged bytes.
Uint8List mergedBytes;
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/migrate/migrate_manifest.dart';
import 'package:flutter_tools/src/migrate/migrate_result.dart';
import 'package:flutter_tools/src/migrate/migrate_utils.dart';
import '../../src/common.dart';
void main() {
late FileSystem fileSystem;
late File manifestFile;
setUpAll(() {
fileSystem = MemoryFileSystem.test();
manifestFile = fileSystem.file('.migrate_manifest');
});
group('manifest file parsing', () {
testWithoutContext('empty fails', () async {
manifestFile.writeAsStringSync('');
bool exceptionFound = false;
try {
MigrateManifest.fromFile(manifestFile);
} on Exception catch (e) {
exceptionFound = true;
expect(e.toString(), 'Exception: Invalid .migrate_manifest file in the migrate working directory. File is not a Yaml map.');
}
expect(exceptionFound, true);
});
testWithoutContext('invalid name fails', () async {
manifestFile.writeAsStringSync('''
merged_files:
conflict_files:
added_filessssss:
deleted_files:
''');
bool exceptionFound = false;
try {
MigrateManifest.fromFile(manifestFile);
} on Exception catch (e) {
exceptionFound = true;
expect(e.toString(), 'Exception: Invalid .migrate_manifest file in the migrate working directory. File is missing an entry.');
}
expect(exceptionFound, true);
});
testWithoutContext('missing name fails', () async {
manifestFile.writeAsStringSync('''
merged_files:
conflict_files:
deleted_files:
''');
bool exceptionFound = false;
try {
MigrateManifest.fromFile(manifestFile);
} on Exception catch (e) {
exceptionFound = true;
expect(e.toString(), 'Exception: Invalid .migrate_manifest file in the migrate working directory. File is missing an entry.');
}
expect(exceptionFound, true);
});
testWithoutContext('wrong entry type fails', () async {
manifestFile.writeAsStringSync('''
merged_files:
conflict_files:
other_key:
added_files:
deleted_files:
''');
bool exceptionFound = false;
try {
MigrateManifest.fromFile(manifestFile);
} on Exception catch (e) {
exceptionFound = true;
expect(e.toString(), 'Exception: Invalid .migrate_manifest file in the migrate working directory. Entry is not a Yaml list.');
}
expect(exceptionFound, true);
});
testWithoutContext('unpopulated succeeds', () async {
manifestFile.writeAsStringSync('''
merged_files:
conflict_files:
added_files:
deleted_files:
''');
final MigrateManifest manifest = MigrateManifest.fromFile(manifestFile);
expect(manifest.mergedFiles.isEmpty, true);
expect(manifest.conflictFiles.isEmpty, true);
expect(manifest.addedFiles.isEmpty, true);
expect(manifest.deletedFiles.isEmpty, true);
});
testWithoutContext('order does not matter', () async {
manifestFile.writeAsStringSync('''
added_files:
merged_files:
deleted_files:
conflict_files:
''');
final MigrateManifest manifest = MigrateManifest.fromFile(manifestFile);
expect(manifest.mergedFiles.isEmpty, true);
expect(manifest.conflictFiles.isEmpty, true);
expect(manifest.addedFiles.isEmpty, true);
expect(manifest.deletedFiles.isEmpty, true);
});
testWithoutContext('basic succeeds', () async {
manifestFile.writeAsStringSync('''
merged_files:
- file1
conflict_files:
- file2
added_files:
- file3
deleted_files:
- file4
''');
final MigrateManifest manifest = MigrateManifest.fromFile(manifestFile);
expect(manifest.mergedFiles.isEmpty, false);
expect(manifest.conflictFiles.isEmpty, false);
expect(manifest.addedFiles.isEmpty, false);
expect(manifest.deletedFiles.isEmpty, false);
expect(manifest.mergedFiles.length, 1);
expect(manifest.conflictFiles.length, 1);
expect(manifest.addedFiles.length, 1);
expect(manifest.deletedFiles.length, 1);
expect(manifest.mergedFiles[0], 'file1');
expect(manifest.conflictFiles[0], 'file2');
expect(manifest.addedFiles[0], 'file3');
expect(manifest.deletedFiles[0], 'file4');
});
testWithoutContext('basic multi-list succeeds', () async {
manifestFile.writeAsStringSync('''
merged_files:
- file1
- file2
conflict_files:
added_files:
deleted_files:
- file3
- file4
''');
final MigrateManifest manifest = MigrateManifest.fromFile(manifestFile);
expect(manifest.mergedFiles.isEmpty, false);
expect(manifest.conflictFiles.isEmpty, true);
expect(manifest.addedFiles.isEmpty, true);
expect(manifest.deletedFiles.isEmpty, false);
expect(manifest.mergedFiles.length, 2);
expect(manifest.conflictFiles.length, 0);
expect(manifest.addedFiles.length, 0);
expect(manifest.deletedFiles.length, 2);
expect(manifest.mergedFiles[0], 'file1');
expect(manifest.mergedFiles[1], 'file2');
expect(manifest.deletedFiles[0], 'file3');
expect(manifest.deletedFiles[1], 'file4');
});
});
group('manifest MigrateResult creation', () {
testWithoutContext('empty MigrateResult', () async {
final MigrateManifest manifest = MigrateManifest(migrateRootDir: fileSystem.directory('root'), migrateResult: MigrateResult(
mergeResults: <MergeResult>[],
addedFiles: <FilePendingMigration>[],
deletedFiles: <FilePendingMigration>[],
mergeTypeMap: <String, MergeType>{},
diffMap: <String, DiffResult>{},
tempDirectories: <Directory>[],
sdkDirs: <String, Directory>{},
));
expect(manifest.mergedFiles.isEmpty, true);
expect(manifest.conflictFiles.isEmpty, true);
expect(manifest.addedFiles.isEmpty, true);
expect(manifest.deletedFiles.isEmpty, true);
});
testWithoutContext('simple MigrateResult', () async {
final MigrateManifest manifest = MigrateManifest(migrateRootDir: fileSystem.directory('root'), migrateResult: MigrateResult(
mergeResults: <MergeResult>[
StringMergeResult.explicit(
localPath: 'merged_file',
mergedString: 'str',
hasConflict: false,
exitCode: 0,
),
StringMergeResult.explicit(
localPath: 'conflict_file',
mergedString: '<<<<<<<<<<<',
hasConflict: true,
exitCode: 1,
),
],
addedFiles: <FilePendingMigration>[FilePendingMigration('added_file', fileSystem.file('added_file'))],
deletedFiles: <FilePendingMigration>[FilePendingMigration('deleted_file', fileSystem.file('deleted_file'))],
// The following are ignored by the manifest.
mergeTypeMap: <String, MergeType>{'test': MergeType.threeWay},
diffMap: <String, DiffResult>{},
tempDirectories: <Directory>[],
sdkDirs: <String, Directory>{},
));
expect(manifest.mergedFiles.isEmpty, false);
expect(manifest.conflictFiles.isEmpty, false);
expect(manifest.addedFiles.isEmpty, false);
expect(manifest.deletedFiles.isEmpty, false);
expect(manifest.mergedFiles.length, 1);
expect(manifest.conflictFiles.length, 1);
expect(manifest.addedFiles.length, 1);
expect(manifest.deletedFiles.length, 1);
expect(manifest.mergedFiles[0], 'merged_file');
expect(manifest.conflictFiles[0], 'conflict_file');
expect(manifest.addedFiles[0], 'added_file');
expect(manifest.deletedFiles[0], 'deleted_file');
});
});
group('manifest write', () {
testWithoutContext('empty', () async {
manifestFile.writeAsStringSync('''
merged_files:
conflict_files:
added_files:
deleted_files:
''');
final MigrateManifest manifest = MigrateManifest.fromFile(manifestFile);
expect(manifest.mergedFiles.isEmpty, true);
expect(manifest.conflictFiles.isEmpty, true);
expect(manifest.addedFiles.isEmpty, true);
expect(manifest.deletedFiles.isEmpty, true);
manifest.writeFile();
expect(manifestFile.readAsStringSync(), '''
merged_files:
conflict_files:
added_files:
deleted_files:
''');
});
testWithoutContext('basic multi-list', () async {
manifestFile.writeAsStringSync('''
merged_files:
- file1
- file2
conflict_files:
added_files:
deleted_files:
- file3
- file4
''');
final MigrateManifest manifest = MigrateManifest.fromFile(manifestFile);
expect(manifest.mergedFiles.isEmpty, false);
expect(manifest.conflictFiles.isEmpty, true);
expect(manifest.addedFiles.isEmpty, true);
expect(manifest.deletedFiles.isEmpty, false);
expect(manifest.mergedFiles.length, 2);
expect(manifest.conflictFiles.length, 0);
expect(manifest.addedFiles.length, 0);
expect(manifest.deletedFiles.length, 2);
expect(manifest.mergedFiles[0], 'file1');
expect(manifest.mergedFiles[1], 'file2');
expect(manifest.deletedFiles[0], 'file3');
expect(manifest.deletedFiles[1], 'file4');
manifest.writeFile();
expect(manifestFile.readAsStringSync(), '''
merged_files:
- file1
- file2
conflict_files:
added_files:
deleted_files:
- file3
- file4
''');
});
});
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// @dart = 2.8
import 'package:file/file.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/process.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/migrate/migrate_utils.dart';
import '../src/common.dart';
void main() {
BufferLogger logger;
FileSystem fileSystem;
Directory projectRoot;
String projectRootPath;
MigrateUtils utils;
ProcessUtils processUtils;
setUpAll(() async {
fileSystem = globals.localFileSystem;
logger = BufferLogger.test();
utils = MigrateUtils(
logger: logger,
fileSystem: fileSystem,
platform: globals.platform,
processManager: globals.processManager,
);
processUtils = ProcessUtils(processManager: globals.processManager, logger: logger);
});
group('git', () {
setUp(() async {
projectRoot = fileSystem.systemTempDirectory.createTempSync('flutter_migrate_utils_test');
projectRoot.createSync(recursive: true);
projectRootPath = projectRoot.path;
});
testWithoutContext('init', () async {
expect(projectRoot.existsSync(), true);
expect(projectRoot.childDirectory('.git').existsSync(), false);
await utils.gitInit(projectRootPath);
expect(projectRoot.childDirectory('.git').existsSync(), true);
});
testWithoutContext('isGitIgnored', () async {
expect(projectRoot.existsSync(), true);
expect(projectRoot.childDirectory('.git').existsSync(), false);
await utils.gitInit(projectRootPath);
expect(projectRoot.childDirectory('.git').existsSync(), true);
projectRoot.childFile('.gitignore')
..createSync()
..writeAsStringSync('ignored_file.dart', flush: true);
expect(await utils.isGitIgnored('ignored_file.dart', projectRootPath), true);
expect(await utils.isGitIgnored('other_file.dart', projectRootPath), false);
});
testWithoutContext('isGitRepo', () async {
expect(projectRoot.existsSync(), true);
expect(projectRoot.childDirectory('.git').existsSync(), false);
expect(await utils.isGitRepo(projectRootPath), false);
await utils.gitInit(projectRootPath);
expect(projectRoot.childDirectory('.git').existsSync(), true);
expect(await utils.isGitRepo(projectRootPath), true);
expect(await utils.isGitRepo(projectRoot.parent.path), false);
});
testWithoutContext('hasUncommittedChanges false on clean repo', () async {
expect(projectRoot.existsSync(), true);
expect(projectRoot.childDirectory('.git').existsSync(), false);
await utils.gitInit(projectRootPath);
expect(projectRoot.childDirectory('.git').existsSync(), true);
projectRoot.childFile('.gitignore')
..createSync()
..writeAsStringSync('ignored_file.dart', flush: true);
await processUtils.run(<String>['git', 'add', '.'], workingDirectory: projectRootPath);
await processUtils.run(<String>['git', 'commit', '-m', 'Initial commit'], workingDirectory: projectRootPath);
expect(await utils.hasUncommittedChanges(projectRootPath), false);
});
testWithoutContext('hasUncommittedChanges true on dirty repo', () async {
expect(projectRoot.existsSync(), true);
expect(projectRoot.childDirectory('.git').existsSync(), false);
await utils.gitInit(projectRootPath);
expect(projectRoot.childDirectory('.git').existsSync(), true);
projectRoot.childFile('some_file.dart')
..createSync()
..writeAsStringSync('void main() {}', flush: true);
expect(await utils.hasUncommittedChanges(projectRootPath), true);
});
testWithoutContext('diffFiles', () async {
expect(projectRoot.existsSync(), true);
expect(projectRoot.childDirectory('.git').existsSync(), false);
await utils.gitInit(projectRootPath);
expect(projectRoot.childDirectory('.git').existsSync(), true);
final File file1 = projectRoot.childFile('some_file.dart')
..createSync()
..writeAsStringSync('void main() {}\n', flush: true);
final File file2 = projectRoot.childFile('some_other_file.dart');
DiffResult result = await utils.diffFiles(file1, file2);
expect(result.diff, null);
expect(result.diffType, DiffType.deletion);
expect(result.exitCode, null);
result = await utils.diffFiles(file2, file1);
expect(result.diff, null);
expect(result.diffType, DiffType.addition);
expect(result.exitCode, null);
file2.createSync();
file2.writeAsStringSync('void main() {}\n', flush: true);
result = await utils.diffFiles(file1, file2);
expect(result.diff, '');
expect(result.diffType, DiffType.command);
expect(result.exitCode, 0);
file2.writeAsStringSync('void main() {}\na second line\na third line\n', flush: true);
result = await utils.diffFiles(file1, file2);
expect(result.diff, contains('@@ -1 +1,3 @@\n void main() {}\n+a second line\n+a third line'));
expect(result.diffType, DiffType.command);
expect(result.exitCode, 1);
});
testWithoutContext('merge', () async {
expect(projectRoot.existsSync(), true);
expect(projectRoot.childDirectory('.git').existsSync(), false);
await utils.gitInit(projectRootPath);
expect(projectRoot.childDirectory('.git').existsSync(), true);
final File file1 = projectRoot.childFile('some_file.dart');
file1.createSync();
file1.writeAsStringSync('void main() {}\n\nline1\nline2\nline3\nline4\nline5\n', flush: true);
final File file2 = projectRoot.childFile('some_other_file.dart');
file2.createSync();
file2.writeAsStringSync('void main() {}\n\nline1\nline2\nline3.0\nline3.5\nline4\nline5\n', flush: true);
final File file3 = projectRoot.childFile('some_other_third_file.dart');
file3.createSync();
file3.writeAsStringSync('void main() {}\n\nline2\nline3\nline4\nline5\n', flush: true);
StringMergeResult result = await utils.gitMergeFile(
base: file1.path,
current: file2.path,
target: file3.path,
localPath: 'some_file.dart',
) as StringMergeResult;
expect(result.mergedString, 'void main() {}\n\nline2\nline3.0\nline3.5\nline4\nline5\n');
expect(result.hasConflict, false);
expect(result.exitCode, 0);
file3.writeAsStringSync('void main() {}\n\nline1\nline2\nline3.1\nline3.5\nline4\nline5\n', flush: true);
result = await utils.gitMergeFile(
base: file1.path,
current: file2.path,
target: file3.path,
localPath: 'some_file.dart',
) as StringMergeResult;
expect(result.mergedString, contains('line3.0\n=======\nline3.1\n>>>>>>>'));
expect(result.hasConflict, true);
expect(result.exitCode, 1);
// Two way merge
result = await utils.gitMergeFile(
base: file1.path,
current: file1.path,
target: file3.path,
localPath: 'some_file.dart',
) as StringMergeResult;
expect(result.mergedString, 'void main() {}\n\nline1\nline2\nline3.1\nline3.5\nline4\nline5\n');
expect(result.hasConflict, false);
expect(result.exitCode, 0);
});
});
group('legacy app creation', () {
testWithoutContext('clone and create', () async {
projectRoot = fileSystem.systemTempDirectory.createTempSync('flutter_sdk_test');
const String revision = '5391447fae6209bb21a89e6a5a6583cac1af9b4b';
expect(await utils.cloneFlutter(revision, projectRoot.path), true);
expect(projectRoot.childFile('README.md').existsSync(), true);
final Directory appDir = fileSystem.systemTempDirectory.createTempSync('flutter_app');
await utils.createFromTemplates(
projectRoot.childDirectory('bin').path,
name: 'testapp',
androidLanguage: 'java',
iosLanguage: 'objc',
outputDirectory: appDir.path,
);
expect(appDir.childFile('pubspec.yaml').existsSync(), true);
expect(appDir.childFile('.metadata').existsSync(), true);
expect(appDir.childFile('.metadata').readAsStringSync(), contains(revision));
expect(appDir.childDirectory('android').existsSync(), true);
expect(appDir.childDirectory('ios').existsSync(), true);
expect(appDir.childDirectory('web').existsSync(), false);
projectRoot.deleteSync(recursive: true);
});
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment