From f46203fe9e8bfe1093bf4f639929714278c9d8ac Mon Sep 17 00:00:00 2001 From: "Carol (Nichols || Goulding)" Date: Sun, 29 Jan 2017 17:38:55 -0500 Subject: [PATCH 1/7] Store version build info uploaded by cargo in the database, also return --- .../20170309163659_add_build_info/down.sql | 1 + .../20170309163659_add_build_info/up.sql | 10 ++ src/lib.rs | 8 + src/schema.rs | 45 +++++ src/tests/version.rs | 166 +++++++++++++++++- src/version/build_info.rs | 51 ++++++ src/version/metadata.rs | 131 +++++++++++++- src/version/mod.rs | 30 ++++ src/views/mod.rs | 118 ++++++++++++- 9 files changed, 554 insertions(+), 6 deletions(-) create mode 100644 migrations/20170309163659_add_build_info/down.sql create mode 100644 migrations/20170309163659_add_build_info/up.sql create mode 100644 src/version/build_info.rs diff --git a/migrations/20170309163659_add_build_info/down.sql b/migrations/20170309163659_add_build_info/down.sql new file mode 100644 index 00000000000..37165aa7212 --- /dev/null +++ b/migrations/20170309163659_add_build_info/down.sql @@ -0,0 +1 @@ +DROP TABLE build_info; diff --git a/migrations/20170309163659_add_build_info/up.sql b/migrations/20170309163659_add_build_info/up.sql new file mode 100644 index 00000000000..bef524f93f2 --- /dev/null +++ b/migrations/20170309163659_add_build_info/up.sql @@ -0,0 +1,10 @@ +CREATE TABLE build_info ( + version_id INTEGER NOT NULL, + rust_version VARCHAR NOT NULL, + target VARCHAR NOT NULL, + passed BOOLEAN NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT now(), + updated_at TIMESTAMP NOT NULL DEFAULT now(), + PRIMARY KEY (version_id, rust_version, target) +); +SELECT diesel_manage_updated_at('build_info'); diff --git a/src/lib.rs b/src/lib.rs index a89502d0bf5..f8965cca0a0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -146,6 +146,10 @@ pub fn middleware(app: Arc) -> MiddlewareBuilder { "/crates/:crate_id/:version/download", C(version::downloads::download), ); + api_router.put( + "/crates/:crate_id/:version/build_info", + C(version::build_info::publish_build_info), + ); // Routes that appear to be unused api_router.get("/versions", C(version::deprecated::index)); @@ -170,6 +174,10 @@ pub fn middleware(app: Arc) -> MiddlewareBuilder { "/crates/:crate_id/:version/authors", C(version::metadata::authors), ); + api_router.get( + "/crates/:crate_id/:version/build_info", + C(version::metadata::build_info), + ); api_router.get( "/crates/:crate_id/downloads", C(krate::downloads::downloads), diff --git a/src/schema.rs b/src/schema.rs index 758d64a4ca0..ec79751db76 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -68,6 +68,50 @@ table! { } } +table! { + /// Representation of the `build_info` table. + /// + /// (Automatically generated by Diesel.) + build_info (version_id, rust_version, target) { + /// The `version_id` column of the `build_info` table. + /// + /// Its SQL type is `Int4`. + /// + /// (Automatically generated by Diesel.) + version_id -> Int4, + /// The `rust_version` column of the `build_info` table. + /// + /// Its SQL type is `Varchar`. + /// + /// (Automatically generated by Diesel.) + rust_version -> Varchar, + /// The `target` column of the `build_info` table. + /// + /// Its SQL type is `Varchar`. + /// + /// (Automatically generated by Diesel.) + target -> Varchar, + /// The `passed` column of the `build_info` table. + /// + /// Its SQL type is `Bool`. + /// + /// (Automatically generated by Diesel.) + passed -> Bool, + /// The `created_at` column of the `build_info` table. + /// + /// Its SQL type is `Timestamp`. + /// + /// (Automatically generated by Diesel.) + created_at -> Timestamp, + /// The `updated_at` column of the `build_info` table. + /// + /// Its SQL type is `Timestamp`. + /// + /// (Automatically generated by Diesel.) + updated_at -> Timestamp, + } +} + table! { /// Representation of the `categories` table. /// @@ -811,6 +855,7 @@ joinable!(versions -> crates (crate_id)); allow_tables_to_appear_in_same_query!( api_tokens, badges, + build_info, categories, crate_downloads, crate_owner_invitations, diff --git a/src/tests/version.rs b/src/tests/version.rs index 32e19897fc1..183214048f1 100644 --- a/src/tests/version.rs +++ b/src/tests/version.rs @@ -8,7 +8,7 @@ use serde_json::Value; use conduit::{Handler, Method}; use self::diesel::prelude::*; -use views::EncodableVersion; +use views::{EncodableVersion, EncodableVersionBuildInfo}; use schema::versions; #[derive(Deserialize)] @@ -109,3 +109,167 @@ fn record_rerendered_readme_time() { version.record_readme_rendering(&conn).unwrap(); } } + +#[test] +fn publish_build_info() { + #[derive(Deserialize)] + struct O { + ok: bool, + } + let (_b, app, middle) = ::app(); + + let mut req = ::new_req(Arc::clone(&app), "publish-build-info", "1.0.0"); + + { + let conn = app.diesel_database.get().unwrap(); + let user = ::new_user("foo").create_or_update(&conn).unwrap(); + ::CrateBuilder::new("publish-build-info", user.id) + .version("1.0.0") + .expect_build(&conn); + ::sign_in_as(&mut req, &user); + } + + let body = r#"{ + "name":"publish-build-info", + "vers":"1.0.0", + "rust_version":"rustc 1.16.0-nightly (df8debf6d 2017-01-25)", + "target":"x86_64-pc-windows-gnu", + "passed":false}"#; + + let mut response = ok_resp!( + middle.call( + req.with_path("/api/v1/crates/publish-build-info/1.0.0/build_info") + .with_method(Method::Put) + .with_body(body.as_bytes()), + ) + ); + assert!(::json::(&mut response).ok); + + let body = r#"{ + "name":"publish-build-info", + "vers":"1.0.0", + "rust_version":"rustc 1.16.0-nightly (df8debf6d 2017-01-25)", + "target":"x86_64-pc-windows-gnu", + "passed":true}"#; + + let mut response = ok_resp!( + middle.call( + req.with_path("/api/v1/crates/publish-build-info/1.0.0/build_info") + .with_method(Method::Put) + .with_body(body.as_bytes()), + ) + ); + assert!(::json::(&mut response).ok); + + let body = r#"{ + "name":"publish-build-info", + "vers":"1.0.0", + "rust_version":"rustc 1.13.0 (df8debf6d 2017-01-25)", + "target":"x86_64-pc-windows-gnu", + "passed":true}"#; + + let mut response = ok_resp!( + middle.call( + req.with_path("/api/v1/crates/publish-build-info/1.0.0/build_info") + .with_method(Method::Put) + .with_body(body.as_bytes()), + ) + ); + assert!(::json::(&mut response).ok); + + let body = r#"{ + "name":"publish-build-info", + "vers":"1.0.0", + "rust_version":"rustc 1.15.0-beta (df8debf6d 2017-01-20)", + "target":"x86_64-pc-windows-gnu", + "passed":true}"#; + + let mut response = ok_resp!( + middle.call( + req.with_path("/api/v1/crates/publish-build-info/1.0.0/build_info") + .with_method(Method::Put) + .with_body(body.as_bytes()), + ) + ); + assert!(::json::(&mut response).ok); + + let mut response = ok_resp!(middle.call(req.with_path( + "/api/v1/crates/publish-build-info/1.0.0/build_info" + ).with_method(Method::Get))); + + #[derive(Deserialize)] + struct R { + build_info: EncodableVersionBuildInfo, + } + + let json = ::json::(&mut response); + assert_eq!( + json.build_info.ordering.get("nightly"), + Some(&vec![String::from("2017-01-25T00:00:00+00:00")]) + ); + assert_eq!( + json.build_info.ordering.get("beta"), + Some(&vec![String::from("2017-01-20T00:00:00+00:00")]) + ); + assert_eq!( + json.build_info.ordering.get("stable"), + Some(&vec![String::from("1.13.0")]) + ); +} + +#[test] +fn bad_rust_version_publish_build_info() { + let (_b, app, middle) = ::app(); + + let mut req = ::new_req(Arc::clone(&app), "bad-rust-vers", "1.0.0"); + + { + let conn = app.diesel_database.get().unwrap(); + let user = ::new_user("foo").create_or_update(&conn).unwrap(); + ::CrateBuilder::new("bad-rust-vers", user.id) + .version("1.0.0") + .expect_build(&conn); + ::sign_in_as(&mut req, &user); + } + + let body = r#"{ + "name":"bad-rust-vers", + "vers":"1.0.0", + "rust_version":"rustc 1.16.0-dev (df8debf6d 2017-01-25)", + "target":"x86_64-pc-windows-gnu", + "passed":true}"#; + + let response = bad_resp!( + middle.call( + req.with_path("/api/v1/crates/bad-rust-vers/1.0.0/build_info") + .with_method(Method::Put) + .with_body(body.as_bytes()), + ) + ); + + assert_eq!( + response.errors[0].detail, + "invalid upload request: rust_version `rustc 1.16.0-dev (df8debf6d 2017-01-25)` not \ + recognized as nightly, beta, or stable at line 4 column 64" + ); + + let body = r#"{ + "name":"bad-rust-vers", + "vers":"1.0.0", + "rust_version":"1.15.0", + "target":"x86_64-pc-windows-gnu", + "passed":true}"#; + + let response = bad_resp!( + middle.call( + req.with_path("/api/v1/crates/bad-rust-vers/1.0.0/build_info") + .with_method(Method::Put) + .with_body(body.as_bytes()), + ) + ); + + assert_eq!( + response.errors[0].detail, + "invalid upload request: rust_version `1.15.0` not recognized; expected format like `rustc X.Y.Z (SHA YYYY-MM-DD)` at line 4 column 31" + ); +} diff --git a/src/version/build_info.rs b/src/version/build_info.rs new file mode 100644 index 00000000000..b8997904139 --- /dev/null +++ b/src/version/build_info.rs @@ -0,0 +1,51 @@ +use conduit::{Request, Response}; +use serde_json; + +use app::RequestApp; +use db::RequestTransaction; +use models::{Rights, Version}; +use owner::rights; +use user::RequestUser; +use util::{human, CargoResult, RequestUtils}; +use version::version_and_crate; +use views::EncodableVersionBuildInfoUpload; + +use schema::*; + +#[derive(Clone, Identifiable, Associations, Debug, Queryable)] +#[belongs_to(Version)] +#[table_name = "build_info"] +#[primary_key(version_id, rust_version, target)] +/// Stores information about whether this version built on the specified Rust version and target. +pub struct BuildInfo { + version_id: i32, + pub rust_version: String, + pub target: String, + pub passed: bool, +} + +/// Handles the `POST /crates/:crate_id/:version/build_info` route for the +/// `cargo publish-build-info` command to report on which versions of Rust +/// a crate builds with. +pub fn publish_build_info(req: &mut Request) -> CargoResult { + let mut body = String::new(); + req.body().read_to_string(&mut body)?; + let info: EncodableVersionBuildInfoUpload = serde_json::from_str(&body) + .map_err(|e| human(&format_args!("invalid upload request: {}", e)))?; + + let (version, krate) = version_and_crate(req)?; + let user = req.user()?; + let tx = req.db_conn()?; + let owners = krate.owners(&tx)?; + if rights(req.app(), &owners, user)? < Rights::Publish { + return Err(human("must already be an owner to publish build info")); + } + + version.store_build_info(&tx, info)?; + + #[derive(Serialize)] + struct R { + ok: bool, + } + Ok(req.json(&R { ok: true })) +} diff --git a/src/version/metadata.rs b/src/version/metadata.rs index 5f878914d6e..6e4ee0ca301 100644 --- a/src/version/metadata.rs +++ b/src/version/metadata.rs @@ -1,18 +1,24 @@ //! Endpoints that expose metadata about crate versions //! -//! These endpoints provide data that could be obtained direclty from the +//! These endpoints provide data that could be obtained directly from the //! index or cached metadata which was extracted (client side) from the -//! `Cargo.toml` file. +//! `Cargo.toml` file, as well as some information stored in crates.io's +//! database. -use conduit::{Request, Response}; +use std::str::FromStr; +use chrono::{DateTime, NaiveDate, Utc}; +use conduit::{Request, Response}; use diesel::prelude::*; + use db::RequestTransaction; use util::{CargoResult, RequestUtils}; -use views::{EncodableDependency, EncodablePublicUser}; +use views::{EncodableDependency, EncodablePublicUser, EncodableVersionBuildInfo, + ParsedRustChannelVersion}; use schema::*; +use super::build_info::BuildInfo; use super::version_and_crate; /// Handles the `GET /crates/:crate_id/:version/dependencies` route. @@ -64,3 +70,120 @@ pub fn authors(req: &mut Request) -> CargoResult { meta: Meta { names: names }, })) } + +/// Handles the `GET /crates/:crate_id/:version/build_info` route. +// We do not wish the frontend to understand how to sort Rust versions +// (semver- *or* date-based), so we return two related pieces of +// information: the ordering of all the releases in each channel and +// the pass/fail for each platform for each (channel, version) pair. +// +// { +// "build_info": { +// "id": 1, +// "ordering": { +// "nightly": ["2017-07-26T00:00:00Z"], +// "beta": ["2017-07-18T00:00:00Z"], +// "stable": ["1.19.0"] +// }, +// "stable": { +// "1.19.0": { "x86_64-apple-darwin": false } +// "1.17.0": { "x86_64-unknown-linux-gnu": true } +// "1.18.0": { "x86_64-pc-windows-gnu": false } +// }, +// "beta": { +// "2017-07-18T00:00:00Z": { "x86_64-apple-darwin": false } +// }, +// "nightly": { +// "2017-07-26T00:00:00Z": { "x86_64-apple-darwin": true } +// } +// } +// } +pub fn build_info(req: &mut Request) -> CargoResult { + use std::collections::{BTreeSet, HashMap}; + + let (version, _) = version_and_crate(req)?; + + let conn = req.db_conn()?; + + let build_infos = BuildInfo::belonging_to(&version) + .select(( + build_info::version_id, + build_info::rust_version, + build_info::target, + build_info::passed, + )) + .load(&*conn)?; + + let mut encodable_build_info = EncodableVersionBuildInfo::default(); + encodable_build_info.id = version.id; + let mut stables = BTreeSet::new(); + let mut betas = BTreeSet::new(); + let mut nightlies = BTreeSet::new(); + + for row in build_infos { + let BuildInfo { + rust_version, + target, + passed, + .. + } = row; + + let rust_version = ParsedRustChannelVersion::from_str(&rust_version)?; + + match rust_version { + ParsedRustChannelVersion::Stable(semver) => { + let key = semver.to_string(); + stables.insert(semver); + encodable_build_info + .stable + .entry(key) + .or_insert_with(HashMap::new) + .insert(target, passed); + } + ParsedRustChannelVersion::Beta(date) => { + betas.insert(date); + encodable_build_info + .beta + .entry(date) + .or_insert_with(HashMap::new) + .insert(target, passed); + } + ParsedRustChannelVersion::Nightly(date) => { + nightlies.insert(date); + encodable_build_info + .nightly + .entry(date) + .or_insert_with(HashMap::new) + .insert(target, passed); + } + } + } + + encodable_build_info.ordering.insert( + String::from("stable"), + stables.into_iter().map(|s| s.to_string()).collect(), + ); + + fn naive_date_to_rfc3339(date: NaiveDate) -> String { + DateTime::::from_utc(date.and_hms(0, 0, 0), Utc).to_rfc3339() + } + + encodable_build_info.ordering.insert( + String::from("beta"), + betas.into_iter().map(naive_date_to_rfc3339).collect(), + ); + + encodable_build_info.ordering.insert( + String::from("nightly"), + nightlies.into_iter().map(naive_date_to_rfc3339).collect(), + ); + + #[derive(Serialize, Debug)] + struct R { + build_info: EncodableVersionBuildInfo, + } + + Ok(req.json(&R { + build_info: encodable_build_info, + })) +} diff --git a/src/version/mod.rs b/src/version/mod.rs index 6ce24333e3a..2cf58aa190f 100644 --- a/src/version/mod.rs +++ b/src/version/mod.rs @@ -13,12 +13,14 @@ use db::RequestTransaction; use util::{human, CargoResult}; use license_exprs; +pub mod build_info; pub mod deprecated; pub mod downloads; pub mod metadata; pub mod yank; use models::{Crate, Dependency}; +use views::EncodableVersionBuildInfoUpload; use schema::*; // Queryable has a custom implementation below @@ -138,6 +140,34 @@ impl Version { .set(rendered_at.eq(now)) .execute(conn) } + + /// When we receive a `POST /crates/:crate_id/:version/build_info` API request that tells us + /// about whether a particular version built successfully on a particular Rust version and + /// target (sent via `cargo publish-build-info`), store that information in the database. + /// Overwrites any previous results reported for the specified `(version_id, rust_version, + /// target)` combination. + pub fn store_build_info( + &self, + conn: &PgConnection, + info: EncodableVersionBuildInfoUpload, + ) -> CargoResult<()> { + use schema::build_info::dsl::*; + use diesel::pg::upsert::excluded; + + diesel::insert_into(build_info) + .values(( + version_id.eq(self.id), + rust_version.eq(info.rust_version.to_string()), + target.eq(info.target), + passed.eq(info.passed), + )) + .on_conflict(build_info.primary_key()) + .do_update() + .set(passed.eq(excluded(passed))) + .execute(conn)?; + + Ok(()) + } } impl NewVersion { diff --git a/src/views/mod.rs b/src/views/mod.rs index 21e2b6e290b..e491371becf 100644 --- a/src/views/mod.rs +++ b/src/views/mod.rs @@ -1,10 +1,18 @@ // TODO: Move all encodable types here // For now, just reexport -use chrono::NaiveDateTime; +use std::collections::HashMap; +use std::fmt; +use std::str::FromStr; + +use chrono::{NaiveDate, NaiveDateTime}; +use semver; +use serde::{self, Deserialize, Deserializer, Serialize, Serializer}; pub use badge::EncodableBadge; +use util::errors::{human, CargoError, CargoResult}; + #[derive(Serialize, Deserialize, Debug)] pub struct EncodableCategory { pub id: String, @@ -49,6 +57,114 @@ pub mod krate_publish; pub use self::krate_publish::CrateDependency as EncodableCrateDependency; pub use self::krate_publish::NewCrate as EncodableCrateUpload; +/// Information about whether this version built on the specified Rust version and target, as +/// uploaded by the `cargo publish-build-info` command. +#[derive(Debug, Serialize, Deserialize)] +pub struct EncodableVersionBuildInfoUpload { + pub rust_version: EncodableRustChannelVersion, + pub target: String, + pub passed: bool, +} + +/// Aggregated build info for a crate version grouped by Rust channel for front-end display +/// convenience. +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct EncodableVersionBuildInfo { + pub id: i32, + pub ordering: HashMap>, + pub stable: HashMap>, + pub beta: HashMap>, + pub nightly: HashMap>, +} + +/// Describes a Rust version by its channel and the released version on that channel. +/// For use in describing what versions of Rust a particular crate version builds with. +/// Contains the original version string for inserting into the database. +#[derive(Debug)] +pub struct EncodableRustChannelVersion { + raw: String, + pub parsed: ParsedRustChannelVersion, +} + +/// A pretty, minimal representation of a Rust version's channel and released version on that +/// channel. Namely, does not include the exact release hash. +#[derive(Debug)] +pub enum ParsedRustChannelVersion { + Stable(semver::Version), + Beta(NaiveDate), + Nightly(NaiveDate), +} + +impl fmt::Display for EncodableRustChannelVersion { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.raw) + } +} + +impl Serialize for EncodableRustChannelVersion { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.raw) + } +} + +impl<'de> Deserialize<'de> for EncodableRustChannelVersion { + fn deserialize>(d: D) -> Result { + let s = String::deserialize(d)?; + + Ok(EncodableRustChannelVersion { + raw: s.clone(), + parsed: ParsedRustChannelVersion::from_str(&s).map_err(serde::de::Error::custom)?, + }) + } +} + +impl FromStr for ParsedRustChannelVersion { + type Err = Box; + + fn from_str(s: &str) -> CargoResult { + // Recognized formats: + // rustc 1.14.0 (e8a012324 2016-12-16) + // rustc 1.15.0-beta.5 (10893a9a3 2017-01-19) + // rustc 1.16.0-nightly (df8debf6d 2017-01-25) + + let pieces: Vec<_> = s.split(&[' ', '(', ')'][..]) + .filter(|s| !s.trim().is_empty()) + .collect(); + + if pieces.len() != 4 { + return Err(human(&format_args!( + "rust_version `{}` not recognized; \ + expected format like `rustc X.Y.Z (SHA YYYY-MM-DD)`", + s + ))); + } + + if pieces[1].contains("nightly") { + Ok(ParsedRustChannelVersion::Nightly( + NaiveDate::parse_from_str(pieces[3], "%Y-%m-%d")?, + )) + } else if pieces[1].contains("beta") { + Ok(ParsedRustChannelVersion::Beta(NaiveDate::parse_from_str( + pieces[3], + "%Y-%m-%d", + )?)) + } else { + let v = semver::Version::parse(pieces[1])?; + if v.pre.is_empty() { + Ok(ParsedRustChannelVersion::Stable(v)) + } else { + Err(human(&format_args!( + "rust_version `{}` not recognized as nightly, beta, or stable", + s + ))) + } + } + } +} + #[cfg(test)] mod tests { use std::collections::HashMap; From 6f7b34e78d4cc4db90bb1cb60c7c80a6dd047aca Mon Sep 17 00:00:00 2001 From: "Carol (Nichols || Goulding)" Date: Sun, 29 Jan 2017 17:55:29 -0500 Subject: [PATCH 2/7] Display the latest version and targets this crate version builds on Icons from https://github.com/vorillaz/devicons --- app/helpers/format-day.js | 8 +++++ app/helpers/target-icons.js | 10 ++++++ app/models/build-info.js | 58 +++++++++++++++++++++++++++++++++ app/models/version.js | 1 + app/styles/crate.scss | 29 +++++++++++++++++ app/templates/crate/version.hbs | 27 +++++++++++++++ public/assets/linux.svg | 6 ++++ public/assets/macos.svg | 6 ++++ public/assets/windows.svg | 6 ++++ src/version/mod.rs | 2 ++ 10 files changed, 153 insertions(+) create mode 100644 app/helpers/format-day.js create mode 100644 app/helpers/target-icons.js create mode 100644 app/models/build-info.js create mode 100644 public/assets/linux.svg create mode 100644 public/assets/macos.svg create mode 100644 public/assets/windows.svg diff --git a/app/helpers/format-day.js b/app/helpers/format-day.js new file mode 100644 index 00000000000..33c2d80b102 --- /dev/null +++ b/app/helpers/format-day.js @@ -0,0 +1,8 @@ +import Ember from 'ember'; +import moment from 'moment'; + +export function formatDay(date) { + return date ? moment(date).utc().format('YYYY-MM-DD') : null; +} + +export default Ember.Helper.helper(params => formatDay(params[0])); diff --git a/app/helpers/target-icons.js b/app/helpers/target-icons.js new file mode 100644 index 00000000000..bd98ae6a70f --- /dev/null +++ b/app/helpers/target-icons.js @@ -0,0 +1,10 @@ +import Ember from 'ember'; + +export function targetIcons(targets) { + return Ember.String.htmlSafe(targets.map(function(target) { + const filename = target.split(' ')[0].toLowerCase(); + return `${target}`; + }).join('')); +} + +export default Ember.Helper.helper(params => targetIcons(params[0])); diff --git a/app/models/build-info.js b/app/models/build-info.js new file mode 100644 index 00000000000..a2e1d5ea516 --- /dev/null +++ b/app/models/build-info.js @@ -0,0 +1,58 @@ +import DS from 'ember-data'; +import Ember from 'ember'; + +const TIER1 = { + 'x86_64-unknown-linux-gnu': 'Linux', + 'x86_64-apple-darwin': 'macOS', + 'x86_64-pc-windows-gnu': 'Windows (GNU)', + 'x86_64-pc-windows-msvc': 'Windows (MSVC)', +}; + +const caseInsensitive = (a, b) => a.toLowerCase().localeCompare(b.toLowerCase()); + +export default DS.Model.extend({ + version: DS.belongsTo('version', { async: true }), + ordering: DS.attr(), + stable: DS.attr(), + beta: DS.attr(), + nightly: DS.attr(), + + latest_positive_results: Ember.computed('ordering', 'stable', 'beta', 'nightly', function() { + const passingTargets = results => ( + Object.entries(results) + .filter(([, value]) => value === true) + .map(([key]) => TIER1[key]) + .sort(caseInsensitive) + ); + + const positiveResults = (versionOrdering, channelResults) => { + const latestVersion = versionOrdering[versionOrdering.length - 1]; + const latestResults = channelResults[latestVersion] || {}; + return [latestVersion, passingTargets(latestResults)]; + }; + + let results = {}; + + const addChannelToResults = (key) => { + const channelOrdering = this.get('ordering')[key]; + const channelResults = this.get(key); + + const [version, targets] = positiveResults(channelOrdering, channelResults); + + if (targets.length > 0) { + results[key] = { version, targets }; + } + }; + + addChannelToResults('stable'); + addChannelToResults('beta'); + addChannelToResults('nightly'); + + return results; + }), + + has_any_positive_results: Ember.computed('latest_positive_results', function() { + const results = this.get('latest_positive_results'); + return Object.keys(results).length > 0; + }), +}); diff --git a/app/models/version.js b/app/models/version.js index 3da95b4300d..6d02500a208 100644 --- a/app/models/version.js +++ b/app/models/version.js @@ -15,6 +15,7 @@ export default DS.Model.extend({ async: false }), authors: DS.hasMany('users', { async: true }), + build_info: DS.belongsTo('build-info', { async: true }), dependencies: DS.hasMany('dependency', { async: true }), version_downloads: DS.hasMany('version-download', { async: true }), diff --git a/app/styles/crate.scss b/app/styles/crate.scss index 5eae23ec0c5..00dd2aa9f49 100644 --- a/app/styles/crate.scss +++ b/app/styles/crate.scss @@ -352,6 +352,35 @@ } } +.build-info { + .build-info-channel { + margin: 10px 0; + img { + width: 20px; + margin-bottom: -3px; + } + } +} + +#crate-build-info { + padding-bottom: 50px; + border-bottom: 5px solid $gray-border; + margin-bottom: 30px; + + .description { + margin-bottom: 30px; + } + + table { + border: 1px solid $gray-border; + td, th { + border: 1px solid $gray-border; + padding: 5px 10px; + text-align: left; + } + } +} + #crate-downloads { @include display-flex; @include flex-wrap(wrap); diff --git a/app/templates/crate/version.hbs b/app/templates/crate/version.hbs index 32f6cb4bf06..b85ad28d21c 100644 --- a/app/templates/crate/version.hbs +++ b/app/templates/crate/version.hbs @@ -123,6 +123,33 @@ {{/each}} + {{#if currentVersion.build_info.has_any_positive_results }} +
+

