Skip to content

Commit 7232096

Browse files
committed
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.
1 parent c7a5da1 commit 7232096

File tree

13 files changed

+210
-15
lines changed

13 files changed

+210
-15
lines changed

app/components/badge-build-info.js

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import Ember from 'ember';
2+
3+
import { formatDay } from 'cargo/helpers/format-day';
4+
5+
export default Ember.Component.extend({
6+
tagName: 'span',
7+
classNames: ['build_info'],
8+
9+
build_info: Ember.computed('crate.max_build_info_stable', 'crate.max_build_info_beta', 'crate.max_build_info_nightly', function() {
10+
if (this.get('crate.max_build_info_stable')) {
11+
return 'stable';
12+
} else if (this.get('crate.max_build_info_beta')) {
13+
return 'beta';
14+
} else if (this.get('crate.max_build_info_nightly')) {
15+
return 'nightly';
16+
} else {
17+
return null;
18+
}
19+
}),
20+
color: Ember.computed('build_info', function() {
21+
if (this.get('build_info') === 'stable') {
22+
return 'brightgreen';
23+
} else if (this.get('build_info') === 'beta') {
24+
return 'yellow';
25+
} else {
26+
return 'orange';
27+
}
28+
}),
29+
version_display: Ember.computed('build_info', 'crate.max_build_info_stable', 'crate.max_build_info_beta', 'crate.max_build_info_nightly', function() {
30+
if (this.get('build_info') === 'stable') {
31+
return this.get('crate.max_build_info_stable');
32+
} else if (this.get('build_info') === 'beta') {
33+
return formatDay(this.get('crate.max_build_info_beta'));
34+
} else {
35+
return formatDay(this.get('crate.max_build_info_nightly'));
36+
}
37+
}),
38+
version_for_shields: Ember.computed('version_display', function() {
39+
return this.get('version_display').replace(/-/g, '--');
40+
}),
41+
});

app/models/crate.js

