From d66e540015a74f102490b0b2493e3a703f76e624 Mon Sep 17 00:00:00 2001 From: Without Boats Date: Mon, 20 Aug 2018 23:30:34 +0200 Subject: [PATCH 1/3] Registries report which commands they support. In the `config.json` of each registry's index, that registry can report which commands it supports, under which versions. For example, crates.io's config.json should look like: ```json { "api": "https://crates.io/", "dl": "https://crates.io/api/v1/crates", "commands": { "publish": ["v1"], "yank": ["v1"], "owner": ["v1"], "search": ["v1"], "login": ["v1"] } } ``` Currently, these five commands only have one version, "v1," but this allows us to release new, breaking changes to these commands' interface with crates.io without disrupting the operation of any alternative registry. Before this is merged, crates.io's config.json will need to be updated, and we should make an effort to inform known maintainers of alternative registries that they need to update their indices as well. --- src/bin/cargo/commands/login.rs | 54 ++++++++++++++++++++--------- src/cargo/ops/registry.rs | 14 ++++---- src/cargo/sources/registry/mod.rs | 3 ++ src/crates-io/lib.rs | 27 +++++++++++++-- tests/testsuite/search.rs | 5 +-- tests/testsuite/support/publish.rs | 5 ++- tests/testsuite/support/registry.rs | 17 ++++++--- 7 files changed, 92 insertions(+), 33 deletions(-) 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..62c0a73456e 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..19894763f29 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; @@ -40,9 +41,11 @@ pub fn setup() -> Repository { &format!( r#"{{ "dl": "{0}", - "api": "{0}" + "api": "{0}", + "commands": "{1}" }}"#, upload() + COMMANDS, ), ) .build() diff --git a/tests/testsuite/support/registry.rs b/tests/testsuite/support/registry.rs index 3e07113d872..b974863b5a8 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, @@ -103,9 +111,9 @@ pub fn init() { "config.json", &format!( r#" - {{"dl":"{0}","api":"{0}"}} + {{"dl":"{0}","api":"{0}","commands":"{1}"}} "#, - dl_url() + dl_url(), COMMANDS, ), ) .build(); @@ -117,10 +125,11 @@ pub fn init() { "config.json", &format!( r#" - {{"dl":"{}","api":"{}"}} + {{"dl":"{}","api":"{}","commands":"{}"}} "#, alt_dl_url(), - alt_api_url() + alt_api_url(), + COMMANDS, ), ) .build(); From e696de6e13cb2eb60e52bd34669ef3ecfbc1099f Mon Sep 17 00:00:00 2001 From: Without Boats Date: Tue, 21 Aug 2018 00:01:41 +0200 Subject: [PATCH 2/3] Fix tests --- tests/testsuite/support/publish.rs | 10 +++++----- tests/testsuite/support/registry.rs | 9 ++++----- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/tests/testsuite/support/publish.rs b/tests/testsuite/support/publish.rs index 19894763f29..cadcfcf7b68 100644 --- a/tests/testsuite/support/publish.rs +++ b/tests/testsuite/support/publish.rs @@ -40,11 +40,11 @@ pub fn setup() -> Repository { "config.json", &format!( r#"{{ - "dl": "{0}", - "api": "{0}", - "commands": "{1}" - }}"#, - upload() + "dl": "{0}", + "api": "{0}", + "commands": "{1}" + }}"#, + upload(), COMMANDS, ), ) diff --git a/tests/testsuite/support/registry.rs b/tests/testsuite/support/registry.rs index b974863b5a8..edda7d5578e 100644 --- a/tests/testsuite/support/registry.rs +++ b/tests/testsuite/support/registry.rs @@ -55,7 +55,7 @@ pub const COMMANDS: &str = r#"{ "search": ["v1"], "owner": ["v1"], "login": ["v1"] -}"; +}"#; pub struct Package { name: String, @@ -110,10 +110,9 @@ pub fn init() { .file( "config.json", &format!( - r#" - {{"dl":"{0}","api":"{0}","commands":"{1}"}} - "#, - dl_url(), COMMANDS, + r#"{{"dl":"{0}","api":"{0}","commands":"{1}"}}"#, + dl_url(), + COMMANDS, ), ) .build(); From 8dfdc197f8f92a3c97537beae42c58caaa778123 Mon Sep 17 00:00:00 2001 From: Without Boats Date: Tue, 21 Aug 2018 00:28:48 +0200 Subject: [PATCH 3/3] Fix tests again --- tests/testsuite/search.rs | 2 +- tests/testsuite/support/publish.rs | 2 +- tests/testsuite/support/registry.rs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/testsuite/search.rs b/tests/testsuite/search.rs index 62c0a73456e..dd77d2c95ed 100644 --- a/tests/testsuite/search.rs +++ b/tests/testsuite/search.rs @@ -65,7 +65,7 @@ fn setup() { // Init a new registry let _ = repo(®istry_path()) - .file("config.json", &format!(r#"{{"dl":"{0}","api":"{0}","commands":"{1}"}}"#, api(), + .file("config.json", &format!(r#"{{"dl":"{0}","api":"{0}","commands":{1}}}"#, api(), COMMANDS)) .build(); diff --git a/tests/testsuite/support/publish.rs b/tests/testsuite/support/publish.rs index cadcfcf7b68..81bd4b5b737 100644 --- a/tests/testsuite/support/publish.rs +++ b/tests/testsuite/support/publish.rs @@ -42,7 +42,7 @@ pub fn setup() -> Repository { r#"{{ "dl": "{0}", "api": "{0}", - "commands": "{1}" + "commands": {1} }}"#, upload(), COMMANDS, diff --git a/tests/testsuite/support/registry.rs b/tests/testsuite/support/registry.rs index edda7d5578e..24b6d122d87 100644 --- a/tests/testsuite/support/registry.rs +++ b/tests/testsuite/support/registry.rs @@ -110,7 +110,7 @@ pub fn init() { .file( "config.json", &format!( - r#"{{"dl":"{0}","api":"{0}","commands":"{1}"}}"#, + r#"{{"dl":"{0}","api":"{0}","commands":{1}}}"#, dl_url(), COMMANDS, ), @@ -124,7 +124,7 @@ pub fn init() { "config.json", &format!( r#" - {{"dl":"{}","api":"{}","commands":"{}"}} + {{"dl":"{}","api":"{}","commands":{}}} "#, alt_dl_url(), alt_api_url(),