diff --git a/src/docbuilder/queue.rs b/src/docbuilder/queue.rs index f0f1df98f..ba9a73a8d 100644 --- a/src/docbuilder/queue.rs +++ b/src/docbuilder/queue.rs @@ -19,12 +19,36 @@ impl DocBuilder { // I belive this will fix ordering of queue if we get more than one crate from changes changes.reverse(); - for krate in changes.iter().filter(|k| k.kind != ChangeKind::Yanked) { - let priority = get_crate_priority(&conn, &krate.name)?; - add_crate_to_queue(&conn, &krate.name, &krate.version, priority).ok(); + for krate in &changes { + match krate.kind { + ChangeKind::Yanked => { + let res = conn.execute( + " + UPDATE releases + SET yanked = TRUE + FROM crates + WHERE crates.id = releases.crate_id + AND name = $1 + AND version = $2 + ", + &[&krate.name, &krate.version], + ); + match res { + Ok(_) => debug!("{}-{} yanked", krate.name, krate.version), + Err(err) => error!( + "error while setting {}-{} to yanked: {}", + krate.name, krate.version, err + ), + } + } + ChangeKind::Added => { + let priority = get_crate_priority(&conn, &krate.name)?; + add_crate_to_queue(&conn, &krate.name, &krate.version, priority).ok(); - debug!("{}-{} added into build queue", krate.name, krate.version); - add_count += 1; + debug!("{}-{} added into build queue", krate.name, krate.version); + add_count += 1; + } + } } Ok(add_count) diff --git a/src/test/fakes.rs b/src/test/fakes.rs index 61ae247ea..1189ad060 100644 --- a/src/test/fakes.rs +++ b/src/test/fakes.rs @@ -86,7 +86,7 @@ impl<'a> FakeRelease<'a> { self } - pub(crate) fn cratesio_data_yanked(mut self, new: bool) -> Self { + pub(crate) fn yanked(mut self, new: bool) -> Self { self.cratesio_data.yanked = new; self } @@ -139,55 +139,58 @@ impl<'a> FakeRelease<'a> { let package = self.package; let db = self.db; - let upload_files = |prefix: &str, files: &[(&str, &[u8])], target: Option<&str>| { - let mut path_prefix = tempdir.path().join(prefix); - if let Some(target) = target { - path_prefix.push(target); - } - fs::create_dir(&path_prefix)?; + let mut source_meta = None; + if self.build_result.successful { + let upload_files = |prefix: &str, files: &[(&str, &[u8])], target: Option<&str>| { + let mut path_prefix = tempdir.path().join(prefix); + if let Some(target) = target { + path_prefix.push(target); + } + fs::create_dir(&path_prefix)?; + + for (path, data) in files { + // allow `src/main.rs` + if let Some(parent) = Path::new(path).parent() { + fs::create_dir_all(path_prefix.join(parent))?; + } + let file = path_prefix.join(&path); + log::debug!("writing file {}", file.display()); + fs::write(file, data)?; + } - for (path, data) in files { - // allow `src/main.rs` - if let Some(parent) = Path::new(path).parent() { - fs::create_dir_all(path_prefix.join(parent))?; + let prefix = format!( + "{}/{}/{}/{}", + prefix, + package.name, + package.version, + target.unwrap_or("") + ); + log::debug!("adding directory {} from {}", prefix, path_prefix.display()); + crate::db::add_path_into_database(&db.conn(), &prefix, path_prefix) + }; + + let index = [&package.name, "index.html"].join("/"); + let mut rustdoc_files = self.rustdoc_files; + if package.is_library() && !rustdoc_files.iter().any(|(path, _)| path == &index) { + rustdoc_files.push((&index, b"default index content")); + } + for (source_path, data) in &self.source_files { + if source_path.starts_with("src/") { + let updated = ["src", &package.name, &source_path[4..]].join("/"); + rustdoc_files.push((Box::leak(Box::new(updated)), data)); } - let file = path_prefix.join(&path); - log::debug!("writing file {}", file.display()); - fs::write(file, data)?; } - - let prefix = format!( - "{}/{}/{}/{}", - prefix, - package.name, - package.version, - target.unwrap_or("") - ); - log::debug!("adding directory {} from {}", prefix, path_prefix.display()); - crate::db::add_path_into_database(&db.conn(), &prefix, path_prefix) - }; - - let index = [&package.name, "index.html"].join("/"); - let mut rustdoc_files = self.rustdoc_files; - if package.is_library() && !rustdoc_files.iter().any(|(path, _)| path == &index) { - rustdoc_files.push((&index, b"default index content")); - } - for (source_path, data) in &self.source_files { - if source_path.starts_with("src/") { - let updated = ["src", &package.name, &source_path[4..]].join("/"); - rustdoc_files.push((Box::leak(Box::new(updated)), data)); + let rustdoc_meta = upload_files("rustdoc", &rustdoc_files, None)?; + log::debug!("added rustdoc files {}", rustdoc_meta); + source_meta = Some(upload_files("source", &self.source_files, None)?); + log::debug!("added source files {}", source_meta.as_ref().unwrap()); + + for target in &package.targets[1..] { + let platform = target.src_path.as_ref().unwrap(); + upload_files("rustdoc", &rustdoc_files, Some(platform))?; + log::debug!("added platform files for {}", platform); } } - let rustdoc_meta = upload_files("rustdoc", &rustdoc_files, None)?; - log::debug!("added rustdoc files {}", rustdoc_meta); - let source_meta = upload_files("source", &self.source_files, None)?; - log::debug!("added source files {}", source_meta); - - for target in &package.targets[1..] { - let platform = target.src_path.as_ref().unwrap(); - upload_files("rustdoc", &rustdoc_files, Some(platform))?; - log::debug!("added platform files for {}", platform); - } let release_id = crate::db::add_package_into_database( &db.conn(), @@ -195,7 +198,7 @@ impl<'a> FakeRelease<'a> { tempdir.path(), &self.build_result, self.default_target.unwrap_or("x86_64-unknown-linux-gnu"), - Some(source_meta), + source_meta, self.doc_targets, &self.cratesio_data, self.has_docs, diff --git a/src/web/crate_details.rs b/src/web/crate_details.rs index e80e7c6e4..71fcdb033 100644 --- a/src/web/crate_details.rs +++ b/src/web/crate_details.rs @@ -40,6 +40,7 @@ pub struct CrateDetails { github_issues: Option, pub(crate) metadata: MetaData, is_library: bool, + yanked: bool, pub(crate) doc_targets: Vec, license: Option, documentation_url: Option, @@ -83,6 +84,7 @@ impl ToJson for CrateDetails { m.insert("github_issues".to_string(), self.github_issues.to_json()); m.insert("metadata".to_string(), self.metadata.to_json()); m.insert("is_library".to_string(), self.is_library.to_json()); + m.insert("yanked".to_string(), self.yanked.to_json()); m.insert("doc_targets".to_string(), self.doc_targets.to_json()); m.insert("license".to_string(), self.license.to_json()); m.insert( @@ -94,10 +96,10 @@ impl ToJson for CrateDetails { } #[derive(Debug, Eq, PartialEq)] -struct Release { - version: String, - build_status: bool, - yanked: bool, +pub struct Release { + pub version: String, + pub build_status: bool, + pub yanked: bool, } impl ToJson for Release { @@ -105,6 +107,7 @@ impl ToJson for Release { let mut m: BTreeMap = BTreeMap::new(); m.insert("version".to_string(), self.version.to_json()); m.insert("build_status".to_string(), self.build_status.to_json()); + m.insert("yanked".to_string(), self.yanked.to_json()); m.to_json() } } @@ -134,6 +137,7 @@ impl CrateDetails { crates.github_forks, crates.github_issues, releases.is_library, + releases.yanked, releases.doc_targets, releases.license, releases.documentation_url, @@ -180,11 +184,11 @@ impl CrateDetails { description: rows.get(0).get(4), rustdoc_status: rows.get(0).get(11), target_name: rows.get(0).get(16), - default_target: rows.get(0).get(25), + default_target: rows.get(0).get(26), }; let doc_targets = { - let data: Json = rows.get(0).get(22); + let data: Json = rows.get(0).get(23); data.as_array() .map(|array| { array @@ -221,9 +225,10 @@ impl CrateDetails { github_issues: rows.get(0).get(20), metadata, is_library: rows.get(0).get(21), + yanked: rows.get(0).get(22), doc_targets, - license: rows.get(0).get(23), - documentation_url: rows.get(0).get(24), + license: rows.get(0).get(24), + documentation_url: rows.get(0).get(25), }; if let Some(repository_url) = crate_details.repository_url.clone() { @@ -271,10 +276,13 @@ impl CrateDetails { Some(crate_details) } - /// Returns the version of the latest release of this crate. - pub fn latest_version(&self) -> &str { - // releases will always contain at least one element - &self.releases[0].version + /// Returns the latest non-yanked release of this crate (or latest yanked if they are all + /// yanked). + pub fn latest_release(&self) -> &Release { + self.releases + .iter() + .find(|release| !release.yanked) + .unwrap_or(&self.releases[0]) } } @@ -367,13 +375,13 @@ mod tests { db.fake_release() .name("foo") .version("0.0.4") - .cratesio_data_yanked(true) + .yanked(true) .create()?; db.fake_release() .name("foo") .version("0.0.5") .build_result_successful(false) - .cratesio_data_yanked(true) + .yanked(true) .create()?; assert_last_successful_build_equals(&db, "foo", "0.0.1", None)?; @@ -403,7 +411,7 @@ mod tests { db.fake_release() .name("foo") .version("0.0.3") - .cratesio_data_yanked(true) + .yanked(true) .create()?; assert_last_successful_build_equals(&db, "foo", "0.0.1", None)?; @@ -427,7 +435,7 @@ mod tests { db.fake_release() .name("foo") .version("0.0.3") - .cratesio_data_yanked(true) + .yanked(true) .create()?; db.fake_release().name("foo").version("0.0.4").create()?; @@ -457,7 +465,7 @@ mod tests { db.fake_release() .name("foo") .version("0.2.0") - .cratesio_data_yanked(true) + .yanked(true) .create()?; db.fake_release() .name("foo") @@ -519,14 +527,62 @@ mod tests { db.fake_release().name("foo").version("0.0.3").create()?; db.fake_release().name("foo").version("0.0.2").create()?; - let details = CrateDetails::new(&db.conn(), "foo", "0.0.1").unwrap(); - assert_eq!(details.latest_version(), "0.0.3"); + for version in &["0.0.1", "0.0.2", "0.0.3"] { + let details = CrateDetails::new(&db.conn(), "foo", version).unwrap(); + assert_eq!(details.latest_release().version, "0.0.3"); + } + + Ok(()) + }) + } + + #[test] + fn test_latest_version_ignores_yanked() { + crate::test::wrapper(|env| { + let db = env.db(); + + db.fake_release().name("foo").version("0.0.1").create()?; + db.fake_release() + .name("foo") + .version("0.0.3") + .yanked(true) + .create()?; + db.fake_release().name("foo").version("0.0.2").create()?; - let details = CrateDetails::new(&db.conn(), "foo", "0.0.2").unwrap(); - assert_eq!(details.latest_version(), "0.0.3"); + for version in &["0.0.1", "0.0.2", "0.0.3"] { + let details = CrateDetails::new(&db.conn(), "foo", version).unwrap(); + assert_eq!(details.latest_release().version, "0.0.2"); + } - let details = CrateDetails::new(&db.conn(), "foo", "0.0.3").unwrap(); - assert_eq!(details.latest_version(), "0.0.3"); + Ok(()) + }) + } + + #[test] + fn test_latest_version_only_yanked() { + crate::test::wrapper(|env| { + let db = env.db(); + + db.fake_release() + .name("foo") + .version("0.0.1") + .yanked(true) + .create()?; + db.fake_release() + .name("foo") + .version("0.0.3") + .yanked(true) + .create()?; + db.fake_release() + .name("foo") + .version("0.0.2") + .yanked(true) + .create()?; + + for version in &["0.0.1", "0.0.2", "0.0.3"] { + let details = CrateDetails::new(&db.conn(), "foo", version).unwrap(); + assert_eq!(details.latest_release().version, "0.0.3"); + } Ok(()) }) diff --git a/src/web/mod.rs b/src/web/mod.rs index af4844cd9..9262977ad 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -56,7 +56,7 @@ mod sitemap; mod source; use self::pool::Pool; -use handlebars_iron::{DirectorySource, HandlebarsEngine}; +use handlebars_iron::{DirectorySource, HandlebarsEngine, SourceError}; use iron::headers::{CacheControl, CacheDirective, ContentType, Expires, HttpDate}; use iron::modifiers::Redirect; use iron::prelude::*; @@ -87,6 +87,17 @@ const DEFAULT_BIND: &str = "0.0.0.0:3000"; type PoolFactoryFn = dyn Fn() -> Pool + Send + Sync; type PoolFactory = Box; +fn handlebars_engine() -> Result { + // TODO: Use DocBuilderOptions for paths + let mut hbse = HandlebarsEngine::new(); + hbse.add(Box::new(DirectorySource::new("./templates", ".hbs"))); + + // load templates + hbse.reload()?; + + Ok(hbse) +} + struct CratesfyiHandler { shared_resource_handler: Box, router_handler: Box, @@ -97,14 +108,7 @@ struct CratesfyiHandler { impl CratesfyiHandler { fn chain(pool_factory: &PoolFactoryFn, base: H) -> Chain { - // TODO: Use DocBuilderOptions for paths - let mut hbse = HandlebarsEngine::new(); - hbse.add(Box::new(DirectorySource::new("./templates", ".hbs"))); - - // load templates - if let Err(e) = hbse.reload() { - panic!("Failed to load handlebar templates: {}", e); - } + let hbse = handlebars_engine().expect("Failed to load handlebar templates"); let mut chain = Chain::new(base); chain.link_before(pool_factory()); @@ -259,10 +263,10 @@ fn match_version(conn: &Connection, name: &str, version: Option<&str>) -> Option .unwrap_or_else(|| "*".into()); let mut corrected_name = None; - let versions: Vec<(String, i32)> = { - let query = "SELECT name, version, releases.id + let versions: Vec<(String, i32, bool)> = { + let query = "SELECT name, version, releases.id, releases.yanked FROM releases INNER JOIN crates ON releases.crate_id = crates.id - WHERE normalize_crate_name(name) = normalize_crate_name($1) AND yanked = false"; + WHERE normalize_crate_name(name) = normalize_crate_name($1)"; let rows = conn.query(query, &[&name]).unwrap(); let mut rows = rows.iter().peekable(); @@ -275,11 +279,12 @@ fn match_version(conn: &Connection, name: &str, version: Option<&str>) -> Option } }; - rows.map(|row| (row.get(1), row.get(2))).collect() + rows.map(|row| (row.get(1), row.get(2), row.get(3))) + .collect() }; // first check for exact match, we can't expect users to use semver in query - if let Some((version, id)) = versions.iter().find(|(vers, _)| vers == &req_version) { + if let Some((version, id, _)) = versions.iter().find(|(vers, _, _)| vers == &req_version) { return Some(MatchVersion { corrected_name, version: MatchSemver::Exact((version.to_owned(), *id)), @@ -293,7 +298,7 @@ fn match_version(conn: &Connection, name: &str, version: Option<&str>) -> Option let versions_sem = { let mut versions_sem: Vec<(Version, i32)> = Vec::new(); - for version in &versions { + for version in versions.iter().filter(|(_, _, yanked)| !yanked) { // in theory a crate must always have a semver compatible version, but check result just in case versions_sem.push((Version::parse(&version.0).ok()?, version.1)); } @@ -314,11 +319,11 @@ fn match_version(conn: &Connection, name: &str, version: Option<&str>) -> Option } // semver is acting weird for '*' (any) range if a crate only have pre-release versions - // return first version if requested version is '*' - if req_version == "*" && !versions_sem.is_empty() { - return Some(MatchVersion { + // return first non-yanked version if requested version is '*' + if req_version == "*" { + return versions_sem.first().map(|v| MatchVersion { corrected_name, - version: MatchSemver::Semver((versions_sem[0].0.to_string(), versions_sem[0].1)), + version: MatchSemver::Semver((v.0.to_string(), v.1)), }); } @@ -571,7 +576,7 @@ impl ToJson for MetaData { #[cfg(test)] mod test { use crate::test::*; - use crate::web::match_version; + use crate::web::{handlebars_engine, match_version}; use html5ever::tendril::TendrilSink; fn release(version: &str, db: &TestDatabase) -> i32 { @@ -778,4 +783,9 @@ mod test { Ok(()) }); } + + #[test] + fn test_templates_are_valid() { + handlebars_engine().expect("Failed to load handlebar templates"); + } } diff --git a/src/web/rustdoc.rs b/src/web/rustdoc.rs index d7fb1d7ab..b37c4c26e 100644 --- a/src/web/rustdoc.rs +++ b/src/web/rustdoc.rs @@ -320,15 +320,23 @@ pub fn rustdoc_html_server_handler(req: &mut Request) -> IronResult { content.full = file_content; - let latest_version = crate_details.latest_version().to_owned(); + let latest_release = crate_details.latest_release(); + let latest_version = latest_release.version.to_owned(); let is_latest_version = latest_version == version; - let path_in_latest = if !is_latest_version { + let latest_path = if is_latest_version { + format!("/{}/{}", name, latest_version) + } else if latest_release.build_status { let mut latest_path = req_path.clone(); latest_path[2] = &latest_version; - path_for_version(&latest_path, &crate_details.doc_targets, &conn) + format!( + "/{}/{}/{}", + name, + latest_version, + path_for_version(&latest_path, &crate_details.doc_targets, &conn) + ) } else { - Default::default() + format!("/crate/{}/{}", name, latest_version) }; // The path within this crate version's rustdoc output @@ -349,7 +357,7 @@ pub fn rustdoc_html_server_handler(req: &mut Request) -> IronResult { .set_true("package_navigation_documentation_tab") .set_true("package_navigation_show_platforms_tab") .set_bool("is_latest_version", is_latest_version) - .set("path_in_latest", &path_in_latest) + .set("latest_path", &latest_path) .set("latest_version", &latest_version) .set("inner_path", &inner_path) .to_resp("rustdoc") @@ -537,21 +545,31 @@ mod test { use reqwest::StatusCode; use std::{collections::BTreeMap, iter::FromIterator}; - fn latest_version_redirect(path: &str, web: &TestFrontend) -> Result { + fn try_latest_version_redirect( + path: &str, + web: &TestFrontend, + ) -> Result, failure::Error> { use html5ever::tendril::TendrilSink; assert_success(path, web)?; let data = web.get(path).send()?.text()?; + println!("{}", data); let dom = kuchiki::parse_html().one(data); if let Some(elem) = dom - .select("form ul li a.warn") + .select("form > ul > li > a.warn") .expect("invalid selector") .next() { let link = elem.attributes.borrow().get("href").unwrap().to_string(); assert_success(&link, web)?; - return Ok(link); + Ok(Some(link)) + } else { + Ok(None) } - panic!("no redirect found for {}", path); + } + + fn latest_version_redirect(path: &str, web: &TestFrontend) -> Result { + try_latest_version_redirect(path, web) + .and_then(|v| v.ok_or_else(|| failure::format_err!("no redirect found for {}", path))) } #[test] @@ -696,14 +714,12 @@ mod test { .version("0.1.0") .add_platform("x86_64-pc-windows-msvc") .rustdoc_file("dummy/struct.Blah.html", b"lah") - .create() - .unwrap(); + .create()?; db.fake_release() .name("dummy") .version("0.2.0") .add_platform("x86_64-pc-windows-msvc") - .create() - .unwrap(); + .create()?; let web = env.frontend(); @@ -745,14 +761,12 @@ mod test { .name("dummy") .version("0.1.0") .rustdoc_file("dummy/index.html", b"lah") - .create() - .unwrap(); + .create()?; db.fake_release() .name("dummy") .version("0.2.0") .build_result_successful(false) - .create() - .unwrap(); + .create()?; let web = env.frontend(); let redirect = latest_version_redirect("/dummy/0.1.0/dummy/", web)?; @@ -762,6 +776,110 @@ mod test { }) } + #[test] + fn redirect_latest_does_not_go_to_yanked_versions() { + wrapper(|env| { + let db = env.db(); + db.fake_release() + .name("dummy") + .version("0.1.0") + .rustdoc_file("dummy/index.html", b"lah") + .create()?; + db.fake_release() + .name("dummy") + .version("0.2.0") + .rustdoc_file("dummy/index.html", b"lah") + .create()?; + db.fake_release() + .name("dummy") + .version("0.2.1") + .rustdoc_file("dummy/index.html", b"lah") + .yanked(true) + .create()?; + + let web = env.frontend(); + let redirect = latest_version_redirect("/dummy/0.1.0/dummy/", web)?; + assert_eq!(redirect, "/dummy/0.2.0/dummy/index.html"); + + let redirect = latest_version_redirect("/dummy/0.2.1/dummy/", web)?; + assert_eq!(redirect, "/dummy/0.2.0/dummy/index.html"); + + Ok(()) + }) + } + + #[test] + fn redirect_latest_with_all_yanked() { + wrapper(|env| { + let db = env.db(); + db.fake_release() + .name("dummy") + .version("0.1.0") + .rustdoc_file("dummy/index.html", b"lah") + .yanked(true) + .create()?; + db.fake_release() + .name("dummy") + .version("0.2.0") + .rustdoc_file("dummy/index.html", b"lah") + .yanked(true) + .create()?; + db.fake_release() + .name("dummy") + .version("0.2.1") + .rustdoc_file("dummy/index.html", b"lah") + .yanked(true) + .create()?; + + let web = env.frontend(); + let redirect = latest_version_redirect("/dummy/0.1.0/dummy/", web)?; + assert_eq!(redirect, "/dummy/0.2.1/dummy/index.html"); + + let redirect = latest_version_redirect("/dummy/0.2.0/dummy/", web)?; + assert_eq!(redirect, "/dummy/0.2.1/dummy/index.html"); + + Ok(()) + }) + } + + #[test] + fn yanked_release_shows_warning_in_nav() { + fn has_yanked_warning(path: &str, web: &TestFrontend) -> Result { + use html5ever::tendril::TendrilSink; + assert_success(path, web)?; + let data = web.get(path).send()?.text()?; + Ok(kuchiki::parse_html() + .one(data) + .select("form > ul > li > .warn") + .expect("invalid selector") + .any(|el| el.text_contents().contains("yanked"))) + } + + wrapper(|env| { + let (db, web) = (env.db(), env.frontend()); + + db.fake_release() + .name("dummy") + .version("0.1.0") + .rustdoc_file("dummy/index.html", b"lah") + .yanked(true) + .create()?; + + assert!(has_yanked_warning("/dummy/0.1.0/dummy/", web)?); + + db.fake_release() + .name("dummy") + .version("0.2.0") + .rustdoc_file("dummy/index.html", b"lah") + .yanked(true) + .create()?; + + assert!(has_yanked_warning("/dummy/0.1.0/dummy/", web)?); + + Ok(()) + }) + } + #[test] fn badges_are_urlencoded() { wrapper(|env| { @@ -789,8 +907,7 @@ mod test { .name("fake-crate") .version("0.0.1") .rustdoc_file("fake_crate/index.html", b"some content") - .create() - .unwrap(); + .create()?; let web = env.frontend(); assert_redirect("/fake%2Dcrate", "/fake-crate/0.0.1/fake_crate/", web)?; @@ -1201,4 +1318,29 @@ mod test { Ok(()) }); } + + #[test] + fn test_fully_yanked_crate_404s() { + crate::test::wrapper(|env| { + let db = env.db(); + + db.fake_release() + .name("dummy") + .version("1.0.0") + .yanked(true) + .create()?; + + assert_eq!( + env.frontend().get("/crate/dummy").send()?.status(), + StatusCode::NOT_FOUND + ); + + assert_eq!( + env.frontend().get("/dummy").send()?.status(), + StatusCode::NOT_FOUND + ); + + Ok(()) + }) + } } diff --git a/templates/crate_details.hbs b/templates/crate_details.hbs index c9ed6093c..1a1385708 100644 --- a/templates/crate_details.hbs +++ b/templates/crate_details.hbs @@ -44,10 +44,18 @@
    {{#each releases}}
  • - {{#if this.build_status}} - {{this.version}} + {{#if this.yanked}} + {{#if this.build_status}} + {{this.version}} + {{else}} + {{this.version}} + {{/if}} {{else}} - {{this.version}} + {{#if this.build_status}} + {{this.version}} + {{else}} + {{this.version}} + {{/if}} {{/if}}
  • {{/each}} @@ -67,6 +75,9 @@ {{#unless is_library}}
    {{name}}-{{version}} is not a library.
    {{else}} + {{#if yanked}} +
    {{name}}-{{version}} has been yanked.
    + {{else}} {{#unless build_status}}
    docs.rs failed to build {{name}}-{{version}}
    Please check the build logs and, if you believe this is docs.rs' fault, open an issue.
    {{#if last_successful_build}} @@ -77,6 +88,7 @@
    {{name}}-{{version}} doesn't have any documentation.
    {{/unless}} {{/unless}} + {{/if}} {{/unless}} {{#if readme}} {{{readme}}} diff --git a/templates/navigation_rustdoc.hbs b/templates/navigation_rustdoc.hbs index 5f6f45f40..ebeeb3707 100644 --- a/templates/navigation_rustdoc.hbs +++ b/templates/navigation_rustdoc.hbs @@ -78,10 +78,18 @@
      {{#each releases}}
    • - {{#if this.build_status}} - {{this.version}} + {{#if this.yanked}} + {{#if this.build_status}} + {{this.version}} + {{else}} + {{this.version}} + {{/if}} {{else}} - {{this.version}} + {{#if this.build_status}} + {{this.version}} + {{else}} + {{this.version}} + {{/if}} {{/if}}
    • {{/each}} @@ -93,11 +101,23 @@ - {{#unless ../../varsb.is_latest_version}} -
    • - Go to latest version -
    • - {{/unless}} + {{#if ../../varsb.is_latest_version}} + {{#if yanked}} +
    • + This release has been yanked +
    • + {{/if}} + {{else}} + {{#if ../../content.crate_details.yanked}} +
    • + This release has been yanked, go to latest version +
    • + {{else}} +
    • + Go to latest version +
    • + {{/if}} + {{/if}}
    • Source
    • diff --git a/templates/style.scss b/templates/style.scss index d31bae130..7759788a6 100644 --- a/templates/style.scss +++ b/templates/style.scss @@ -160,7 +160,7 @@ div.nav-container { font-size: 0.8em; } - a { + .pure-menu-link { font-size: 0.8em; font-weight: 400; } @@ -220,11 +220,11 @@ div.nav-container { } // used for latest version warning - .warn { + .warn, .warn:hover { color: $color-type; } - .warn:hover { + a.warn:hover { color: darken($color-type, 10%); }