use std::cmp;
use std::collections::{BTreeMap, HashSet};
use std::fs::File;
use std::io::{self, BufRead};
use std::iter::repeat;
use std::path::PathBuf;
use std::str;
use std::task::Poll;
use std::time::Duration;
use anyhow::{anyhow, bail, format_err, Context as _};
use cargo_util::paths;
use crates_io::{self, NewCrate, NewCrateDependency, Registry};
use curl::easy::{Easy, InfoType, SslOpt, SslVersion};
use log::{log, Level};
use pasetors::keys::{AsymmetricKeyPair, Generate};
use pasetors::paserk::FormatAsPaserk;
use termcolor::Color::Green;
use termcolor::ColorSpec;
use url::Url;
use crate::core::dependency::DepKind;
use crate::core::dependency::Dependency;
use crate::core::manifest::ManifestMetadata;
use crate::core::resolver::CliFeatures;
use crate::core::source::Source;
use crate::core::QueryKind;
use crate::core::{Package, SourceId, Workspace};
use crate::ops;
use crate::ops::Packages;
use crate::sources::{RegistrySource, SourceConfigMap, CRATES_IO_DOMAIN, CRATES_IO_REGISTRY};
use crate::util::auth::{
paserk_public_from_paserk_secret, Secret, {self, AuthorizationError},
};
use crate::util::config::{Config, SslVersionConfig, SslVersionConfigRange};
use crate::util::errors::CargoResult;
use crate::util::important_paths::find_root_manifest_for_wd;
use crate::util::{truncate_with_ellipsis, IntoUrl};
use crate::{drop_print, drop_println, version};
#[derive(Debug, PartialEq)]
pub enum RegistryCredentialConfig {
None,
Token(Secret<String>),
Process((PathBuf, Vec<String>)),
AsymmetricKey((Secret<String>, Option<String>)),
}
impl RegistryCredentialConfig {
pub fn is_none(&self) -> bool {
matches!(self, Self::None)
}
pub fn is_token(&self) -> bool {
matches!(self, Self::Token(..))
}
pub fn is_asymmetric_key(&self) -> bool {
matches!(self, Self::AsymmetricKey(..))
}
pub fn as_token(&self) -> Option<Secret<&str>> {
if let Self::Token(v) = self {
Some(v.as_deref())
} else {
None
}
}
pub fn as_process(&self) -> Option<&(PathBuf, Vec<String>)> {
if let Self::Process(v) = self {
Some(v)
} else {
None
}
}
pub fn as_asymmetric_key(&self) -> Option<&(Secret<String>, Option<String>)> {
if let Self::AsymmetricKey(v) = self {
Some(v)
} else {
None
}
}
}
pub struct PublishOpts<'cfg> {
pub config: &'cfg Config,
pub token: Option<Secret<String>>,
pub index: Option<String>,
pub verify: bool,
pub allow_dirty: bool,
pub jobs: Option<i32>,
pub keep_going: bool,
pub to_publish: ops::Packages,
pub targets: Vec<String>,
pub dry_run: bool,
pub registry: Option<String>,
pub cli_features: CliFeatures,
}
pub fn publish(ws: &Workspace<'_>, opts: &PublishOpts<'_>) -> CargoResult<()> {
let specs = opts.to_publish.to_package_id_specs(ws)?;
if specs.len() > 1 {
bail!("the `-p` argument must be specified to select a single package to publish")
}
if Packages::Default == opts.to_publish && ws.is_virtual() {
bail!("the `-p` argument must be specified in the root of a virtual workspace")
}
let member_ids = ws.members().map(|p| p.package_id());
specs[0].query(member_ids)?;
let mut pkgs = ws.members_with_features(&specs, &opts.cli_features)?;
pkgs = pkgs
.into_iter()
.filter(|(m, _)| specs.iter().any(|spec| spec.matches(m.package_id())))
.collect();
assert_eq!(pkgs.len(), 1);
let (pkg, cli_features) = pkgs.pop().unwrap();
let mut publish_registry = opts.registry.clone();
if let Some(ref allowed_registries) = *pkg.publish() {
if publish_registry.is_none() && allowed_registries.len() == 1 {
let default_registry = &allowed_registries[0];
if default_registry != CRATES_IO_REGISTRY {
opts.config.shell().note(&format!(
"Found `{}` as only allowed registry. Publishing to it automatically.",
default_registry
))?;
publish_registry = Some(default_registry.clone());
}
}
let reg_name = publish_registry
.clone()
.unwrap_or_else(|| CRATES_IO_REGISTRY.to_string());
if allowed_registries.is_empty() {
bail!(
"`{}` cannot be published.\n\
`package.publish` is set to `false` or an empty list in Cargo.toml and prevents publishing.",
pkg.name(),
);
} else if !allowed_registries.contains(®_name) {
bail!(
"`{}` cannot be published.\n\
The registry `{}` is not listed in the `package.publish` value in Cargo.toml.",
pkg.name(),
reg_name
);
}
}
let ver = pkg.version().to_string();
let mutation = auth::Mutation::PrePublish;
let (mut registry, reg_ids) = registry(
opts.config,
opts.token.as_ref().map(Secret::as_deref),
opts.index.as_deref(),
publish_registry.as_deref(),
true,
Some(mutation).filter(|_| !opts.dry_run),
)?;
verify_dependencies(pkg, ®istry, reg_ids.original)?;
let tarball = ops::package_one(
ws,
pkg,
&ops::PackageOpts {
config: opts.config,
verify: opts.verify,
list: false,
check_metadata: true,
allow_dirty: opts.allow_dirty,
to_package: ops::Packages::Default,
targets: opts.targets.clone(),
jobs: opts.jobs,
keep_going: opts.keep_going,
cli_features: cli_features,
},
)?
.unwrap();
if !opts.dry_run {
let hash = cargo_util::Sha256::new()
.update_file(tarball.file())?
.finish_hex();
let mutation = Some(auth::Mutation::Publish {
name: pkg.name().as_str(),
vers: &ver,
cksum: &hash,
});
registry.set_token(Some(auth::auth_token(
&opts.config,
®_ids.original,
None,
mutation,
)?));
}
opts.config
.shell()
.status("Uploading", pkg.package_id().to_string())?;
transmit(
opts.config,
pkg,
tarball.file(),
&mut registry,
reg_ids.original,
opts.dry_run,
)?;
if !opts.dry_run {
const DEFAULT_TIMEOUT: u64 = 60;
let timeout = if opts.config.cli_unstable().publish_timeout {
let timeout: Option<u64> = opts.config.get("publish.timeout")?;
timeout.unwrap_or(DEFAULT_TIMEOUT)
} else {
DEFAULT_TIMEOUT
};
if 0 < timeout {
let timeout = std::time::Duration::from_secs(timeout);
wait_for_publish(opts.config, reg_ids.original, pkg, timeout)?;
}
}
Ok(())
}
fn verify_dependencies(
pkg: &Package,
registry: &Registry,
registry_src: SourceId,
) -> CargoResult<()> {
for dep in pkg.dependencies().iter() {
if super::check_dep_has_version(dep, true)? {
continue;
}
if dep.source_id() != registry_src {
if !dep.source_id().is_registry() {
panic!("unexpected source kind for dependency {:?}", dep);
}
if registry_src.is_crates_io() || registry.host_is_crates_io() {
bail!("crates cannot be published to crates.io with dependencies sourced from other\n\
registries. `{}` needs to be published to crates.io before publishing this crate.\n\
(crate `{}` is pulled from {})",
dep.package_name(),
dep.package_name(),
dep.source_id());
}
}
}
Ok(())
}
fn transmit(
config: &Config,
pkg: &Package,
tarball: &File,
registry: &mut Registry,
registry_id: SourceId,
dry_run: bool,
) -> CargoResult<()> {
let deps = pkg
.dependencies()
.iter()
.filter(|dep| {
dep.is_transitive() || dep.specified_req()
})
.map(|dep| {
let dep_registry_id = match dep.registry_id() {
Some(id) => id,
None => SourceId::crates_io(config)?,
};
let dep_registry = if dep_registry_id != registry_id {
Some(dep_registry_id.url().to_string())
} else {
None
};
Ok(NewCrateDependency {
optional: dep.is_optional(),
default_features: dep.uses_default_features(),
name: dep.package_name().to_string(),
features: dep.features().iter().map(|s| s.to_string()).collect(),
version_req: dep.version_req().to_string(),
target: dep.platform().map(|s| s.to_string()),
kind: match dep.kind() {
DepKind::Normal => "normal",
DepKind::Build => "build",
DepKind::Development => "dev",
}
.to_string(),
registry: dep_registry,
explicit_name_in_toml: dep.explicit_name_in_toml().map(|s| s.to_string()),
})
})
.collect::<CargoResult<Vec<NewCrateDependency>>>()?;
let manifest = pkg.manifest();
let ManifestMetadata {
ref authors,
ref description,
ref homepage,
ref documentation,
ref keywords,
ref readme,
ref repository,
ref license,
ref license_file,
ref categories,
ref badges,
ref links,
} = *manifest.metadata();
let readme_content = readme
.as_ref()
.map(|readme| {
paths::read(&pkg.root().join(readme))
.with_context(|| format!("failed to read `readme` file for package `{}`", pkg))
})
.transpose()?;
if let Some(ref file) = *license_file {
if !pkg.root().join(file).exists() {
bail!("the license file `{}` does not exist", file)
}
}
if dry_run {
config.shell().warn("aborting upload due to dry run")?;
return Ok(());
}
let string_features = match manifest.original().features() {
Some(features) => features
.iter()
.map(|(feat, values)| {
(
feat.to_string(),
values.iter().map(|fv| fv.to_string()).collect(),
)
})
.collect::<BTreeMap<String, Vec<String>>>(),
None => BTreeMap::new(),
};
let warnings = registry
.publish(
&NewCrate {
name: pkg.name().to_string(),
vers: pkg.version().to_string(),
deps,
features: string_features,
authors: authors.clone(),
description: description.clone(),
homepage: homepage.clone(),
documentation: documentation.clone(),
keywords: keywords.clone(),
categories: categories.clone(),
readme: readme_content,
readme_file: readme.clone(),
repository: repository.clone(),
license: license.clone(),
license_file: license_file.clone(),
badges: badges.clone(),
links: links.clone(),
},
tarball,
)
.with_context(|| format!("failed to publish to registry at {}", registry.host()))?;
if !warnings.invalid_categories.is_empty() {
let msg = format!(
"the following are not valid category slugs and were \
ignored: {}. Please see https://crates.io/category_slugs \
for the list of all category slugs. \
",
warnings.invalid_categories.join(", ")
);
config.shell().warn(&msg)?;
}
if !warnings.invalid_badges.is_empty() {
let msg = format!(
"the following are not valid badges and were ignored: {}. \
Either the badge type specified is unknown or a required \
attribute is missing. Please see \
https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata \
for valid badge types and their required attributes.",
warnings.invalid_badges.join(", ")
);
config.shell().warn(&msg)?;
}
if !warnings.other.is_empty() {
for msg in warnings.other {
config.shell().warn(&msg)?;
}
}
Ok(())
}
fn wait_for_publish(
config: &Config,
registry_src: SourceId,
pkg: &Package,
timeout: std::time::Duration,
) -> CargoResult<()> {
let version_req = format!("={}", pkg.version());
let mut source = SourceConfigMap::empty(config)?.load(registry_src, &HashSet::new())?;
let source_description = source.describe();
let query = Dependency::parse(pkg.name(), Some(&version_req), registry_src)?;
let now = std::time::Instant::now();
let sleep_time = std::time::Duration::from_secs(1);
let mut logged = false;
loop {
{
let _lock = config.acquire_package_cache_lock()?;
config
.updated_sources()
.remove(&source.replaced_source_id());
source.invalidate_cache();
let summaries = loop {
match source.query_vec(&query, QueryKind::Exact) {
std::task::Poll::Ready(res) => {
break res?;
}
std::task::Poll::Pending => source.block_until_ready()?,
}
};
if !summaries.is_empty() {
break;
}
}
if timeout < now.elapsed() {
config.shell().warn(format!(
"timed out waiting for `{}` to be in {}",
pkg.name(),
source_description
))?;
break;
}
if !logged {
config.shell().status(
"Waiting",
format!(
"on `{}` to propagate to {} (ctrl-c to wait asynchronously)",
pkg.name(),
source_description
),
)?;
logged = true;
}
std::thread::sleep(sleep_time);
}
Ok(())
}
fn registry(
config: &Config,
token_from_cmdline: Option<Secret<&str>>,
index: Option<&str>,
registry: Option<&str>,
force_update: bool,
token_required: Option<auth::Mutation<'_>>,
) -> CargoResult<(Registry, RegistrySourceIds)> {
let source_ids = get_source_id(config, index, registry)?;
if token_required.is_some() && index.is_some() && token_from_cmdline.is_none() {
bail!("command-line argument --index requires --token to be specified");
}
if let Some(token) = token_from_cmdline {
auth::cache_token(config, &source_ids.original, token);
}
let cfg = {
let _lock = config.acquire_package_cache_lock()?;
let mut src = RegistrySource::remote(source_ids.replacement, &HashSet::new(), config)?;
if force_update {
src.invalidate_cache()
}
let cfg = loop {
match src.config()? {
Poll::Pending => src
.block_until_ready()
.with_context(|| format!("failed to update {}", source_ids.replacement))?,
Poll::Ready(cfg) => break cfg,
}
};
cfg.expect("remote registries must have config")
};
let api_host = cfg
.api
.ok_or_else(|| format_err!("{} does not support API commands", source_ids.replacement))?;
let token = if token_required.is_some() || cfg.auth_required {
Some(auth::auth_token(
config,
&source_ids.original,
None,
token_required,
)?)
} else {
None
};
let handle = http_handle(config)?;
Ok((
Registry::new_handle(api_host, token, handle, cfg.auth_required),
source_ids,
))
}
pub fn http_handle(config: &Config) -> CargoResult<Easy> {
let (mut handle, timeout) = http_handle_and_timeout(config)?;
timeout.configure(&mut handle)?;
Ok(handle)
}
pub fn http_handle_and_timeout(config: &Config) -> CargoResult<(Easy, HttpTimeout)> {
if config.frozen() {
bail!(
"attempting to make an HTTP request, but --frozen was \
specified"
)
}
if config.offline() {
bail!(
"attempting to make an HTTP request, but --offline was \
specified"
)
}
let mut handle = Easy::new();
let timeout = configure_http_handle(config, &mut handle)?;
Ok((handle, timeout))
}
pub fn needs_custom_http_transport(config: &Config) -> CargoResult<bool> {
Ok(http_proxy_exists(config)?
|| *config.http_config()? != Default::default()
|| config.get_env_os("HTTP_TIMEOUT").is_some())
}
pub fn configure_http_handle(config: &Config, handle: &mut Easy) -> CargoResult<HttpTimeout> {
let http = config.http_config()?;
if let Some(proxy) = http_proxy(config)? {
handle.proxy(&proxy)?;
}
if let Some(cainfo) = &http.cainfo {
let cainfo = cainfo.resolve_path(config);
handle.cainfo(&cainfo)?;
}
if let Some(check) = http.check_revoke {
handle.ssl_options(SslOpt::new().no_revoke(!check))?;
}
if let Some(user_agent) = &http.user_agent {
handle.useragent(user_agent)?;
} else {
handle.useragent(&format!("cargo {}", version()))?;
}
handle.accept_encoding("")?;
fn to_ssl_version(s: &str) -> CargoResult<SslVersion> {
let version = match s {
"default" => SslVersion::Default,
"tlsv1" => SslVersion::Tlsv1,
"tlsv1.0" => SslVersion::Tlsv10,
"tlsv1.1" => SslVersion::Tlsv11,
"tlsv1.2" => SslVersion::Tlsv12,
"tlsv1.3" => SslVersion::Tlsv13,
_ => bail!(
"Invalid ssl version `{}`,\
choose from 'default', 'tlsv1', 'tlsv1.0', 'tlsv1.1', 'tlsv1.2', 'tlsv1.3'.",
s
),
};
Ok(version)
}
if let Some(ssl_version) = &http.ssl_version {
match ssl_version {
SslVersionConfig::Single(s) => {
let version = to_ssl_version(s.as_str())?;
handle.ssl_version(version)?;
}
SslVersionConfig::Range(SslVersionConfigRange { min, max }) => {
let min_version = min
.as_ref()
.map_or(Ok(SslVersion::Default), |s| to_ssl_version(s))?;
let max_version = max
.as_ref()
.map_or(Ok(SslVersion::Default), |s| to_ssl_version(s))?;
handle.ssl_min_max_version(min_version, max_version)?;
}
}
} else if cfg!(windows) {
handle.ssl_min_max_version(SslVersion::Default, SslVersion::Tlsv12)?;
}
if let Some(true) = http.debug {
handle.verbose(true)?;
log::debug!("{:#?}", curl::Version::get());
handle.debug_function(|kind, data| {
let (prefix, level) = match kind {
InfoType::Text => ("*", Level::Debug),
InfoType::HeaderIn => ("<", Level::Debug),
InfoType::HeaderOut => (">", Level::Debug),
InfoType::DataIn => ("{", Level::Trace),
InfoType::DataOut => ("}", Level::Trace),
InfoType::SslDataIn | InfoType::SslDataOut => return,
_ => return,
};
match str::from_utf8(data) {
Ok(s) => {
for mut line in s.lines() {
if line.starts_with("Authorization:") {
line = "Authorization: [REDACTED]";
} else if line[..line.len().min(10)].eq_ignore_ascii_case("set-cookie") {
line = "set-cookie: [REDACTED]";
}
log!(level, "http-debug: {} {}", prefix, line);
}
}
Err(_) => {
log!(
level,
"http-debug: {} ({} bytes of data)",
prefix,
data.len()
);
}
}
})?;
}
HttpTimeout::new(config)
}
#[must_use]
pub struct HttpTimeout {
pub dur: Duration,
pub low_speed_limit: u32,
}
impl HttpTimeout {
pub fn new(config: &Config) -> CargoResult<HttpTimeout> {
let http_config = config.http_config()?;
let low_speed_limit = http_config.low_speed_limit.unwrap_or(10);
let seconds = http_config
.timeout
.or_else(|| {
config
.get_env("HTTP_TIMEOUT")
.ok()
.and_then(|s| s.parse().ok())
})
.unwrap_or(30);
Ok(HttpTimeout {
dur: Duration::new(seconds, 0),
low_speed_limit,
})
}
pub fn configure(&self, handle: &mut Easy) -> CargoResult<()> {
handle.connect_timeout(self.dur)?;
handle.low_speed_time(self.dur)?;
handle.low_speed_limit(self.low_speed_limit)?;
Ok(())
}
}
fn http_proxy(config: &Config) -> CargoResult<Option<String>> {
let http = config.http_config()?;
if let Some(s) = &http.proxy {
return Ok(Some(s.clone()));
}
if let Ok(cfg) = git2::Config::open_default() {
if let Ok(s) = cfg.get_string("http.proxy") {
return Ok(Some(s));
}
}
Ok(None)
}
fn http_proxy_exists(config: &Config) -> CargoResult<bool> {
if http_proxy(config)?.is_some() {
Ok(true)
} else {
Ok(["http_proxy", "HTTP_PROXY", "https_proxy", "HTTPS_PROXY"]
.iter()
.any(|v| config.get_env(v).is_ok()))
}
}
pub fn registry_login(
config: &Config,
token: Option<Secret<&str>>,
reg: Option<&str>,
generate_keypair: bool,
secret_key_required: bool,
key_subject: Option<&str>,
) -> CargoResult<()> {
let source_ids = get_source_id(config, None, reg)?;
let reg_cfg = auth::registry_credential_config(config, &source_ids.original)?;
let login_url = match registry(config, token.clone(), None, reg, false, None) {
Ok((registry, _)) => Some(format!("{}/me", registry.host())),
Err(e) if e.is::<AuthorizationError>() => e
.downcast::<AuthorizationError>()
.unwrap()
.login_url
.map(|u| u.to_string()),
Err(e) => return Err(e),
};
let new_token;
if generate_keypair || secret_key_required || key_subject.is_some() {
if !config.cli_unstable().registry_auth {
let flag = if generate_keypair {
"generate-keypair"
} else if secret_key_required {
"secret-key"
} else if key_subject.is_some() {
"key-subject"
} else {
unreachable!("how did we get here");
};
bail!(
"the `{flag}` flag is unstable, pass `-Z registry-auth` to enable it\n\
See https://github.com/rust-lang/cargo/issues/10519 for more \
information about the `{flag}` flag."
);
}
assert!(token.is_none());
let (old_secret_key, old_key_subject) = match ®_cfg {
RegistryCredentialConfig::AsymmetricKey((old_secret_key, old_key_subject)) => {
(Some(old_secret_key), old_key_subject.clone())
}
_ => (None, None),
};
let secret_key: Secret<String>;
if generate_keypair {
assert!(!secret_key_required);
let kp = AsymmetricKeyPair::<pasetors::version3::V3>::generate().unwrap();
secret_key = Secret::default().map(|mut key| {
FormatAsPaserk::fmt(&kp.secret, &mut key).unwrap();
key
});
} else if secret_key_required {
assert!(!generate_keypair);
drop_println!(config, "please paste the API secret key below");
secret_key = Secret::default()
.map(|mut line| {
let input = io::stdin();
input
.lock()
.read_line(&mut line)
.with_context(|| "failed to read stdin")
.map(|_| line.trim().to_string())
})
.transpose()?;
} else {
secret_key = old_secret_key
.cloned()
.ok_or_else(|| anyhow!("need a secret_key to set a key_subject"))?;
}
if let Some(p) = paserk_public_from_paserk_secret(secret_key.as_deref()) {
drop_println!(config, "{}", &p);
} else {
bail!("not a validly formatted PASERK secret key");
}
new_token = RegistryCredentialConfig::AsymmetricKey((
secret_key,
match key_subject {
Some(key_subject) => Some(key_subject.to_string()),
None => old_key_subject,
},
));
} else {
new_token = RegistryCredentialConfig::Token(match token {
Some(token) => token.owned(),
None => {
if let Some(login_url) = login_url {
drop_println!(
config,
"please paste the token found on {} below",
login_url
)
} else {
drop_println!(
config,
"please paste the token for {} below",
source_ids.original.display_registry_name()
)
}
let mut line = String::new();
let input = io::stdin();
input
.lock()
.read_line(&mut line)
.with_context(|| "failed to read stdin")?;
Secret::from(line.replace("cargo login", "").trim().to_string())
}
});
if let Some(tok) = new_token.as_token() {
crates_io::check_token(tok.as_ref().expose())?;
}
}
if ®_cfg == &new_token {
config.shell().status("Login", "already logged in")?;
return Ok(());
}
auth::login(config, &source_ids.original, new_token)?;
config.shell().status(
"Login",
format!("token for `{}` saved", reg.unwrap_or(CRATES_IO_DOMAIN)),
)?;
Ok(())
}
pub fn registry_logout(config: &Config, reg: Option<&str>) -> CargoResult<()> {
let source_ids = get_source_id(config, None, reg)?;
let reg_cfg = auth::registry_credential_config(config, &source_ids.original)?;
let reg_name = source_ids.original.display_registry_name();
if reg_cfg.is_none() {
config.shell().status(
"Logout",
format!("not currently logged in to `{}`", reg_name),
)?;
return Ok(());
}
auth::logout(config, &source_ids.original)?;
config.shell().status(
"Logout",
format!(
"token for `{}` has been removed from local storage",
reg_name
),
)?;
Ok(())
}
pub struct OwnersOptions {
pub krate: Option<String>,
pub token: Option<Secret<String>>,
pub index: Option<String>,
pub to_add: Option<Vec<String>>,
pub to_remove: Option<Vec<String>>,
pub list: bool,
pub registry: Option<String>,
}
pub fn modify_owners(config: &Config, opts: &OwnersOptions) -> CargoResult<()> {
let name = match opts.krate {
Some(ref name) => name.clone(),
None => {
let manifest_path = find_root_manifest_for_wd(config.cwd())?;
let ws = Workspace::new(&manifest_path, config)?;
ws.current()?.package_id().name().to_string()
}
};
let mutation = auth::Mutation::Owners { name: &name };
let (mut registry, _) = registry(
config,
opts.token.as_ref().map(Secret::as_deref),
opts.index.as_deref(),
opts.registry.as_deref(),
true,
Some(mutation),
)?;
if let Some(ref v) = opts.to_add {
let v = v.iter().map(|s| &s[..]).collect::<Vec<_>>();
let msg = registry.add_owners(&name, &v).with_context(|| {
format!(
"failed to invite owners to crate `{}` on registry at {}",
name,
registry.host()
)
})?;
config.shell().status("Owner", msg)?;
}
if let Some(ref v) = opts.to_remove {
let v = v.iter().map(|s| &s[..]).collect::<Vec<_>>();
config
.shell()
.status("Owner", format!("removing {:?} from crate {}", v, name))?;
registry.remove_owners(&name, &v).with_context(|| {
format!(
"failed to remove owners from crate `{}` on registry at {}",
name,
registry.host()
)
})?;
}
if opts.list {
let owners = registry.list_owners(&name).with_context(|| {
format!(
"failed to list owners of crate `{}` on registry at {}",
name,
registry.host()
)
})?;
for owner in owners.iter() {
drop_print!(config, "{}", owner.login);
match (owner.name.as_ref(), owner.email.as_ref()) {
(Some(name), Some(email)) => drop_println!(config, " ({} <{}>)", name, email),
(Some(s), None) | (None, Some(s)) => drop_println!(config, " ({})", s),
(None, None) => drop_println!(config),
}
}
}
Ok(())
}
pub fn yank(
config: &Config,
krate: Option<String>,
version: Option<String>,
token: Option<Secret<String>>,
index: Option<String>,
undo: bool,
reg: Option<String>,
) -> CargoResult<()> {
let name = match krate {
Some(name) => name,
None => {
let manifest_path = find_root_manifest_for_wd(config.cwd())?;
let ws = Workspace::new(&manifest_path, config)?;
ws.current()?.package_id().name().to_string()
}
};
let version = match version {
Some(v) => v,
None => bail!("a version must be specified to yank"),
};
let message = if undo {
auth::Mutation::Unyank {
name: &name,
vers: &version,
}
} else {
auth::Mutation::Yank {
name: &name,
vers: &version,
}
};
let (mut registry, _) = registry(
config,
token.as_ref().map(Secret::as_deref),
index.as_deref(),
reg.as_deref(),
true,
Some(message),
)?;
let package_spec = format!("{}@{}", name, version);
if undo {
config.shell().status("Unyank", package_spec)?;
registry.unyank(&name, &version).with_context(|| {
format!(
"failed to undo a yank from the registry at {}",
registry.host()
)
})?;
} else {
config.shell().status("Yank", package_spec)?;
registry
.yank(&name, &version)
.with_context(|| format!("failed to yank from the registry at {}", registry.host()))?;
}
Ok(())
}
fn get_source_id(
config: &Config,
index: Option<&str>,
reg: Option<&str>,
) -> CargoResult<RegistrySourceIds> {
let sid = match (reg, index) {
(None, None) => SourceId::crates_io(config)?,
(_, Some(i)) => SourceId::for_registry(&i.into_url()?)?,
(Some(r), None) => SourceId::alt_registry(config, r)?,
};
let builtin_replacement_sid = SourceConfigMap::empty(config)?
.load(sid, &HashSet::new())?
.replaced_source_id();
let replacement_sid = SourceConfigMap::new(config)?
.load(sid, &HashSet::new())?
.replaced_source_id();
if reg.is_none() && index.is_none() && replacement_sid != builtin_replacement_sid {
if let Some(replacement_name) = replacement_sid.alt_registry_key() {
bail!("crates-io is replaced with remote registry {replacement_name};\ninclude `--registry {replacement_name}` or `--registry crates-io`");
} else {
bail!("crates-io is replaced with non-remote-registry source {replacement_sid};\ninclude `--registry crates-io` to use crates.io");
}
} else {
Ok(RegistrySourceIds {
original: sid,
replacement: builtin_replacement_sid,
})
}
}
struct RegistrySourceIds {
original: SourceId,
replacement: SourceId,
}
pub fn search(
query: &str,
config: &Config,
index: Option<String>,
limit: u32,
reg: Option<String>,
) -> CargoResult<()> {
let (mut registry, source_ids) =
registry(config, None, index.as_deref(), reg.as_deref(), false, None)?;
let (crates, total_crates) = registry.search(query, limit).with_context(|| {
format!(
"failed to retrieve search results from the registry at {}",
registry.host()
)
})?;
let names = crates
.iter()
.map(|krate| format!("{} = \"{}\"", krate.name, krate.max_version))
.collect::<Vec<String>>();
let description_margin = names.iter().map(|s| s.len() + 4).max().unwrap_or_default();
let description_length = cmp::max(80, 128 - description_margin);
let descriptions = crates.iter().map(|krate| {
krate
.description
.as_ref()
.map(|desc| truncate_with_ellipsis(&desc.replace("\n", " "), description_length))
});
for (name, description) in names.into_iter().zip(descriptions) {
let line = match description {
Some(desc) => {
let space = repeat(' ')
.take(description_margin - name.len())
.collect::<String>();
name + &space + "# " + &desc
}
None => name,
};
let mut fragments = line.split(query).peekable();
while let Some(fragment) = fragments.next() {
let _ = config.shell().write_stdout(fragment, &ColorSpec::new());
if fragments.peek().is_some() {
let _ = config
.shell()
.write_stdout(query, &ColorSpec::new().set_bold(true).set_fg(Some(Green)));
}
}
let _ = config.shell().write_stdout("\n", &ColorSpec::new());
}
let search_max_limit = 100;
if total_crates > limit && limit < search_max_limit {
let _ = config.shell().write_stdout(
format_args!(
"... and {} crates more (use --limit N to see more)\n",
total_crates - limit
),
&ColorSpec::new(),
);
} else if total_crates > limit && limit >= search_max_limit {
let extra = if source_ids.original.is_crates_io() {
let url = Url::parse_with_params("https://crates.io/search", &[("q", query)])?;
format!(" (go to {url} to see more)")
} else {
String::new()
};
let _ = config.shell().write_stdout(
format_args!("... and {} crates more{}\n", total_crates - limit, extra),
&ColorSpec::new(),
);
}
Ok(())
}