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 return client.get(server.resolve("/packages/versions/new.json")).chain((re sponse) { |
42 client.get(server.resolve("/packages/versions/new.json")), | |
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); | 43 var parameters = _parseJson(response); |
52 | 44 |
53 var url = _expectField(parameters, 'url', response); | 45 var url = _expectField(parameters, 'url', response); |
54 if (url is! String) _invalidServerResponse(response); | 46 if (url is! String) _invalidServerResponse(response); |
55 cloudStorageUrl = new Uri.fromString(url); | 47 cloudStorageUrl = new Uri.fromString(url); |
56 var request = new http.MultipartRequest('POST', cloudStorageUrl); | 48 var request = new http.MultipartRequest('POST', cloudStorageUrl); |
57 | 49 |
58 var fields = _expectField(parameters, 'fields', response); | 50 var fields = _expectField(parameters, 'fields', response); |
59 if (fields is! Map) _invalidServerResponse(response); | 51 if (fields is! Map) _invalidServerResponse(response); |
60 fields.forEach((key, value) { | 52 fields.forEach((key, value) { |
(...skipping 30 matching lines...) Expand all Loading... | |
91 var errorMap = _parseJson(e.response); | 83 var errorMap = _parseJson(e.response); |
92 if (errorMap['error'] is! Map || | 84 if (errorMap['error'] is! Map || |
93 !errorMap['error'].containsKey('message') || | 85 !errorMap['error'].containsKey('message') || |
94 errorMap['error']['message'] is! String) { | 86 errorMap['error']['message'] is! String) { |
95 _invalidServerResponse(e.response); | 87 _invalidServerResponse(e.response); |
96 } | 88 } |
97 throw errorMap['error']['message']; | 89 throw errorMap['error']['message']; |
98 } | 90 } |
99 } else if (e is oauth2.ExpirationException) { | 91 } else if (e is oauth2.ExpirationException) { |
100 log.error("Pub's authorization to upload packages has expired and " | 92 log.error("Pub's authorization to upload packages has expired and " |
101 "can't be automatically refreshed."); | 93 "can't be automatically refreshed."); |
102 return onRun(); | 94 return _publish(packageBytes); |
103 } else if (e is oauth2.AuthorizationException) { | 95 } else if (e is oauth2.AuthorizationException) { |
104 var message = "OAuth2 authorization failed"; | 96 var message = "OAuth2 authorization failed"; |
105 if (e.description != null) message = "$message (${e.description})"; | 97 if (e.description != null) message = "$message (${e.description})"; |
106 log.error("$message."); | 98 log.error("$message."); |
107 return oauth2.clearCredentials(cache).chain((_) => onRun()); | 99 return oauth2.clearCredentials(cache).chain((_) => |
100 _publish(packageBytes)); | |
108 } else { | 101 } else { |
109 throw e; | 102 throw e; |
110 } | 103 } |
111 }); | 104 }); |
112 } | 105 } |
113 | 106 |
107 Future onRun() { | |
108 var files; | |
109 return _filesToPublish.transform((f) { | |
110 files = f; | |
111 log.fine('Archiving and publishing ${entrypoint.root}.'); | |
112 return createTarGz(files, baseDir: entrypoint.root.dir); | |
113 }).chain(consumeInputStream).chain((packageBytes) { | |
114 // Show the package contents so the user can verify they look OK. | |
115 var package = entrypoint.root; | |
116 log.message( | |
117 'Publishing "${package.name}" ${package.version}:\n' | |
118 '${generateTree(files)}'); | |
119 | |
120 // Validate the package. | |
121 return _validate().chain((_) => _publish(packageBytes)); | |
122 }); | |
123 } | |
124 | |
114 /// The basenames of files that are automatically excluded from archives. | 125 /// The basenames of files that are automatically excluded from archives. |
115 final _BLACKLISTED_FILES = const ['pubspec.lock']; | 126 final _BLACKLISTED_FILES = const ['.DS_Store', 'pubspec.lock']; |
116 | 127 |
117 /// The basenames of directories that are automatically excluded from | 128 /// The basenames of directories that are automatically excluded from |
118 /// archives. | 129 /// archives. |
119 final _BLACKLISTED_DIRECTORIES = const ['packages']; | 130 final _BLACKLISTED_DIRECTORIES = const ['packages']; |
120 | 131 |
121 /// Returns a list of files that should be included in the published package. | 132 /// 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 | 133 /// If this is a Git repository, this will respect .gitignore; otherwise, it |
123 /// will return all non-hidden files. | 134 /// will return all non-hidden files. |
124 Future<List<String>> get _filesToPublish { | 135 Future<List<String>> get _filesToPublish { |
125 var rootDir = entrypoint.root.dir; | 136 var rootDir = entrypoint.root.dir; |
137 | |
138 // TODO(rnystrom): listDir() returns real file paths after symlinks are | |
139 // resolved. This means if libDir contains a symlink, the resulting paths | |
140 // won't appear to be within it, which confuses relativeTo(). Work around | |
141 // that here by making sure we have the real path to libDir. Remove this | |
142 // when #7346 is fixed. | |
143 rootDir = new File(rootDir).fullPathSync(); | |
144 | |
126 return Futures.wait([ | 145 return Futures.wait([ |
127 dirExists(join(rootDir, '.git')), | 146 dirExists(join(rootDir, '.git')), |
128 git.isInstalled | 147 git.isInstalled |
129 ]).chain((results) { | 148 ]).chain((results) { |
130 if (results[0] && results[1]) { | 149 if (results[0] && results[1]) { |
131 // List all files that aren't gitignored, including those not checked in | 150 // List all files that aren't gitignored, including those not checked in |
132 // to Git. | 151 // to Git. |
133 return git.run(["ls-files", "--cached", "--others"]); | 152 return git.run(["ls-files", "--cached", "--others"]); |
134 } | 153 } |
135 | 154 |
136 return listDir(rootDir, recursive: true).chain((entries) { | 155 return listDir(rootDir, recursive: true).chain((entries) { |
137 return Futures.wait(entries.map((entry) { | 156 return Futures.wait(entries.map((entry) { |
138 return fileExists(entry).transform((isFile) => isFile ? entry : null); | 157 return fileExists(entry).transform((isFile) { |
158 // Skip directories. | |
159 if (!isFile) return null; | |
160 | |
161 // Make it relative to the package root. | |
162 return relativeTo(entry, rootDir); | |
163 }); | |
139 })); | 164 })); |
140 }); | 165 }); |
141 }).transform((files) => files.filter((file) { | 166 }).transform((files) => files.filter((file) { |
142 if (file == null || _BLACKLISTED_FILES.contains(basename(file))) { | 167 if (file == null || _BLACKLISTED_FILES.contains(basename(file))) { |
143 return false; | 168 return false; |
144 } | 169 } |
145 return !splitPath(relativeTo(file, rootDir)) | 170 |
146 .some(_BLACKLISTED_DIRECTORIES.contains); | 171 return !splitPath(file).some(_BLACKLISTED_DIRECTORIES.contains); |
147 })); | 172 })); |
148 } | 173 } |
149 | 174 |
150 /// Parses a response body, assuming it's JSON-formatted. Throws a | 175 /// 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 | 176 /// user-friendly error if the response body is invalid JSON, or if it's not a |
152 /// map. | 177 /// map. |
153 Map _parseJson(http.Response response) { | 178 Map _parseJson(http.Response response) { |
154 var value; | 179 var value; |
155 try { | 180 try { |
156 value = JSON.parse(response.body); | 181 value = JSON.parse(response.body); |
(...skipping 16 matching lines...) Expand all Loading... | |
173 void _invalidServerResponse(http.Response response) { | 198 void _invalidServerResponse(http.Response response) { |
174 throw 'Invalid server response:\n${response.body}'; | 199 throw 'Invalid server response:\n${response.body}'; |
175 } | 200 } |
176 | 201 |
177 /// Validates the package. Throws an exception if it's invalid. | 202 /// Validates the package. Throws an exception if it's invalid. |
178 Future _validate() { | 203 Future _validate() { |
179 return Validator.runAll(entrypoint).chain((pair) { | 204 return Validator.runAll(entrypoint).chain((pair) { |
180 var errors = pair.first; | 205 var errors = pair.first; |
181 var warnings = pair.last; | 206 var warnings = pair.last; |
182 | 207 |
183 if (errors.isEmpty && warnings.isEmpty) return new Future.immediate(null); | 208 if (!errors.isEmpty) { |
184 if (!errors.isEmpty) throw "Package validation failed."; | 209 throw "Sorry, your package is missing " |
210 "${(errors.length > 1) ? 'some requirements' : 'a requirement'} " | |
211 "and can't be published yet.\nFor more information, see: " | |
212 "http://pub.dartlang.org/doc/pub-lish.html.\n"; | |
213 } | |
185 | 214 |
186 var s = warnings.length == 1 ? '' : 's'; | 215 var message = 'Looks great! Are you ready to upload your package'; |
187 stdout.writeString("Package has ${warnings.length} warning$s. Upload " | 216 |
188 "anyway (y/n)? "); | 217 if (!warnings.isEmpty) { |
189 return readLine().transform((line) { | 218 var s = warnings.length == 1 ? '' : 's'; |
190 if (new RegExp(r"^[yY]").hasMatch(line)) return; | 219 message = "Package has ${warnings.length} warning$s. Upload anyway"; |
nweiz
2012/12/12 21:21:21
It's potentially confusing that warnings are refer
| |
191 throw "Package upload canceled."; | 220 } |
221 | |
222 return confirm(message).transform((confirmed) { | |
223 if (!confirmed) throw "Package upload canceled."; | |
192 }); | 224 }); |
193 }); | 225 }); |
194 } | 226 } |
195 } | 227 } |
OLD | NEW |