44
55import 'dart:async' ;
66import 'dart:convert' ;
7- import 'dart:io' ;
7+ import 'dart:io' as io ;
88
99import 'package:file/file.dart' ;
1010import 'package:git/git.dart' ;
1111import 'package:meta/meta.dart' ;
1212import 'package:path/path.dart' as p;
13+ import 'package:pub_semver/pub_semver.dart' ;
14+ import 'package:pubspec_parse/pubspec_parse.dart' ;
1315import 'package:yaml/yaml.dart' ;
1416
1517import 'common.dart' ;
@@ -32,10 +34,12 @@ class PublishPluginCommand extends PluginCommand {
3234 FileSystem fileSystem, {
3335 ProcessRunner processRunner = const ProcessRunner (),
3436 Print print = print,
35- Stdin stdinput,
37+ io.Stdin stdinput,
38+ GitDir gitDir,
3639 }) : _print = print,
37- _stdin = stdinput ?? stdin,
38- super (packagesDir, fileSystem, processRunner: processRunner) {
40+ _stdin = stdinput ?? io.stdin,
41+ super (packagesDir, fileSystem,
42+ processRunner: processRunner, gitDir: gitDir) {
3943 argParser.addOption (
4044 _packageOption,
4145 help: 'The package to publish.'
@@ -64,13 +68,31 @@ class PublishPluginCommand extends PluginCommand {
6468 // Flutter convention is to use "upstream" for the single source of truth, and "origin" for personal forks.
6569 defaultsTo: 'upstream' ,
6670 );
71+ argParser.addFlag (
72+ _allFlag,
73+ help:
74+ 'Release all plugins that contains pubspec changes at the current commit compares to the base-sha.\n '
75+ 'The $_packageOption option is ignored if this is on.' ,
76+ defaultsTo: false ,
77+ );
78+ argParser.addFlag (
79+ _dryRunFlag,
80+ help:
81+ 'Skips the real `pub publish` and `git tag` commands and assumes both commands are successful.\n '
82+ 'This does not run `pub publish --dry-run`.\n '
83+ 'If you want to run the command with `pub publish --dry-run`, use `pub-publish-flags=--dry-run`' ,
84+ defaultsTo: false ,
85+ negatable: true ,
86+ );
6787 }
6888
6989 static const String _packageOption = 'package' ;
7090 static const String _tagReleaseOption = 'tag-release' ;
7191 static const String _pushTagsOption = 'push-tags' ;
7292 static const String _pubFlagsOption = 'pub-publish-flags' ;
7393 static const String _remoteOption = 'remote' ;
94+ static const String _allFlag = 'all' ;
95+ static const String _dryRunFlag = 'dry-run' ;
7496
7597 // Version tags should follow <package-name>-v<semantic-version>. For example,
7698 // `flutter_plugin_tools-v0.0.24`.
@@ -84,14 +106,15 @@ class PublishPluginCommand extends PluginCommand {
84106 'Attempts to publish the given plugin and tag its release on GitHub.' ;
85107
86108 final Print _print;
87- final Stdin _stdin;
88- // The directory of the actual package that we are publishing.
109+ final io.Stdin _stdin;
89110 StreamSubscription <String > _stdinSubscription;
111+ bool _startedListenToStdStream = false ;
90112
91113 @override
92114 Future <void > run () async {
93115 final String package = argResults[_packageOption] as String ;
94- if (package == null ) {
116+ final bool all = argResults[_allFlag] as bool ;
117+ if (package == null && ! all) {
95118 _print (
96119 'Must specify a package to publish. See `plugin_tools help publish-plugin`.' );
97120 throw ToolExit (1 );
@@ -102,6 +125,8 @@ class PublishPluginCommand extends PluginCommand {
102125 _print ('$packagesDir is not a valid Git repository.' );
103126 throw ToolExit (1 );
104127 }
128+ final GitDir baseGitDir =
129+ await GitDir .fromExisting (packagesDir.path, allowSubdirectory: true );
105130
106131 final bool shouldPushTag = argResults[_pushTagsOption] == true ;
107132 final String remote = argResults[_remoteOption] as String ;
@@ -111,7 +136,74 @@ class PublishPluginCommand extends PluginCommand {
111136 }
112137 _print ('Local repo is ready!' );
113138
114- final Directory packageDir = _getPackageDir (package);
139+ if (all) {
140+ await _publishAllPackages (
141+ remote: remote,
142+ remoteUrl: remoteUrl,
143+ shouldPushTag: shouldPushTag,
144+ baseGitDir: baseGitDir);
145+ } else {
146+ await _publishAndReleasePackage (
147+ packageDir: _getPackageDir (package),
148+ remote: remote,
149+ remoteUrl: remoteUrl,
150+ shouldPushTag: shouldPushTag);
151+ }
152+ await _finishSuccesfully ();
153+ }
154+
155+ Future <void > _publishAllPackages (
156+ {String remote,
157+ String remoteUrl,
158+ bool shouldPushTag,
159+ GitDir baseGitDir}) async {
160+ final List <String > packagesReleased = < String > [];
161+ final GitVersionFinder gitVersionFinder = await retrieveVersionFinder ();
162+ final List <String > changedPubspecs =
163+ await gitVersionFinder.getChangedPubSpecs ();
164+ if (changedPubspecs.isEmpty) {
165+ _print ('No version updates in this commit, exiting...' );
166+ return ;
167+ }
168+ _print ('Getting existing tags...' );
169+ final io.ProcessResult existingTagsResult =
170+ await baseGitDir.runCommand (< String > ['tag' , '--sort=-committerdate' ]);
171+ final List <String > existingTags = (existingTagsResult.stdout as String )
172+ .split ('\n ' )
173+ ..removeWhere ((element) => element == '' );
174+ for (final String pubspecPath in changedPubspecs) {
175+ final File pubspecFile =
176+ fileSystem.directory (baseGitDir.path).childFile (pubspecPath);
177+ if (! pubspecFile.existsSync ()) {
178+ printErrorAndExit (
179+ errorMessage:
180+ 'Fatal: The pubspec file at ${pubspecFile .path } does not exist.' );
181+ }
182+ final bool needsRelease = await _checkNeedsRelease (
183+ pubspecPath: pubspecPath,
184+ pubspecFile: pubspecFile,
185+ gitVersionFinder: gitVersionFinder,
186+ existingTags: existingTags);
187+ if (! needsRelease) {
188+ continue ;
189+ }
190+ _print ('\n ' );
191+ await _publishAndReleasePackage (
192+ packageDir: pubspecFile.parent,
193+ remote: remote,
194+ remoteUrl: remoteUrl,
195+ shouldPushTag: shouldPushTag);
196+ packagesReleased.add (pubspecFile.parent.basename);
197+ _print ('\n ' );
198+ }
199+ _print ('Packages released: ${packagesReleased .join (', ' )}' );
200+ }
201+
202+ Future <void > _publishAndReleasePackage (
203+ {@required Directory packageDir,
204+ @required String remote,
205+ @required String remoteUrl,
206+ @required bool shouldPushTag}) async {
115207 await _publishPlugin (packageDir: packageDir);
116208 if (argResults[_tagReleaseOption] as bool ) {
117209 await _tagRelease (
@@ -120,7 +212,48 @@ class PublishPluginCommand extends PluginCommand {
120212 remoteUrl: remoteUrl,
121213 shouldPushTag: shouldPushTag);
122214 }
123- await _finishSuccesfully ();
215+ _print ('Release ${packageDir .basename } successful.' );
216+ }
217+
218+ // Returns `true` if needs to release the version, `false` if needs to skip
219+ Future <bool > _checkNeedsRelease ({
220+ @required String pubspecPath,
221+ @required File pubspecFile,
222+ @required GitVersionFinder gitVersionFinder,
223+ @required List <String > existingTags,
224+ }) async {
225+ final Pubspec pubspec = Pubspec .parse (pubspecFile.readAsStringSync ());
226+ if (pubspec.publishTo == 'none' ) {
227+ return false ;
228+ }
229+
230+ final Version headVersion =
231+ await gitVersionFinder.getPackageVersion (pubspecPath, gitRef: 'HEAD' );
232+ if (headVersion == null ) {
233+ printErrorAndExit (
234+ errorMessage: 'No version found. A package that '
235+ 'intentionally has no version should be marked '
236+ '"publish_to: none".' );
237+ }
238+
239+ if (pubspec.name == null ) {
240+ printErrorAndExit (errorMessage: 'Fatal: Package name is null.' );
241+ }
242+ // Get latest tagged version and compare with the current version.
243+ final String latestTag = existingTags.isNotEmpty
244+ ? existingTags
245+ .firstWhere ((String tag) => tag.split ('-v' ).first == pubspec.name)
246+ : '' ;
247+ if (latestTag.isNotEmpty) {
248+ final String latestTaggedVersion = latestTag.split ('-v' ).last;
249+ final Version latestVersion = Version .parse (latestTaggedVersion);
250+ if (pubspec.version < latestVersion) {
251+ _print (
252+ 'The new version (${pubspec .version }) is lower than the current version ($latestVersion ) for ${pubspec .name }.\n This git commit is a revert, no release is tagged.' );
253+ return false ;
254+ }
255+ }
256+ return true ;
124257 }
125258
126259 Future <void > _publishPlugin ({@required Directory packageDir}) async {
@@ -135,6 +268,14 @@ class PublishPluginCommand extends PluginCommand {
135268 @required String remoteUrl,
136269 @required bool shouldPushTag}) async {
137270 final String tag = _getTag (packageDir);
271+ if (argResults[_dryRunFlag] as bool ) {
272+ _print ('DRY RUN: Tagging release $tag ...' );
273+ if (! shouldPushTag) {
274+ return ;
275+ }
276+ _print ('DRY RUN: Pushing tag to $remote ...' );
277+ return ;
278+ }
138279 _print ('Tagging release $tag ...' );
139280 await processRunner.runAndExitOnError ('git' , < String > ['tag' , tag],
140281 workingDir: packageDir);
@@ -147,7 +288,9 @@ class PublishPluginCommand extends PluginCommand {
147288 }
148289
149290 Future <void > _finishSuccesfully () async {
150- await _stdinSubscription.cancel ();
291+ if (_stdinSubscription != null ) {
292+ await _stdinSubscription.cancel ();
293+ }
151294 _print ('Done!' );
152295 }
153296
@@ -163,7 +306,7 @@ class PublishPluginCommand extends PluginCommand {
163306 }
164307
165308 Future <void > _checkGitStatus (Directory packageDir) async {
166- final ProcessResult statusResult = await processRunner.runAndExitOnError (
309+ final io. ProcessResult statusResult = await processRunner.runAndExitOnError (
167310 'git' ,
168311 < String > [
169312 'status' ,
@@ -184,7 +327,7 @@ class PublishPluginCommand extends PluginCommand {
184327 }
185328
186329 Future <String > _verifyRemote (String remote) async {
187- final ProcessResult remoteInfo = await processRunner.runAndExitOnError (
330+ final io. ProcessResult remoteInfo = await processRunner.runAndExitOnError (
188331 'git' , < String > ['remote' , 'get-url' , remote],
189332 workingDir: packagesDir);
190333 return remoteInfo.stdout as String ;
@@ -193,20 +336,31 @@ class PublishPluginCommand extends PluginCommand {
193336 Future <void > _publish (Directory packageDir) async {
194337 final List <String > publishFlags =
195338 argResults[_pubFlagsOption] as List <String >;
339+ if (argResults[_dryRunFlag] as bool ) {
340+ _print (
341+ 'DRY RUN: Running `pub publish ${publishFlags .join (' ' )}` in ${packageDir .absolute .path }...\n ' );
342+ return ;
343+ }
344+
196345 _print (
197346 'Running `pub publish ${publishFlags .join (' ' )}` in ${packageDir .absolute .path }...\n ' );
198- final Process publish = await processRunner.start (
347+ final io. Process publish = await processRunner.start (
199348 'flutter' , < String > ['pub' , 'publish' ] + publishFlags,
200349 workingDirectory: packageDir);
201- publish.stdout
202- .transform (utf8.decoder)
203- .listen ((String data) => _print (data));
204- publish.stderr
205- .transform (utf8.decoder)
206- .listen ((String data) => _print (data));
207- _stdinSubscription = _stdin
208- .transform (utf8.decoder)
209- .listen ((String data) => publish.stdin.writeln (data));
350+
351+ if (! _startedListenToStdStream) {
352+ publish.stdout
353+ .transform (utf8.decoder)
354+ .listen ((String data) => _print (data));
355+ publish.stderr
356+ .transform (utf8.decoder)
357+ .listen ((String data) => _print (data));
358+ _stdinSubscription = _stdin
359+ .transform (utf8.decoder)
360+ .listen ((String data) => publish.stdin.writeln (data));
361+
362+ _startedListenToStdStream = true ;
363+ }
210364 final int result = await publish.exitCode;
211365 if (result != 0 ) {
212366 _print ('Publish failed. Exiting.' );
0 commit comments