+3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ export default DS.Model.extend({
1717
documentation: DS.attr('string'),
1818
repository: DS.attr('string'),
1919
exact_match: DS.attr('boolean'),
20+
max_build_info_nightly: DS.attr('date'),
21+
max_build_info_beta: DS.attr('date'),
22+
max_build_info_stable: DS.attr('string'),
2023

2124
versions: DS.hasMany('versions', { async: true }),
2225
badges: DS.attr(),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{{#if build_info}}
2+
<img
3+
src="https://img.shields.io/badge/builds_on-{{ version_for_shields }}-{{color}}.svg"
4+
alt="Known to build on {{ build_info }} {{ version_display }}"
5+
title="Known to build on {{ build_info }} {{ version_display }}" />
6+
{{/if}}

app/templates/components/crate-row.hbs

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
{{#each crate.annotated_badges as |badge|}}
1212
{{component badge.component_name badge=badge data-test-badge=badge.badge_type}}
1313
{{/each}}
14+
{{badge-build-info crate=crate}}
1415
</div>
1516
<div class='summary' data-test-description>
1617
<span class='small'>

src/krate/metadata.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ pub fn summary(req: &mut Request) -> CargoResult<Response> {
4040
.map(|versions| Version::max(versions.into_iter().map(|v| v.num)))
4141
.zip(krates)
4242
.map(|(max_version, krate)| {
43-
Ok(krate.minimal_encodable(&max_version, None, false, None))
43+
Ok(krate.minimal_encodable(&max_version, None, false, None, None))
4444
})
4545
.collect()
4646
};
@@ -156,6 +156,7 @@ pub fn show(req: &mut Request) -> CargoResult<Response> {
156156
Some(badges),
157157
false,
158158
recent_downloads,
159+
None,
159160
),
160161
versions: versions
161162
.into_iter()

src/krate/mod.rs

+14-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use url::Url;
99
use app::App;
1010
use util::{human, CargoResult};
1111

12-
use views::EncodableBadge;
12+
use views::{EncodableBadge, EncodableMaxVersionBuildInfo};
1313
use models::{Badge, Category, CrateOwner, Keyword, NewCrateOwnerInvitation, Owner, OwnerKind,
1414
ReverseDependency, User, Version};
1515

@@ -113,6 +113,9 @@ pub struct EncodableCrate {
113113
pub repository: Option<String>,
114114
pub links: CrateLinks,
115115
pub exact_match: bool,
116+
pub max_build_info_stable: Option<String>,
117+
pub max_build_info_beta: Option<String>,
118+
pub max_build_info_nightly: Option<String>,
116119
}
117120

118121
#[derive(Serialize, Deserialize, Debug)]
@@ -315,6 +318,7 @@ impl Crate {
315318
badges: Option<Vec<Badge>>,
316319
exact_match: bool,
317320
recent_downloads: Option<i64>,
321+
max_build_info: Option<EncodableMaxVersionBuildInfo>,
318322
) -> EncodableCrate {
319323
self.encodable(
320324
max_version,
@@ -324,6 +328,7 @@ impl Crate {
324328
badges,
325329
exact_match,
326330
recent_downloads,
331+
max_build_info,
327332
)
328333
}
329334

@@ -337,6 +342,7 @@ impl Crate {
337342
badges: Option<Vec<Badge>>,
338343
exact_match: bool,
339344
recent_downloads: Option<i64>,
345+
max_build_info: Option<EncodableMaxVersionBuildInfo>,
340346
) -> EncodableCrate {
341347
let Crate {
342348
name,
@@ -357,6 +363,7 @@ impl Crate {
357363
let category_ids = categories.map(|cats| cats.iter().map(|cat| cat.slug.clone()).collect());
358364
let badges = badges.map(|bs| bs.into_iter().map(|b| b.encodable()).collect());
359365
let documentation = Crate::remove_blacklisted_documentation_urls(documentation);
366+
let max_build_info = max_build_info.unwrap_or_else(EncodableMaxVersionBuildInfo::default);
360367

361368
EncodableCrate {
362369
id: name.clone(),
@@ -370,6 +377,9 @@ impl Crate {
370377
categories: category_ids,
371378
badges: badges,
372379
max_version: max_version.to_string(),
380+
max_build_info_stable: max_build_info.stable,
381+
max_build_info_beta: max_build_info.beta,
382+
max_build_info_nightly: max_build_info.nightly,
373383
documentation: documentation,
374384
homepage: homepage,
375385
exact_match: exact_match,
@@ -606,6 +616,9 @@ mod tests {
606616
downloads: 0,
607617
recent_downloads: None,
608618
max_version: "".to_string(),
619+
max_build_info_stable: None,
620+
max_build_info_beta: None,
621+
max_build_info_nightly: None,
609622
description: None,
610623
homepage: None,
611624
documentation: None,

src/krate/publish.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ pub fn publish(req: &mut Request) -> CargoResult<Response> {
181181
warnings: Warnings<'a>,
182182
}
183183
Ok(req.json(&R {
184-
krate: krate.minimal_encodable(&max_version, None, false, None),
184+
krate: krate.minimal_encodable(&max_version, None, false, None, None),
185185
warnings: warnings,
186186
}))
187187
})

src/krate/search.rs

+23-5
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use user::RequestUser;
1010
use util::{CargoResult, RequestUtils};
1111

1212
use views::EncodableCrate;
13-
use models::{Badge, Crate, OwnerKind, Version};
13+
use models::{Badge, BuildInfo, Crate, OwnerKind, Version};
1414
use schema::*;
1515

1616
use super::{canon_crate_name, ALL_COLUMNS};
@@ -183,28 +183,46 @@ pub fn search(req: &mut Request) -> CargoResult<Response> {
183183
.load::<Version>(&*conn)?
184184
.grouped_by(&crates)
185185
.into_iter()
186-
.map(|versions| Version::max(versions.into_iter().map(|v| v.num)));
186+
.map(|versions| {
187+
versions
188+
.into_iter()
189+
.max_by(Version::semantically_newest_first)
190+
.unwrap()
191+
})
192+
.collect::<Vec<_>>();
193+
194+
let build_infos = BuildInfo::belonging_to(&versions)
195+
.filter(build_info::passed.eq(true))
196+
.select(::version::build_info::BUILD_INFO_FIELDS)
197+
.load::<BuildInfo>(&*conn)?
198+
.grouped_by(&versions)
199+
.into_iter()
200+
.map(BuildInfo::max);
187201

188202
let crates = versions
203+
.into_iter()
189204
.zip(crates)
190205
.zip(perfect_matches)
191206
.zip(recent_downloads)
207+
.zip(build_infos)
192208
.map(
193-
|(((max_version, krate), perfect_match), recent_downloads)| {
209+
|((((max_version, krate), perfect_match), recent_downloads), build_info)| {
210+
let build_info = build_info?;
194211
// FIXME: If we add crate_id to the Badge enum we can eliminate
195212
// this N+1
196213
let badges = badges::table
197214
.filter(badges::crate_id.eq(krate.id))
198215
.load::<Badge>(&*conn)?;
199216
Ok(krate.minimal_encodable(
200-
&max_version,
217+
&max_version.num,
201218
Some(badges),
202219
perfect_match,
203220
Some(recent_downloads),
221+
Some(build_info.encode()),
204222
))
205223
},
206224
)
207-
.collect::<Result<_, ::diesel::result::Error>>()?;
225+
.collect::<CargoResult<_>>()?;
208226

209227
#[derive(Serialize)]
210228
struct R {

src/models/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub use owner::{CrateOwner, NewTeam, Owner, OwnerKind, Rights, Team};
99
pub use user::{Email, NewUser, User};
1010
pub use token::ApiToken;
1111
pub use version::{NewVersion, Version};
12+
pub use version::build_info::BuildInfo;
1213

1314
mod category;
1415
mod keyword;

src/version/build_info.rs

+80-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
use std::str::FromStr;
2+
3+
use chrono::{DateTime, NaiveDate, Utc};
14
use conduit::{Request, Response};
5+
use semver;
26
use serde_json;
37

48
use app::RequestApp;
@@ -8,7 +12,8 @@ use owner::rights;
812
use user::RequestUser;
913
use util::{human, CargoResult, RequestUtils};
1014
use version::version_and_crate;
11-
use views::EncodableVersionBuildInfoUpload;
15+
use views::{EncodableMaxVersionBuildInfo, EncodableVersionBuildInfoUpload,
16+
ParsedRustChannelVersion};
1217

1318
use schema::*;
1419

@@ -24,6 +29,80 @@ pub struct BuildInfo {
2429
pub passed: bool,
2530
}
2631

32+
/// The columns to select from the `build_info` table. The table also stores `created_at` and
33+
/// `updated_at` metadata for each row, but we're not displaying those anywhere so we're not
34+
/// bothering to select them.
35+
pub const BUILD_INFO_FIELDS: (
36+
build_info::version_id,
37+
build_info::rust_version,
38+
build_info::target,
39+
build_info::passed,
40+
) = (
41+
build_info::version_id,
42+
build_info::rust_version,
43+
build_info::target,
44+
build_info::passed,
45+
);
46+
47+
#[derive(Debug)]
48+
/// The maximum version of Rust from each channel that a crate version successfully builds with.
49+
/// Used for summarizing this information in badge form on crate list pages.
50+
pub struct MaxBuildInfo {
51+
pub stable: Option<semver::Version>,
52+
pub beta: Option<NaiveDate>,
53+
pub nightly: Option<NaiveDate>,
54+
}
55+
56+
impl MaxBuildInfo {
57+
/// Encode stable semver number as a string and beta and nightly as times appropriate for
58+
/// JSON.
59+
pub fn encode(self) -> EncodableMaxVersionBuildInfo {
60+
fn naive_date_to_rfc3339(date: NaiveDate) -> String {
61+
DateTime::<Utc>::from_utc(date.and_hms(0, 0, 0), Utc).to_rfc3339()
62+
}
63+
64+
EncodableMaxVersionBuildInfo {
65+
stable: self.stable.map(|v| v.to_string()),
66+
beta: self.beta.map(naive_date_to_rfc3339),
67+
nightly: self.nightly.map(naive_date_to_rfc3339),
68+
}
69+
}
70+
}
71+
72+
impl BuildInfo {
73+
/// From a set of build information data, Find the largest or latest Rust versions that we know
74+
/// about for each channel. Stable uses the largest semver version number; beta and nightly use
75+
/// the latest date.
76+
pub fn max<I>(build_infos: I) -> CargoResult<MaxBuildInfo>
77+
where
78+
I: IntoIterator<Item = BuildInfo>,
79+
{
80+
let build_infos = build_infos
81+
.into_iter()
82+
.map(|bi| ParsedRustChannelVersion::from_str(&bi.rust_version))
83+
.collect::<Result<Vec<_>, _>>()?;
84+
85+
let stable = build_infos
86+
.iter()
87+
.filter_map(ParsedRustChannelVersion::as_stable)
88+
.max();
89+
let beta = build_infos
90+
.iter()
91+
.filter_map(ParsedRustChannelVersion::as_beta)
92+
.max();
93+
let nightly = build_infos
94+
.iter()
95+
.filter_map(ParsedRustChannelVersion::as_nightly)
96+
.max();
97+
98+
Ok(MaxBuildInfo {
99+
stable: stable.cloned(),
100+
beta: beta.cloned(),
101+
nightly: nightly.cloned(),
102+
})
103+
}
104+
}
105+
27106
/// Handles the `POST /crates/:crate_id/:version/build_info` route for the
28107
/// `cargo publish-build-info` command to report on which versions of Rust
29108
/// a crate builds with.

src/version/metadata.rs

+1-6
Original file line numberDiff line numberDiff line change
@@ -106,12 +106,7 @@ pub fn build_info(req: &mut Request) -> CargoResult<Response> {
106106
let conn = req.db_conn()?;
107107

108108
let build_infos = BuildInfo::belonging_to(&version)
109-
.select((
110-
build_info::version_id,
111-
build_info::rust_version,
112-
build_info::target,
113-
build_info::passed,
114-
))
109+
.select(::version::build_info::BUILD_INFO_FIELDS)
115110
.load(&*conn)?;
116111

117112
let mut encodable_build_info = EncodableVersionBuildInfo::default();

src/version/mod.rs

+5
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,11 @@ impl Version {
170170

171171
Ok(())
172172
}
173+
174+
/// Orders SemVer numbers so that "higher" version numbers appear first.
175+
pub fn semantically_newest_first(a: &Self, b: &Self) -> ::std::cmp::Ordering {
176+
b.num.cmp(&a.num)
177+
}
173178
}
174179

175180
impl NewVersion {

0 commit comments

Comments
 (0)