- {{ crate.name }} ({{ crate.max_version }})
+ {{ crate.name }}
{{svg-jar "right-arrow"}}
diff --git a/app/templates/components/crate-list-newest.hbs b/app/templates/components/crate-list-newest.hbs
new file mode 100644
index 00000000000..39a7659743e
--- /dev/null
+++ b/app/templates/components/crate-list-newest.hbs
@@ -0,0 +1,12 @@
+
+ {{#each this.crates as |crate index|}}
+ -
+
+ {{ crate.name }} ({{ crate.newest_version }})
+
+ {{svg-jar "right-arrow"}}
+
+
+
+ {{/each}}
+
diff --git a/app/templates/index.hbs b/app/templates/index.hbs
index 9a32236f93b..4b6e4d401a3 100644
--- a/app/templates/index.hbs
+++ b/app/templates/index.hbs
@@ -38,19 +38,19 @@
Most Recent Downloads
-
+
Popular Keywords (see all)
diff --git a/mirage/factories/crate.js b/mirage/factories/crate.js
index f354a1d5aa5..899f2a0dd02 100644
--- a/mirage/factories/crate.js
+++ b/mirage/factories/crate.js
@@ -16,6 +16,7 @@ export default Factory.extend({
homepage: () => faker.internet.url(),
repository: () => faker.internet.url(),
max_version: () => faker.system.semver(),
+ newest_version: () => faker.system.semver(),
created_at: () => faker.date.past(),
updated_at() {
diff --git a/mirage/fixtures/crates.js b/mirage/fixtures/crates.js
index be295dacba3..8ec46073fa6 100644
--- a/mirage/fixtures/crates.js
+++ b/mirage/fixtures/crates.js
@@ -17,6 +17,7 @@ export default [
id: 'kinetic-rust',
keywords: ['Protocol', 'Kinetic', 'Storage'],
max_version: '0.0.16',
+ newest_version: '0.0.16',
name: 'kinetic-rust',
repository: 'https://github.com/icorderi/kinetic-rust/',
updated_at: '2015-04-21T00:15:49Z',
@@ -41,6 +42,7 @@ export default [
id: 'nanomsg',
keywords: ['network'],
max_version: '0.7.0-alpha',
+ newest_version: '0.6.1',
name: 'nanomsg',
repository: 'https://github.com/thehydroimpulse/nanomsg.rs',
updated_at: '2016-12-28T08:40:00Z',
@@ -70,6 +72,7 @@ export default [
id: 'rust_mixin',
keywords: ['rust', 'plugin', 'code-generation'],
max_version: '0.0.1',
+ newest_version: '0.0.1',
name: 'rust_mixin',
repository: 'https://github.com/huonw/external_mixin',
updated_at: '2015-02-27T11:52:13Z',
@@ -142,6 +145,7 @@ export default [
id: 'external_mixin',
keywords: ['python', 'ruby', 'shell', 'plugin', 'code-generation'],
max_version: '0.0.1',
+ newest_version: '0.0.1',
name: 'external_mixin',
repository: 'https://github.com/huonw/external_mixin',
updated_at: '2015-02-27T11:51:58Z',
@@ -157,6 +161,7 @@ export default [
id: 'external_mixin_umbrella',
keywords: ['plugin', 'code-generation'],
max_version: '0.0.2',
+ newest_version: '0.0.2',
name: 'external_mixin_umbrella',
repository: 'https://github.com/huonw/external_mixin',
updated_at: '2015-02-27T11:52:30Z',
@@ -173,6 +178,7 @@ export default [
id: 'Inflector',
keywords: ['string', 'case', 'camel', 'snake', 'inflection'],
max_version: '0.1.6',
+ newest_version: '0.1.6',
name: 'Inflector',
repository: 'https://github.com/whatisinternet/inflector',
updated_at: '2015-10-27T01:51:42Z',
@@ -188,6 +194,7 @@ export default [
id: 'rs-es',
keywords: ['elasticsearch', 'elastic'],
max_version: '0.1.17',
+ newest_version: '0.1.17',
name: 'rs-es',
repository: 'https://github.com/benashford/rs-es',
updated_at: '2015-09-09T15:34:50Z',
@@ -203,6 +210,7 @@ export default [
id: 'rust-crypto',
keywords: ['Crypto', 'MD5', 'Sha1', 'Sha2', 'AES'],
max_version: '0.2.34',
+ newest_version: '0.2.34',
name: 'rust-crypto',
repository: 'https://github.com/DaGenix/rust-crypto/',
updated_at: '2015-10-29T01:16:17Z',
@@ -218,6 +226,7 @@ export default [
id: 'rust-htslib',
keywords: ['htslib', 'bam', 'bioinformatics', 'pileup', 'sequencing'],
max_version: '0.5.2',
+ newest_version: '0.5.2',
name: 'rust-htslib',
repository: 'https://github.com/rust-bio/rust-htslib.git',
updated_at: '2015-11-11T00:10:43Z',
@@ -233,6 +242,7 @@ export default [
id: 'rustless',
keywords: ['api', 'web', 'hyper', 'iron', 'rest'],
max_version: '0.8.0',
+ newest_version: '0.8.0',
name: 'rustless',
repository: 'https://crates.io/crates/rustless',
updated_at: '2015-10-31T11:49:29Z',
@@ -248,6 +258,7 @@ export default [
id: 'serde',
keywords: ['serde', 'serialization'],
max_version: '0.6.1',
+ newest_version: '0.6.1',
name: 'serde',
repository: 'https://github.com/serde-rs/serde',
updated_at: '2015-10-18T03:10:21Z',
@@ -263,6 +274,7 @@ export default [
id: 'rusted_cypher',
keywords: ['neo4j', 'database', 'query', 'cypher', 'graph'],
max_version: '0.7.1',
+ newest_version: '0.7.1',
name: 'rusted_cypher',
repository: 'https://github.com/livioribeiro/rusted-cypher',
updated_at: '2015-11-07T17:26:55Z',
@@ -279,6 +291,7 @@ export default [
id: 'zlib',
keywords: [],
max_version: '0.0.1',
+ newest_version: '0.0.1',
name: 'zlib',
repository: null,
updated_at: '2015-01-02T20:54:04Z',
@@ -295,6 +308,7 @@ export default [
id: 'rustful',
keywords: ['web', 'rest', 'framework', 'http', 'routing'],
max_version: '0.5.0',
+ newest_version: '0.5.0',
name: 'rustful',
repository: 'https://github.com/Ogeon/rustful',
updated_at: '2015-09-19T21:10:27Z',
@@ -310,6 +324,7 @@ export default [
id: 'postgres',
keywords: ['database', 'sql'],
max_version: '0.10.1',
+ newest_version: '0.10.1',
name: 'postgres',
repository: 'https://github.com/sfackler/rust-postgres',
updated_at: '2015-11-08T00:48:59Z',
@@ -325,6 +340,7 @@ export default [
id: 'quickcheck',
keywords: ['testing', 'quickcheck', 'property', 'shrinking', 'fuzz'],
max_version: '0.2.24',
+ newest_version: '0.2.24',
name: 'quickcheck',
repository: 'https://github.com/BurntSushi/quickcheck',
updated_at: '2015-09-20T21:53:38Z',
@@ -340,6 +356,7 @@ export default [
id: 'quickcheck_macros',
keywords: ['testing', 'quickcheck', 'property', 'shrinking', 'fuzz'],
max_version: '0.2.24',
+ newest_version: '0.2.24',
name: 'quickcheck_macros',
repository: 'https://github.com/BurntSushi/quickcheck',
updated_at: '2015-09-20T21:53:57Z',
@@ -356,6 +373,7 @@ export default [
id: 'nc_rustlex',
keywords: ['lexer', 'lexical', 'analyser', 'generator'],
max_version: '0.3.1',
+ newest_version: '0.3.1',
name: 'nc_rustlex',
repository: 'https://github.com/nicolas-cherel/rustlex',
updated_at: '2015-08-25T19:15:35Z',
@@ -371,6 +389,7 @@ export default [
id: 'nom',
keywords: ['parser', 'parser-combinators', 'parsing', 'streaming', 'bit'],
max_version: '1.0.1',
+ newest_version: '1.0.1',
name: 'nom',
repository: 'https://github.com/Geal/nom',
updated_at: '2015-11-22T22:00:41Z',
diff --git a/mirage/fixtures/versions.js b/mirage/fixtures/versions.js
index 5a6a7347ed1..e7aaa14bd6f 100644
--- a/mirage/fixtures/versions.js
+++ b/mirage/fixtures/versions.js
@@ -1,7 +1,7 @@
export default [
{
crate: 'nanomsg',
- created_at: '2016-12-27T08:40:00Z',
+ created_at: '2016-12-20T07:30:00Z',
dl_path: '/api/v1/crates/nanomsg/0.7.0-alpha.1/download',
downloads: 260,
features: {
@@ -9,7 +9,7 @@ export default [
},
id: 40906,
num: '0.7.0-alpha.1',
- updated_at: '2016-12-27T08:40:00Z',
+ updated_at: '2016-12-20T07:30:00Z',
yanked: false,
license: 'MIT',
crate_size: 912355,
diff --git a/mirage/serializers/crate.js b/mirage/serializers/crate.js
index 940a8f3d27e..ef9ad34957c 100644
--- a/mirage/serializers/crate.js
+++ b/mirage/serializers/crate.js
@@ -14,6 +14,7 @@ export default BaseSerializer.extend({
'keywords',
'links',
'max_version',
+ 'newest_version',
'name',
'repository',
'updated_at',
diff --git a/src/controllers/krate/metadata.rs b/src/controllers/krate/metadata.rs
index 14e42c2c533..2817ebf9c54 100644
--- a/src/controllers/krate/metadata.rs
+++ b/src/controllers/krate/metadata.rs
@@ -1,10 +1,11 @@
//! Endpoints that expose metadata about a crate
//!
-//! 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.
use crate::controllers::frontend_prelude::*;
+
use crate::models::{
Category, Crate, CrateCategory, CrateKeyword, CrateVersions, Keyword, RecentCrateDownloads,
User, Version, VersionOwnerAction,
@@ -31,10 +32,10 @@ pub fn summary(req: &mut dyn Request) -> AppResult {
versions
.grouped_by(&krates)
.into_iter()
- .map(|versions| Version::max(versions.into_iter().map(|v| v.num)))
+ .map(|versions| Version::top(versions.into_iter().map(|v| (v.created_at, v.num))))
.zip(krates)
- .map(|(max_version, krate)| {
- Ok(krate.minimal_encodable(&max_version, None, false, None))
+ .map(|(top_versions, krate)| {
+ Ok(krate.minimal_encodable(&top_versions, None, false, None))
})
.collect()
};
@@ -142,7 +143,7 @@ pub fn show(req: &mut dyn Request) -> AppResult {
let badges = badges::table
.filter(badges::crate_id.eq(krate.id))
.load(&*conn)?;
- let max_version = krate.max_version(&conn)?;
+ let top_versions = krate.top_versions(&conn)?;
#[derive(Serialize)]
struct R {
@@ -154,7 +155,7 @@ pub fn show(req: &mut dyn Request) -> AppResult {
}
Ok(req.json(&R {
krate: krate.clone().encodable(
- &max_version,
+ &top_versions,
Some(ids),
Some(&kws),
Some(&cats),
diff --git a/src/controllers/krate/publish.rs b/src/controllers/krate/publish.rs
index 09dce9934aa..b3d93a9f672 100644
--- a/src/controllers/krate/publish.rs
+++ b/src/controllers/krate/publish.rs
@@ -168,7 +168,7 @@ pub fn publish(req: &mut dyn Request) -> AppResult {
// Update all badges for this crate, collecting any invalid badges in
// order to be able to warn about them
let ignored_invalid_badges = Badge::update_crate(&conn, &krate, new_crate.badges.as_ref())?;
- let max_version = krate.max_version(&conn)?;
+ let top_versions = krate.top_versions(&conn)?;
if let Some(readme) = new_crate.readme {
render::render_and_upload_readme(
@@ -215,7 +215,7 @@ pub fn publish(req: &mut dyn Request) -> AppResult {
};
Ok(req.json(&GoodCrate {
- krate: krate.minimal_encodable(&max_version, None, false, None),
+ krate: krate.minimal_encodable(&top_versions, None, false, None),
warnings,
}))
})
diff --git a/src/controllers/krate/search.rs b/src/controllers/krate/search.rs
index 2ad799bd4ce..05aaa6c9bc1 100644
--- a/src/controllers/krate/search.rs
+++ b/src/controllers/krate/search.rs
@@ -179,7 +179,7 @@ pub fn search(req: &mut dyn Request) -> AppResult {
.load::(&*conn)?
.grouped_by(&crates)
.into_iter()
- .map(|versions| Version::max(versions.into_iter().map(|v| v.num)));
+ .map(|versions| Version::top(versions.into_iter().map(|v| (v.created_at, v.num))));
let badges = CrateBadge::belonging_to(&crates)
.select((badges::crate_id, badges::all_columns))
diff --git a/src/models/krate.rs b/src/models/krate.rs
index a6839b19e92..46d035a4dae 100644
--- a/src/models/krate.rs
+++ b/src/models/krate.rs
@@ -8,12 +8,12 @@ use url::Url;
use crate::app::App;
use crate::email;
-use crate::util::{cargo_err, AppResult};
-
+use crate::models::version::TopVersions;
use crate::models::{
Badge, Category, CrateOwner, CrateOwnerInvitation, Keyword, NewCrateOwnerInvitation, Owner,
OwnerKind, ReverseDependency, User, Version,
};
+use crate::util::{cargo_err, AppResult};
use crate::views::{EncodableCrate, EncodableCrateLinks};
use crate::models::helpers::with_count::*;
@@ -204,7 +204,7 @@ impl<'a> NewCrate<'a> {
}
impl Crate {
- /// SQL filter based on whether the crate's name loosly matches the given
+ /// SQL filter based on whether the crate's name loosely matches the given
/// string.
///
/// The operator used varies based on the input.
@@ -281,13 +281,13 @@ impl Crate {
pub fn minimal_encodable(
self,
- max_version: &semver::Version,
+ top_versions: &TopVersions,
badges: Option>,
exact_match: bool,
recent_downloads: Option,
) -> EncodableCrate {
self.encodable(
- max_version,
+ top_versions,
None,
None,
None,
@@ -300,7 +300,7 @@ impl Crate {
#[allow(clippy::too_many_arguments)]
pub fn encodable(
self,
- max_version: &semver::Version,
+ top_versions: &TopVersions,
versions: Option>,
keywords: Option<&[Keyword]>,
categories: Option<&[Category]>,
@@ -339,7 +339,8 @@ impl Crate {
keywords: keyword_ids,
categories: category_ids,
badges,
- max_version: max_version.to_string(),
+ max_version: top_versions.highest.to_string(),
+ newest_version: top_versions.newest.to_string(),
documentation,
homepage,
exact_match,
@@ -384,16 +385,16 @@ impl Crate {
}
}
- pub fn max_version(&self, conn: &PgConnection) -> AppResult {
+ /// Return both the newest (most recently updated) and
+ /// highest version (in semver order) for the current crate.
+ pub fn top_versions(&self, conn: &PgConnection) -> AppResult {
use crate::schema::versions::dsl::*;
- let vs = self
- .versions()
- .select(num)
- .load::(conn)?
- .into_iter()
- .map(|s| semver::Version::parse(&s).unwrap());
- Ok(Version::max(vs))
+ Ok(Version::top(
+ self.versions()
+ .select((updated_at, num))
+ .load::<(NaiveDateTime, semver::Version)>(conn)?,
+ ))
}
pub fn owners(&self, conn: &PgConnection) -> AppResult> {
diff --git a/src/models/version.rs b/src/models/version.rs
index 0ee71275d64..54345f746ff 100644
--- a/src/models/version.rs
+++ b/src/models/version.rs
@@ -37,6 +37,25 @@ pub struct NewVersion {
published_by: i32,
}
+/// The highest version (semver order) and the most recently updated version.
+/// Typically used for a single crate.
+#[derive(Debug, Clone)]
+pub struct TopVersions {
+ pub highest: semver::Version,
+ pub newest: semver::Version,
+}
+
+/// A default semver value, "0.0.0", for use in TopVersions
+fn default_semver_version() -> semver::Version {
+ semver::Version {
+ major: 0,
+ minor: 0,
+ patch: 0,
+ pre: vec![],
+ build: vec![],
+ }
+}
+
impl Version {
pub fn encodable(
self,
@@ -96,20 +115,28 @@ impl Version {
.load(conn)
}
- pub fn max(versions: T) -> semver::Version
+ /// Return both the newest (most recently updated) and the
+ /// highest version (in semver order) for a collection of date/version pairs.
+ pub fn top(pairs: T) -> TopVersions
where
- T: IntoIterator- ,
+ T: Clone + IntoIterator
- ,
{
- versions
- .into_iter()
- .max()
- .unwrap_or_else(|| semver::Version {
- major: 0,
- minor: 0,
- patch: 0,
- pre: vec![],
- build: vec![],
- })
+ TopVersions {
+ newest: pairs
+ .clone()
+ .into_iter()
+ .max()
+ .unwrap_or((
+ NaiveDateTime::from_timestamp(0, 0),
+ default_semver_version(),
+ ))
+ .1,
+ highest: pairs
+ .into_iter()
+ .map(|(_, v)| v)
+ .max()
+ .unwrap_or_else(default_semver_version),
+ }
}
pub fn record_readme_rendering(version_id_: i32, conn: &PgConnection) -> QueryResult {
diff --git a/src/tests/krate.rs b/src/tests/krate.rs
index 82815951f77..f85912dc487 100644
--- a/src/tests/krate.rs
+++ b/src/tests/krate.rs
@@ -1243,6 +1243,12 @@ fn summary_new_crates() {
let krate3 = CrateBuilder::new("just_updated", user.id)
.version(VersionBuilder::new("0.1.0"))
+ .version(VersionBuilder::new("0.1.2"))
+ .expect_build(conn);
+
+ let krate4 = CrateBuilder::new("just_updated_patch", user.id)
+ .version(VersionBuilder::new("0.1.0"))
+ .version(VersionBuilder::new("0.2.0"))
.expect_build(conn);
CrateBuilder::new("with_downloads", user.id)
@@ -1269,11 +1275,25 @@ fn summary_new_crates() {
.set(crates::updated_at.eq(updated))
.execute(&*conn)
.unwrap();
+
+ let plus_two = Utc::now().naive_utc() + chrono::Duration::seconds(2);
+ let newer = VersionBuilder::new("0.1.1").expect_build(krate4.id, user.id, conn);
+
+ // Update the patch version to be newer than the other versions, including the higher one.
+ update(&newer)
+ .set(versions::created_at.eq(plus_two))
+ .execute(&*conn)
+ .unwrap();
+
+ update(&krate4)
+ .set(crates::updated_at.eq(plus_two))
+ .execute(&*conn)
+ .unwrap();
});
let json: SummaryResponse = anon.get("/api/v1/summary").good();
- assert_eq!(json.num_crates, 4);
+ assert_eq!(json.num_crates, 5);
assert_eq!(json.num_downloads, 6000);
assert_eq!(json.most_downloaded[0].name, "most_recent_downloads");
assert_eq!(
@@ -1282,9 +1302,17 @@ fn summary_new_crates() {
);
assert_eq!(json.popular_keywords[0].keyword, "popular");
assert_eq!(json.popular_categories[0].category, "Category 1");
- assert_eq!(json.just_updated.len(), 1);
- assert_eq!(json.just_updated[0].name, "just_updated");
- assert_eq!(json.new_crates.len(), 4);
+ assert_eq!(json.just_updated.len(), 2);
+
+ assert_eq!(json.just_updated[0].name, "just_updated_patch");
+ assert_eq!(json.just_updated[0].max_version, "0.2.0");
+ assert_eq!(json.just_updated[0].newest_version, "0.1.1");
+
+ assert_eq!(json.just_updated[1].name, "just_updated");
+ assert_eq!(json.just_updated[1].max_version, "0.1.2");
+ assert_eq!(json.just_updated[1].newest_version, "0.1.2");
+
+ assert_eq!(json.new_crates.len(), 5);
}
#[test]
diff --git a/src/tests/util.rs b/src/tests/util.rs
index 054a44d92da..76b2e8b180b 100644
--- a/src/tests/util.rs
+++ b/src/tests/util.rs
@@ -305,7 +305,7 @@ impl TestAppBuilder {
}
}
-/// A colleciton of helper methods for the 3 authentication types
+/// A collection of helper methods for the 3 authentication types
///
/// Helper methods go through public APIs, and should not modify the database directly
pub trait RequestHelper {
diff --git a/src/views.rs b/src/views.rs
index 981578e8be7..e5b86a8f98b 100644
--- a/src/views.rs
+++ b/src/views.rs
@@ -97,6 +97,7 @@ pub struct EncodableCrate {
pub recent_downloads: Option,
// NOTE: Used by shields.io, altering `max_version` requires a PR with shields.io
pub max_version: String,
+ pub newest_version: String, // Most recently updated version, which may not be max
pub description: Option,
pub homepage: Option,
pub documentation: Option,
@@ -363,6 +364,7 @@ mod tests {
downloads: 0,
recent_downloads: None,
max_version: "".to_string(),
+ newest_version: "".to_string(),
description: None,
homepage: None,
documentation: None,
diff --git a/tests/acceptance/crate-test.js b/tests/acceptance/crate-test.js
index 3bab3e97591..2d4561be7a4 100644
--- a/tests/acceptance/crate-test.js
+++ b/tests/acceptance/crate-test.js
@@ -61,13 +61,17 @@ module('Acceptance | crate page', function(hooks) {
});
test('visiting a crate page from the front page', async function(assert) {
- this.server.create('crate', 'withVersion', { id: 'nanomsg' });
+ this.server.create('crate', { id: 'nanomsg', newest_version: '0.6.1' });
+ this.server.create('version', { crate: 'nanomsg', num: '0.6.1' });
await visit('/');
await click('[data-test-just-updated] [data-test-crate-link="0"]');
- assert.equal(currentURL(), '/crates/nanomsg');
+ assert.equal(currentURL(), '/crates/nanomsg/0.6.1');
assert.equal(title(), 'nanomsg - crates.io: Rust Package Registry');
+
+ assert.dom('[data-test-heading] [data-test-crate-name]').hasText('nanomsg');
+ assert.dom('[data-test-heading] [data-test-crate-version]').hasText('0.6.1');
});
test('visiting /crates/nanomsg', async function(assert) {
diff --git a/tests/acceptance/front-page-test.js b/tests/acceptance/front-page-test.js
index f31eeb9a0b5..8fc023b87b0 100644
--- a/tests/acceptance/front-page-test.js
+++ b/tests/acceptance/front-page-test.js
@@ -36,13 +36,13 @@ module('Acceptance | front page', function(hooks) {
assert.dom('[data-test-total-crates]').hasText('19');
assert.dom('[data-test-new-crates] [data-test-crate-link="0"]').hasText('Inflector (0.1.6)');
- assert.dom('[data-test-new-crates] [data-test-crate-link="0"]').hasAttribute('href', '/crates/Inflector');
+ assert.dom('[data-test-new-crates] [data-test-crate-link="0"]').hasAttribute('href', '/crates/Inflector/0.1.6');
- assert.dom('[data-test-most-downloaded] [data-test-crate-link="0"]').hasText('serde (0.6.1)');
+ assert.dom('[data-test-most-downloaded] [data-test-crate-link="0"]').hasText('serde');
assert.dom('[data-test-most-downloaded] [data-test-crate-link="0"]').hasAttribute('href', '/crates/serde');
- assert.dom('[data-test-just-updated] [data-test-crate-link="0"]').hasText('nanomsg (0.7.0-alpha)');
- assert.dom('[data-test-just-updated] [data-test-crate-link="0"]').hasAttribute('href', '/crates/nanomsg');
+ assert.dom('[data-test-just-updated] [data-test-crate-link="0"]').hasText('nanomsg (0.6.1)');
+ assert.dom('[data-test-just-updated] [data-test-crate-link="0"]').hasAttribute('href', '/crates/nanomsg/0.6.1');
percySnapshot(assert);
});