| // 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; |
| } |