blob: f7e3b5deeecf829a7d929162a4bda8f8727a185e [file] [log] [blame]
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:file/file.dart';
import 'package:git/git.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as p;
import 'package:yaml/yaml.dart';
import 'common.dart';
/// Wraps pub publish with a few niceties used by the flutter/plugin team.
///
/// 1. Checks for any modified files in git and refuses to publish if there's an
/// issue.
/// 2. Tags the release with the format <package-name>-v<package-version>.
/// 3. Pushes the release to a remote.
///
/// Both 2 and 3 are optional, see `plugin_tools help publish-plugin` for full
/// usage information.
///
/// [processRunner], [print], and [stdin] can be overriden for easier testing.
class PublishPluginCommand extends PluginCommand {
PublishPluginCommand(
Directory packagesDir,
FileSystem fileSystem, {
ProcessRunner processRunner = const ProcessRunner(),
Print print = print,
Stdin stdinput,
}) : _print = print,
_stdin = stdinput ?? stdin,
super(packagesDir, fileSystem, processRunner: processRunner) {
argParser.addOption(
_packageOption,
help: 'The package to publish.'
'If the package directory name is different than its pubspec.yaml name, then this should specify the directory.',
);
argParser.addMultiOption(_pubFlagsOption,
help:
'A list of options that will be forwarded on to pub. Separate multiple flags with commas.');
argParser.addFlag(
_tagReleaseOption,
help: 'Whether or not to tag the release.',
defaultsTo: true,
negatable: true,
);
argParser.addFlag(
_pushTagsOption,
help:
'Whether or not tags should be pushed to a remote after creation. Ignored if tag-release is false.',
defaultsTo: true,
negatable: true,
);
argParser.addOption(
_remoteOption,
help:
'The name of the remote to push the tags to. Ignored if push-tags or tag-release is false.',
// Flutter convention is to use "upstream" for the single source of truth, and "origin" for personal forks.
defaultsTo: 'upstream',
);
}
static const String _packageOption = 'package';
static const String _tagReleaseOption = 'tag-release';
static const String _pushTagsOption = 'push-tags';
static const String _pubFlagsOption = 'pub-publish-flags';
static const String _remoteOption = 'remote';
// Version tags should follow <package-name>-v<semantic-version>. For example,
// `flutter_plugin_tools-v0.0.24`.
static const String _tagFormat = '%PACKAGE%-v%VERSION%';
@override
final String name = 'publish-plugin';
@override
final String description =
'Attempts to publish the given plugin and tag its release on GitHub.';
final Print _print;
final Stdin _stdin;
// The directory of the actual package that we are publishing.
Directory _packageDir;
StreamSubscription<String> _stdinSubscription;
@override
Future<Null> run() async {
checkSharding();
_print('Checking local repo...');
_packageDir = _checkPackageDir();
await _checkGitStatus();
final bool shouldPushTag = argResults[_pushTagsOption];
final String remote = argResults[_remoteOption];
String remoteUrl;
if (shouldPushTag) {
remoteUrl = await _verifyRemote(remote);
}
_print('Local repo is ready!');
await _publish();
_print('Package published!');
if (!argResults[_tagReleaseOption]) {
return await _finishSuccesfully();
}
_print('Tagging release...');
final String tag = _getTag();
await processRunner.runAndExitOnError('git', <String>['tag', tag],
workingDir: _packageDir);
if (!shouldPushTag) {
return await _finishSuccesfully();
}
_print('Pushing tag to $remote...');
await _pushTagToRemote(remote: remote, tag: tag, remoteUrl: remoteUrl);
await _finishSuccesfully();
}
Future<void> _finishSuccesfully() async {
await _stdinSubscription.cancel();
_print('Done!');
}
Directory _checkPackageDir() {
final String package = argResults[_packageOption];
if (package == null) {
_print(
'Must specify a package to publish. See `plugin_tools help publish-plugin`.');
throw ToolExit(1);
}
final Directory _packageDir = packagesDir.childDirectory(package);
if (!_packageDir.existsSync()) {
_print('${_packageDir.absolute.path} does not exist.');
throw ToolExit(1);
}
return _packageDir;
}
Future<void> _checkGitStatus() async {
if (!await GitDir.isGitDir(packagesDir.path)) {
_print('$packagesDir is not a valid Git repository.');
throw ToolExit(1);
}
final ProcessResult statusResult = await processRunner.runAndExitOnError(
'git',
<String>[
'status',
'--porcelain',
'--ignored',
_packageDir.absolute.path
],
workingDir: _packageDir);
final String statusOutput = statusResult.stdout;
if (statusOutput.isNotEmpty) {
_print(
"There are files in the package directory that haven't been saved in git. Refusing to publish these files:\n\n"
'$statusOutput\n'
'If the directory should be clean, you can run `git clean -xdf && git reset --hard HEAD` to wipe all local changes.');
throw ToolExit(1);
}
}
Future<String> _verifyRemote(String remote) async {
final ProcessResult remoteInfo = await processRunner.runAndExitOnError(
'git', <String>['remote', 'get-url', remote],
workingDir: _packageDir);
return remoteInfo.stdout;
}
Future<void> _publish() async {
final List<String> publishFlags = argResults[_pubFlagsOption];
_print(
'Running `pub publish ${publishFlags.join(' ')}` in ${_packageDir.absolute.path}...\n');
final Process publish = await processRunner.start(
'flutter', <String>['pub', 'publish'] + publishFlags,
workingDirectory: _packageDir);
publish.stdout
.transform(utf8.decoder)
.listen((String data) => _print(data));
publish.stderr
.transform(utf8.decoder)
.listen((String data) => _print(data));
_stdinSubscription = _stdin
.transform(utf8.decoder)
.listen((String data) => publish.stdin.writeln(data));
final int result = await publish.exitCode;
if (result != 0) {
_print('Publish failed. Exiting.');
throw ToolExit(result);
}
}
String _getTag() {
final File pubspecFile =
fileSystem.file(p.join(_packageDir.path, 'pubspec.yaml'));
final YamlMap pubspecYaml = loadYaml(pubspecFile.readAsStringSync());
final String name = pubspecYaml['name'];
final String version = pubspecYaml['version'];
// We should have failed to publish if these were unset.
assert(name.isNotEmpty && version.isNotEmpty);
return _tagFormat
.replaceAll('%PACKAGE%', name)
.replaceAll('%VERSION%', version);
}
Future<void> _pushTagToRemote(
{@required String remote,
@required String tag,
@required String remoteUrl}) async {
assert(remote != null && tag != null && remoteUrl != null);
_print('Ready to push $tag to $remoteUrl (y/n)?');
final String input = _stdin.readLineSync();
if (input.toLowerCase() != 'y') {
_print('Tag push canceled.');
throw ToolExit(1);
}
await processRunner.runAndExitOnError('git', <String>['push', remote, tag],
workingDir: packagesDir);
}
}