| OLD | NEW |
| 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 library command_lish; | 5 library command_lish; |
| 6 | 6 |
| 7 import 'dart:io'; | 7 import 'dart:io'; |
| 8 import 'dart:json'; | 8 import 'dart:json'; |
| 9 import 'dart:uri'; | 9 import 'dart:uri'; |
| 10 | 10 |
| 11 import '../../pkg/args/lib/args.dart'; | 11 import '../../pkg/args/lib/args.dart'; |
| 12 import '../../pkg/http/lib/http.dart' as http; | 12 import '../../pkg/http/lib/http.dart' as http; |
| 13 import 'directory_tree.dart'; |
| 13 import 'git.dart' as git; | 14 import 'git.dart' as git; |
| 14 import 'io.dart'; | 15 import 'io.dart'; |
| 15 import 'log.dart' as log; | 16 import 'log.dart' as log; |
| 16 import 'oauth2.dart' as oauth2; | 17 import 'oauth2.dart' as oauth2; |
| 17 import 'pub.dart'; | 18 import 'pub.dart'; |
| 18 import 'validator.dart'; | 19 import 'validator.dart'; |
| 19 | 20 |
| 20 /// Handles the `lish` and `publish` pub commands. | 21 /// Handles the `lish` and `publish` pub commands. |
| 21 class LishCommand extends PubCommand { | 22 class LishCommand extends PubCommand { |
| 22 final description = "Publish the current package to pub.dartlang.org."; | 23 final description = "Publish the current package to pub.dartlang.org."; |
| 23 final usage = "pub publish [options]"; | 24 final usage = "pub publish [options]"; |
| 24 final aliases = const ["lish", "lush"]; | 25 final aliases = const ["lish", "lush"]; |
| 25 | 26 |
| 26 ArgParser get commandParser { | 27 ArgParser get commandParser { |
| 27 var parser = new ArgParser(); | 28 var parser = new ArgParser(); |
| 28 parser.addOption('server', defaultsTo: 'https://pub.dartlang.org', | 29 parser.addOption('server', defaultsTo: 'https://pub.dartlang.org', |
| 29 help: 'The package server to which to upload this package'); | 30 help: 'The package server to which to upload this package'); |
| 30 return parser; | 31 return parser; |
| 31 } | 32 } |
| 32 | 33 |
| 33 /// The URL of the server to which to upload the package. | 34 /// The URL of the server to which to upload the package. |
| 34 Uri get server => new Uri.fromString(commandOptions['server']); | 35 Uri get server => new Uri.fromString(commandOptions['server']); |
| 35 | 36 |
| 36 Future onRun() { | 37 Future _publish(packageBytes) { |
| 37 var cloudStorageUrl; | 38 var cloudStorageUrl; |
| 38 return oauth2.withClient(cache, (client) { | 39 return oauth2.withClient(cache, (client) { |
| 39 // TODO(nweiz): Cloud Storage can provide an XML-formatted error. We | 40 // TODO(nweiz): Cloud Storage can provide an XML-formatted error. We |
| 40 // should report that error and exit. | 41 // should report that error and exit. |
| 41 return Futures.wait([ | 42 var newUri = server.resolve("/packages/versions/new.json"); |
| 42 client.get(server.resolve("/packages/versions/new.json")), | 43 return client.get(newUri).chain((response) { |
| 43 _filesToPublish.transform((files) { | |
| 44 log.fine('Archiving and publishing ${entrypoint.root}.'); | |
| 45 return createTarGz(files, baseDir: entrypoint.root.dir); | |
| 46 }).chain(consumeInputStream), | |
| 47 _validate() | |
| 48 ]).chain((results) { | |
| 49 var response = results[0]; | |
| 50 var packageBytes = results[1]; | |
| 51 var parameters = _parseJson(response); | 44 var parameters = _parseJson(response); |
| 52 | 45 |
| 53 var url = _expectField(parameters, 'url', response); | 46 var url = _expectField(parameters, 'url', response); |
| 54 if (url is! String) _invalidServerResponse(response); | 47 if (url is! String) _invalidServerResponse(response); |
| 55 cloudStorageUrl = new Uri.fromString(url); | 48 cloudStorageUrl = new Uri.fromString(url); |
| 56 var request = new http.MultipartRequest('POST', cloudStorageUrl); | 49 var request = new http.MultipartRequest('POST', cloudStorageUrl); |
| 57 | 50 |
| 58 var fields = _expectField(parameters, 'fields', response); | 51 var fields = _expectField(parameters, 'fields', response); |
| 59 if (fields is! Map) _invalidServerResponse(response); | 52 if (fields is! Map) _invalidServerResponse(response); |
| 60 fields.forEach((key, value) { | 53 fields.forEach((key, value) { |
| (...skipping 30 matching lines...) Expand all Loading... |
| 91 var errorMap = _parseJson(e.response); | 84 var errorMap = _parseJson(e.response); |
| 92 if (errorMap['error'] is! Map || | 85 if (errorMap['error'] is! Map || |
| 93 !errorMap['error'].containsKey('message') || | 86 !errorMap['error'].containsKey('message') || |
| 94 errorMap['error']['message'] is! String) { | 87 errorMap['error']['message'] is! String) { |
| 95 _invalidServerResponse(e.response); | 88 _invalidServerResponse(e.response); |
| 96 } | 89 } |
| 97 throw errorMap['error']['message']; | 90 throw errorMap['error']['message']; |
| 98 } | 91 } |
| 99 } else if (e is oauth2.ExpirationException) { | 92 } else if (e is oauth2.ExpirationException) { |
| 100 log.error("Pub's authorization to upload packages has expired and " | 93 log.error("Pub's authorization to upload packages has expired and " |
| 101 "can't be automatically refreshed."); | 94 "can't be automatically refreshed."); |
| 102 return onRun(); | 95 return _publish(packageBytes); |
| 103 } else if (e is oauth2.AuthorizationException) { | 96 } else if (e is oauth2.AuthorizationException) { |
| 104 var message = "OAuth2 authorization failed"; | 97 var message = "OAuth2 authorization failed"; |
| 105 if (e.description != null) message = "$message (${e.description})"; | 98 if (e.description != null) message = "$message (${e.description})"; |
| 106 log.error("$message."); | 99 log.error("$message."); |
| 107 return oauth2.clearCredentials(cache).chain((_) => onRun()); | 100 return oauth2.clearCredentials(cache).chain((_) => |
| 101 _publish(packageBytes)); |
| 108 } else { | 102 } else { |
| 109 throw e; | 103 throw e; |
| 110 } | 104 } |
| 111 }); | 105 }); |
| 112 } | 106 } |
| 113 | 107 |
| 108 Future onRun() { |
| 109 var files; |
| 110 return _filesToPublish.transform((f) { |
| 111 files = f; |
| 112 log.fine('Archiving and publishing ${entrypoint.root}.'); |
| 113 return createTarGz(files, baseDir: entrypoint.root.dir); |
| 114 }).chain(consumeInputStream).chain((packageBytes) { |
| 115 // Show the package contents so the user can verify they look OK. |
| 116 var package = entrypoint.root; |
| 117 log.message( |
| 118 'Publishing "${package.name}" ${package.version}:\n' |
| 119 '${generateTree(files)}'); |
| 120 |
| 121 // Validate the package. |
| 122 return _validate().chain((_) => _publish(packageBytes)); |
| 123 }); |
| 124 } |
| 125 |
| 114 /// The basenames of files that are automatically excluded from archives. | 126 /// The basenames of files that are automatically excluded from archives. |
| 115 final _BLACKLISTED_FILES = const ['pubspec.lock']; | 127 final _BLACKLISTED_FILES = const ['pubspec.lock']; |
| 116 | 128 |
| 117 /// The basenames of directories that are automatically excluded from | 129 /// The basenames of directories that are automatically excluded from |
| 118 /// archives. | 130 /// archives. |
| 119 final _BLACKLISTED_DIRECTORIES = const ['packages']; | 131 final _BLACKLISTED_DIRECTORIES = const ['packages']; |
| 120 | 132 |
| 121 /// Returns a list of files that should be included in the published package. | 133 /// Returns a list of files that should be included in the published package. |
| 122 /// If this is a Git repository, this will respect .gitignore; otherwise, it | 134 /// If this is a Git repository, this will respect .gitignore; otherwise, it |
| 123 /// will return all non-hidden files. | 135 /// will return all non-hidden files. |
| 124 Future<List<String>> get _filesToPublish { | 136 Future<List<String>> get _filesToPublish { |
| 125 var rootDir = entrypoint.root.dir; | 137 var rootDir = entrypoint.root.dir; |
| 138 |
| 139 // TODO(rnystrom): listDir() returns real file paths after symlinks are |
| 140 // resolved. This means if libDir contains a symlink, the resulting paths |
| 141 // won't appear to be within it, which confuses relativeTo(). Work around |
| 142 // that here by making sure we have the real path to libDir. Remove this |
| 143 // when #7346 is fixed. |
| 144 rootDir = new File(rootDir).fullPathSync(); |
| 145 |
| 126 return Futures.wait([ | 146 return Futures.wait([ |
| 127 dirExists(join(rootDir, '.git')), | 147 dirExists(join(rootDir, '.git')), |
| 128 git.isInstalled | 148 git.isInstalled |
| 129 ]).chain((results) { | 149 ]).chain((results) { |
| 130 if (results[0] && results[1]) { | 150 if (results[0] && results[1]) { |
| 131 // List all files that aren't gitignored, including those not checked in | 151 // List all files that aren't gitignored, including those not checked in |
| 132 // to Git. | 152 // to Git. |
| 133 return git.run(["ls-files", "--cached", "--others"]); | 153 return git.run(["ls-files", "--cached", "--others"]); |
| 134 } | 154 } |
| 135 | 155 |
| 136 return listDir(rootDir, recursive: true).chain((entries) { | 156 return listDir(rootDir, recursive: true).chain((entries) { |
| 137 return Futures.wait(entries.map((entry) { | 157 return Futures.wait(entries.map((entry) { |
| 138 return fileExists(entry).transform((isFile) => isFile ? entry : null); | 158 return fileExists(entry).transform((isFile) { |
| 159 // Skip directories. |
| 160 if (!isFile) return null; |
| 161 |
| 162 // TODO(rnystrom): Making these relative will break archive |
| 163 // creation if the cwd is ever *not* the package root directory. |
| 164 // Should instead only make these relative right before generating |
| 165 // the tree display (which is what really needs them to be). |
| 166 // Make it relative to the package root. |
| 167 return relativeTo(entry, rootDir); |
| 168 }); |
| 139 })); | 169 })); |
| 140 }); | 170 }); |
| 141 }).transform((files) => files.filter((file) { | 171 }).transform((files) => files.filter((file) { |
| 142 if (file == null || _BLACKLISTED_FILES.contains(basename(file))) { | 172 if (file == null || _BLACKLISTED_FILES.contains(basename(file))) { |
| 143 return false; | 173 return false; |
| 144 } | 174 } |
| 145 return !splitPath(relativeTo(file, rootDir)) | 175 |
| 146 .some(_BLACKLISTED_DIRECTORIES.contains); | 176 return !splitPath(file).some(_BLACKLISTED_DIRECTORIES.contains); |
| 147 })); | 177 })); |
| 148 } | 178 } |
| 149 | 179 |
| 150 /// Parses a response body, assuming it's JSON-formatted. Throws a | 180 /// Parses a response body, assuming it's JSON-formatted. Throws a |
| 151 /// user-friendly error if the response body is invalid JSON, or if it's not a | 181 /// user-friendly error if the response body is invalid JSON, or if it's not a |
| 152 /// map. | 182 /// map. |
| 153 Map _parseJson(http.Response response) { | 183 Map _parseJson(http.Response response) { |
| 154 var value; | 184 var value; |
| 155 try { | 185 try { |
| 156 value = JSON.parse(response.body); | 186 value = JSON.parse(response.body); |
| (...skipping 16 matching lines...) Expand all Loading... |
| 173 void _invalidServerResponse(http.Response response) { | 203 void _invalidServerResponse(http.Response response) { |
| 174 throw 'Invalid server response:\n${response.body}'; | 204 throw 'Invalid server response:\n${response.body}'; |
| 175 } | 205 } |
| 176 | 206 |
| 177 /// Validates the package. Throws an exception if it's invalid. | 207 /// Validates the package. Throws an exception if it's invalid. |
| 178 Future _validate() { | 208 Future _validate() { |
| 179 return Validator.runAll(entrypoint).chain((pair) { | 209 return Validator.runAll(entrypoint).chain((pair) { |
| 180 var errors = pair.first; | 210 var errors = pair.first; |
| 181 var warnings = pair.last; | 211 var warnings = pair.last; |
| 182 | 212 |
| 183 if (errors.isEmpty && warnings.isEmpty) return new Future.immediate(null); | 213 if (!errors.isEmpty) { |
| 184 if (!errors.isEmpty) throw "Package validation failed."; | 214 throw "Sorry, your package is missing " |
| 215 "${(errors.length > 1) ? 'some requirements' : 'a requirement'} " |
| 216 "and can't be published yet.\nFor more information, see: " |
| 217 "http://pub.dartlang.org/doc/pub-lish.html.\n"; |
| 218 } |
| 185 | 219 |
| 186 var s = warnings.length == 1 ? '' : 's'; | 220 var message = 'Looks great! Are you ready to upload your package'; |
| 187 stdout.writeString("Package has ${warnings.length} warning$s. Upload " | 221 |
| 188 "anyway (y/n)? "); | 222 if (!warnings.isEmpty) { |
| 189 return readLine().transform((line) { | 223 var s = warnings.length == 1 ? '' : 's'; |
| 190 if (new RegExp(r"^[yY]").hasMatch(line)) return; | 224 message = "Package has ${warnings.length} warning$s. Upload anyway"; |
| 191 throw "Package upload canceled."; | 225 } |
| 226 |
| 227 return confirm(message).transform((confirmed) { |
| 228 if (!confirmed) throw "Package upload canceled."; |
| 192 }); | 229 }); |
| 193 }); | 230 }); |
| 194 } | 231 } |
| 195 } | 232 } |
| OLD | NEW |