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 |