]> git.proxmox.com Git - cargo.git/blob - src/cargo/ops/registry.rs
Remove the update method on registry functions. Instead of explicitly
[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::task::Poll;
8 use std::time::Duration;
9 use std::{cmp, env};
10
11 use anyhow::{bail, format_err, Context as _};
12 use cargo_util::paths;
13 use crates_io::{self, NewCrate, NewCrateDependency, Registry};
14 use curl::easy::{Easy, InfoType, SslOpt, SslVersion};
15 use log::{log, Level};
16 use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
17 use termcolor::Color::Green;
18 use termcolor::ColorSpec;
19
20 use crate::core::dependency::DepKind;
21 use crate::core::manifest::ManifestMetadata;
22 use crate::core::resolver::CliFeatures;
23 use crate::core::source::Source;
24 use crate::core::{Package, SourceId, Workspace};
25 use crate::ops;
26 use crate::sources::{RegistrySource, SourceConfigMap, CRATES_IO_DOMAIN, CRATES_IO_REGISTRY};
27 use crate::util::config::{self, Config, SslVersionConfig, SslVersionConfigRange};
28 use crate::util::errors::CargoResult;
29 use crate::util::important_paths::find_root_manifest_for_wd;
30 use crate::util::IntoUrl;
31 use crate::{drop_print, drop_println, version};
32
33 mod auth;
34
35 /// Registry settings loaded from config files.
36 ///
37 /// This is loaded based on the `--registry` flag and the config settings.
38 #[derive(Debug)]
39 pub struct RegistryConfig {
40 /// The index URL. If `None`, use crates.io.
41 pub index: Option<String>,
42 /// The authentication token.
43 pub token: Option<String>,
44 /// Process used for fetching a token.
45 pub credential_process: Option<(PathBuf, Vec<String>)>,
46 }
47
48 pub struct PublishOpts<'cfg> {
49 pub config: &'cfg Config,
50 pub token: Option<String>,
51 pub index: Option<String>,
52 pub verify: bool,
53 pub allow_dirty: bool,
54 pub jobs: Option<u32>,
55 pub to_publish: ops::Packages,
56 pub targets: Vec<String>,
57 pub dry_run: bool,
58 pub registry: Option<String>,
59 pub cli_features: CliFeatures,
60 }
61
62 pub fn publish(ws: &Workspace<'_>, opts: &PublishOpts<'_>) -> CargoResult<()> {
63 let specs = opts.to_publish.to_package_id_specs(ws)?;
64 let mut pkgs = ws.members_with_features(&specs, &opts.cli_features)?;
65
66 let (pkg, cli_features) = pkgs.pop().unwrap();
67
68 let mut publish_registry = opts.registry.clone();
69 if let Some(ref allowed_registries) = *pkg.publish() {
70 if publish_registry.is_none() && allowed_registries.len() == 1 {
71 // If there is only one allowed registry, push to that one directly,
72 // even though there is no registry specified in the command.
73 let default_registry = &allowed_registries[0];
74 if default_registry != CRATES_IO_REGISTRY {
75 // Don't change the registry for crates.io and don't warn the user.
76 // crates.io will be defaulted even without this.
77 opts.config.shell().note(&format!(
78 "Found `{}` as only allowed registry. Publishing to it automatically.",
79 default_registry
80 ))?;
81 publish_registry = Some(default_registry.clone());
82 }
83 }
84
85 let reg_name = publish_registry
86 .clone()
87 .unwrap_or_else(|| CRATES_IO_REGISTRY.to_string());
88 if !allowed_registries.contains(&reg_name) {
89 bail!(
90 "`{}` cannot be published.\n\
91 The registry `{}` is not listed in the `publish` value in Cargo.toml.",
92 pkg.name(),
93 reg_name
94 );
95 }
96 }
97
98 let (mut registry, _reg_cfg, reg_id) = registry(
99 opts.config,
100 opts.token.clone(),
101 opts.index.clone(),
102 publish_registry,
103 true,
104 !opts.dry_run,
105 )?;
106 verify_dependencies(pkg, &registry, reg_id)?;
107
108 // Prepare a tarball, with a non-suppressible warning if metadata
109 // is missing since this is being put online.
110 let tarball = ops::package_one(
111 ws,
112 pkg,
113 &ops::PackageOpts {
114 config: opts.config,
115 verify: opts.verify,
116 list: false,
117 check_metadata: true,
118 allow_dirty: opts.allow_dirty,
119 to_package: ops::Packages::Default,
120 targets: opts.targets.clone(),
121 jobs: opts.jobs,
122 cli_features: cli_features,
123 },
124 )?
125 .unwrap();
126
127 opts.config
128 .shell()
129 .status("Uploading", pkg.package_id().to_string())?;
130 transmit(
131 opts.config,
132 pkg,
133 tarball.file(),
134 &mut registry,
135 reg_id,
136 opts.dry_run,
137 )?;
138
139 Ok(())
140 }
141
142 fn verify_dependencies(
143 pkg: &Package,
144 registry: &Registry,
145 registry_src: SourceId,
146 ) -> CargoResult<()> {
147 for dep in pkg.dependencies().iter() {
148 if super::check_dep_has_version(dep, true)? {
149 continue;
150 }
151 // TomlManifest::prepare_for_publish will rewrite the dependency
152 // to be just the `version` field.
153 if dep.source_id() != registry_src {
154 if !dep.source_id().is_registry() {
155 // Consider making SourceId::kind a public type that we can
156 // exhaustively match on. Using match can help ensure that
157 // every kind is properly handled.
158 panic!("unexpected source kind for dependency {:?}", dep);
159 }
160 // Block requests to send to crates.io with alt-registry deps.
161 // This extra hostname check is mostly to assist with testing,
162 // but also prevents someone using `--index` to specify
163 // something that points to crates.io.
164 if registry_src.is_default_registry() || registry.host_is_crates_io() {
165 bail!("crates cannot be published to crates.io with dependencies sourced from other\n\
166 registries. `{}` needs to be published to crates.io before publishing this crate.\n\
167 (crate `{}` is pulled from {})",
168 dep.package_name(),
169 dep.package_name(),
170 dep.source_id());
171 }
172 }
173 }
174 Ok(())
175 }
176
177 fn transmit(
178 config: &Config,
179 pkg: &Package,
180 tarball: &File,
181 registry: &mut Registry,
182 registry_id: SourceId,
183 dry_run: bool,
184 ) -> CargoResult<()> {
185 let deps = pkg
186 .dependencies()
187 .iter()
188 .filter(|dep| {
189 // Skip dev-dependency without version.
190 dep.is_transitive() || dep.specified_req()
191 })
192 .map(|dep| {
193 // If the dependency is from a different registry, then include the
194 // registry in the dependency.
195 let dep_registry_id = match dep.registry_id() {
196 Some(id) => id,
197 None => SourceId::crates_io(config)?,
198 };
199 // In the index and Web API, None means "from the same registry"
200 // whereas in Cargo.toml, it means "from crates.io".
201 let dep_registry = if dep_registry_id != registry_id {
202 Some(dep_registry_id.url().to_string())
203 } else {
204 None
205 };
206
207 Ok(NewCrateDependency {
208 optional: dep.is_optional(),
209 default_features: dep.uses_default_features(),
210 name: dep.package_name().to_string(),
211 features: dep.features().iter().map(|s| s.to_string()).collect(),
212 version_req: dep.version_req().to_string(),
213 target: dep.platform().map(|s| s.to_string()),
214 kind: match dep.kind() {
215 DepKind::Normal => "normal",
216 DepKind::Build => "build",
217 DepKind::Development => "dev",
218 }
219 .to_string(),
220 registry: dep_registry,
221 explicit_name_in_toml: dep.explicit_name_in_toml().map(|s| s.to_string()),
222 })
223 })
224 .collect::<CargoResult<Vec<NewCrateDependency>>>()?;
225 let manifest = pkg.manifest();
226 let ManifestMetadata {
227 ref authors,
228 ref description,
229 ref homepage,
230 ref documentation,
231 ref keywords,
232 ref readme,
233 ref repository,
234 ref license,
235 ref license_file,
236 ref categories,
237 ref badges,
238 ref links,
239 } = *manifest.metadata();
240 let readme_content = readme
241 .as_ref()
242 .map(|readme| {
243 paths::read(&pkg.root().join(readme))
244 .with_context(|| format!("failed to read `readme` file for package `{}`", pkg))
245 })
246 .transpose()?;
247 if let Some(ref file) = *license_file {
248 if !pkg.root().join(file).exists() {
249 bail!("the license file `{}` does not exist", file)
250 }
251 }
252
253 // Do not upload if performing a dry run
254 if dry_run {
255 config.shell().warn("aborting upload due to dry run")?;
256 return Ok(());
257 }
258
259 let string_features = match manifest.original().features() {
260 Some(features) => features
261 .iter()
262 .map(|(feat, values)| {
263 (
264 feat.to_string(),
265 values.iter().map(|fv| fv.to_string()).collect(),
266 )
267 })
268 .collect::<BTreeMap<String, Vec<String>>>(),
269 None => BTreeMap::new(),
270 };
271
272 let warnings = registry
273 .publish(
274 &NewCrate {
275 name: pkg.name().to_string(),
276 vers: pkg.version().to_string(),
277 deps,
278 features: string_features,
279 authors: authors.clone(),
280 description: description.clone(),
281 homepage: homepage.clone(),
282 documentation: documentation.clone(),
283 keywords: keywords.clone(),
284 categories: categories.clone(),
285 readme: readme_content,
286 readme_file: readme.clone(),
287 repository: repository.clone(),
288 license: license.clone(),
289 license_file: license_file.clone(),
290 badges: badges.clone(),
291 links: links.clone(),
292 },
293 tarball,
294 )
295 .with_context(|| format!("failed to publish to registry at {}", registry.host()))?;
296
297 if !warnings.invalid_categories.is_empty() {
298 let msg = format!(
299 "the following are not valid category slugs and were \
300 ignored: {}. Please see https://crates.io/category_slugs \
301 for the list of all category slugs. \
302 ",
303 warnings.invalid_categories.join(", ")
304 );
305 config.shell().warn(&msg)?;
306 }
307
308 if !warnings.invalid_badges.is_empty() {
309 let msg = format!(
310 "the following are not valid badges and were ignored: {}. \
311 Either the badge type specified is unknown or a required \
312 attribute is missing. Please see \
313 https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata \
314 for valid badge types and their required attributes.",
315 warnings.invalid_badges.join(", ")
316 );
317 config.shell().warn(&msg)?;
318 }
319
320 if !warnings.other.is_empty() {
321 for msg in warnings.other {
322 config.shell().warn(&msg)?;
323 }
324 }
325
326 Ok(())
327 }
328
329 /// Returns the index and token from the config file for the given registry.
330 ///
331 /// `registry` is typically the registry specified on the command-line. If
332 /// `None`, `index` is set to `None` to indicate it should use crates.io.
333 pub fn registry_configuration(
334 config: &Config,
335 registry: Option<&str>,
336 ) -> CargoResult<RegistryConfig> {
337 let err_both = |token_key: &str, proc_key: &str| {
338 Err(format_err!(
339 "both `{TOKEN_KEY}` and `{PROC_KEY}` \
340 were specified in the config\n\
341 Only one of these values may be set, remove one or the other to proceed.",
342 TOKEN_KEY = token_key,
343 PROC_KEY = proc_key,
344 ))
345 };
346 // `registry.default` is handled in command-line parsing.
347 let (index, token, process) = match registry {
348 Some(registry) => {
349 let index = Some(config.get_registry_index(registry)?.to_string());
350 let token_key = format!("registries.{}.token", registry);
351 let token = config.get_string(&token_key)?.map(|p| p.val);
352 let process = if config.cli_unstable().credential_process {
353 let mut proc_key = format!("registries.{}.credential-process", registry);
354 let mut process = config.get::<Option<config::PathAndArgs>>(&proc_key)?;
355 if process.is_none() && token.is_none() {
356 // This explicitly ignores the global credential-process if
357 // the token is set, as that is "more specific".
358 proc_key = String::from("registry.credential-process");
359 process = config.get::<Option<config::PathAndArgs>>(&proc_key)?;
360 } else if process.is_some() && token.is_some() {
361 return err_both(&token_key, &proc_key);
362 }
363 process
364 } else {
365 None
366 };
367 (index, token, process)
368 }
369 None => {
370 // Use crates.io default.
371 config.check_registry_index_not_set()?;
372 let token = config.get_string("registry.token")?.map(|p| p.val);
373 let process = if config.cli_unstable().credential_process {
374 let process =
375 config.get::<Option<config::PathAndArgs>>("registry.credential-process")?;
376 if token.is_some() && process.is_some() {
377 return err_both("registry.token", "registry.credential-process");
378 }
379 process
380 } else {
381 None
382 };
383 (None, token, process)
384 }
385 };
386
387 let credential_process =
388 process.map(|process| (process.path.resolve_program(config), process.args));
389
390 Ok(RegistryConfig {
391 index,
392 token,
393 credential_process,
394 })
395 }
396
397 /// Returns the `Registry` and `Source` based on command-line and config settings.
398 ///
399 /// * `token`: The token from the command-line. If not set, uses the token
400 /// from the config.
401 /// * `index`: The index URL from the command-line. This is ignored if
402 /// `registry` is set.
403 /// * `registry`: The registry name from the command-line. If neither
404 /// `registry`, or `index` are set, then uses `crates-io`, honoring
405 /// `[source]` replacement if defined.
406 /// * `force_update`: If `true`, forces the index to be updated.
407 /// * `validate_token`: If `true`, the token must be set.
408 fn registry(
409 config: &Config,
410 token: Option<String>,
411 index: Option<String>,
412 registry: Option<String>,
413 force_update: bool,
414 validate_token: bool,
415 ) -> CargoResult<(Registry, RegistryConfig, SourceId)> {
416 if index.is_some() && registry.is_some() {
417 // Otherwise we would silently ignore one or the other.
418 bail!("both `--index` and `--registry` should not be set at the same time");
419 }
420 // Parse all configuration options
421 let reg_cfg = registry_configuration(config, registry.as_deref())?;
422 let opt_index = reg_cfg.index.as_deref().or_else(|| index.as_deref());
423 let sid = get_source_id(config, opt_index, registry.as_deref())?;
424 if !sid.is_remote_registry() {
425 bail!(
426 "{} does not support API commands.\n\
427 Check for a source-replacement in .cargo/config.",
428 sid
429 );
430 }
431 let api_host = {
432 let _lock = config.acquire_package_cache_lock()?;
433 let mut src = RegistrySource::remote(sid, &HashSet::new(), config);
434 // Only update the index if the config is not available or `force` is set.
435 if force_update {
436 src.invalidate_cache()
437 }
438 let cfg = loop {
439 match src.config()? {
440 Poll::Pending => src
441 .block_until_ready()
442 .with_context(|| format!("failed to update {}", sid))?,
443 Poll::Ready(cfg) => break cfg,
444 }
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(config: &Config, index: Option<&str>, reg: Option<&str>) -> CargoResult<SourceId> {
896 match (reg, index) {
897 (Some(r), _) => SourceId::alt_registry(config, r),
898 (_, Some(i)) => SourceId::for_registry(&i.into_url()?),
899 _ => {
900 let map = SourceConfigMap::new(config)?;
901 let src = map.load(SourceId::crates_io(config)?, &HashSet::new())?;
902 Ok(src.replaced_source_id())
903 }
904 }
905 }
906
907 pub fn search(
908 query: &str,
909 config: &Config,
910 index: Option<String>,
911 limit: u32,
912 reg: Option<String>,
913 ) -> CargoResult<()> {
914 fn truncate_with_ellipsis(s: &str, max_width: usize) -> String {
915 // We should truncate at grapheme-boundary and compute character-widths,
916 // yet the dependencies on unicode-segmentation and unicode-width are
917 // not worth it.
918 let mut chars = s.chars();
919 let mut prefix = (&mut chars).take(max_width - 1).collect::<String>();
920 if chars.next().is_some() {
921 prefix.push('…');
922 }
923 prefix
924 }
925
926 let (mut registry, _, source_id) = registry(config, None, index, reg, false, false)?;
927 let (crates, total_crates) = registry.search(query, limit).with_context(|| {
928 format!(
929 "failed to retrieve search results from the registry at {}",
930 registry.host()
931 )
932 })?;
933
934 let names = crates
935 .iter()
936 .map(|krate| format!("{} = \"{}\"", krate.name, krate.max_version))
937 .collect::<Vec<String>>();
938
939 let description_margin = names.iter().map(|s| s.len() + 4).max().unwrap_or_default();
940
941 let description_length = cmp::max(80, 128 - description_margin);
942
943 let descriptions = crates.iter().map(|krate| {
944 krate
945 .description
946 .as_ref()
947 .map(|desc| truncate_with_ellipsis(&desc.replace("\n", " "), description_length))
948 });
949
950 for (name, description) in names.into_iter().zip(descriptions) {
951 let line = match description {
952 Some(desc) => {
953 let space = repeat(' ')
954 .take(description_margin - name.len())
955 .collect::<String>();
956 name + &space + "# " + &desc
957 }
958 None => name,
959 };
960 let mut fragments = line.split(query).peekable();
961 while let Some(fragment) = fragments.next() {
962 let _ = config.shell().write_stdout(fragment, &ColorSpec::new());
963 if fragments.peek().is_some() {
964 let _ = config
965 .shell()
966 .write_stdout(query, &ColorSpec::new().set_bold(true).set_fg(Some(Green)));
967 }
968 }
969 let _ = config.shell().write_stdout("\n", &ColorSpec::new());
970 }
971
972 let search_max_limit = 100;
973 if total_crates > limit && limit < search_max_limit {
974 let _ = config.shell().write_stdout(
975 format_args!(
976 "... and {} crates more (use --limit N to see more)\n",
977 total_crates - limit
978 ),
979 &ColorSpec::new(),
980 );
981 } else if total_crates > limit && limit >= search_max_limit {
982 let extra = if source_id.is_default_registry() {
983 format!(
984 " (go to https://crates.io/search?q={} to see more)",
985 percent_encode(query.as_bytes(), NON_ALPHANUMERIC)
986 )
987 } else {
988 String::new()
989 };
990 let _ = config.shell().write_stdout(
991 format_args!("... and {} crates more{}\n", total_crates - limit, extra),
992 &ColorSpec::new(),
993 );
994 }
995
996 Ok(())
997 }