diff --git a/app/routes/crate/version.js b/app/routes/crate/version.js index 94c08cb4046..8785c3aca39 100644 --- a/app/routes/crate/version.js +++ b/app/routes/crate/version.js @@ -29,7 +29,7 @@ export default Ember.Route.extend({ return crate.get('versions') .then(versions => { const version = versions.find(version => version.get('num') === params.version_num); - if (!version) { + if (params.version_num && !version) { this.controllerFor('application').set('nextFlashError', `Version '${params.version_num}' of crate '${crate.get('name')}' does not exist`); } diff --git a/src/bin/update-max-versions.rs b/src/bin/update-max-versions.rs new file mode 100644 index 00000000000..2448250d21f --- /dev/null +++ b/src/bin/update-max-versions.rs @@ -0,0 +1,105 @@ +// Update the max_version for all crates. +// +// Usage: +// cargo run --bin update-max-versions + +#![deny(warnings)] + +extern crate cargo_registry; +extern crate postgres; +extern crate semver; + +fn main() { + let conn = cargo_registry::db::connect_now(); + { + let tx = conn.transaction().unwrap(); + update(&tx); + tx.set_commit(); + tx.finish().unwrap(); + } +} + +fn update(tx: &postgres::transaction::Transaction) { + let crate_ids = tx.query("SELECT id FROM crates", &[]).unwrap(); + for crate_id in crate_ids.iter() { + let crate_id: i32 = crate_id.get("id"); + let new_max = tx.query("SELECT num FROM versions WHERE crate_id = $1 AND yanked = FALSE", + &[&crate_id]).unwrap() + .iter() + .map(|r| r.get::<&str, String>("num")) + .filter_map(|v| semver::Version::parse(&v).ok()) + .max(); + tx.execute("UPDATE crates SET max_version = $1 WHERE id = $2", + &[&new_max.map(|v| v.to_string()), &crate_id]).unwrap(); + } +} + +#[cfg(test)] +mod test { + use std::collections::HashMap; + + use postgres; + use semver; + + use cargo_registry::{Version, Crate, User, Model, env}; + + fn conn() -> postgres::Connection { + postgres::Connection::connect(&env("TEST_DATABASE_URL")[..], + postgres::TlsMode::None).unwrap() + } + + fn user(conn: &postgres::transaction::Transaction) -> User{ + User::find_or_insert(conn, 2, "login", None, None, None, + "access_token", "api_token").unwrap() + } + + #[test] + fn max_to_null() { + let conn = conn(); + let tx = conn.transaction().unwrap(); + let user = user(&tx); + let krate = Crate::find_or_insert(&tx, "foo", user.id, &None, &None, + &None, &None, &None, &None, + &None, None).unwrap(); + let v1 = semver::Version::parse("1.0.0").unwrap(); + let version = Version::insert(&tx, krate.id, &v1, &HashMap::new(), &[]).unwrap(); + version.yank(&conn, true).unwrap(); + ::update(&tx); + assert_eq!(Crate::find(&tx, krate.id).unwrap().max_version, None); + } + + #[test] + fn max_to_same() { + let conn = conn(); + let tx = conn.transaction().unwrap(); + let user = user(&tx); + let krate = Crate::find_or_insert(&tx, "foo", user.id, &None, &None, + &None, &None, &None, &None, + &None, None).unwrap(); + let v1 = semver::Version::parse("1.0.0").unwrap(); + Version::insert(&tx, krate.id, &v1, &HashMap::new(), &[]).unwrap(); + ::update(&tx); + assert_eq!(Crate::find(&tx, krate.id).unwrap().max_version, Some(v1)); + } + + #[test] + fn multiple_crates() { + let conn = conn(); + let tx = conn.transaction().unwrap(); + let user = user(&tx); + let krate1 = Crate::find_or_insert(&tx, "foo1", user.id, &None, &None, + &None, &None, &None, &None, + &None, None).unwrap(); + let krate2 = Crate::find_or_insert(&tx, "foo2", user.id, &None, &None, + &None, &None, &None, &None, + &None, None).unwrap(); + let v1 = semver::Version::parse("1.0.0").unwrap(); + let krate1_ver = Version::insert(&tx, krate1.id, &v1, &HashMap::new(), + &[]).unwrap(); + Version::insert(&tx, krate2.id, &v1, &HashMap::new(), &[]).unwrap(); + krate1_ver.yank(&conn, true).unwrap(); + ::update(&tx); + assert_eq!(Crate::find(&tx, krate1.id).unwrap().max_version, None); + assert_eq!(Crate::find(&tx, krate2.id).unwrap().max_version, Some(v1)); + } +} diff --git a/src/krate.rs b/src/krate.rs index b2057221c52..fba1dca81b0 100644 --- a/src/krate.rs +++ b/src/krate.rs @@ -44,7 +44,7 @@ pub struct Crate { pub updated_at: Timespec, pub created_at: Timespec, pub downloads: i32, - pub max_version: semver::Version, + pub max_version: Option, pub description: Option, pub homepage: Option, pub documentation: Option, @@ -65,7 +65,7 @@ pub struct EncodableCrate { pub badges: Option>, pub created_at: String, pub downloads: i32, - pub max_version: String, + pub max_version: Option, pub description: Option, pub homepage: Option, pub documentation: Option, @@ -266,7 +266,7 @@ impl Crate { keywords: keyword_ids, categories: category_ids, badges: badges, - max_version: max_version.to_string(), + max_version: max_version.map(|v| v.to_string()), documentation: documentation, homepage: homepage, description: description, @@ -384,12 +384,18 @@ impl Crate { None => {} } let zero = semver::Version::parse("0.0.0").unwrap(); - if *ver > self.max_version || self.max_version == zero { - self.max_version = ver.clone(); + let new_max = match self.max_version { + None => true, + Some(ref max_version) if *ver > *max_version || *max_version == zero => true, + _ => false, + }; + if new_max { + self.max_version = Some(ver.clone()); } let stmt = conn.prepare("UPDATE crates SET max_version = $1 WHERE id = $2 RETURNING updated_at")?; - let rows = stmt.query(&[&self.max_version.to_string(), &self.id])?; + let max_version = self.max_version.clone().map(|v| v.to_string()); + let rows = stmt.query(&[&max_version, &self.id])?; self.updated_at = rows.get(0).get("updated_at"); Version::insert(conn, self.id, ver, features, authors) } @@ -460,7 +466,7 @@ impl Crate { impl Model for Crate { fn from_row(row: &Row) -> Crate { - let max: String = row.get("max_version"); + let max: Option = row.get("max_version"); Crate { id: row.get("id"), name: row.get("name"), @@ -471,7 +477,7 @@ impl Model for Crate { documentation: row.get("documentation"), homepage: row.get("homepage"), readme: row.get("readme"), - max_version: semver::Version::parse(&max).unwrap(), + max_version: max.map(|m| semver::Version::parse(&m).unwrap()), license: row.get("license"), repository: row.get("repository"), max_upload_size: row.get("max_upload_size"), diff --git a/src/tests/all.rs b/src/tests/all.rs index a008de37d67..cf4b6315a88 100755 --- a/src/tests/all.rs +++ b/src/tests/all.rs @@ -193,7 +193,7 @@ fn krate(name: &str) -> Crate { updated_at: time::now().to_timespec(), created_at: time::now().to_timespec(), downloads: 10, - max_version: semver::Version::parse("0.0.0").unwrap(), + max_version: Some(semver::Version::parse("0.0.0").unwrap()), documentation: None, homepage: None, description: None, diff --git a/src/tests/http-data/krate_publish_after_yank_max_version b/src/tests/http-data/krate_publish_after_yank_max_version new file mode 100644 index 00000000000..2de5bd31263 --- /dev/null +++ b/src/tests/http-data/krate_publish_after_yank_max_version @@ -0,0 +1,41 @@ +===REQUEST 339 +PUT http://alexcrichton-test.s3.amazonaws.com/crates/fyk_max/fyk_max-1.0.0.crate HTTP/1.1 +Accept: */* +Proxy-Connection: Keep-Alive +Authorization: AWS AKIAJF3GEK7N44BACDZA:GDxGb6r3SIqo9wXuzHrgMNWekwk= +Content-Length: 0 +Host: alexcrichton-test.s3.amazonaws.com +Content-Type: application/x-tar +Date: Sun, 28 Jun 2015 14:07:17 -0700 + + +===RESPONSE 258 +HTTP/1.1 200 +x-amz-request-id: CB0E925D8E3AB3E8 +x-amz-id-2: SiaMwszM1p2TzXlLauvZ6kRKcUCg7HoyBW29vts42w9ArrLwkJWl8vuvPuGFkpM6XGH+YXN852g= +date: Sun, 28 Jun 2015 21:07:51 GMT +etag: "d41d8cd98f00b204e9800998ecf8427e" +content-length: 0 +server: AmazonS3 + + +===REQUEST 339 +PUT http://alexcrichton-test.s3.amazonaws.com/crates/fyk_max/fyk_max-2.0.0.crate HTTP/1.1 +Accept: */* +Proxy-Connection: Keep-Alive +Authorization: AWS AKIAJF3GEK7N44BACDZA:GDxGb6r3SIqo9wXuzHrgMNWekwk= +Content-Length: 0 +Host: alexcrichton-test.s3.amazonaws.com +Content-Type: application/x-tar +Date: Sun, 28 Jun 2015 14:07:17 -0700 + + +===RESPONSE 258 +HTTP/1.1 200 +x-amz-request-id: CB0E925D8E3AB3E8 +x-amz-id-2: SiaMwszM1p2TzXlLauvZ6kRKcUCg7HoyBW29vts42w9ArrLwkJWl8vuvPuGFkpM6XGH+YXN852g= +date: Sun, 28 Jun 2015 21:07:51 GMT +etag: "d41d8cd98f00b204e9800998ecf8427e" +content-length: 0 +server: AmazonS3 + diff --git a/src/tests/http-data/krate_yank_max_version b/src/tests/http-data/krate_yank_max_version new file mode 100644 index 00000000000..2de5bd31263 --- /dev/null +++ b/src/tests/http-data/krate_yank_max_version @@ -0,0 +1,41 @@ +===REQUEST 339 +PUT http://alexcrichton-test.s3.amazonaws.com/crates/fyk_max/fyk_max-1.0.0.crate HTTP/1.1 +Accept: */* +Proxy-Connection: Keep-Alive +Authorization: AWS AKIAJF3GEK7N44BACDZA:GDxGb6r3SIqo9wXuzHrgMNWekwk= +Content-Length: 0 +Host: alexcrichton-test.s3.amazonaws.com +Content-Type: application/x-tar +Date: Sun, 28 Jun 2015 14:07:17 -0700 + + +===RESPONSE 258 +HTTP/1.1 200 +x-amz-request-id: CB0E925D8E3AB3E8 +x-amz-id-2: SiaMwszM1p2TzXlLauvZ6kRKcUCg7HoyBW29vts42w9ArrLwkJWl8vuvPuGFkpM6XGH+YXN852g= +date: Sun, 28 Jun 2015 21:07:51 GMT +etag: "d41d8cd98f00b204e9800998ecf8427e" +content-length: 0 +server: AmazonS3 + + +===REQUEST 339 +PUT http://alexcrichton-test.s3.amazonaws.com/crates/fyk_max/fyk_max-2.0.0.crate HTTP/1.1 +Accept: */* +Proxy-Connection: Keep-Alive +Authorization: AWS AKIAJF3GEK7N44BACDZA:GDxGb6r3SIqo9wXuzHrgMNWekwk= +Content-Length: 0 +Host: alexcrichton-test.s3.amazonaws.com +Content-Type: application/x-tar +Date: Sun, 28 Jun 2015 14:07:17 -0700 + + +===RESPONSE 258 +HTTP/1.1 200 +x-amz-request-id: CB0E925D8E3AB3E8 +x-amz-id-2: SiaMwszM1p2TzXlLauvZ6kRKcUCg7HoyBW29vts42w9ArrLwkJWl8vuvPuGFkpM6XGH+YXN852g= +date: Sun, 28 Jun 2015 21:07:51 GMT +etag: "d41d8cd98f00b204e9800998ecf8427e" +content-length: 0 +server: AmazonS3 + diff --git a/src/tests/krate.rs b/src/tests/krate.rs index 8b60d8b77ef..d57bafbe68e 100644 --- a/src/tests/krate.rs +++ b/src/tests/krate.rs @@ -347,7 +347,8 @@ fn new_krate() { let mut response = ok_resp!(middle.call(&mut req)); let json: GoodCrate = ::json(&mut response); assert_eq!(json.krate.name, "foo_new"); - assert_eq!(json.krate.max_version, "1.0.0"); + assert!(json.krate.max_version.is_some()); + assert_eq!(json.krate.max_version.unwrap(), "1.0.0"); } #[test] @@ -360,7 +361,8 @@ fn new_krate_weird_version() { let mut response = ok_resp!(middle.call(&mut req)); let json: GoodCrate = ::json(&mut response); assert_eq!(json.krate.name, "foo_weird"); - assert_eq!(json.krate.max_version, "0.0.0-pre"); + assert!(json.krate.max_version.is_some()); + assert_eq!(json.krate.max_version.unwrap(), "0.0.0-pre"); } #[test] @@ -869,6 +871,140 @@ fn yank_not_owner() { ::json::<::Bad>(&mut response); } +#[test] +fn yank_max_version() { + #[derive(RustcDecodable)] + struct O { + ok: bool, + } + let (_b, app, middle) = ::app(); + + // Upload a new crate + let mut req = ::new_req(app, "fyk_max", "1.0.0"); + ::mock_user(&mut req, ::user("foo")); + let mut response = ok_resp!(middle.call(&mut req)); + + // double check the max version + let json: GoodCrate = ::json(&mut response); + assert!(json.krate.max_version.is_some()); + assert_eq!(json.krate.max_version.unwrap(), "1.0.0"); + + // add version 2.0.0 + let body = ::new_req_body_version_2(::krate("fyk_max")); + let mut response = ok_resp!(middle.call(req.with_path("/api/v1/crates/new") + .with_method(Method::Put) + .with_body(&body))); + let json: GoodCrate = ::json(&mut response); + assert!(json.krate.max_version.is_some()); + assert_eq!(json.krate.max_version.unwrap(), "2.0.0"); + + // yank version 1.0.0 + let mut r = ok_resp!(middle.call(req.with_method(Method::Delete) + .with_path("/api/v1/crates/fyk_max/1.0.0/yank"))); + assert!(::json::(&mut r).ok); + let mut response = ok_resp!(middle.call(req.with_method(Method::Get) + .with_path("/api/v1/crates/fyk_max"))); + let json: CrateResponse = ::json(&mut response); + assert!(json.krate.max_version.is_some()); + assert_eq!(json.krate.max_version.unwrap(), "2.0.0"); + + // unyank version 1.0.0 + let mut r = ok_resp!(middle.call(req.with_method(Method::Put) + .with_path("/api/v1/crates/fyk_max/1.0.0/unyank"))); + assert!(::json::(&mut r).ok); + let mut response = ok_resp!(middle.call(req.with_method(Method::Get) + .with_path("/api/v1/crates/fyk_max"))); + let json: CrateResponse = ::json(&mut response); + assert!(json.krate.max_version.is_some()); + assert_eq!(json.krate.max_version.unwrap(), "2.0.0"); + + // yank version 2.0.0 + let mut r = ok_resp!(middle.call(req.with_method(Method::Delete) + .with_path("/api/v1/crates/fyk_max/2.0.0/yank"))); + assert!(::json::(&mut r).ok); + let mut response = ok_resp!(middle.call(req.with_method(Method::Get) + .with_path("/api/v1/crates/fyk_max"))); + let json: CrateResponse = ::json(&mut response); + assert!(json.krate.max_version.is_some()); + assert_eq!(json.krate.max_version.unwrap(), "1.0.0"); + + // yank version 1.0.0 + let mut r = ok_resp!(middle.call(req.with_method(Method::Delete) + .with_path("/api/v1/crates/fyk_max/1.0.0/yank"))); + assert!(::json::(&mut r).ok); + let mut response = ok_resp!(middle.call(req.with_method(Method::Get) + .with_path("/api/v1/crates/fyk_max"))); + let json: CrateResponse = ::json(&mut response); + assert!(json.krate.max_version.is_none()); + + // unyank version 2.0.0 + let mut r = ok_resp!(middle.call(req.with_method(Method::Put) + .with_path("/api/v1/crates/fyk_max/2.0.0/unyank"))); + assert!(::json::(&mut r).ok); + let mut response = ok_resp!(middle.call(req.with_method(Method::Get) + .with_path("/api/v1/crates/fyk_max"))); + let json: CrateResponse = ::json(&mut response); + assert!(json.krate.max_version.is_some()); + assert_eq!(json.krate.max_version.unwrap(), "2.0.0"); + + // unyank version 1.0.0 + let mut r = ok_resp!(middle.call(req.with_method(Method::Put) + .with_path("/api/v1/crates/fyk_max/1.0.0/unyank"))); + assert!(::json::(&mut r).ok); + let mut response = ok_resp!(middle.call(req.with_method(Method::Get) + .with_path("/api/v1/crates/fyk_max"))); + let json: CrateResponse = ::json(&mut response); + assert!(json.krate.max_version.is_some()); + assert_eq!(json.krate.max_version.unwrap(), "2.0.0"); +} + +#[test] +fn publish_after_yank_max_version() { + #[derive(RustcDecodable)] + struct O { + ok: bool, + } + let (_b, app, middle) = ::app(); + + // Upload a new crate + let mut req = ::new_req(app, "fyk_max", "1.0.0"); + ::mock_user(&mut req, ::user("foo")); + let mut response = ok_resp!(middle.call(&mut req)); + + // double check the max version + let json: GoodCrate = ::json(&mut response); + assert!(json.krate.max_version.is_some()); + assert_eq!(json.krate.max_version.unwrap(), "1.0.0"); + + // yank version 1.0.0 + let mut r = ok_resp!(middle.call(req.with_method(Method::Delete) + .with_path("/api/v1/crates/fyk_max/1.0.0/yank"))); + assert!(::json::(&mut r).ok); + let mut response = ok_resp!(middle.call(req.with_method(Method::Get) + .with_path("/api/v1/crates/fyk_max"))); + let json: CrateResponse = ::json(&mut response); + assert!(json.krate.max_version.is_none()); + + // add version 2.0.0 + let body = ::new_req_body_version_2(::krate("fyk_max")); + let mut response = ok_resp!(middle.call(req.with_path("/api/v1/crates/new") + .with_method(Method::Put) + .with_body(&body))); + let json: GoodCrate = ::json(&mut response); + assert!(json.krate.max_version.is_some()); + assert_eq!(json.krate.max_version.unwrap(), "2.0.0"); + + // unyank version 1.0.0 + let mut r = ok_resp!(middle.call(req.with_method(Method::Put) + .with_path("/api/v1/crates/fyk_max/1.0.0/unyank"))); + assert!(::json::(&mut r).ok); + let mut response = ok_resp!(middle.call(req.with_method(Method::Get) + .with_path("/api/v1/crates/fyk_max"))); + let json: CrateResponse = ::json(&mut response); + assert!(json.krate.max_version.is_some()); + assert_eq!(json.krate.max_version.unwrap(), "2.0.0"); +} + #[test] fn bad_keywords() { let (_b, app, middle) = ::app(); @@ -917,7 +1053,8 @@ fn good_categories() { let mut response = ok_resp!(middle.call(&mut req)); let json: GoodCrate = ::json(&mut response); assert_eq!(json.krate.name, "foo_good_cat"); - assert_eq!(json.krate.max_version, "1.0.0"); + assert!(json.krate.max_version.is_some()); + assert_eq!(json.krate.max_version.unwrap(), "1.0.0"); assert_eq!(json.warnings.invalid_categories.len(), 0); } @@ -931,7 +1068,8 @@ fn ignored_categories() { let mut response = ok_resp!(middle.call(&mut req)); let json: GoodCrate = ::json(&mut response); assert_eq!(json.krate.name, "foo_ignored_cat"); - assert_eq!(json.krate.max_version, "1.0.0"); + assert!(json.krate.max_version.is_some()); + assert_eq!(json.krate.max_version.unwrap(), "1.0.0"); assert_eq!(json.warnings.invalid_categories, vec!["bar".to_string()]); } @@ -954,7 +1092,8 @@ fn good_badges() { let json: GoodCrate = ::json(&mut response); assert_eq!(json.krate.name, "foobadger"); - assert_eq!(json.krate.max_version, "1.0.0"); + assert!(json.krate.max_version.is_some()); + assert_eq!(json.krate.max_version.unwrap(), "1.0.0"); let mut response = ok_resp!( middle.call(req.with_method(Method::Get) @@ -1000,7 +1139,8 @@ fn ignored_badges() { let json: GoodCrate = ::json(&mut response); assert_eq!(json.krate.name, "foo_ignored_badge"); - assert_eq!(json.krate.max_version, "1.0.0"); + assert!(json.krate.max_version.is_some()); + assert_eq!(json.krate.max_version.unwrap(), "1.0.0"); assert_eq!(json.warnings.invalid_badges.len(), 2); assert!(json.warnings.invalid_badges.contains(&"travis-ci".to_string())); assert!(json.warnings.invalid_badges.contains(&"not-a-badge".to_string())); diff --git a/src/version.rs b/src/version.rs index 19cb08ab57d..2dc62b32448 100644 --- a/src/version.rs +++ b/src/version.rs @@ -193,6 +193,34 @@ impl Version { pub fn yank(&self, conn: &GenericConnection, yanked: bool) -> CargoResult<()> { conn.execute("UPDATE versions SET yanked = $1 WHERE id = $2", &[&yanked, &self.id])?; + + let rows = conn.query("SELECT max_version FROM crates WHERE id = $1", + &[&self.crate_id])?; + let max_version = rows.iter() + .next() + .and_then(|r| r.get::<&str, Option>("max_version")) + .map(|v| semver::Version::parse(&v).unwrap()); + let max_update_stmt = conn.prepare("UPDATE crates SET max_version = $1 WHERE id = $2")?; + if let Some(max_version) = max_version { + let zero = semver::Version::parse("0.0.0").unwrap(); + if yanked && max_version == self.num { + let new_max = + conn.query("SELECT num FROM versions WHERE crate_id = $1 AND yanked = FALSE \ + AND id != $2", + &[&self.crate_id, &self.id])? + .iter() + .map(|r| r.get::<&str, String>("num")) + .filter_map(|v| semver::Version::parse(&v).ok()) + .max(); + max_update_stmt.execute(&[&new_max.map(|v| v.to_string()), &self.crate_id])?; + } else if !yanked && (self.num > max_version || max_version == zero) { + max_update_stmt.execute(&[&self.num.to_string(), &self.crate_id])?; + } + } else if !yanked { + // no max version, all versions yanked + max_update_stmt.execute(&[&self.num.to_string(), &self.crate_id])?; + } + Ok(()) } }