blob: cef7ea4278269f7d4ef19e1185cb3d1d2e11489e [file] [log] [blame]
// Copyright 2013 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 'result.dart';
import '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;
}