diff options
author | Kornel <kornel@geekhood.net> | 2020-01-26 03:48:59 +0000 |
---|---|---|
committer | Kornel <kornel@geekhood.net> | 2020-01-26 03:48:59 +0000 |
commit | 916f06583e8b4539b7ef802f02e1ace543ba702f (patch) | |
tree | 2ab2ddb85f4de37a92a7b712620abe0ff9cbe0b0 | |
parent | b5da3d19ab96a17ea3b3662c6b70b434f5388bb4 (diff) |
Rev deps page
-rw-r--r-- | front_end/src/front_end.rs | 12 | ||||
-rw-r--r-- | front_end/src/reverse_dependencies.rs | 118 | ||||
-rw-r--r-- | front_end/src/urler.rs | 11 | ||||
-rw-r--r-- | front_end/templates/base.rs.html | 4 | ||||
-rw-r--r-- | front_end/templates/crate_page.rs.html | 4 | ||||
-rw-r--r-- | front_end/templates/reverse_dependencies.rs.html | 73 | ||||
-rw-r--r-- | kitchen_sink/src/deps_stats.rs | 6 | ||||
-rw-r--r-- | kitchen_sink/src/index.rs | 6 | ||||
-rw-r--r-- | kitchen_sink/src/lib_kitchen_sink.rs | 4 | ||||
-rw-r--r-- | server/Cargo.toml | 2 | ||||
-rw-r--r-- | server/src/main.rs | 30 | ||||
m--------- | style | 0 |
12 files changed, 258 insertions, 12 deletions
diff --git a/front_end/src/front_end.rs b/front_end/src/front_end.rs index 2be6076..8a9f6c4 100644 --- a/front_end/src/front_end.rs +++ b/front_end/src/front_end.rs @@ -14,6 +14,7 @@ mod not_found_page; mod search_page; mod install_page; mod urler; +mod reverse_dependencies; pub use crate::not_found_page::*; pub use crate::search_page::*; @@ -133,6 +134,17 @@ pub fn render_crate_page<W: Write>(out: &mut W, all: &RichCrate, ver: &RichCrate Ok(c.page_title()) } +/// See `reverse_dependencies.rs.html` +pub fn render_crate_reverse_dependencies<W: Write>(out: &mut W, ver: &RichCrateVersion, kitchen_sink: &KitchenSink, renderer: &Renderer) -> Result<(), failure::Error> { + if stopped() { + Err(KitchenSinkErr::Stopped)?; + } + let urler = Urler::new(None); + let c = reverse_dependencies::CratePageRevDeps::new(ver, kitchen_sink, renderer)?; + templates::reverse_dependencies(out, &urler, &c)?; + Ok(()) +} + /// See `install.rs.html` pub fn render_install_page(out: &mut impl Write, ver: &RichCrateVersion, kitchen_sink: &KitchenSink, renderer: &Renderer) -> Result<(), failure::Error> { if stopped() { diff --git a/front_end/src/reverse_dependencies.rs b/front_end/src/reverse_dependencies.rs new file mode 100644 index 0000000..b2cd9d0 --- /dev/null +++ b/front_end/src/reverse_dependencies.rs @@ -0,0 +1,118 @@ +use crate::Page; +use kitchen_sink::CResult; +use kitchen_sink::KitchenSink; +use kitchen_sink::Origin; +use kitchen_sink::RevDependencies; +use kitchen_sink::SemVer; +use locale::Numeric; +use rayon::prelude::*; +use render_readme::Renderer; + +use rich_crate::RichCrateVersion; +use semver::VersionReq; +use std::fmt::Display; + +pub struct CratePageRevDeps<'a> { + pub ver: &'a RichCrateVersion, + pub deps: Vec<RevDepInf<'a>>, + pub stats: &'a RevDependencies, +} + +pub struct RevDepInf<'a> { + pub origin: Origin, + pub downloads: usize, + pub rev_dep: &'a kitchen_sink::Version, + pub is_optional: bool, + pub matches_latest: bool, + pub kind: &'a str, + pub req: VersionReq, +} + +impl<'a> CratePageRevDeps<'a> { + pub fn new(ver: &'a RichCrateVersion, kitchen_sink: &'a KitchenSink, _markup: &'a Renderer) -> CResult<Self> { + let deps = kitchen_sink.index.deps_stats()?; + let own_name = ver.short_name(); + // RichCrateVersion may be unstable + let latest_stable_semver = kitchen_sink.index.crate_highest_version(&own_name.to_lowercase(), true)?.version().parse()?; + let stats = deps.counts.get(own_name).ok_or_else(|| failure::err_msg("bad crate name"))?; + + let mut deps: Vec<_> = stats.rev_dep_names.iter().par_bridge().map(|rev_dep| { + let origin = Origin::from_crates_io_name(rev_dep); + let downloads = kitchen_sink.downloads_per_month(&origin).ok().and_then(|x| x).unwrap_or(0); + let rev_dep = kitchen_sink.index.crate_highest_version(&rev_dep.to_lowercase(), true).expect("rev dep integrity"); + let (is_optional, req, kind) = rev_dep.dependencies().iter().filter(|d| { + own_name == d.crate_name() + }) + .next() + .map(|d| { + (d.is_optional(), d.requirement(), d.kind().unwrap_or_default()) + }) + .unwrap_or_default(); + + let req = req.parse().unwrap_or_else(|_| VersionReq::any()); + let matches_latest = req.matches(&latest_stable_semver); + + RevDepInf { + origin, + rev_dep, downloads, is_optional, req, kind, + matches_latest, + } + }).collect(); + + // sort by downloads if > 100, then by name + deps.sort_by(|a, b| { + b.downloads.max(100).cmp(&a.downloads.max(100)) + .then_with(|| { + a.rev_dep.name().cmp(b.rev_dep.name()) + }) + }); + deps.truncate(1000); + + Ok(Self { + ver, + deps, + stats, + }) + } + + /// Nicely rounded number of downloads + /// + /// To show that these numbers are just approximate. + pub fn downloads(&self, num: usize) -> (String, &str) { + match num { + a @ 0..=99 => (format!("{}", a), ""), + a @ 0..=500 => (format!("{}", a / 10 * 10), ""), + a @ 0..=999 => (format!("{}", a / 50 * 50), ""), + a @ 0..=9999 => (format!("{}.{}", a / 1000, a % 1000 / 100), "K"), + a @ 0..=999_999 => (format!("{}", a / 1000), "K"), + a => (format!("{}.{}", a / 1_000_000, a % 1_000_000 / 100_000), "M"), + } + } + + pub fn format_number(&self, num: impl Display) -> String { + Numeric::english().format_int(num) + } + + // version, deps, normalized popularity 0..100 + pub fn version_breakdown(&self) -> Vec<(SemVer, u16, f32)> { + let mut ver: Vec<_> = self.stats.versions.iter().map(|(k, v)| { + (k.to_semver(), *v, 0.) + }).collect(); + + let max = ver.iter().map(|(_, n, _)| *n).max().unwrap_or(1) as f32; + ver.iter_mut().for_each(|i| i.2 = i.1 as f32 / max * 100.0); + ver.sort_by(|a, b| b.0.cmp(&a.0)); + ver + } + + pub fn page(&self) -> Page { + Page { + title: format!("Reverse dependencies of {}", self.ver.short_name()), + item_name: Some(self.ver.short_name().to_string()), + item_description: self.ver.description().map(|d| d.to_string()), + noindex: true, + search_meta: false, + ..Default::default() + } + } +} diff --git a/front_end/src/urler.rs b/front_end/src/urler.rs index 06c11f3..4ef1cd8 100644 --- a/front_end/src/urler.rs +++ b/front_end/src/urler.rs @@ -68,8 +68,15 @@ impl Urler { } } - pub fn reverse_deps(&self, krate: &RichCrateVersion) -> String { - format!("https://crates.io/crates/{}/reverse_dependencies", encode(krate.short_name())) + pub fn reverse_deps(&self, origin: &Origin) -> String { + match origin { + Origin::CratesIo(lowercase_name) => { + format!("/crates/{}/rev", encode(lowercase_name)) + }, + Origin::GitHub {package, ..} | Origin::GitLab {package, ..} => { + format!("/crates/{}/rev", encode(package)) // FIXME: that's bogus, return None + }, + } } pub fn crates_io_crate(&self, origin: &Origin) -> Option<String> { diff --git a/front_end/templates/base.rs.html b/front_end/templates/base.rs.html index 8d7fba8..3e7a179 100644 --- a/front_end/templates/base.rs.html +++ b/front_end/templates/base.rs.html @@ -9,14 +9,14 @@ <title>@props.title // Lib.rs</title> <meta name="twitter:title" content="@props.title"> - <link rel=preload as=style href="/index.css?ia" crossorigin="anonymous"> + <link rel=preload as=style href="/index.css?re" crossorigin="anonymous"> <link rel=preload as=font href="/fira/va9B4kDNxMZdWfMOD5VnLK3eRhf6Xl7Glw.woff2" crossorigin="anonymous"> <link rel=preload as=font href="/fira/va9B4kDNxMZdWfMOD5VnPKreRhf6Xl7Glw.woff2" crossorigin="anonymous"> <link rel=preload as=font href="/fira/va9E4kDNxMZdWfMOD5Vvl4jLazX3dA.woff2" crossorigin="anonymous"> <style>@props.critical_css()</style> @if let Some(l) = props.local_css_data() {<style>@l</style>} - <link rel=stylesheet href="/index.css?ia" crossorigin="anonymous"> + <link rel=stylesheet href="/index.css?re" crossorigin="anonymous"> @if props.noindex { <meta name="robots" content="noindex, follow"> diff --git a/front_end/templates/crate_page.rs.html b/front_end/templates/crate_page.rs.html index d7dc821..151906d 100644 --- a/front_end/templates/crate_page.rs.html +++ b/front_end/templates/crate_page.rs.html @@ -218,11 +218,11 @@ (via <a href="@url.crate_by_origin(&Origin::from_crates_io_name(name))">@name</a>) } } else { - (<a href="@url.reverse_deps(&c.ver)">@c.format_number(direct) directly</a>) + (<a href="@url.reverse_deps(c.ver.origin())">@c.format_number(direct) directly</a>) } } else { - Used in <a href="@url.reverse_deps(&c.ver)"><b>@c.format_number(deps)</b> crate@if deps != 1 {s}</a> + Used in <a href="@url.reverse_deps(c.ver.origin())"><b>@c.format_number(deps)</b> crate@if deps != 1 {s}</a> } } } diff --git a/front_end/templates/reverse_dependencies.rs.html b/front_end/templates/reverse_dependencies.rs.html new file mode 100644 index 0000000..fee4d24 --- /dev/null +++ b/front_end/templates/reverse_dependencies.rs.html @@ -0,0 +1,73 @@ +@use crate::templates::base; +@use crate::reverse_dependencies::CratePageRevDeps; +@use crate::Urler; + +@(url: &Urler, p: &CratePageRevDeps) + +@:base(&p.page(), { + <header id="rev-deps" @if p.ver.is_yanked() {class="yanked"} else {@if p.ver.is_nightly() {class="nightly"}}> + <div class="inner-col"> + <div class="breadcrumbs"> + <a href="/"><span>Lib</span>.rs</a> › + <h1> + <a href="@url.krate(&p.ver)" rel="up">@p.ver.capitalized_name()</a> + </h1> + › Reverse dependencies + </div> + </div> + </header> + <main> + <div class="inner-col"> + <p> + @if p.stats.runtime.all() > 0 { + <a href="@url.krate(&p.ver)" rel="up">@p.ver.capitalized_name()</a> is included in @p.format_number(p.stats.runtime.all()) crate@if p.stats.runtime.all() != 1 {s}@if p.stats.runtime.all() > u32::from(p.stats.direct.runtime) { + (@if p.stats.runtime.opt > 0 {of which @p.format_number(p.stats.runtime.opt) optionally, }@p.format_number(p.stats.direct.runtime) directly)}. + } + + @if p.stats.build.all() > 0 { + It's used at build time in @p.format_number(p.stats.build.all()) crate@if p.stats.build.all() != 1 {s}@if p.stats.build.all() > u32::from(p.stats.direct.build) { + (@if p.stats.build.opt > 0 {of which @p.format_number(p.stats.build.opt) optionally, }@p.format_number(p.stats.direct.build) directly)}. + } + + @if p.stats.dev > 0 { + It's used as a dev dependency in @p.format_number(p.stats.dev) crate@if p.stats.dev != 1 {s}@if p.stats.dev > p.stats.direct.dev && p.stats.direct.dev > 0 { + (@p.format_number(p.stats.direct.dev) directly)}. + } + </p> + + @if p.stats.versions.len() > 1 { + <table class="version-pop"> + <thead><th>Number of dependers</th><th>@p.ver.capitalized_name() version</th></thead> + @for (ver, num, perc) in p.version_breakdown() { + <tr><td>@if perc <= 10.0 {@num} <span style="width:@perc%">@if perc > 10.0 {@num}</span></td><th>@ver</th></tr> + } + </table> + } + + @if !p.deps.is_empty() { + <table class="reverse-deps"> + <thead><th></th><th>Depender <small>(with downloads)</small></th><th colspan="2">@p.ver.capitalized_name() version</th></thead> + @for r in &p.deps { + <tr> + <td>@if r.downloads > 100 { + <span class=downloads>@if let Some((num,unit)) = Some(p.downloads(r.downloads)) {@num<b>@unit</b>}</span> + }</td> + <td><a href="@url.crate_by_origin(&r.origin)">@r.rev_dep.name()</a> <a class="more" href="@url.reverse_deps(&r.origin)">↑</a> </td> + <td>@if r.is_optional {<span class=feature>optional</span>} + @if r.kind != "normal" && r.kind != "" {<span class="label label-@r.kind">@r.kind</span>}</td> + <td @if !r.matches_latest {class="outdated"}>@r.req</td> + </tr> + } + </table> + } else { + <p>This crate isn't used by any other public crates.</p> + } + </div> + </main> + + <footer> + <div class="inner-col"> + <p>Back to <a href="@url.krate(&p.ver)" rel="up">@p.ver.capitalized_name()</a>.</p> + </div> + </footer> +}) diff --git a/kitchen_sink/src/deps_stats.rs b/kitchen_sink/src/deps_stats.rs index 9d938fa..f3d1617 100644 --- a/kitchen_sink/src/deps_stats.rs +++ b/kitchen_sink/src/deps_stats.rs @@ -20,6 +20,12 @@ pub struct RevDepCount { pub opt: u16, } +impl RevDepCount { + pub fn all(&self) -> u32 { + self.def as u32 + self.opt as u32 + } +} + #[derive(Debug, Clone, Default)] pub struct DirectDepCount { pub runtime: u16, diff --git a/kitchen_sink/src/index.rs b/kitchen_sink/src/index.rs index 1aa8225..38d8853 100644 --- a/kitchen_sink/src/index.rs +++ b/kitchen_sink/src/index.rs @@ -4,7 +4,7 @@ use crate::KitchenSink; use crate::KitchenSinkErr; use crates_index::Crate; use crates_index::Dependency; -use crates_index::Version; +pub use crates_index::Version; use crates_index; use lazyonce::LazyOnce; use parking_lot::Mutex; @@ -202,9 +202,9 @@ impl Index { .ok_or_else(|| KitchenSinkErr::CrateNotFound(Origin::from_crates_io_name(name))) } - pub fn crate_version_latest_unstable(&self, name: &str) -> Result<&Version, KitchenSinkErr> { + pub fn crate_highest_version(&self, name: &str, stable_only: bool) -> Result<&Version, KitchenSinkErr> { debug_assert_eq!(name, name.to_ascii_lowercase()); - Ok(Self::highest_crates_io_version(self.crates_io_crate_by_lowercase_name(name)?, false)) + Ok(Self::highest_crates_io_version(self.crates_io_crate_by_lowercase_name(name)?, stable_only)) } fn highest_crates_io_version(krate: &Crate, stable_only: bool) -> &Version { diff --git a/kitchen_sink/src/lib_kitchen_sink.rs b/kitchen_sink/src/lib_kitchen_sink.rs index 15b1b20..0b4d7de 100644 --- a/kitchen_sink/src/lib_kitchen_sink.rs +++ b/kitchen_sink/src/lib_kitchen_sink.rs @@ -1104,7 +1104,7 @@ impl KitchenSink { let (src, manifest, _warn) = match origin { Origin::CratesIo(ref name) => { - let ver = self.index.crate_version_latest_unstable(name).context("rich_crate_version2")?; + let ver = self.index.crate_highest_version(name, false).context("rich_crate_version2")?; self.rich_crate_version_data_from_crates_io(ver).context("rich_crate_version_data_from_crates_io")? }, Origin::GitHub {..} | Origin::GitLab {..} => { @@ -1325,7 +1325,7 @@ impl KitchenSink { .into_iter() .filter_map(|origin| { match origin { - Origin::CratesIo(name) => self.index.crate_version_latest_unstable(&name).ok(), + Origin::CratesIo(name) => self.index.crate_highest_version(&name, false).ok(), _ => None, } }) diff --git a/server/Cargo.toml b/server/Cargo.toml index 9789f96..1baaec3 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "crates-server" -version = "0.8.3" +version = "0.8.4" authors = ["Kornel <kornel@geekhood.net>"] edition = "2018" description = "Crates.rs web server" diff --git a/server/src/main.rs b/server/src/main.rs index 9df2c91..4f648d4 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -128,6 +128,7 @@ fn run_server() -> Result<(), failure::Error> { .resource("/index", |r| r.method(Method::GET).f(handle_search)) // old crates.rs/index url .resource("/keywords/{keyword}", |r| r.method(Method::GET).f(handle_keyword)) .resource("/crates/{crate}", |r| r.method(Method::GET).f(handle_crate)) + .resource("/crates/{crate}/rev", |r| r.method(Method::GET).f(handle_crate_reverse_dependencies)) .resource("/install/{crate:.*}", |r| r.method(Method::GET).f(handle_install)) .resource("/debug/{crate:.*}", |r| r.method(Method::GET).f(handle_debug)) .resource("/gh/{owner}/{repo}/{crate}", |r| r.method(Method::GET).f(handle_github_crate)) @@ -401,6 +402,23 @@ fn handle_crate(req: &HttpRequest<AServerState>) -> FutureResponse<HttpResponse> .responder() } +fn handle_crate_reverse_dependencies(req: &HttpRequest<AServerState>) -> FutureResponse<HttpResponse> { + let crate_name: String = req.match_info().query("crate").expect("arg"); + println!("rev deps for {:?}", crate_name); + let state = Arc::clone(req.state()); + let crates = state.crates.load(); + let origin = Origin::from_crates_io_name(&crate_name); + if !is_alnum(&crate_name) || !crates.crate_exists(&origin) { + return Box::new(future::result(render_404_page(&state, &crate_name))); + } + render_crate_reverse_dependencies(&state, origin) + .timeout(Duration::from_secs(20)) + .map_err(map_err) + .from_err() + .and_then(serve_cached) + .responder() +} + /// takes path to storage, freshness in seconds, and a function to call on cache miss /// returns (page, fresh in seconds) fn with_file_cache<F>(cache_file: PathBuf, cache_time: u32, generate: impl FnOnce() -> F) -> impl Future<Item=(Vec<u8>, u32, bool), Error=failure::Error> @@ -458,6 +476,18 @@ fn render_crate_page(state: &AServerState, origin: Origin) -> impl Future<Item = }) } +fn render_crate_reverse_dependencies(state: &AServerState, origin: Origin) -> impl Future<Item = (Vec<u8>, u32, bool), Error = failure::Error> { + let state2 = Arc::clone(state); + state.render_pool.spawn_fn(move || { + let crates = state2.crates.load(); + crates.prewarm(); + let ver = crates.rich_crate_version(&origin)?; + let mut page: Vec<u8> = Vec::with_capacity(50000); + front_end::render_crate_reverse_dependencies(&mut page, &ver, &crates, &state2.markup)?; + Ok((page, 24*3600, false)) + }) +} + fn handle_keyword(req: &HttpRequest<AServerState>) -> FutureResponse<HttpResponse> { let kw: Result<String, _> = req.match_info().query("keyword"); match kw { diff --git a/style b/style -Subproject 772f786c70286624c366c72167d8f0e45b9a383 +Subproject b413215efe6fd3633f7052b7a5f8afd21ab5eb3 |