diff --git a/app/routes/crate/version.js b/app/routes/crate/version.js index 94c08cb4046..4f26c77f9aa 100644 --- a/app/routes/crate/version.js +++ b/app/routes/crate/version.js @@ -11,7 +11,7 @@ export default Ember.Route.extend({ const maxVersion = crate.get('max_version'); // Fall back to the crate's `max_version` property - if (!requestedVersion) { + if (!requestedVersion && maxVersion !== '0.0.0') { params.version_num = maxVersion; } @@ -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/krate.rs b/src/krate.rs index 6a228471f6e..d4f4afb389a 100644 --- a/src/krate.rs +++ b/src/krate.rs @@ -44,7 +44,6 @@ pub struct Crate { pub updated_at: Timespec, pub created_at: Timespec, pub downloads: i32, - pub max_version: semver::Version, pub description: Option, pub homepage: Option, pub documentation: Option, @@ -233,18 +232,20 @@ impl Crate { } pub fn minimal_encodable(self, + max_version: semver::Version, badges: Option>) -> EncodableCrate { - self.encodable(None, None, None, badges) + self.encodable(max_version, None, None, None, badges) } pub fn encodable(self, + max_version: semver::Version, versions: Option>, keywords: Option<&[Keyword]>, categories: Option<&[Category]>, badges: Option>) -> EncodableCrate { let Crate { - name, created_at, updated_at, downloads, max_version, description, + name, created_at, updated_at, downloads, description, homepage, documentation, license, repository, readme: _, id: _, max_upload_size: _, } = self; @@ -255,7 +256,7 @@ impl Crate { let keyword_ids = keywords.map(|kws| kws.iter().map(|kw| kw.keyword.clone()).collect()); let category_ids = categories.map(|cats| cats.iter().map(|cat| cat.slug.clone()).collect()); let badges = badges.map(|bs| { - bs.iter().map(|b| b.clone().encodable()).collect() + bs.into_iter().map(|b| b.encodable()).collect() }); EncodableCrate { id: name.clone(), @@ -282,6 +283,16 @@ impl Crate { } } + pub fn max_version(&self, conn: &GenericConnection) -> CargoResult { + let stmt = conn.prepare("SELECT num FROM versions WHERE crate_id = $1 + AND yanked = 'f'")?; + let rows = stmt.query(&[&self.id])?; + Ok(rows.iter() + .map(|r| semver::Version::parse(&r.get::<_, String>("num")).unwrap()) + .max() + .unwrap_or_else(|| semver::Version::parse("0.0.0").unwrap())) + } + pub fn versions(&self, conn: &GenericConnection) -> CargoResult> { let stmt = conn.prepare("SELECT * FROM versions \ WHERE crate_id = $1")?; @@ -384,14 +395,6 @@ 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 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])?; - self.updated_at = rows.get(0).get("updated_at"); Version::insert(conn, self.id, ver, features, authors) } @@ -434,7 +437,7 @@ impl Crate { INNER JOIN crates ON crates.id = versions.crate_id WHERE dependencies.crate_id = $1 - AND versions.num = crates.max_version + AND versions.num = $2 "; let fetch_sql = format!("SELECT DISTINCT ON (crate_downloads, crate_name) dependencies.*, @@ -442,18 +445,19 @@ impl Crate { crates.name AS crate_name {} ORDER BY crate_downloads DESC - OFFSET $2 - LIMIT $3", + OFFSET $3 + LIMIT $4", select_sql); let count_sql = format!("SELECT COUNT(DISTINCT(crates.id)) {}", select_sql); let stmt = conn.prepare(&fetch_sql)?; - let vec: Vec<_> = stmt.query(&[&self.id, &offset, &limit])? + let max_version = self.max_version(conn)?.to_string(); + let vec: Vec<_> = stmt.query(&[&self.id, &max_version, &offset, &limit])? .iter() .map(|r| (Model::from_row(&r), r.get("crate_name"), r.get("crate_downloads"))) .collect(); let stmt = conn.prepare(&count_sql)?; - let cnt: i64 = stmt.query(&[&self.id])?.iter().next().unwrap().get(0); + let cnt: i64 = stmt.query(&[&self.id, &max_version])?.iter().next().unwrap().get(0); Ok((vec, cnt)) } @@ -461,7 +465,6 @@ impl Crate { impl Model for Crate { fn from_row(row: &Row) -> Crate { - let max: String = row.get("max_version"); Crate { id: row.get("id"), name: row.get("name"), @@ -472,7 +475,6 @@ impl Model for Crate { documentation: row.get("documentation"), homepage: row.get("homepage"), readme: row.get("readme"), - max_version: semver::Version::parse(&max).unwrap(), license: row.get("license"), repository: row.get("repository"), max_upload_size: row.get("max_upload_size"), @@ -603,8 +605,9 @@ pub fn index(req: &mut Request) -> CargoResult { let mut crates = Vec::new(); for row in stmt.query(&args)?.iter() { let krate: Crate = Model::from_row(&row); - let badges = krate.badges(conn); - crates.push(krate.minimal_encodable(badges.ok())); + let badges = krate.badges(conn)?; + let max_version = krate.max_version(conn)?; + crates.push(krate.minimal_encodable(max_version, Some(badges))); } // Query for the total count of crates @@ -637,10 +640,11 @@ pub fn summary(req: &mut Request) -> CargoResult { let to_crates = |stmt: pg::stmt::Statement| -> CargoResult> { let rows = stmt.query(&[])?; - Ok(rows.iter().map(|r| { + rows.iter().map(|r| { let krate: Crate = Model::from_row(&r); - krate.minimal_encodable(None) - }).collect::>()) + let max_version = krate.max_version(tx)?; + Ok(krate.minimal_encodable(max_version, None)) + }).collect() }; let new_crates = tx.prepare("SELECT * FROM crates \ ORDER BY created_at DESC LIMIT 10")?; @@ -692,6 +696,7 @@ pub fn show(req: &mut Request) -> CargoResult { let kws = krate.keywords(conn)?; let cats = krate.categories(conn)?; let badges = krate.badges(conn)?; + let max_version = krate.max_version(conn)?; #[derive(RustcEncodable)] struct R { @@ -702,7 +707,7 @@ pub fn show(req: &mut Request) -> CargoResult { } Ok(req.json(&R { krate: krate.clone().encodable( - Some(ids), Some(&kws), Some(&cats), Some(badges) + max_version, Some(ids), Some(&kws), Some(&cats), Some(badges) ), versions: versions.into_iter().map(|v| { v.encodable(&krate.name) @@ -785,6 +790,7 @@ pub fn new(req: &mut Request) -> CargoResult { &krate, new_crate.badges.unwrap_or_else(HashMap::new) )?; + let max_version = krate.max_version(req.tx()?)?; // Upload the crate to S3 let mut handle = req.app().handle(); @@ -855,7 +861,7 @@ pub fn new(req: &mut Request) -> CargoResult { #[derive(RustcEncodable)] struct R { krate: EncodableCrate, warnings: Warnings } Ok(req.json(&R { - krate: krate.minimal_encodable(None), + krate: krate.minimal_encodable(max_version, None), warnings: warnings })) } diff --git a/src/tests/all.rs b/src/tests/all.rs index a008de37d67..7d4acf62173 100755 --- a/src/tests/all.rs +++ b/src/tests/all.rs @@ -193,7 +193,6 @@ 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(), 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 767eade8491..4ada73d6eb7 100644 --- a/src/tests/krate.rs +++ b/src/tests/krate.rs @@ -882,6 +882,130 @@ 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_eq!(json.krate.max_version, "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_eq!(json.krate.max_version, "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_eq!(json.krate.max_version, "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_eq!(json.krate.max_version, "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_eq!(json.krate.max_version, "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_eq!(json.krate.max_version, "0.0.0"); + + // 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_eq!(json.krate.max_version, "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_eq!(json.krate.max_version, "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_eq!(json.krate.max_version, "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_eq!(json.krate.max_version, "0.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_eq!(json.krate.max_version, "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_eq!(json.krate.max_version, "2.0.0"); +} + #[test] fn bad_keywords() { let (_b, app, middle) = ::app(); diff --git a/src/user/mod.rs b/src/user/mod.rs index 111127c5986..6ca3c47b84c 100644 --- a/src/user/mod.rs +++ b/src/user/mod.rs @@ -337,8 +337,9 @@ pub fn updates(req: &mut Request) -> CargoResult { // Encode everything! let crates = crates.into_iter().map(|c| { - c.minimal_encodable(None) - }).collect(); + let max_version = c.max_version(tx)?; + Ok(c.minimal_encodable(max_version, None)) + }).collect::>()?; let versions = versions.into_iter().map(|v| { let id = v.crate_id; v.encodable(&map[&id])