]> git.proxmox.com Git - cargo.git/blob - src/cargo/ops/registry.rs
Stabilize namespaced and weak dependency features.
[cargo.git] / src / cargo / ops / registry.rs
1 use std::collections::{BTreeMap, HashSet};
2 use std::fs::File;
3 use std::io::{self, BufRead};
4 use std::iter::repeat;
5 use std::path::PathBuf;
6 use std::str;
7 use std::time::Duration;
8 use std::{cmp, env};
9
10 use anyhow::{bail, format_err, Context as _};
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};
16
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};
22 use crate::ops;
23 use crate::sources::{RegistrySource, SourceConfigMap, CRATES_IO_DOMAIN, CRATES_IO_REGISTRY};
24 use crate::util::config::{self, Config, SslVersionConfig, SslVersionConfigRange};
25 use crate::util::errors::CargoResult;
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};
30
31 mod auth;
32
33 /// Registry settings loaded from config files.
34 ///
35 /// This is loaded based on the `--registry` flag and the config settings.
36 #[derive(Debug)]
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>)>,
44 }
45
46 pub struct PublishOpts<'cfg> {
47 pub config: &'cfg Config,
48 pub token: Option<String>,
49 pub index: Option<String>,
50 pub verify: bool,
51 pub allow_dirty: bool,
52 pub jobs: Option<u32>,
53 pub to_publish: ops::Packages,
54 pub targets: Vec<String>,
55 pub dry_run: bool,
56 pub registry: Option<String>,
57 pub cli_features: CliFeatures,
58 }
59
60 pub fn publish(ws: &Workspace<'_>, opts: &PublishOpts<'_>) -> CargoResult<()> {
61 let specs = opts.to_publish.to_package_id_specs(ws)?;
62 let mut pkgs = ws.members_with_features(&specs, &opts.cli_features)?;
63
64 let (pkg, cli_features) = pkgs.pop().unwrap();
65
66 let mut publish_registry = opts.registry.clone();
67 if let Some(ref allowed_registries) = *pkg.publish() {
68 if publish_registry.is_none() && allowed_registries.len() == 1 {
69 // If there is only one allowed registry, push to that one directly,
70 // even though there is no registry specified in the command.
71 let default_registry = &allowed_registries[0];
72 if default_registry != CRATES_IO_REGISTRY {
73 // Don't change the registry for crates.io and don't warn the user.
74 // crates.io will be defaulted even without this.
75 opts.config.shell().note(&format!(
76 "Found `{}` as only allowed registry. Publishing to it automatically.",
77 default_registry
78 ))?;
79 publish_registry = Some(default_registry.clone());
80 }
81 }
82
83 let reg_name = publish_registry
84 .clone()
85 .unwrap_or_else(|| CRATES_IO_REGISTRY.to_string());
86 if !allowed_registries.contains(&reg_name) {
87 bail!(
88 "`{}` cannot be published.\n\
89 The registry `{}` is not listed in the `publish` value in Cargo.toml.",
90 pkg.name(),
91 reg_name
92 );
93 }
94 }
95
96 let (mut registry, _reg_cfg, reg_id) = registry(
97 opts.config,
98 opts.token.clone(),
99 opts.index.clone(),
100 publish_registry,
101 true,
102 !opts.dry_run,
103 )?;
104 verify_dependencies(pkg, &registry, reg_id)?;
105
106 // Prepare a tarball, with a non-suppressible warning if metadata
107 // is missing since this is being put online.
108 let tarball = ops::package_one(
109 ws,
110 pkg,
111 &ops::PackageOpts {
112 config: opts.config,
113 verify: opts.verify,
114 list: false,
115 check_metadata: true,
116 allow_dirty: opts.allow_dirty,
117 to_package: ops::Packages::Default,
118 targets: opts.targets.clone(),
119 jobs: opts.jobs,
120 cli_features: cli_features,
121 },
122 )?
123 .unwrap();
124
125 opts.config
126 .shell()
127 .status("Uploading", pkg.package_id().to_string())?;
128 transmit(
129 opts.config,
130 pkg,
131 tarball.file(),
132 &mut registry,
133 reg_id,
134 opts.dry_run,
135 )?;
136
137 Ok(())
138 }
139
140 fn verify_dependencies(
141 pkg: &Package,
142 registry: &Registry,
143 registry_src: SourceId,
144 ) -> CargoResult<()> {
145 for dep in pkg.dependencies().iter() {
146 if super::check_dep_has_version(dep, true)? {
147 continue;
148 }
149 // TomlManifest::prepare_for_publish will rewrite the dependency
150 // to be just the `version` field.
151 if dep.source_id() != registry_src {
152 if !dep.source_id().is_registry() {
153 // Consider making SourceId::kind a public type that we can
154 // exhaustively match on. Using match can help ensure that
155 // every kind is properly handled.
156 panic!("unexpected source kind for dependency {:?}", dep);
157 }
158 // Block requests to send to crates.io with alt-registry deps.
159 // This extra hostname check is mostly to assist with testing,
160 // but also prevents someone using `--index` to specify
161 // something that points to crates.io.
162 if registry_src.is_default_registry() || registry.host_is_crates_io() {
163 bail!("crates cannot be published to crates.io with dependencies sourced from other\n\
164 registries. `{}` needs to be published to crates.io before publishing this crate.\n\
165 (crate `{}` is pulled from {})",
166 dep.package_name(),
167 dep.package_name(),
168 dep.source_id());
169 }
170 }
171 }
172 Ok(())
173 }
174
175 fn transmit(
176 config: &Config,
177 pkg: &Package,
178 tarball: &File,
179 registry: &mut Registry,
180 registry_id: SourceId,
181 dry_run: bool,
182 ) -> CargoResult<()> {
183 let deps = pkg
184 .dependencies()
185 .iter()
186 .filter(|dep| {
187 // Skip dev-dependency without version.
188 dep.is_transitive() || dep.specified_req()
189 })
190 .map(|dep| {
191 // If the dependency is from a different registry, then include the
192 // registry in the dependency.
193 let dep_registry_id = match dep.registry_id() {
194 Some(id) => id,
195 None => SourceId::crates_io(config)?,
196 };
197 // In the index and Web API, None means "from the same registry"
198 // whereas in Cargo.toml, it means "from crates.io".
199 let dep_registry = if dep_registry_id != registry_id {
200 Some(dep_registry_id.url().to_string())
201 } else {
202 None
203 };
204
205 Ok(NewCrateDependency {
206 optional: dep.is_optional(),
207 default_features: dep.uses_default_features(),
208 name: dep.package_name().to_string(),
209 features: dep.features().iter().map(|s| s.to_string()).collect(),
210 version_req: dep.version_req().to_string(),
211 target: dep.platform().map(|s| s.to_string()),
212 kind: match dep.kind() {
213 DepKind::Normal => "normal",
214 DepKind::Build => "build",
215 DepKind::Development => "dev",
216 }
217 .to_string(),
218 registry: dep_registry,
219 explicit_name_in_toml: dep.explicit_name_in_toml().map(|s| s.to_string()),
220 })
221 })
222 .collect::<CargoResult<Vec<NewCrateDependency>>>()?;
223 let manifest = pkg.manifest();
224 let ManifestMetadata {
225 ref authors,
226 ref description,
227 ref homepage,
228 ref documentation,
229 ref keywords,
230 ref readme,
231 ref repository,
232 ref license,
233 ref license_file,
234 ref categories,
235 ref badges,
236 ref links,
237 } = *manifest.metadata();
238 let readme_content = readme
239 .as_ref()
240 .map(|readme| {
241 paths::read(&pkg.root().join(readme))
242 .with_context(|| format!("failed to read `readme` file for package `{}`", pkg))
243 })
244 .transpose()?;
245 if let Some(ref file) = *license_file {
246 if !pkg.root().join(file).exists() {
247 bail!("the license file `{}` does not exist", file)
248 }
249 }
250
251 // Do not upload if performing a dry run
252 if dry_run {
253 config.shell().warn("aborting upload due to dry run")?;
254 return Ok(());
255 }
256
257 let string_features = match manifest.original().features() {
258 Some(features) => features
259 .iter()
260 .map(|(feat, values)| {
261 (
262 feat.to_string(),
263 values.iter().map(|fv| fv.to_string()).collect(),
264 )
265 })
266 .collect::<BTreeMap<String, Vec<String>>>(),
267 None => BTreeMap::new(),
268 };
269
270 let warnings = registry
271 .publish(
272 &NewCrate {
273 name: pkg.name().to_string(),
274 vers: pkg.version().to_string(),
275 deps,
276 features: string_features,
277 authors: authors.clone(),
278 description: description.clone(),
279 homepage: homepage.clone(),
280 documentation: documentation.clone(),
281 keywords: keywords.clone(),
282 categories: categories.clone(),
283 readme: readme_content,
284 readme_file: readme.clone(),
285 repository: repository.clone(),
286 license: license.clone(),
287 license_file: license_file.clone(),
288 badges: badges.clone(),
289 links: links.clone(),
290 },
291 tarball,
292 )
293 .with_context(|| format!("failed to publish to registry at {}", registry.host()))?;
294
295 if !warnings.invalid_categories.is_empty() {
296 let msg = format!(
297 "the following are not valid category slugs and were \
298 ignored: {}. Please see https://crates.io/category_slugs \
299 for the list of all category slugs. \
300 ",
301 warnings.invalid_categories.join(", ")
302 );
303 config.shell().warn(&msg)?;
304 }
305
306 if !warnings.invalid_badges.is_empty() {
307 let msg = format!(
308 "the following are not valid badges and were ignored: {}. \
309 Either the badge type specified is unknown or a required \
310 attribute is missing. Please see \
311 https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata \
312 for valid badge types and their required attributes.",
313 warnings.invalid_badges.join(", ")
314 );
315 config.shell().warn(&msg)?;
316 }
317
318 if !warnings.other.is_empty() {
319 for msg in warnings.other {
320 config.shell().warn(&msg)?;
321 }
322 }
323
324 Ok(())
325 }
326
327 /// Returns the index and token from the config file for the given registry.
328 ///
329 /// `registry` is typically the registry specified on the command-line. If
330 /// `None`, `index` is set to `None` to indicate it should use crates.io.
331 pub fn registry_configuration(
332 config: &Config,
333 registry: Option<&str>,
334 ) -> CargoResult<RegistryConfig> {
335 let err_both = |token_key: &str, proc_key: &str| {
336 Err(format_err!(
337 "both `{TOKEN_KEY}` and `{PROC_KEY}` \
338 were specified in the config\n\
339 Only one of these values may be set, remove one or the other to proceed.",
340 TOKEN_KEY = token_key,
341 PROC_KEY = proc_key,
342 ))
343 };
344 // `registry.default` is handled in command-line parsing.
345 let (index, token, process) = match registry {
346 Some(registry) => {
347 validate_package_name(registry, "registry name", "")?;
348 let index = Some(config.get_registry_index(registry)?.to_string());
349 let token_key = format!("registries.{}.token", registry);
350 let token = config.get_string(&token_key)?.map(|p| p.val);
351 let process = if config.cli_unstable().credential_process {
352 let mut proc_key = format!("registries.{}.credential-process", registry);
353 let mut process = config.get::<Option<config::PathAndArgs>>(&proc_key)?;
354 if process.is_none() && token.is_none() {
355 // This explicitly ignores the global credential-process if
356 // the token is set, as that is "more specific".
357 proc_key = String::from("registry.credential-process");
358 process = config.get::<Option<config::PathAndArgs>>(&proc_key)?;
359 } else if process.is_some() && token.is_some() {
360 return err_both(&token_key, &proc_key);
361 }
362 process
363 } else {
364 None
365 };
366 (index, token, process)
367 }
368 None => {
369 // Use crates.io default.
370 config.check_registry_index_not_set()?;
371 let token = config.get_string("registry.token")?.map(|p| p.val);
372 let process = if config.cli_unstable().credential_process {
373 let process =
374 config.get::<Option<config::PathAndArgs>>("registry.credential-process")?;
375 if token.is_some() && process.is_some() {
376 return err_both("registry.token", "registry.credential-process");
377 }
378 process
379 } else {
380 None
381 };
382 (None, token, process)
383 }
384 };
385
386 let credential_process =
387 process.map(|process| (process.path.resolve_program(config), process.args));
388
389 Ok(RegistryConfig {
390 index,
391 token,
392 credential_process,
393 })
394 }
395
396 /// Returns the `Registry` and `Source` based on command-line and config settings.
397 ///
398 /// * `token`: The token from the command-line. If not set, uses the token
399 /// from the config.
400 /// * `index`: The index URL from the command-line. This is ignored if
401 /// `registry` is set.
402 /// * `registry`: The registry name from the command-line. If neither
403 /// `registry`, or `index` are set, then uses `crates-io`, honoring
404 /// `[source]` replacement if defined.
405 /// * `force_update`: If `true`, forces the index to be updated.
406 /// * `validate_token`: If `true`, the token must be set.
407 fn registry(
408 config: &Config,
409 token: Option<String>,
410 index: Option<String>,
411 registry: Option<String>,
412 force_update: bool,
413 validate_token: bool,
414 ) -> CargoResult<(Registry, RegistryConfig, SourceId)> {
415 if index.is_some() && registry.is_some() {
416 // Otherwise we would silently ignore one or the other.
417 bail!("both `--index` and `--registry` should not be set at the same time");
418 }
419 // Parse all configuration options
420 let reg_cfg = registry_configuration(config, registry.as_deref())?;
421 let opt_index = reg_cfg.index.as_ref().or_else(|| index.as_ref());
422 let sid = get_source_id(config, opt_index, registry.as_ref())?;
423 if !sid.is_remote_registry() {
424 bail!(
425 "{} does not support API commands.\n\
426 Check for a source-replacement in .cargo/config.",
427 sid
428 );
429 }
430 let api_host = {
431 let _lock = config.acquire_package_cache_lock()?;
432 let mut src = RegistrySource::remote(sid, &HashSet::new(), config);
433 // Only update the index if the config is not available or `force` is set.
434 let cfg = src.config();
435 let mut updated_cfg = || {
436 src.update()
437 .with_context(|| format!("failed to update {}", sid))?;
438 src.config()
439 };
440
441 let cfg = if force_update {
442 updated_cfg()?
443 } else {
444 cfg.or_else(|_| updated_cfg())?
445 };
446
447 cfg.and_then(|cfg| cfg.api)
448 .ok_or_else(|| format_err!("{} does not support API commands", sid))?
449 };
450 let token = if validate_token {
451 if index.is_some() {
452 if token.is_none() {
453 bail!("command-line argument --index requires --token to be specified");
454 }
455 token
456 } else {
457 // Check `is_default_registry` so that the crates.io index can
458 // change config.json's "api" value, and this won't affect most
459 // people. It will affect those using source replacement, but
460 // hopefully that's a relatively small set of users.
461 if token.is_none()
462 && reg_cfg.token.is_some()
463 && registry.is_none()
464 && !sid.is_default_registry()
465 && !crates_io::is_url_crates_io(&api_host)
466 {
467 config.shell().warn(
468 "using `registry.token` config value with source \
469 replacement is deprecated\n\
470 This may become a hard error in the future; \
471 see <https://github.com/rust-lang/cargo/issues/xxx>.\n\
472 Use the --token command-line flag to remove this warning.",
473 )?;
474 reg_cfg.token.clone()
475 } else {
476 let token = auth::auth_token(
477 config,
478 token.as_deref(),
479 reg_cfg.token.as_deref(),
480 reg_cfg.credential_process.as_ref(),
481 registry.as_deref(),
482 &api_host,
483 )?;
484 Some(token)
485 }
486 }
487 } else {
488 None
489 };
490 let handle = http_handle(config)?;
491 Ok((Registry::new_handle(api_host, token, handle), reg_cfg, sid))
492 }
493
494 /// Creates a new HTTP handle with appropriate global configuration for cargo.
495 pub fn http_handle(config: &Config) -> CargoResult<Easy> {
496 let (mut handle, timeout) = http_handle_and_timeout(config)?;
497 timeout.configure(&mut handle)?;
498 Ok(handle)
499 }
500
501 pub fn http_handle_and_timeout(config: &Config) -> CargoResult<(Easy, HttpTimeout)> {
502 if config.frozen() {
503 bail!(
504 "attempting to make an HTTP request, but --frozen was \
505 specified"
506 )
507 }
508 if !config.network_allowed() {
509 bail!("can't make HTTP request in the offline mode")
510 }
511
512 // The timeout option for libcurl by default times out the entire transfer,
513 // but we probably don't want this. Instead we only set timeouts for the
514 // connect phase as well as a "low speed" timeout so if we don't receive
515 // many bytes in a large-ish period of time then we time out.
516 let mut handle = Easy::new();
517 let timeout = configure_http_handle(config, &mut handle)?;
518 Ok((handle, timeout))
519 }
520
521 pub fn needs_custom_http_transport(config: &Config) -> CargoResult<bool> {
522 Ok(http_proxy_exists(config)?
523 || *config.http_config()? != Default::default()
524 || env::var_os("HTTP_TIMEOUT").is_some())
525 }
526
527 /// Configure a libcurl http handle with the defaults options for Cargo
528 pub fn configure_http_handle(config: &Config, handle: &mut Easy) -> CargoResult<HttpTimeout> {
529 let http = config.http_config()?;
530 if let Some(proxy) = http_proxy(config)? {
531 handle.proxy(&proxy)?;
532 }
533 if let Some(cainfo) = &http.cainfo {
534 let cainfo = cainfo.resolve_path(config);
535 handle.cainfo(&cainfo)?;
536 }
537 if let Some(check) = http.check_revoke {
538 handle.ssl_options(SslOpt::new().no_revoke(!check))?;
539 }
540
541 if let Some(user_agent) = &http.user_agent {
542 handle.useragent(user_agent)?;
543 } else {
544 handle.useragent(&format!("cargo {}", version()))?;
545 }
546
547 fn to_ssl_version(s: &str) -> CargoResult<SslVersion> {
548 let version = match s {
549 "default" => SslVersion::Default,
550 "tlsv1" => SslVersion::Tlsv1,
551 "tlsv1.0" => SslVersion::Tlsv10,
552 "tlsv1.1" => SslVersion::Tlsv11,
553 "tlsv1.2" => SslVersion::Tlsv12,
554 "tlsv1.3" => SslVersion::Tlsv13,
555 _ => bail!(
556 "Invalid ssl version `{}`,\
557 choose from 'default', 'tlsv1', 'tlsv1.0', 'tlsv1.1', 'tlsv1.2', 'tlsv1.3'.",
558 s
559 ),
560 };
561 Ok(version)
562 }
563 if let Some(ssl_version) = &http.ssl_version {
564 match ssl_version {
565 SslVersionConfig::Single(s) => {
566 let version = to_ssl_version(s.as_str())?;
567 handle.ssl_version(version)?;
568 }
569 SslVersionConfig::Range(SslVersionConfigRange { min, max }) => {
570 let min_version = min
571 .as_ref()
572 .map_or(Ok(SslVersion::Default), |s| to_ssl_version(s))?;
573 let max_version = max
574 .as_ref()
575 .map_or(Ok(SslVersion::Default), |s| to_ssl_version(s))?;
576 handle.ssl_min_max_version(min_version, max_version)?;
577 }
578 }
579 }
580
581 if let Some(true) = http.debug {
582 handle.verbose(true)?;
583 log::debug!("{:#?}", curl::Version::get());
584 handle.debug_function(|kind, data| {
585 let (prefix, level) = match kind {
586 InfoType::Text => ("*", Level::Debug),
587 InfoType::HeaderIn => ("<", Level::Debug),
588 InfoType::HeaderOut => (">", Level::Debug),
589 InfoType::DataIn => ("{", Level::Trace),
590 InfoType::DataOut => ("}", Level::Trace),
591 InfoType::SslDataIn | InfoType::SslDataOut => return,
592 _ => return,
593 };
594 match str::from_utf8(data) {
595 Ok(s) => {
596 for mut line in s.lines() {
597 if line.starts_with("Authorization:") {
598 line = "Authorization: [REDACTED]";
599 } else if line[..line.len().min(10)].eq_ignore_ascii_case("set-cookie") {
600 line = "set-cookie: [REDACTED]";
601 }
602 log!(level, "http-debug: {} {}", prefix, line);
603 }
604 }
605 Err(_) => {
606 log!(
607 level,
608 "http-debug: {} ({} bytes of data)",
609 prefix,
610 data.len()
611 );
612 }
613 }
614 })?;
615 }
616
617 HttpTimeout::new(config)
618 }
619
620 #[must_use]
621 pub struct HttpTimeout {
622 pub dur: Duration,
623 pub low_speed_limit: u32,
624 }
625
626 impl HttpTimeout {
627 pub fn new(config: &Config) -> CargoResult<HttpTimeout> {
628 let config = config.http_config()?;
629 let low_speed_limit = config.low_speed_limit.unwrap_or(10);
630 let seconds = config
631 .timeout
632 .or_else(|| env::var("HTTP_TIMEOUT").ok().and_then(|s| s.parse().ok()))
633 .unwrap_or(30);
634 Ok(HttpTimeout {
635 dur: Duration::new(seconds, 0),
636 low_speed_limit,
637 })
638 }
639
640 pub fn configure(&self, handle: &mut Easy) -> CargoResult<()> {
641 // The timeout option for libcurl by default times out the entire
642 // transfer, but we probably don't want this. Instead we only set
643 // timeouts for the connect phase as well as a "low speed" timeout so
644 // if we don't receive many bytes in a large-ish period of time then we
645 // time out.
646 handle.connect_timeout(self.dur)?;
647 handle.low_speed_time(self.dur)?;
648 handle.low_speed_limit(self.low_speed_limit)?;
649 Ok(())
650 }
651 }
652
653 /// Finds an explicit HTTP proxy if one is available.
654 ///
655 /// Favor cargo's `http.proxy`, then git's `http.proxy`. Proxies specified
656 /// via environment variables are picked up by libcurl.
657 fn http_proxy(config: &Config) -> CargoResult<Option<String>> {
658 let http = config.http_config()?;
659 if let Some(s) = &http.proxy {
660 return Ok(Some(s.clone()));
661 }
662 if let Ok(cfg) = git2::Config::open_default() {
663 if let Ok(s) = cfg.get_string("http.proxy") {
664 return Ok(Some(s));
665 }
666 }
667 Ok(None)
668 }
669
670 /// Determine if an http proxy exists.
671 ///
672 /// Checks the following for existence, in order:
673 ///
674 /// * cargo's `http.proxy`
675 /// * git's `http.proxy`
676 /// * `http_proxy` env var
677 /// * `HTTP_PROXY` env var
678 /// * `https_proxy` env var
679 /// * `HTTPS_PROXY` env var
680 fn http_proxy_exists(config: &Config) -> CargoResult<bool> {
681 if http_proxy(config)?.is_some() {
682 Ok(true)
683 } else {
684 Ok(["http_proxy", "HTTP_PROXY", "https_proxy", "HTTPS_PROXY"]
685 .iter()
686 .any(|v| env::var(v).is_ok()))
687 }
688 }
689
690 pub fn registry_login(
691 config: &Config,
692 token: Option<String>,
693 reg: Option<String>,
694 ) -> CargoResult<()> {
695 let (registry, reg_cfg, _) = registry(config, token.clone(), None, reg.clone(), false, false)?;
696
697 let token = match token {
698 Some(token) => token,
699 None => {
700 drop_println!(
701 config,
702 "please paste the API Token found on {}/me below",
703 registry.host()
704 );
705 let mut line = String::new();
706 let input = io::stdin();
707 input
708 .lock()
709 .read_line(&mut line)
710 .with_context(|| "failed to read stdin")?;
711 // Automatically remove `cargo login` from an inputted token to
712 // allow direct pastes from `registry.host()`/me.
713 line.replace("cargo login", "").trim().to_string()
714 }
715 };
716
717 if let Some(old_token) = &reg_cfg.token {
718 if old_token == &token {
719 config.shell().status("Login", "already logged in")?;
720 return Ok(());
721 }
722 }
723
724 auth::login(
725 config,
726 token,
727 reg_cfg.credential_process.as_ref(),
728 reg.as_deref(),
729 registry.host(),
730 )?;
731
732 config.shell().status(
733 "Login",
734 format!(
735 "token for `{}` saved",
736 reg.as_ref().map_or(CRATES_IO_DOMAIN, String::as_str)
737 ),
738 )?;
739 Ok(())
740 }
741
742 pub fn registry_logout(config: &Config, reg: Option<String>) -> CargoResult<()> {
743 let (registry, reg_cfg, _) = registry(config, None, None, reg.clone(), false, false)?;
744 let reg_name = reg.as_deref().unwrap_or(CRATES_IO_DOMAIN);
745 if reg_cfg.credential_process.is_none() && reg_cfg.token.is_none() {
746 config.shell().status(
747 "Logout",
748 format!("not currently logged in to `{}`", reg_name),
749 )?;
750 return Ok(());
751 }
752 auth::logout(
753 config,
754 reg_cfg.credential_process.as_ref(),
755 reg.as_deref(),
756 registry.host(),
757 )?;
758 config.shell().status(
759 "Logout",
760 format!(
761 "token for `{}` has been removed from local storage",
762 reg_name
763 ),
764 )?;
765 Ok(())
766 }
767
768 pub struct OwnersOptions {
769 pub krate: Option<String>,
770 pub token: Option<String>,
771 pub index: Option<String>,
772 pub to_add: Option<Vec<String>>,
773 pub to_remove: Option<Vec<String>>,
774 pub list: bool,
775 pub registry: Option<String>,
776 }
777
778 pub fn modify_owners(config: &Config, opts: &OwnersOptions) -> CargoResult<()> {
779 let name = match opts.krate {
780 Some(ref name) => name.clone(),
781 None => {
782 let manifest_path = find_root_manifest_for_wd(config.cwd())?;
783 let ws = Workspace::new(&manifest_path, config)?;
784 ws.current()?.package_id().name().to_string()
785 }
786 };
787
788 let (mut registry, _, _) = registry(
789 config,
790 opts.token.clone(),
791 opts.index.clone(),
792 opts.registry.clone(),
793 true,
794 true,
795 )?;
796
797 if let Some(ref v) = opts.to_add {
798 let v = v.iter().map(|s| &s[..]).collect::<Vec<_>>();
799 let msg = registry.add_owners(&name, &v).with_context(|| {
800 format!(
801 "failed to invite owners to crate `{}` on registry at {}",
802 name,
803 registry.host()
804 )
805 })?;
806
807 config.shell().status("Owner", msg)?;
808 }
809
810 if let Some(ref v) = opts.to_remove {
811 let v = v.iter().map(|s| &s[..]).collect::<Vec<_>>();
812 config
813 .shell()
814 .status("Owner", format!("removing {:?} from crate {}", v, name))?;
815 registry.remove_owners(&name, &v).with_context(|| {
816 format!(
817 "failed to remove owners from crate `{}` on registry at {}",
818 name,
819 registry.host()
820 )
821 })?;
822 }
823
824 if opts.list {
825 let owners = registry.list_owners(&name).with_context(|| {
826 format!(
827 "failed to list owners of crate `{}` on registry at {}",
828 name,
829 registry.host()
830 )
831 })?;
832 for owner in owners.iter() {
833 drop_print!(config, "{}", owner.login);
834 match (owner.name.as_ref(), owner.email.as_ref()) {
835 (Some(name), Some(email)) => drop_println!(config, " ({} <{}>)", name, email),
836 (Some(s), None) | (None, Some(s)) => drop_println!(config, " ({})", s),
837 (None, None) => drop_println!(config),
838 }
839 }
840 }
841
842 Ok(())
843 }
844
845 pub fn yank(
846 config: &Config,
847 krate: Option<String>,
848 version: Option<String>,
849 token: Option<String>,
850 index: Option<String>,
851 undo: bool,
852 reg: Option<String>,
853 ) -> CargoResult<()> {
854 let name = match krate {
855 Some(name) => name,
856 None => {
857 let manifest_path = find_root_manifest_for_wd(config.cwd())?;
858 let ws = Workspace::new(&manifest_path, config)?;
859 ws.current()?.package_id().name().to_string()
860 }
861 };
862 let version = match version {
863 Some(v) => v,
864 None => bail!("a version must be specified to yank"),
865 };
866
867 let (mut registry, _, _) = registry(config, token, index, reg, true, true)?;
868
869 if undo {
870 config
871 .shell()
872 .status("Unyank", format!("{}:{}", name, version))?;
873 registry.unyank(&name, &version).with_context(|| {
874 format!(
875 "failed to undo a yank from the registry at {}",
876 registry.host()
877 )
878 })?;
879 } else {
880 config
881 .shell()
882 .status("Yank", format!("{}:{}", name, version))?;
883 registry
884 .yank(&name, &version)
885 .with_context(|| format!("failed to yank from the registry at {}", registry.host()))?;
886 }
887
888 Ok(())
889 }
890
891 /// Gets the SourceId for an index or registry setting.
892 ///
893 /// The `index` and `reg` values are from the command-line or config settings.
894 /// If both are None, returns the source for crates.io.
895 fn get_source_id(
896 config: &Config,
897 index: Option<&String>,
898 reg: Option<&String>,
899 ) -> CargoResult<SourceId> {
900 match (reg, index) {
901 (Some(r), _) => SourceId::alt_registry(config, r),
902 (_, Some(i)) => SourceId::for_registry(&i.into_url()?),
903 _ => {
904 let map = SourceConfigMap::new(config)?;
905 let src = map.load(SourceId::crates_io(config)?, &HashSet::new())?;
906 Ok(src.replaced_source_id())
907 }
908 }
909 }
910
911 pub fn search(
912 query: &str,
913 config: &Config,
914 index: Option<String>,
915 limit: u32,
916 reg: Option<String>,
917 ) -> CargoResult<()> {
918 fn truncate_with_ellipsis(s: &str, max_width: usize) -> String {
919 // We should truncate at grapheme-boundary and compute character-widths,
920 // yet the dependencies on unicode-segmentation and unicode-width are
921 // not worth it.
922 let mut chars = s.chars();
923 let mut prefix = (&mut chars).take(max_width - 1).collect::<String>();
924 if chars.next().is_some() {
925 prefix.push('…');
926 }
927 prefix
928 }
929
930 let (mut registry, _, source_id) = registry(config, None, index, reg, false, false)?;
931 let (crates, total_crates) = registry.search(query, limit).with_context(|| {
932 format!(
933 "failed to retrieve search results from the registry at {}",
934 registry.host()
935 )
936 })?;
937
938 let names = crates
939 .iter()
940 .map(|krate| format!("{} = \"{}\"", krate.name, krate.max_version))
941 .collect::<Vec<String>>();
942
943 let description_margin = names.iter().map(|s| s.len() + 4).max().unwrap_or_default();
944
945 let description_length = cmp::max(80, 128 - description_margin);
946
947 let descriptions = crates.iter().map(|krate| {
948 krate
949 .description
950 .as_ref()
951 .map(|desc| truncate_with_ellipsis(&desc.replace("\n", " "), description_length))
952 });
953
954 for (name, description) in names.into_iter().zip(descriptions) {
955 let line = match description {
956 Some(desc) => {
957 let space = repeat(' ')
958 .take(description_margin - name.len())
959 .collect::<String>();
960 name + &space + "# " + &desc
961 }
962 None => name,
963 };
964 drop_println!(config, "{}", line);
965 }
966
967 let search_max_limit = 100;
968 if total_crates > limit && limit < search_max_limit {
969 drop_println!(
970 config,
971 "... and {} crates more (use --limit N to see more)",
972 total_crates - limit
973 );
974 } else if total_crates > limit && limit >= search_max_limit {
975 let extra = if source_id.is_default_registry() {
976 format!(
977 " (go to https://crates.io/search?q={} to see more)",
978 percent_encode(query.as_bytes(), NON_ALPHANUMERIC)
979 )
980 } else {
981 String::new()
982 };
983 drop_println!(
984 config,
985 "... and {} crates more{}",
986 total_crates - limit,
987 extra
988 );
989 }
990
991 Ok(())
992 }