Works on

+ + {{#if currentVersion.build_info.latest_positive_results.stable }} +
+ Stable {{ currentVersion.build_info.latest_positive_results.stable.version }} + on {{ target-icons currentVersion.build_info.latest_positive_results.stable.targets }} +
+ {{/if}} + + {{#if currentVersion.build_info.latest_positive_results.beta }} +
+ Beta {{ format-day currentVersion.build_info.latest_positive_results.beta.version }} + on {{ target-icons currentVersion.build_info.latest_positive_results.beta.targets }} +
+ {{/if}} + + {{#if currentVersion.build_info.latest_positive_results.nightly }} +
+ Nightly {{ format-day currentVersion.build_info.latest_positive_results.nightly.version }} + on {{ target-icons currentVersion.build_info.latest_positive_results.nightly.targets }} +
+ {{/if}} +
+ {{/if}} +

Authors

    diff --git a/public/assets/linux.svg b/public/assets/linux.svg new file mode 100644 index 00000000000..fb432127fa1 --- /dev/null +++ b/public/assets/linux.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/assets/macos.svg b/public/assets/macos.svg new file mode 100644 index 00000000000..a764263ee9d --- /dev/null +++ b/public/assets/macos.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/assets/windows.svg b/public/assets/windows.svg new file mode 100644 index 00000000000..b261954f55f --- /dev/null +++ b/public/assets/windows.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/version/mod.rs b/src/version/mod.rs index 2cf58aa190f..1364cd52a35 100644 --- a/src/version/mod.rs +++ b/src/version/mod.rs @@ -68,6 +68,7 @@ pub struct EncodableVersionLinks { pub dependencies: String, pub version_downloads: String, pub authors: String, + pub build_info: String, } impl Version { @@ -100,6 +101,7 @@ impl Version { dependencies: format!("/api/v1/crates/{}/{}/dependencies", crate_name, num), version_downloads: format!("/api/v1/crates/{}/{}/downloads", crate_name, num), authors: format!("/api/v1/crates/{}/{}/authors", crate_name, num), + build_info: format!("/api/v1/crates/{}/{}/build_info", crate_name, num), }, } } From a5dc65170c7805f734a57b19933f3406afc8c33c Mon Sep 17 00:00:00 2001 From: "Carol (Nichols || Goulding)" Date: Sun, 29 Jan 2017 18:01:19 -0500 Subject: [PATCH 3/7] Show badge for latest version build status on crate list pages If the latest version builds on any stable version, show that it builds on stable. If not and it builds on any beta version, show that. If not and it builds on any nightly version, show that. Otherwise, don't show any badge. --- app/components/badge-build-info.js | 41 ++++++++++ app/models/crate.js | 3 + app/templates/components/badge-build-info.hbs | 6 ++ app/templates/components/crate-row.hbs | 1 + src/krate/metadata.rs | 3 +- src/krate/mod.rs | 15 +++- src/krate/publish.rs | 2 +- src/krate/search.rs | 28 +++++-- src/models/mod.rs | 1 + src/version/build_info.rs | 81 ++++++++++++++++++- src/version/metadata.rs | 7 +- src/version/mod.rs | 5 ++ src/views/mod.rs | 32 ++++++++ 13 files changed, 210 insertions(+), 15 deletions(-) create mode 100644 app/components/badge-build-info.js create mode 100644 app/templates/components/badge-build-info.hbs diff --git a/app/components/badge-build-info.js b/app/components/badge-build-info.js new file mode 100644 index 00000000000..2fe3efbaef6 --- /dev/null +++ b/app/components/badge-build-info.js @@ -0,0 +1,41 @@ +import Ember from 'ember'; + +import { formatDay } from 'cargo/helpers/format-day'; + +export default Ember.Component.extend({ + tagName: 'span', + classNames: ['build_info'], + + build_info: Ember.computed('crate.max_build_info_stable', 'crate.max_build_info_beta', 'crate.max_build_info_nightly', function() { + if (this.get('crate.max_build_info_stable')) { + return 'stable'; + } else if (this.get('crate.max_build_info_beta')) { + return 'beta'; + } else if (this.get('crate.max_build_info_nightly')) { + return 'nightly'; + } else { + return null; + } + }), + color: Ember.computed('build_info', function() { + if (this.get('build_info') === 'stable') { + return 'brightgreen'; + } else if (this.get('build_info') === 'beta') { + return 'yellow'; + } else { + return 'orange'; + } + }), + version_display: Ember.computed('build_info', 'crate.max_build_info_stable', 'crate.max_build_info_beta', 'crate.max_build_info_nightly', function() { + if (this.get('build_info') === 'stable') { + return this.get('crate.max_build_info_stable'); + } else if (this.get('build_info') === 'beta') { + return formatDay(this.get('crate.max_build_info_beta')); + } else { + return formatDay(this.get('crate.max_build_info_nightly')); + } + }), + version_for_shields: Ember.computed('version_display', function() { + return this.get('version_display').replace(/-/g, '--'); + }), +}); diff --git a/app/models/crate.js b/app/models/crate.js index 9bd5893ad33..9d2e40fd78f 100644 --- a/app/models/crate.js +++ b/app/models/crate.js @@ -17,6 +17,9 @@ export default DS.Model.extend({ documentation: DS.attr('string'), repository: DS.attr('string'), exact_match: DS.attr('boolean'), + max_build_info_nightly: DS.attr('date'), + max_build_info_beta: DS.attr('date'), + max_build_info_stable: DS.attr('string'), versions: DS.hasMany('versions', { async: true }), badges: DS.attr(), diff --git a/app/templates/components/badge-build-info.hbs b/app/templates/components/badge-build-info.hbs new file mode 100644 index 00000000000..f0a6cb8d356 --- /dev/null +++ b/app/templates/components/badge-build-info.hbs @@ -0,0 +1,6 @@ +{{#if build_info}} +Known to build on {{ build_info }} {{ version_display }} +{{/if}} diff --git a/app/templates/components/crate-row.hbs b/app/templates/components/crate-row.hbs index 8337a333805..12b06c61421 100644 --- a/app/templates/components/crate-row.hbs +++ b/app/templates/components/crate-row.hbs @@ -11,6 +11,7 @@ {{#each crate.annotated_badges as |badge|}} {{component badge.component_name badge=badge data-test-badge=badge.badge_type}} {{/each}} + {{badge-build-info crate=crate}}
diff --git a/src/krate/metadata.rs b/src/krate/metadata.rs index d674fea8e4c..110d310c384 100644 --- a/src/krate/metadata.rs +++ b/src/krate/metadata.rs @@ -40,7 +40,7 @@ pub fn summary(req: &mut Request) -> CargoResult { .map(|versions| Version::max(versions.into_iter().map(|v| v.num))) .zip(krates) .map(|(max_version, krate)| { - Ok(krate.minimal_encodable(&max_version, None, false, None)) + Ok(krate.minimal_encodable(&max_version, None, false, None, None)) }) .collect() }; @@ -156,6 +156,7 @@ pub fn show(req: &mut Request) -> CargoResult { Some(badges), false, recent_downloads, + None, ), versions: versions .into_iter() diff --git a/src/krate/mod.rs b/src/krate/mod.rs index a304d19e91b..338a6ca52db 100644 --- a/src/krate/mod.rs +++ b/src/krate/mod.rs @@ -9,7 +9,7 @@ use url::Url; use app::App; use util::{human, CargoResult}; -use views::EncodableBadge; +use views::{EncodableBadge, EncodableMaxVersionBuildInfo}; use models::{Badge, Category, CrateOwner, Keyword, NewCrateOwnerInvitation, Owner, OwnerKind, ReverseDependency, User, Version}; @@ -113,6 +113,9 @@ pub struct EncodableCrate { pub repository: Option, pub links: CrateLinks, pub exact_match: bool, + pub max_build_info_stable: Option, + pub max_build_info_beta: Option, + pub max_build_info_nightly: Option, } #[derive(Serialize, Deserialize, Debug)] @@ -315,6 +318,7 @@ impl Crate { badges: Option>, exact_match: bool, recent_downloads: Option, + max_build_info: Option, ) -> EncodableCrate { self.encodable( max_version, @@ -324,6 +328,7 @@ impl Crate { badges, exact_match, recent_downloads, + max_build_info, ) } @@ -337,6 +342,7 @@ impl Crate { badges: Option>, exact_match: bool, recent_downloads: Option, + max_build_info: Option, ) -> EncodableCrate { let Crate { name, @@ -357,6 +363,7 @@ impl Crate { let category_ids = categories.map(|cats| cats.iter().map(|cat| cat.slug.clone()).collect()); let badges = badges.map(|bs| bs.into_iter().map(|b| b.encodable()).collect()); let documentation = Crate::remove_blacklisted_documentation_urls(documentation); + let max_build_info = max_build_info.unwrap_or_else(EncodableMaxVersionBuildInfo::default); EncodableCrate { id: name.clone(), @@ -370,6 +377,9 @@ impl Crate { categories: category_ids, badges: badges, max_version: max_version.to_string(), + max_build_info_stable: max_build_info.stable, + max_build_info_beta: max_build_info.beta, + max_build_info_nightly: max_build_info.nightly, documentation: documentation, homepage: homepage, exact_match: exact_match, @@ -606,6 +616,9 @@ mod tests { downloads: 0, recent_downloads: None, max_version: "".to_string(), + max_build_info_stable: None, + max_build_info_beta: None, + max_build_info_nightly: None, description: None, homepage: None, documentation: None, diff --git a/src/krate/publish.rs b/src/krate/publish.rs index 3fd760147a1..9ddca71cbe9 100644 --- a/src/krate/publish.rs +++ b/src/krate/publish.rs @@ -181,7 +181,7 @@ pub fn publish(req: &mut Request) -> CargoResult { warnings: Warnings<'a>, } Ok(req.json(&R { - krate: krate.minimal_encodable(&max_version, None, false, None), + krate: krate.minimal_encodable(&max_version, None, false, None, None), warnings: warnings, })) }) diff --git a/src/krate/search.rs b/src/krate/search.rs index 00f1b99c80a..5f71c647f0e 100644 --- a/src/krate/search.rs +++ b/src/krate/search.rs @@ -10,7 +10,7 @@ use user::RequestUser; use util::{CargoResult, RequestUtils}; use views::EncodableCrate; -use models::{Badge, Crate, OwnerKind, Version}; +use models::{Badge, BuildInfo, Crate, OwnerKind, Version}; use schema::*; use super::{canon_crate_name, ALL_COLUMNS}; @@ -183,28 +183,46 @@ pub fn search(req: &mut Request) -> CargoResult { .load::(&*conn)? .grouped_by(&crates) .into_iter() - .map(|versions| Version::max(versions.into_iter().map(|v| v.num))); + .map(|versions| { + versions + .into_iter() + .max_by(Version::semantically_newest_first) + .unwrap() + }) + .collect::>(); + + let build_infos = BuildInfo::belonging_to(&versions) + .filter(build_info::passed.eq(true)) + .select(::version::build_info::BUILD_INFO_FIELDS) + .load::(&*conn)? + .grouped_by(&versions) + .into_iter() + .map(BuildInfo::max); let crates = versions + .into_iter() .zip(crates) .zip(perfect_matches) .zip(recent_downloads) + .zip(build_infos) .map( - |(((max_version, krate), perfect_match), recent_downloads)| { + |((((max_version, krate), perfect_match), recent_downloads), build_info)| { + let build_info = build_info?; // FIXME: If we add crate_id to the Badge enum we can eliminate // this N+1 let badges = badges::table .filter(badges::crate_id.eq(krate.id)) .load::(&*conn)?; Ok(krate.minimal_encodable( - &max_version, + &max_version.num, Some(badges), perfect_match, Some(recent_downloads), + Some(build_info.encode()), )) }, ) - .collect::>()?; + .collect::>()?; #[derive(Serialize)] struct R { diff --git a/src/models/mod.rs b/src/models/mod.rs index 5e0aefb024a..42a269e4e35 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -9,6 +9,7 @@ pub use owner::{CrateOwner, NewTeam, Owner, OwnerKind, Rights, Team}; pub use user::{Email, NewUser, User}; pub use token::ApiToken; pub use version::{NewVersion, Version}; +pub use version::build_info::BuildInfo; mod category; mod keyword; diff --git a/src/version/build_info.rs b/src/version/build_info.rs index b8997904139..f69289fb6c8 100644 --- a/src/version/build_info.rs +++ b/src/version/build_info.rs @@ -1,4 +1,8 @@ +use std::str::FromStr; + +use chrono::{DateTime, NaiveDate, Utc}; use conduit::{Request, Response}; +use semver; use serde_json; use app::RequestApp; @@ -8,7 +12,8 @@ use owner::rights; use user::RequestUser; use util::{human, CargoResult, RequestUtils}; use version::version_and_crate; -use views::EncodableVersionBuildInfoUpload; +use views::{EncodableMaxVersionBuildInfo, EncodableVersionBuildInfoUpload, + ParsedRustChannelVersion}; use schema::*; @@ -24,6 +29,80 @@ pub struct BuildInfo { pub passed: bool, } +/// The columns to select from the `build_info` table. The table also stores `created_at` and +/// `updated_at` metadata for each row, but we're not displaying those anywhere so we're not +/// bothering to select them. +pub const BUILD_INFO_FIELDS: ( + build_info::version_id, + build_info::rust_version, + build_info::target, + build_info::passed, +) = ( + build_info::version_id, + build_info::rust_version, + build_info::target, + build_info::passed, +); + +#[derive(Debug)] +/// The maximum version of Rust from each channel that a crate version successfully builds with. +/// Used for summarizing this information in badge form on crate list pages. +pub struct MaxBuildInfo { + pub stable: Option, + pub beta: Option, + pub nightly: Option, +} + +impl MaxBuildInfo { + /// Encode stable semver number as a string and beta and nightly as times appropriate for + /// JSON. + pub fn encode(self) -> EncodableMaxVersionBuildInfo { + fn naive_date_to_rfc3339(date: NaiveDate) -> String { + DateTime::::from_utc(date.and_hms(0, 0, 0), Utc).to_rfc3339() + } + + EncodableMaxVersionBuildInfo { + stable: self.stable.map(|v| v.to_string()), + beta: self.beta.map(naive_date_to_rfc3339), + nightly: self.nightly.map(naive_date_to_rfc3339), + } + } +} + +impl BuildInfo { + /// From a set of build information data, Find the largest or latest Rust versions that we know + /// about for each channel. Stable uses the largest semver version number; beta and nightly use + /// the latest date. + pub fn max(build_infos: I) -> CargoResult + where + I: IntoIterator, + { + let build_infos = build_infos + .into_iter() + .map(|bi| ParsedRustChannelVersion::from_str(&bi.rust_version)) + .collect::, _>>()?; + + let stable = build_infos + .iter() + .filter_map(ParsedRustChannelVersion::as_stable) + .max(); + let beta = build_infos + .iter() + .filter_map(ParsedRustChannelVersion::as_beta) + .max(); + let nightly = build_infos + .iter() + .filter_map(ParsedRustChannelVersion::as_nightly) + .max(); + + Ok(MaxBuildInfo { + stable: stable.cloned(), + beta: beta.cloned(), + nightly: nightly.cloned(), + }) + } +} + /// Handles the `POST /crates/:crate_id/:version/build_info` route for the /// `cargo publish-build-info` command to report on which versions of Rust /// a crate builds with. diff --git a/src/version/metadata.rs b/src/version/metadata.rs index 6e4ee0ca301..bcd58af9cdf 100644 --- a/src/version/metadata.rs +++ b/src/version/metadata.rs @@ -106,12 +106,7 @@ pub fn build_info(req: &mut Request) -> CargoResult { let conn = req.db_conn()?; let build_infos = BuildInfo::belonging_to(&version) - .select(( - build_info::version_id, - build_info::rust_version, - build_info::target, - build_info::passed, - )) + .select(::version::build_info::BUILD_INFO_FIELDS) .load(&*conn)?; let mut encodable_build_info = EncodableVersionBuildInfo::default(); diff --git a/src/version/mod.rs b/src/version/mod.rs index 1364cd52a35..d59c9644b8f 100644 --- a/src/version/mod.rs +++ b/src/version/mod.rs @@ -170,6 +170,11 @@ impl Version { Ok(()) } + + /// Orders SemVer numbers so that "higher" version numbers appear first. + pub fn semantically_newest_first(a: &Self, b: &Self) -> ::std::cmp::Ordering { + b.num.cmp(&a.num) + } } impl NewVersion { diff --git a/src/views/mod.rs b/src/views/mod.rs index e491371becf..0e304190337 100644 --- a/src/views/mod.rs +++ b/src/views/mod.rs @@ -77,6 +77,14 @@ pub struct EncodableVersionBuildInfo { pub nightly: HashMap>, } +/// `MaxBuildInfo` in JSON form. +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct EncodableMaxVersionBuildInfo { + pub stable: Option, + pub beta: Option, + pub nightly: Option, +} + /// Describes a Rust version by its channel and the released version on that channel. /// For use in describing what versions of Rust a particular crate version builds with. /// Contains the original version string for inserting into the database. @@ -165,6 +173,29 @@ impl FromStr for ParsedRustChannelVersion { } } +impl ParsedRustChannelVersion { + pub fn as_stable(&self) -> Option<&semver::Version> { + match *self { + ParsedRustChannelVersion::Stable(ref v) => Some(v), + _ => None, + } + } + + pub fn as_beta(&self) -> Option<&NaiveDate> { + match *self { + ParsedRustChannelVersion::Beta(ref v) => Some(v), + _ => None, + } + } + + pub fn as_nightly(&self) -> Option<&NaiveDate> { + match *self { + ParsedRustChannelVersion::Nightly(ref v) => Some(v), + _ => None, + } + } +} + #[cfg(test)] mod tests { use std::collections::HashMap; @@ -243,6 +274,7 @@ mod tests { dependencies: "".to_string(), version_downloads: "".to_string(), authors: "".to_string(), + build_info: "".to_string(), }, }; let json = serde_json::to_string(&ver).unwrap(); From c49242020f9aebe9dbbd6c53dcfa698415a9f0f7 Mon Sep 17 00:00:00 2001 From: Jake Goulding Date: Fri, 21 Apr 2017 15:15:37 -0400 Subject: [PATCH 4/7] Show detailed build information on a separate page --- app/controllers/crate/build-info.js | 49 +++++++++++++++++ app/helpers/build-info-pass-fail.js | 13 +++++ app/router.js | 1 + app/routes/crate/build_info.js | 24 +++++++++ app/templates/crate/build-info.hbs | 83 +++++++++++++++++++++++++++++ app/templates/crate/version.hbs | 4 ++ 6 files changed, 174 insertions(+) create mode 100644 app/controllers/crate/build-info.js create mode 100644 app/helpers/build-info-pass-fail.js create mode 100644 app/routes/crate/build_info.js create mode 100644 app/templates/crate/build-info.hbs diff --git a/app/controllers/crate/build-info.js b/app/controllers/crate/build-info.js new file mode 100644 index 00000000000..d27182af5ee --- /dev/null +++ b/app/controllers/crate/build-info.js @@ -0,0 +1,49 @@ +import Ember from 'ember'; + +const { computed } = Ember; + +function flattenBuildInfo(buildOrdering, builds) { + if (!buildOrdering || !builds) { + return []; + } + + return buildOrdering.map(version => { + const thisVersion = builds[version]; + + return { + version, + 'x86_64-apple-darwin': thisVersion['x86_64-apple-darwin'], + 'x86_64-pc-windows-gnu': thisVersion['x86_64-pc-windows-gnu'], + 'x86_64-pc-windows-msvc': thisVersion['x86_64-pc-windows-msvc'], + 'x86_64-unknown-linux-gnu': thisVersion['x86_64-unknown-linux-gnu'], + }; + }); +} + +export default Ember.Controller.extend({ + id: computed.alias('model.crate.id'), + name: computed.alias('model.crate.name'), + version: computed.alias('model.num'), + build_info: computed.alias('model.build_info'), + stable_build: computed('build_info.ordering.stable', 'build_info.stable', function() { + const ordering = this.get('build_info.ordering.stable'); + const stable = this.get('build_info.stable'); + + return flattenBuildInfo(ordering, stable); + }), + beta_build: computed('build_info.ordering.beta', 'build_info.beta', function() { + const ordering = this.get('build_info.ordering.beta'); + const beta = this.get('build_info.beta'); + + return flattenBuildInfo(ordering, beta); + }), + nightly_build: computed('build_info.ordering.nightly', 'build_info.nightly', function() { + const ordering = this.get('build_info.ordering.nightly'); + const nightly = this.get('build_info.nightly'); + + return flattenBuildInfo(ordering, nightly); + }), + has_stable_builds: computed.gt('stable_build.length', 0), + has_beta_builds: computed.gt('beta_build.length', 0), + has_nightly_builds: computed.gt('nightly_build.length', 0), +}); diff --git a/app/helpers/build-info-pass-fail.js b/app/helpers/build-info-pass-fail.js new file mode 100644 index 00000000000..200199fc2d5 --- /dev/null +++ b/app/helpers/build-info-pass-fail.js @@ -0,0 +1,13 @@ +import Ember from 'ember'; + +export function buildInfoPassFail(status) { + if (status === true) { + return '✅ Pass'; + } else if (status === false) { + return '❌ Fail'; + } else { + return ''; + } +} + +export default Ember.Helper.helper(params => buildInfoPassFail(params[0])); diff --git a/app/router.js b/app/router.js index fb1be2e9a59..53015a9de93 100644 --- a/app/router.js +++ b/app/router.js @@ -18,6 +18,7 @@ Router.map(function() { this.route('download'); this.route('versions'); this.route('version', { path: '/:version_num' }); + this.route('build_info', { path: '/:version_num/build_info' }); this.route('reverse_dependencies'); diff --git a/app/routes/crate/build_info.js b/app/routes/crate/build_info.js new file mode 100644 index 00000000000..e31b685aa2e --- /dev/null +++ b/app/routes/crate/build_info.js @@ -0,0 +1,24 @@ +import Ember from 'ember'; + +export default Ember.Route.extend({ + model(params) { + const requestedVersion = params.version_num; + const crate = this.modelFor('crate'); + + // Find version model + return crate.get('versions') + .then(versions => { + const version = versions.find(version => version.get('num') === requestedVersion); + if (!version) { + this.controllerFor('application').set('nextFlashError', + `Version '${requestedVersion}' of crate '${crate.get('name')}' does not exist`); + } + return version; + }); + }, + + serialize(model) { + let version_num = model ? model.get('num') : ''; + return { version_num }; + }, +}); diff --git a/app/templates/crate/build-info.hbs b/app/templates/crate/build-info.hbs new file mode 100644 index 00000000000..d52fdd2975d --- /dev/null +++ b/app/templates/crate/build-info.hbs @@ -0,0 +1,83 @@ +{{title name}} + +
+ {{#link-to 'crate' id}}⬅ Back to {{ name }}{{/link-to}} +
+ +
+
+
+ +

Build info for {{ name }}

+

{{ version }}

+
+
+
+ +
+ {{#if has_stable_builds}} +

Stable channel

+ + + + + + + + + {{#each stable_build as |build|}} + + + + + + + + {{/each}} +
Rust VersionLinux 64 bitmacOS 64 bitWindows (GNU) 64 bitWindows (MSVC) 64 bit
{{ build.version }}{{ build-info-pass-fail build.x86_64-unknown-linux-gnu }}{{ build-info-pass-fail build.x86_64-apple-darwin }}{{ build-info-pass-fail build.x86_64-pc-windows-gnu }}{{ build-info-pass-fail build.x86_64-pc-windows-msvc }}
+ {{/if}} + + {{#if has_beta_builds}} +

Beta channel

+ + + + + + + + + {{#each beta_build as |build|}} + + + + + + + + {{/each}} +
Rust VersionLinux 64 bitmacOS 64 bitWindows (GNU) 64 bitWindows (MSVC) 64 bit
{{ format-day build.version }}{{ build-info-pass-fail build.x86_64-unknown-linux-gnu }}{{ build-info-pass-fail build.x86_64-apple-darwin }}{{ build-info-pass-fail build.x86_64-pc-windows-gnu }}{{ build-info-pass-fail build.x86_64-pc-windows-msvc }}
+ {{/if}} + + {{#if has_nightly_builds}} +

Nightly channel

+ + + + + + + + + {{#each nightly_build as |build|}} + + + + + + + + {{/each}} +
Rust VersionLinux 64 bitmacOS 64 bitWindows (GNU) 64 bitWindows (MSVC) 64 bit
{{ format-day build.version }}{{ build-info-pass-fail build.x86_64-unknown-linux-gnu }}{{ build-info-pass-fail build.x86_64-apple-darwin }}{{ build-info-pass-fail build.x86_64-pc-windows-gnu }}{{ build-info-pass-fail build.x86_64-pc-windows-msvc }}
+ {{/if}} +
diff --git a/app/templates/crate/version.hbs b/app/templates/crate/version.hbs index b85ad28d21c..5af909a80d0 100644 --- a/app/templates/crate/version.hbs +++ b/app/templates/crate/version.hbs @@ -147,6 +147,10 @@ on {{ target-icons currentVersion.build_info.latest_positive_results.nightly.targets }}
{{/if}} + + {{#link-to 'crate.build_info' currentVersion.num}} + More build info + {{/link-to}} {{/if}} From 6cdb0e79a6aa77cc34eb087d3ba89b122c2191ea Mon Sep 17 00:00:00 2001 From: "Carol (Nichols || Goulding)" Date: Wed, 28 Feb 2018 20:30:53 -0500 Subject: [PATCH 5/7] Failing test for build info JSON key mismatches --- src/tests/version.rs | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/src/tests/version.rs b/src/tests/version.rs index 183214048f1..bdd9dfe7830 100644 --- a/src/tests/version.rs +++ b/src/tests/version.rs @@ -203,17 +203,47 @@ fn publish_build_info() { } let json = ::json::(&mut response); + + let nightly_key_string = String::from("2017-01-25T00:00:00+00:00"); assert_eq!( json.build_info.ordering.get("nightly"), - Some(&vec![String::from("2017-01-25T00:00:00+00:00")]) + Some(&vec![nightly_key_string.clone()]) ); + assert_eq!( + json.build_info + .nightly + .keys() + .map(ToString::to_string) + .collect::>(), + vec![nightly_key_string] + ); + + let beta_key_string = String::from("2017-01-20T00:00:00+00:00"); assert_eq!( json.build_info.ordering.get("beta"), - Some(&vec![String::from("2017-01-20T00:00:00+00:00")]) + Some(&vec![beta_key_string.clone()]) ); + assert_eq!( + json.build_info + .beta + .keys() + .map(ToString::to_string) + .collect::>(), + vec![beta_key_string] + ); + + let stable_key_string = String::from("1.13.0"); assert_eq!( json.build_info.ordering.get("stable"), - Some(&vec![String::from("1.13.0")]) + Some(&vec![stable_key_string.clone()]) + ); + assert_eq!( + json.build_info + .stable + .keys() + .map(ToString::to_string) + .collect::>(), + vec![stable_key_string] ); } From 881ff2cd4460c61d309ccfb2367f460be6d9f8de Mon Sep 17 00:00:00 2001 From: Justin Geibel Date: Wed, 28 Feb 2018 20:15:31 -0500 Subject: [PATCH 6/7] Change date format in build-info ordering and use SVG instead of PNG The NaiveDate type doesn't include any time or timezone information, so I'm explicitly formatting as year-month-day ("%Y-%m-%d"). --- app/templates/crate/build-info.hbs | 2 +- src/tests/version.rs | 4 ++-- src/version/metadata.rs | 18 +++++++++--------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/templates/crate/build-info.hbs b/app/templates/crate/build-info.hbs index d52fdd2975d..05b337d6d2c 100644 --- a/app/templates/crate/build-info.hbs +++ b/app/templates/crate/build-info.hbs @@ -7,7 +7,7 @@
- +

Build info for {{ name }}

{{ version }}

diff --git a/src/tests/version.rs b/src/tests/version.rs index bdd9dfe7830..e1fc45b6ffb 100644 --- a/src/tests/version.rs +++ b/src/tests/version.rs @@ -204,7 +204,7 @@ fn publish_build_info() { let json = ::json::(&mut response); - let nightly_key_string = String::from("2017-01-25T00:00:00+00:00"); + let nightly_key_string = String::from("2017-01-25"); assert_eq!( json.build_info.ordering.get("nightly"), Some(&vec![nightly_key_string.clone()]) @@ -218,7 +218,7 @@ fn publish_build_info() { vec![nightly_key_string] ); - let beta_key_string = String::from("2017-01-20T00:00:00+00:00"); + let beta_key_string = String::from("2017-01-20"); assert_eq!( json.build_info.ordering.get("beta"), Some(&vec![beta_key_string.clone()]) diff --git a/src/version/metadata.rs b/src/version/metadata.rs index bcd58af9cdf..2e09d2ace48 100644 --- a/src/version/metadata.rs +++ b/src/version/metadata.rs @@ -7,7 +7,7 @@ use std::str::FromStr; -use chrono::{DateTime, NaiveDate, Utc}; +use chrono::NaiveDate; use conduit::{Request, Response}; use diesel::prelude::*; @@ -81,8 +81,8 @@ pub fn authors(req: &mut Request) -> CargoResult { // "build_info": { // "id": 1, // "ordering": { -// "nightly": ["2017-07-26T00:00:00Z"], -// "beta": ["2017-07-18T00:00:00Z"], +// "nightly": ["2017-07-26"], +// "beta": ["2017-07-18"], // "stable": ["1.19.0"] // }, // "stable": { @@ -91,10 +91,10 @@ pub fn authors(req: &mut Request) -> CargoResult { // "1.18.0": { "x86_64-pc-windows-gnu": false } // }, // "beta": { -// "2017-07-18T00:00:00Z": { "x86_64-apple-darwin": false } +// "2017-07-18": { "x86_64-apple-darwin": false } // }, // "nightly": { -// "2017-07-26T00:00:00Z": { "x86_64-apple-darwin": true } +// "2017-07-26": { "x86_64-apple-darwin": true } // } // } // } @@ -159,18 +159,18 @@ pub fn build_info(req: &mut Request) -> CargoResult { stables.into_iter().map(|s| s.to_string()).collect(), ); - fn naive_date_to_rfc3339(date: NaiveDate) -> String { - DateTime::::from_utc(date.and_hms(0, 0, 0), Utc).to_rfc3339() + fn naive_date_to_string(date: NaiveDate) -> String { + date.format("%Y-%m-%d").to_string() } encodable_build_info.ordering.insert( String::from("beta"), - betas.into_iter().map(naive_date_to_rfc3339).collect(), + betas.into_iter().map(naive_date_to_string).collect(), ); encodable_build_info.ordering.insert( String::from("nightly"), - nightlies.into_iter().map(naive_date_to_rfc3339).collect(), + nightlies.into_iter().map(naive_date_to_string).collect(), ); #[derive(Serialize, Debug)] From 6c9794dea5d8139a89063511fdbb183ed2c0dd83 Mon Sep 17 00:00:00 2001 From: Justin Geibel Date: Wed, 7 Mar 2018 20:04:45 -0500 Subject: [PATCH 7/7] Swap order of semver comparison when returning search results This should fix a regression introduced in this commit series. We should add more tests for the search route. --- src/controllers/krate/search.rs | 7 +------ src/version/mod.rs | 4 ++-- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/controllers/krate/search.rs b/src/controllers/krate/search.rs index f9c9be8b9d3..074fbab4533 100644 --- a/src/controllers/krate/search.rs +++ b/src/controllers/krate/search.rs @@ -178,12 +178,7 @@ pub fn search(req: &mut Request) -> CargoResult { .load::(&*conn)? .grouped_by(&crates) .into_iter() - .map(|versions| { - versions - .into_iter() - .max_by(Version::semantically_newest_first) - .unwrap() - }) + .map(|versions| versions.into_iter().max_by(Version::semver_cmp).unwrap()) .collect::>(); let build_infos = BuildInfo::belonging_to(&versions) diff --git a/src/version/mod.rs b/src/version/mod.rs index 386ede728f5..71dfd5c026b 100644 --- a/src/version/mod.rs +++ b/src/version/mod.rs @@ -175,8 +175,8 @@ impl Version { } /// Orders SemVer numbers so that "higher" version numbers appear first. - pub fn semantically_newest_first(a: &Self, b: &Self) -> ::std::cmp::Ordering { - b.num.cmp(&a.num) + pub fn semver_cmp(a: &Self, b: &Self) -> ::std::cmp::Ordering { + a.num.cmp(&b.num) } }