summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKornel <kornel@geekhood.net>2020-01-26 03:48:59 +0000
committerKornel <kornel@geekhood.net>2020-01-26 03:48:59 +0000
commit916f06583e8b4539b7ef802f02e1ace543ba702f (patch)
tree2ab2ddb85f4de37a92a7b712620abe0ff9cbe0b0
parentb5da3d19ab96a17ea3b3662c6b70b434f5388bb4 (diff)
Rev deps page
-rw-r--r--front_end/src/front_end.rs12
-rw-r--r--front_end/src/reverse_dependencies.rs118
-rw-r--r--front_end/src/urler.rs11
-rw-r--r--front_end/templates/base.rs.html4
-rw-r--r--front_end/templates/crate_page.rs.html4
-rw-r--r--front_end/templates/reverse_dependencies.rs.html73
-rw-r--r--kitchen_sink/src/deps_stats.rs6
-rw-r--r--kitchen_sink/src/index.rs6
-rw-r--r--kitchen_sink/src/lib_kitchen_sink.rs4
-rw-r--r--server/Cargo.toml2
-rw-r--r--server/src/main.rs30
m---------style0
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)&nbsp;directly</a>)
+ (<a href="@url.reverse_deps(c.ver.origin())">@c.format_number(direct)&nbsp;directly</a>)
}
} else {
- Used in <a href="@url.reverse_deps(&c.ver)"><b>@c.format_number(deps)</b>&nbsp;crate@if deps != 1 {s}</a>
+ Used in <a href="@url.reverse_deps(c.ver.origin())"><b>@c.format_number(deps)</b>&nbsp;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