diff --git a/src/bin/cargo/commands/login.rs b/src/bin/cargo/commands/login.rs index eea176748e4..c46f4aacbf1 100644 --- a/src/bin/cargo/commands/login.rs +++ b/src/bin/cargo/commands/login.rs @@ -19,29 +19,49 @@ pub fn cli() -> App { } pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { + // 1. Get the registry src for this command let registry = args.registry(config)?; + let src_id = match ®istry { + Some(registry) => SourceId::alt_registry(config, registry), + None => SourceId::crates_io(config), + }?; + let mut src = RegistrySource::remote(&src_id, config); + // 2. Update the src so that we have the latest information + src.update()?; + + // 3. Check if this registry supports cargo login v1 + let reg_cfg = src.config()?.unwrap(); + if !reg_cfg.commands.get("login").unwrap_or(&vec![]).iter().any(|v| v == "v1") { + let registry = match ®istry { + Some(registry) => registry, + None => "crates-io", + }; + Err(format_err!("`{}` does not support the `cargo login` command with \ + version `v1`", registry))?; + } + + // 4. Either we already have the token, or we get it from the command line let token = match args.value_of("token") { Some(token) => token.to_string(), None => { - let host = match registry { - Some(ref _registry) => { - return Err(format_err!( - "token must be provided when \ - --registry is provided." - ).into()); - } - None => { - let src = SourceId::crates_io(config)?; - let mut src = RegistrySource::remote(&src, config); - src.update()?; - let config = src.config()?.unwrap(); - args.value_of("host") - .map(|s| s.to_string()) - .unwrap_or_else(|| config.api.unwrap()) - } + // Print instructions to stdout. The exact wording of the message is determined by + // whether or not the user passed `--registry`. + let host = args.value_of("host") + .map(|s| s.to_string()) + .unwrap_or_else(|| reg_cfg.api.unwrap()); + let separator = match host.ends_with("/") { + true => "", + false => "/", }; - println!("please visit {}me and paste the API Token below", host); + if registry.is_some() { + println!("please paste the API Token below (you may be able to obtain your token + by visiting {}{}me", host, separator); + } else { + println!("please visit {}{}me and paste the API Token below", host, separator); + } + + // Read the token from stdin. let mut line = String::new(); let input = io::stdin(); input diff --git a/src/cargo/ops/registry.rs b/src/cargo/ops/registry.rs index 0a2244bb794..0f997c2151e 100644 --- a/src/cargo/ops/registry.rs +++ b/src/cargo/ops/registry.rs @@ -315,14 +315,15 @@ pub fn registry( } = registry_configuration(config, registry.clone())?; let token = token.or(token_config); let sid = get_source_id(config, index_config.or(index), registry)?; - let api_host = { + let (api_host, commands) = { let mut src = RegistrySource::remote(&sid, config); src.update() .chain_err(|| format!("failed to update {}", sid))?; - (src.config()?).unwrap().api.unwrap() + let cfg = src.config()?.unwrap(); + (cfg.api.unwrap(), cfg.commands) }; let handle = http_handle(config)?; - Ok((Registry::new_handle(api_host, token, handle), sid)) + Ok((Registry::new_handle(api_host, commands, token, handle), sid)) } /// Create a new HTTP handle with appropriate global configuration for cargo. @@ -599,11 +600,12 @@ pub fn search( .chain_err(|| format!("failed to update {}", &sid))?; regsrc.config()? } - }; + }.unwrap(); - let api_host = cfg.unwrap().api.unwrap(); + let api_host = cfg.api.unwrap(); + let commands = cfg.commands; let handle = http_handle(config)?; - let mut registry = Registry::new_handle(api_host, None, handle); + let mut registry = Registry::new_handle(api_host, commands, None, handle); let (crates, total_crates) = registry .search(query, limit) .chain_err(|| "failed to retrieve search results from the registry")?; diff --git a/src/cargo/sources/registry/mod.rs b/src/cargo/sources/registry/mod.rs index 7dc1ea2813f..253bafc641b 100644 --- a/src/cargo/sources/registry/mod.rs +++ b/src/cargo/sources/registry/mod.rs @@ -210,6 +210,9 @@ pub struct RegistryConfig { /// API endpoint for the registry. This is what's actually hit to perform /// operations like yanks, owner modifications, publish new crates, etc. pub api: Option, + + #[serde(default)] + pub commands: BTreeMap>, } #[derive(Deserialize)] diff --git a/src/crates-io/lib.rs b/src/crates-io/lib.rs index c3267c706fc..0d4bd924557 100644 --- a/src/crates-io/lib.rs +++ b/src/crates-io/lib.rs @@ -18,9 +18,11 @@ use curl::easy::{Easy, List}; use url::percent_encoding::{percent_encode, QUERY_ENCODE_SET}; pub type Result = std::result::Result; +pub type Commands = BTreeMap>; pub struct Registry { host: String, + commands: Commands, token: Option, handle: Easy, } @@ -120,19 +122,23 @@ struct Crates { meta: TotalCrates, } impl Registry { - pub fn new(host: String, token: Option) -> Registry { - Registry::new_handle(host, token, Easy::new()) + pub fn new(host: String, commands: Commands, token: Option) -> Registry { + Registry::new_handle(host, commands, token, Easy::new()) } - pub fn new_handle(host: String, token: Option, handle: Easy) -> Registry { + pub fn new_handle(host: String, commands: Commands, token: Option, handle: Easy) + -> Registry + { Registry { host, + commands, token, handle, } } pub fn add_owners(&mut self, krate: &str, owners: &[&str]) -> Result { + self.supports_command("owner", "v1")?; let body = serde_json::to_string(&OwnersReq { users: owners })?; let body = self.put(&format!("/crates/{}/owners", krate), body.as_bytes())?; assert!(serde_json::from_str::(&body)?.ok); @@ -140,6 +146,7 @@ impl Registry { } pub fn remove_owners(&mut self, krate: &str, owners: &[&str]) -> Result<()> { + self.supports_command("owner", "v1")?; let body = serde_json::to_string(&OwnersReq { users: owners })?; let body = self.delete(&format!("/crates/{}/owners", krate), Some(body.as_bytes()))?; assert!(serde_json::from_str::(&body)?.ok); @@ -147,11 +154,13 @@ impl Registry { } pub fn list_owners(&mut self, krate: &str) -> Result> { + self.supports_command("owner", "v1")?; let body = self.get(&format!("/crates/{}/owners", krate))?; Ok(serde_json::from_str::(&body)?.users) } pub fn publish(&mut self, krate: &NewCrate, tarball: &File) -> Result { + self.supports_command("publish", "v1")?; let json = serde_json::to_string(krate)?; // Prepare the body. The format of the upload request is: // @@ -227,6 +236,7 @@ impl Registry { } pub fn search(&mut self, query: &str, limit: u32) -> Result<(Vec, u32)> { + self.supports_command("search", "v1")?; let formatted_query = percent_encode(query.as_bytes(), QUERY_ENCODE_SET); let body = self.req( &format!("/crates?q={}&per_page={}", formatted_query, limit), @@ -239,17 +249,28 @@ impl Registry { } pub fn yank(&mut self, krate: &str, version: &str) -> Result<()> { + self.supports_command("yank", "v1")?; let body = self.delete(&format!("/crates/{}/{}/yank", krate, version), None)?; assert!(serde_json::from_str::(&body)?.ok); Ok(()) } pub fn unyank(&mut self, krate: &str, version: &str) -> Result<()> { + self.supports_command("yank", "v1")?; let body = self.put(&format!("/crates/{}/{}/unyank", krate, version), &[])?; assert!(serde_json::from_str::(&body)?.ok); Ok(()) } + fn supports_command(&self, cmd: &str, version: &str) -> Result<()> { + if let Some(versions) = self.commands.get(cmd) { + if versions.iter().any(|v| v == version) { + return Ok(()) + } + } + bail!("`{}` does not support `cargo {}` with version `{}`", self.host, cmd, version) + } + fn put(&mut self, path: &str, b: &[u8]) -> Result { self.handle.put(true)?; self.req(path, Some(b), Auth::Authorized) diff --git a/tests/testsuite/search.rs b/tests/testsuite/search.rs index f595c6b957e..dd77d2c95ed 100644 --- a/tests/testsuite/search.rs +++ b/tests/testsuite/search.rs @@ -5,7 +5,7 @@ use std::path::Path; use support::{cargo_process, execs}; use support::git::repo; use support::paths; -use support::registry::{api_path, registry as registry_url, registry_path}; +use support::registry::{api_path, registry as registry_url, registry_path, COMMANDS}; use support::hamcrest::assert_that; use url::Url; @@ -65,7 +65,8 @@ fn setup() { // Init a new registry let _ = repo(®istry_path()) - .file("config.json", &format!(r#"{{"dl":"{0}","api":"{0}"}}"#, api())) + .file("config.json", &format!(r#"{{"dl":"{0}","api":"{0}","commands":{1}}}"#, api(), + COMMANDS)) .build(); let base = api_path().join("api/v1/crates"); diff --git a/tests/testsuite/support/publish.rs b/tests/testsuite/support/publish.rs index cc30b46f63b..81bd4b5b737 100644 --- a/tests/testsuite/support/publish.rs +++ b/tests/testsuite/support/publish.rs @@ -4,6 +4,7 @@ use std::fs::{self, File}; use support::paths; use support::git::{repo, Repository}; +use support::registry::COMMANDS; use url::Url; @@ -39,10 +40,12 @@ pub fn setup() -> Repository { "config.json", &format!( r#"{{ - "dl": "{0}", - "api": "{0}" - }}"#, - upload() + "dl": "{0}", + "api": "{0}", + "commands": {1} + }}"#, + upload(), + COMMANDS, ), ) .build() diff --git a/tests/testsuite/support/registry.rs b/tests/testsuite/support/registry.rs index 3e07113d872..24b6d122d87 100644 --- a/tests/testsuite/support/registry.rs +++ b/tests/testsuite/support/registry.rs @@ -49,6 +49,14 @@ pub fn alt_api_url() -> Url { Url::from_file_path(&*alt_api_path()).ok().unwrap() } +pub const COMMANDS: &str = r#"{ + "publish": ["v1"], + "yank": ["v1"], + "search": ["v1"], + "owner": ["v1"], + "login": ["v1"] +}"#; + pub struct Package { name: String, vers: String, @@ -102,10 +110,9 @@ pub fn init() { .file( "config.json", &format!( - r#" - {{"dl":"{0}","api":"{0}"}} - "#, - dl_url() + r#"{{"dl":"{0}","api":"{0}","commands":{1}}}"#, + dl_url(), + COMMANDS, ), ) .build(); @@ -117,10 +124,11 @@ pub fn init() { "config.json", &format!( r#" - {{"dl":"{}","api":"{}"}} + {{"dl":"{}","api":"{}","commands":{}}} "#, alt_dl_url(), - alt_api_url() + alt_api_url(), + COMMANDS, ), ) .build();