diff --git a/packages/flutter_tools/lib/src/commands/migrate.dart b/packages/flutter_tools/lib/src/commands/migrate.dart index 7d592abec04a..67b51251dc3e 100644 --- a/packages/flutter_tools/lib/src/commands/migrate.dart +++ b/packages/flutter_tools/lib/src/commands/migrate.dart @@ -2,20 +2,34 @@ // 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_status.dart'; /// Base command for the migration tool. class MigrateCommand extends FlutterCommand { MigrateCommand({ + bool verbose = false, required this.logger, - // TODO(garyq): Add each parameters in as subcommands land. + // TODO(garyq): Add parameter in as they are needed for subcommands. + required FileSystem fileSystem, + required Platform platform, + required ProcessManager processManager, }) { - // TODO(garyq): Add subcommands. + // TODO(garyq): Add each subcommand back in as they land. + addSubcommand(MigrateStatusCommand( + verbose: verbose, + logger: logger, + fileSystem: fileSystem, + platform: platform, + processManager: processManager + )); } final Logger logger; diff --git a/packages/flutter_tools/lib/src/commands/migrate_status.dart b/packages/flutter_tools/lib/src/commands/migrate_status.dart new file mode 100644 index 000000000000..26dcc5d79c57 --- /dev/null +++ b/packages/flutter_tools/lib/src/commands/migrate_status.dart @@ -0,0 +1,176 @@ +// 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> get requiredArtifacts async => const {}; + + /// 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 _initialDiffLines = {0, 1}; + + @override + Future 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 files = []; + 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); + } +} diff --git a/packages/flutter_tools/lib/src/migrate/migrate_utils.dart b/packages/flutter_tools/lib/src/migrate/migrate_utils.dart index 16bc87e9be05..25891fe3c927 100644 --- a/packages/flutter_tools/lib/src/migrate/migrate_utils.dart +++ b/packages/flutter_tools/lib/src/migrate/migrate_utils.dart @@ -14,7 +14,7 @@ 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'; +const String kDefaultMigrateStagingDirectoryName = 'migrate_staging_dir'; /// Utility class that contains methods that wrap git and other shell commands. class MigrateUtils { @@ -179,14 +179,14 @@ class MigrateUtils { } /// Returns true if the workingDirectory git repo has any uncommited changes. - Future hasUncommittedChanges(String workingDirectory, {String? migrateWorkingDir}) async { + Future hasUncommittedChanges(String workingDirectory, {String? migrateStagingDir}) async { final List cmdArgs = [ 'git', 'ls-files', '--deleted', '--modified', '--others', - '--exclude=${migrateWorkingDir ?? kDefaultMigrateWorkingDirectoryName}' + '--exclude=${migrateStagingDir ?? kDefaultMigrateStagingDirectoryName}' ]; final RunResult result = await _processUtils.run(cmdArgs, workingDirectory: workingDirectory); checkForErrors(result, allowedExitCodes: [-1], commandDescription: cmdArgs.join(' ')); diff --git a/packages/flutter_tools/test/commands.shard/permeable/migrate_status_test.dart b/packages/flutter_tools/test/commands.shard/permeable/migrate_status_test.dart new file mode 100644 index 000000000000..1d179e23ba8e --- /dev/null +++ b/packages/flutter_tools/test/commands.shard/permeable/migrate_status_test.dart @@ -0,0 +1,197 @@ +// 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: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/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() { + FileSystem fileSystem; + BufferLogger logger; + Platform platform; + // TODO(garyq): Add terminal back in when other subcommands land. + ProcessManager processManager; + Directory appDir; + + setUp(() { + fileSystem = globals.localFileSystem; + appDir = fileSystem.systemTempDirectory.createTempSync('apptestdir'); + logger = BufferLogger.test(); + platform = FakePlatform(); + 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, + 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( + [ + '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( + [ + '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: { + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + Platform: () => platform, + }); +}