1 use std
::collections
::{BTreeMap, HashSet}
;
3 use std
::io
::{self, BufRead}
;
5 use std
::path
::PathBuf
;
7 use std
::time
::Duration
;
10 use anyhow
::{bail, format_err}
;
11 use cargo_util
::paths
;
12 use crates_io
::{self, NewCrate, NewCrateDependency, Registry}
;
13 use curl
::easy
::{Easy, InfoType, SslOpt, SslVersion}
;
14 use log
::{log, Level}
;
15 use percent_encoding
::{percent_encode, NON_ALPHANUMERIC}
;
17 use crate::core
::dependency
::DepKind
;
18 use crate::core
::manifest
::ManifestMetadata
;
19 use crate::core
::resolver
::CliFeatures
;
20 use crate::core
::source
::Source
;
21 use crate::core
::{Package, SourceId, Workspace}
;
23 use crate::sources
::{RegistrySource, SourceConfigMap, CRATES_IO_REGISTRY}
;
24 use crate::util
::config
::{self, Config, SslVersionConfig, SslVersionConfigRange}
;
25 use crate::util
::errors
::{CargoResult, CargoResultExt}
;
26 use crate::util
::important_paths
::find_root_manifest_for_wd
;
27 use crate::util
::validate_package_name
;
28 use crate::util
::IntoUrl
;
29 use crate::{drop_print, drop_println, version}
;
33 /// Registry settings loaded from config files.
35 /// This is loaded based on the `--registry` flag and the config settings.
37 pub struct RegistryConfig
{
38 /// The index URL. If `None`, use crates.io.
39 pub index
: Option
<String
>,
40 /// The authentication token.
41 pub token
: Option
<String
>,
42 /// Process used for fetching a token.
43 pub credential_process
: Option
<(PathBuf
, Vec
<String
>)>,
46 pub struct PublishOpts
<'cfg
> {
47 pub config
: &'cfg Config
,
48 pub token
: Option
<String
>,
49 pub index
: Option
<String
>,
51 pub allow_dirty
: bool
,
52 pub jobs
: Option
<u32>,
53 pub targets
: Vec
<String
>,
55 pub registry
: Option
<String
>,
56 pub cli_features
: CliFeatures
,
59 pub fn publish(ws
: &Workspace
<'_
>, opts
: &PublishOpts
<'_
>) -> CargoResult
<()> {
60 let pkg
= ws
.current()?
;
61 let mut publish_registry
= opts
.registry
.clone();
63 if let Some(ref allowed_registries
) = *pkg
.publish() {
64 if publish_registry
.is_none() && allowed_registries
.len() == 1 {
65 // If there is only one allowed registry, push to that one directly,
66 // even though there is no registry specified in the command.
67 let default_registry
= &allowed_registries
[0];
68 if default_registry
!= CRATES_IO_REGISTRY
{
69 // Don't change the registry for crates.io and don't warn the user.
70 // crates.io will be defaulted even without this.
71 opts
.config
.shell().note(&format
!(
72 "Found `{}` as only allowed registry. Publishing to it automatically.",
75 publish_registry
= Some(default_registry
.clone());
79 let reg_name
= publish_registry
81 .unwrap_or_else(|| CRATES_IO_REGISTRY
.to_string());
82 if !allowed_registries
.contains(®_name
) {
84 "`{}` cannot be published.\n\
85 The registry `{}` is not listed in the `publish` value in Cargo.toml.",
92 let (mut registry
, _reg_cfg
, reg_id
) = registry(
100 verify_dependencies(pkg
, ®istry
, reg_id
)?
;
102 // Prepare a tarball, with a non-suppressible warning if metadata
103 // is missing since this is being put online.
104 let tarball
= ops
::package(
110 check_metadata
: true,
111 allow_dirty
: opts
.allow_dirty
,
112 targets
: opts
.targets
.clone(),
114 cli_features
: opts
.cli_features
.clone(),
119 // Upload said tarball to the specified destination
122 .status("Uploading", pkg
.package_id().to_string())?
;
135 fn verify_dependencies(
138 registry_src
: SourceId
,
139 ) -> CargoResult
<()> {
140 for dep
in pkg
.dependencies().iter() {
141 if dep
.source_id().is_path() || dep
.source_id().is_git() {
142 if !dep
.specified_req() {
143 if !dep
.is_transitive() {
144 // dev-dependencies will be stripped in TomlManifest::prepare_for_publish
147 let which
= if dep
.source_id().is_path() {
152 let dep_version_source
= dep
.registry_id().map_or_else(
153 || "crates.io".to_string(),
154 |registry_id
| registry_id
.display_registry_name(),
157 "all dependencies must have a version specified when publishing.\n\
158 dependency `{}` does not specify a version\n\
159 Note: The published dependency will use the version from {},\n\
160 the `{}` specification will be removed from the dependency declaration.",
166 // TomlManifest::prepare_for_publish will rewrite the dependency
167 // to be just the `version` field.
168 } else if dep
.source_id() != registry_src
{
169 if !dep
.source_id().is_registry() {
170 // Consider making SourceId::kind a public type that we can
171 // exhaustively match on. Using match can help ensure that
172 // every kind is properly handled.
173 panic
!("unexpected source kind for dependency {:?}", dep
);
175 // Block requests to send to crates.io with alt-registry deps.
176 // This extra hostname check is mostly to assist with testing,
177 // but also prevents someone using `--index` to specify
178 // something that points to crates.io.
179 if registry_src
.is_default_registry() || registry
.host_is_crates_io() {
180 bail
!("crates cannot be published to crates.io with dependencies sourced from other\n\
181 registries. `{}` needs to be published to crates.io before publishing this crate.\n\
182 (crate `{}` is pulled from {})",
196 registry
: &mut Registry
,
197 registry_id
: SourceId
,
199 ) -> CargoResult
<()> {
204 // Skip dev-dependency without version.
205 dep
.is_transitive() || dep
.specified_req()
208 // If the dependency is from a different registry, then include the
209 // registry in the dependency.
210 let dep_registry_id
= match dep
.registry_id() {
212 None
=> SourceId
::crates_io(config
)?
,
214 // In the index and Web API, None means "from the same registry"
215 // whereas in Cargo.toml, it means "from crates.io".
216 let dep_registry
= if dep_registry_id
!= registry_id
{
217 Some(dep_registry_id
.url().to_string())
222 Ok(NewCrateDependency
{
223 optional
: dep
.is_optional(),
224 default_features
: dep
.uses_default_features(),
225 name
: dep
.package_name().to_string(),
226 features
: dep
.features().iter().map(|s
| s
.to_string()).collect(),
227 version_req
: dep
.version_req().to_string(),
228 target
: dep
.platform().map(|s
| s
.to_string()),
229 kind
: match dep
.kind() {
230 DepKind
::Normal
=> "normal",
231 DepKind
::Build
=> "build",
232 DepKind
::Development
=> "dev",
235 registry
: dep_registry
,
236 explicit_name_in_toml
: dep
.explicit_name_in_toml().map(|s
| s
.to_string()),
239 .collect
::<CargoResult
<Vec
<NewCrateDependency
>>>()?
;
240 let manifest
= pkg
.manifest();
241 let ManifestMetadata
{
254 } = *manifest
.metadata();
255 let readme_content
= readme
258 paths
::read(&pkg
.root().join(readme
))
259 .chain_err(|| format
!("failed to read `readme` file for package `{}`", pkg
))
262 if let Some(ref file
) = *license_file
{
263 if !pkg
.root().join(file
).exists() {
264 bail
!("the license file `{}` does not exist", file
)
268 // Do not upload if performing a dry run
270 config
.shell().warn("aborting upload due to dry run")?
;
274 let string_features
= match manifest
.original().features() {
275 Some(features
) => features
277 .map(|(feat
, values
)| {
280 values
.iter().map(|fv
| fv
.to_string()).collect(),
283 .collect
::<BTreeMap
<String
, Vec
<String
>>>(),
284 None
=> BTreeMap
::new(),
287 let warnings
= registry
290 name
: pkg
.name().to_string(),
291 vers
: pkg
.version().to_string(),
293 features
: string_features
,
294 authors
: authors
.clone(),
295 description
: description
.clone(),
296 homepage
: homepage
.clone(),
297 documentation
: documentation
.clone(),
298 keywords
: keywords
.clone(),
299 categories
: categories
.clone(),
300 readme
: readme_content
,
301 readme_file
: readme
.clone(),
302 repository
: repository
.clone(),
303 license
: license
.clone(),
304 license_file
: license_file
.clone(),
305 badges
: badges
.clone(),
306 links
: links
.clone(),
311 .chain_err(|| format
!("failed to publish to registry at {}", registry
.host()))?
;
313 if !warnings
.invalid_categories
.is_empty() {
315 "the following are not valid category slugs and were \
316 ignored: {}. Please see https://crates.io/category_slugs \
317 for the list of all category slugs. \
319 warnings
.invalid_categories
.join(", ")
321 config
.shell().warn(&msg
)?
;
324 if !warnings
.invalid_badges
.is_empty() {
326 "the following are not valid badges and were ignored: {}. \
327 Either the badge type specified is unknown or a required \
328 attribute is missing. Please see \
329 https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata \
330 for valid badge types and their required attributes.",
331 warnings
.invalid_badges
.join(", ")
333 config
.shell().warn(&msg
)?
;
336 if !warnings
.other
.is_empty() {
337 for msg
in warnings
.other
{
338 config
.shell().warn(&msg
)?
;
345 /// Returns the index and token from the config file for the given registry.
347 /// `registry` is typically the registry specified on the command-line. If
348 /// `None`, `index` is set to `None` to indicate it should use crates.io.
349 pub fn registry_configuration(
351 registry
: Option
<&str>,
352 ) -> CargoResult
<RegistryConfig
> {
353 let err_both
= |token_key
: &str, proc_key
: &str| {
355 "both `{TOKEN_KEY}` and `{PROC_KEY}` \
356 were specified in the config\n\
357 Only one of these values may be set, remove one or the other to proceed.",
358 TOKEN_KEY
= token_key
,
362 // `registry.default` is handled in command-line parsing.
363 let (index
, token
, process
) = match registry
{
365 validate_package_name(registry
, "registry name", "")?
;
366 let index
= Some(config
.get_registry_index(registry
)?
.to_string());
367 let token_key
= format
!("registries.{}.token", registry
);
368 let token
= config
.get_string(&token_key
)?
.map(|p
| p
.val
);
369 let process
= if config
.cli_unstable().credential_process
{
370 let mut proc_key
= format
!("registries.{}.credential-process", registry
);
371 let mut process
= config
.get
::<Option
<config
::PathAndArgs
>>(&proc_key
)?
;
372 if process
.is_none() && token
.is_none() {
373 // This explicitly ignores the global credential-process if
374 // the token is set, as that is "more specific".
375 proc_key
= String
::from("registry.credential-process");
376 process
= config
.get
::<Option
<config
::PathAndArgs
>>(&proc_key
)?
;
377 } else if process
.is_some() && token
.is_some() {
378 return err_both(&token_key
, &proc_key
);
384 (index
, token
, process
)
387 // Use crates.io default.
388 config
.check_registry_index_not_set()?
;
389 let token
= config
.get_string("registry.token")?
.map(|p
| p
.val
);
390 let process
= if config
.cli_unstable().credential_process
{
392 config
.get
::<Option
<config
::PathAndArgs
>>("registry.credential-process")?
;
393 if token
.is_some() && process
.is_some() {
394 return err_both("registry.token", "registry.credential-process");
400 (None
, token
, process
)
404 let credential_process
=
405 process
.map(|process
| (process
.path
.resolve_program(config
), process
.args
));
414 /// Returns the `Registry` and `Source` based on command-line and config settings.
416 /// * `token`: The token from the command-line. If not set, uses the token
418 /// * `index`: The index URL from the command-line. This is ignored if
419 /// `registry` is set.
420 /// * `registry`: The registry name from the command-line. If neither
421 /// `registry`, or `index` are set, then uses `crates-io`, honoring
422 /// `[source]` replacement if defined.
423 /// * `force_update`: If `true`, forces the index to be updated.
424 /// * `validate_token`: If `true`, the token must be set.
427 token
: Option
<String
>,
428 index
: Option
<String
>,
429 registry
: Option
<String
>,
431 validate_token
: bool
,
432 ) -> CargoResult
<(Registry
, RegistryConfig
, SourceId
)> {
433 if index
.is_some() && registry
.is_some() {
434 // Otherwise we would silently ignore one or the other.
435 bail
!("both `--index` and `--registry` should not be set at the same time");
437 // Parse all configuration options
438 let reg_cfg
= registry_configuration(config
, registry
.as_deref())?
;
439 let opt_index
= reg_cfg
.index
.as_ref().or_else(|| index
.as_ref());
440 let sid
= get_source_id(config
, opt_index
, registry
.as_ref())?
;
441 if !sid
.is_remote_registry() {
443 "{} does not support API commands.\n\
444 Check for a source-replacement in .cargo/config.",
449 let _lock
= config
.acquire_package_cache_lock()?
;
450 let mut src
= RegistrySource
::remote(sid
, &HashSet
::new(), config
);
451 // Only update the index if the config is not available or `force` is set.
452 let cfg
= src
.config();
453 let mut updated_cfg
= || {
455 .chain_err(|| format
!("failed to update {}", sid
))?
;
459 let cfg
= if force_update
{
462 cfg
.or_else(|_
| updated_cfg())?
465 cfg
.and_then(|cfg
| cfg
.api
)
466 .ok_or_else(|| format_err
!("{} does not support API commands", sid
))?
468 let token
= if validate_token
{
471 bail
!("command-line argument --index requires --token to be specified");
475 // Check `is_default_registry` so that the crates.io index can
476 // change config.json's "api" value, and this won't affect most
477 // people. It will affect those using source replacement, but
478 // hopefully that's a relatively small set of users.
480 && reg_cfg
.token
.is_some()
481 && registry
.is_none()
482 && !sid
.is_default_registry()
483 && !crates_io
::is_url_crates_io(&api_host
)
486 "using `registry.token` config value with source \
487 replacement is deprecated\n\
488 This may become a hard error in the future; \
489 see <https://github.com/rust-lang/cargo/issues/xxx>.\n\
490 Use the --token command-line flag to remove this warning.",
492 reg_cfg
.token
.clone()
494 let token
= auth
::auth_token(
497 reg_cfg
.token
.as_deref(),
498 reg_cfg
.credential_process
.as_ref(),
502 log
::debug
!("found token {:?}", token
);
509 let handle
= http_handle(config
)?
;
510 Ok((Registry
::new_handle(api_host
, token
, handle
), reg_cfg
, sid
))
513 /// Creates a new HTTP handle with appropriate global configuration for cargo.
514 pub fn http_handle(config
: &Config
) -> CargoResult
<Easy
> {
515 let (mut handle
, timeout
) = http_handle_and_timeout(config
)?
;
516 timeout
.configure(&mut handle
)?
;
520 pub fn http_handle_and_timeout(config
: &Config
) -> CargoResult
<(Easy
, HttpTimeout
)> {
523 "attempting to make an HTTP request, but --frozen was \
527 if !config
.network_allowed() {
528 bail
!("can't make HTTP request in the offline mode")
531 // The timeout option for libcurl by default times out the entire transfer,
532 // but we probably don't want this. Instead we only set timeouts for the
533 // connect phase as well as a "low speed" timeout so if we don't receive
534 // many bytes in a large-ish period of time then we time out.
535 let mut handle
= Easy
::new();
536 let timeout
= configure_http_handle(config
, &mut handle
)?
;
537 Ok((handle
, timeout
))
540 pub fn needs_custom_http_transport(config
: &Config
) -> CargoResult
<bool
> {
541 Ok(http_proxy_exists(config
)?
542 || *config
.http_config()?
!= Default
::default()
543 || env
::var_os("HTTP_TIMEOUT").is_some())
546 /// Configure a libcurl http handle with the defaults options for Cargo
547 pub fn configure_http_handle(config
: &Config
, handle
: &mut Easy
) -> CargoResult
<HttpTimeout
> {
548 let http
= config
.http_config()?
;
549 if let Some(proxy
) = http_proxy(config
)?
{
550 handle
.proxy(&proxy
)?
;
552 if let Some(cainfo
) = &http
.cainfo
{
553 let cainfo
= cainfo
.resolve_path(config
);
554 handle
.cainfo(&cainfo
)?
;
556 if let Some(check
) = http
.check_revoke
{
557 handle
.ssl_options(SslOpt
::new().no_revoke(!check
))?
;
560 if let Some(user_agent
) = &http
.user_agent
{
561 handle
.useragent(user_agent
)?
;
563 handle
.useragent(&version().to_string())?
;
566 fn to_ssl_version(s
: &str) -> CargoResult
<SslVersion
> {
567 let version
= match s
{
568 "default" => SslVersion
::Default
,
569 "tlsv1" => SslVersion
::Tlsv1
,
570 "tlsv1.0" => SslVersion
::Tlsv10
,
571 "tlsv1.1" => SslVersion
::Tlsv11
,
572 "tlsv1.2" => SslVersion
::Tlsv12
,
573 "tlsv1.3" => SslVersion
::Tlsv13
,
575 "Invalid ssl version `{}`,\
576 choose from 'default', 'tlsv1', 'tlsv1.0', 'tlsv1.1', 'tlsv1.2', 'tlsv1.3'.",
582 if let Some(ssl_version
) = &http
.ssl_version
{
584 SslVersionConfig
::Single(s
) => {
585 let version
= to_ssl_version(s
.as_str())?
;
586 handle
.ssl_version(version
)?
;
588 SslVersionConfig
::Range(SslVersionConfigRange { min, max }
) => {
589 let min_version
= min
591 .map_or(Ok(SslVersion
::Default
), |s
| to_ssl_version(s
))?
;
592 let max_version
= max
594 .map_or(Ok(SslVersion
::Default
), |s
| to_ssl_version(s
))?
;
595 handle
.ssl_min_max_version(min_version
, max_version
)?
;
600 if let Some(true) = http
.debug
{
601 handle
.verbose(true)?
;
602 log
::debug
!("{:#?}", curl
::Version
::get());
603 handle
.debug_function(|kind
, data
| {
604 let (prefix
, level
) = match kind
{
605 InfoType
::Text
=> ("*", Level
::Debug
),
606 InfoType
::HeaderIn
=> ("<", Level
::Debug
),
607 InfoType
::HeaderOut
=> (">", Level
::Debug
),
608 InfoType
::DataIn
=> ("{", Level
::Trace
),
609 InfoType
::DataOut
=> ("}", Level
::Trace
),
610 InfoType
::SslDataIn
| InfoType
::SslDataOut
=> return,
613 match str::from_utf8(data
) {
615 for mut line
in s
.lines() {
616 if line
.starts_with("Authorization:") {
617 line
= "Authorization: [REDACTED]";
618 } else if line
[..line
.len().min(10)].eq_ignore_ascii_case("set-cookie") {
619 line
= "set-cookie: [REDACTED]";
621 log
!(level
, "http-debug: {} {}", prefix
, line
);
627 "http-debug: {} ({} bytes of data)",
636 HttpTimeout
::new(config
)
640 pub struct HttpTimeout
{
642 pub low_speed_limit
: u32,
646 pub fn new(config
: &Config
) -> CargoResult
<HttpTimeout
> {
647 let config
= config
.http_config()?
;
648 let low_speed_limit
= config
.low_speed_limit
.unwrap_or(10);
651 .or_else(|| env
::var("HTTP_TIMEOUT").ok().and_then(|s
| s
.parse().ok()))
654 dur
: Duration
::new(seconds
, 0),
659 pub fn configure(&self, handle
: &mut Easy
) -> CargoResult
<()> {
660 // The timeout option for libcurl by default times out the entire
661 // transfer, but we probably don't want this. Instead we only set
662 // timeouts for the connect phase as well as a "low speed" timeout so
663 // if we don't receive many bytes in a large-ish period of time then we
665 handle
.connect_timeout(self.dur
)?
;
666 handle
.low_speed_time(self.dur
)?
;
667 handle
.low_speed_limit(self.low_speed_limit
)?
;
672 /// Finds an explicit HTTP proxy if one is available.
674 /// Favor cargo's `http.proxy`, then git's `http.proxy`. Proxies specified
675 /// via environment variables are picked up by libcurl.
676 fn http_proxy(config
: &Config
) -> CargoResult
<Option
<String
>> {
677 let http
= config
.http_config()?
;
678 if let Some(s
) = &http
.proxy
{
679 return Ok(Some(s
.clone()));
681 if let Ok(cfg
) = git2
::Config
::open_default() {
682 if let Ok(s
) = cfg
.get_string("http.proxy") {
689 /// Determine if an http proxy exists.
691 /// Checks the following for existence, in order:
693 /// * cargo's `http.proxy`
694 /// * git's `http.proxy`
695 /// * `http_proxy` env var
696 /// * `HTTP_PROXY` env var
697 /// * `https_proxy` env var
698 /// * `HTTPS_PROXY` env var
699 fn http_proxy_exists(config
: &Config
) -> CargoResult
<bool
> {
700 if http_proxy(config
)?
.is_some() {
703 Ok(["http_proxy", "HTTP_PROXY", "https_proxy", "HTTPS_PROXY"]
705 .any(|v
| env
::var(v
).is_ok()))
709 pub fn registry_login(
711 token
: Option
<String
>,
713 ) -> CargoResult
<()> {
714 let (registry
, reg_cfg
, _
) = registry(config
, token
.clone(), None
, reg
.clone(), false, false)?
;
716 let token
= match token
{
717 Some(token
) => token
,
721 "please paste the API Token found on {}/me below",
724 let mut line
= String
::new();
725 let input
= io
::stdin();
728 .read_line(&mut line
)
729 .chain_err(|| "failed to read stdin")?
;
730 // Automatically remove `cargo login` from an inputted token to
731 // allow direct pastes from `registry.host()`/me.
732 line
.replace("cargo login", "").trim().to_string()
736 if let Some(old_token
) = ®_cfg
.token
{
737 if old_token
== &token
{
738 config
.shell().status("Login", "already logged in")?
;
746 reg_cfg
.credential_process
.as_ref(),
751 config
.shell().status(
754 "token for `{}` saved",
755 reg
.as_ref().map_or("crates.io", String
::as_str
)
761 pub fn registry_logout(config
: &Config
, reg
: Option
<String
>) -> CargoResult
<()> {
762 let (registry
, reg_cfg
, _
) = registry(config
, None
, None
, reg
.clone(), false, false)?
;
763 let reg_name
= reg
.as_deref().unwrap_or("crates.io");
764 if reg_cfg
.credential_process
.is_none() && reg_cfg
.token
.is_none() {
765 config
.shell().status(
767 format
!("not currently logged in to `{}`", reg_name
),
773 reg_cfg
.credential_process
.as_ref(),
777 config
.shell().status(
780 "token for `{}` has been removed from local storage",
787 pub struct OwnersOptions
{
788 pub krate
: Option
<String
>,
789 pub token
: Option
<String
>,
790 pub index
: Option
<String
>,
791 pub to_add
: Option
<Vec
<String
>>,
792 pub to_remove
: Option
<Vec
<String
>>,
794 pub registry
: Option
<String
>,
797 pub fn modify_owners(config
: &Config
, opts
: &OwnersOptions
) -> CargoResult
<()> {
798 let name
= match opts
.krate
{
799 Some(ref name
) => name
.clone(),
801 let manifest_path
= find_root_manifest_for_wd(config
.cwd())?
;
802 let ws
= Workspace
::new(&manifest_path
, config
)?
;
803 ws
.current()?
.package_id().name().to_string()
807 let (mut registry
, _
, _
) = registry(
811 opts
.registry
.clone(),
816 if let Some(ref v
) = opts
.to_add
{
817 let v
= v
.iter().map(|s
| &s
[..]).collect
::<Vec
<_
>>();
818 let msg
= registry
.add_owners(&name
, &v
).chain_err(|| {
820 "failed to invite owners to crate `{}` on registry at {}",
826 config
.shell().status("Owner", msg
)?
;
829 if let Some(ref v
) = opts
.to_remove
{
830 let v
= v
.iter().map(|s
| &s
[..]).collect
::<Vec
<_
>>();
833 .status("Owner", format
!("removing {:?} from crate {}", v
, name
))?
;
834 registry
.remove_owners(&name
, &v
).chain_err(|| {
836 "failed to remove owners from crate `{}` on registry at {}",
844 let owners
= registry
.list_owners(&name
).chain_err(|| {
846 "failed to list owners of crate `{}` on registry at {}",
851 for owner
in owners
.iter() {
852 drop_print
!(config
, "{}", owner
.login
);
853 match (owner
.name
.as_ref(), owner
.email
.as_ref()) {
854 (Some(name
), Some(email
)) => drop_println
!(config
, " ({} <{}>)", name
, email
),
855 (Some(s
), None
) | (None
, Some(s
)) => drop_println
!(config
, " ({})", s
),
856 (None
, None
) => drop_println
!(config
),
866 krate
: Option
<String
>,
867 version
: Option
<String
>,
868 token
: Option
<String
>,
869 index
: Option
<String
>,
872 ) -> CargoResult
<()> {
873 let name
= match krate
{
876 let manifest_path
= find_root_manifest_for_wd(config
.cwd())?
;
877 let ws
= Workspace
::new(&manifest_path
, config
)?
;
878 ws
.current()?
.package_id().name().to_string()
881 let version
= match version
{
883 None
=> bail
!("a version must be specified to yank"),
886 let (mut registry
, _
, _
) = registry(config
, token
, index
, reg
, true, true)?
;
891 .status("Unyank", format
!("{}:{}", name
, version
))?
;
892 registry
.unyank(&name
, &version
).chain_err(|| {
894 "failed to undo a yank from the registry at {}",
901 .status("Yank", format
!("{}:{}", name
, version
))?
;
903 .yank(&name
, &version
)
904 .chain_err(|| format
!("failed to yank from the registry at {}", registry
.host()))?
;
910 /// Gets the SourceId for an index or registry setting.
912 /// The `index` and `reg` values are from the command-line or config settings.
913 /// If both are None, returns the source for crates.io.
916 index
: Option
<&String
>,
917 reg
: Option
<&String
>,
918 ) -> CargoResult
<SourceId
> {
920 (Some(r
), _
) => SourceId
::alt_registry(config
, r
),
921 (_
, Some(i
)) => SourceId
::for_registry(&i
.into_url()?
),
923 let map
= SourceConfigMap
::new(config
)?
;
924 let src
= map
.load(SourceId
::crates_io(config
)?
, &HashSet
::new())?
;
925 Ok(src
.replaced_source_id())
933 index
: Option
<String
>,
936 ) -> CargoResult
<()> {
937 fn truncate_with_ellipsis(s
: &str, max_width
: usize) -> String
{
938 // We should truncate at grapheme-boundary and compute character-widths,
939 // yet the dependencies on unicode-segmentation and unicode-width are
941 let mut chars
= s
.chars();
942 let mut prefix
= (&mut chars
).take(max_width
- 1).collect
::<String
>();
943 if chars
.next().is_some() {
949 let (mut registry
, _
, source_id
) = registry(config
, None
, index
, reg
, false, false)?
;
950 let (crates
, total_crates
) = registry
.search(query
, limit
).chain_err(|| {
952 "failed to retrieve search results from the registry at {}",
959 .map(|krate
| format
!("{} = \"{}\"", krate
.name
, krate
.max_version
))
960 .collect
::<Vec
<String
>>();
962 let description_margin
= names
.iter().map(|s
| s
.len() + 4).max().unwrap_or_default();
964 let description_length
= cmp
::max(80, 128 - description_margin
);
966 let descriptions
= crates
.iter().map(|krate
| {
970 .map(|desc
| truncate_with_ellipsis(&desc
.replace("\n", " "), description_length
))
973 for (name
, description
) in names
.into_iter().zip(descriptions
) {
974 let line
= match description
{
976 let space
= repeat(' '
)
977 .take(description_margin
- name
.len())
978 .collect
::<String
>();
979 name
+ &space
+ "# " + &desc
983 drop_println
!(config
, "{}", line
);
986 let search_max_limit
= 100;
987 if total_crates
> limit
&& limit
< search_max_limit
{
990 "... and {} crates more (use --limit N to see more)",
993 } else if total_crates
> limit
&& limit
>= search_max_limit
{
994 let extra
= if source_id
.is_default_registry() {
996 " (go to https://crates.io/search?q={} to see more)",
997 percent_encode(query
.as_bytes(), NON_ALPHANUMERIC
)
1004 "... and {} crates more{}",
1005 total_crates
- limit
,