Allow analyzing crates in sub-directories of repo root (#170)

* Allow analyzing crates in sub-directories (#95)

* Add field to the main page form for selecting an inner path

* chore: make clippy happy

* Display sub-directory tree in status overview

* Append the query parameter to the SVG links

* Clippy fixes

* Update assets/links.js

Co-authored-by: Eduardo Pinho <enet4mikeenet@gmail.com>

Co-authored-by: Eduardo Pinho <enet4mikeenet@gmail.com>
This commit is contained in:
Felix Suchert 2022-11-09 17:21:48 +01:00 committed by GitHub
parent 477d83a4e0
commit f9d545f9ff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 96 additions and 28 deletions

14
assets/links.js vendored
View file

@ -4,6 +4,12 @@ function buildRepoLink() {
let hoster = formRef.elements["hosterSelect"].value.toLowerCase(); let hoster = formRef.elements["hosterSelect"].value.toLowerCase();
let owner = formRef.elements["owner"].value; let owner = formRef.elements["owner"].value;
let repoName = formRef.elements["repoName"].value; let repoName = formRef.elements["repoName"].value;
let innerPath = formRef.elements["innerPath"].value;
let qparams = "";
if (innerPath.length > 0) {
qparams = "?path=" + encodeURIComponent(innerPath);
}
if (hoster === "gitea") { if (hoster === "gitea") {
let baseUrl = formRef.elements["baseUrl"].value; let baseUrl = formRef.elements["baseUrl"].value;
@ -18,9 +24,9 @@ function buildRepoLink() {
return; return;
} }
window.location.href = `/repo/${hoster}/${baseUrl}/${owner}/${repoName}`; window.location.assign(`/repo/${hoster}/${baseUrl}/${owner}/${repoName}${qparams}`);
} else { } else {
window.location.href = `/repo/${hoster}/${owner}/${repoName}`; window.location.assign(`/repo/${hoster}/${owner}/${repoName}${qparams}`);
} }
} }
@ -32,8 +38,8 @@ function buildCrateLink() {
if (crateVer.length == 0) { if (crateVer.length == 0) {
// default to latest version // default to latest version
window.location.href = `/crate/${crate}`; window.location.assign(`/crate/${crate}`);
} else { } else {
window.location.href = `/crate/${crate}/${crateVer}`; window.location.assign(`/crate/${crate}/${crateVer}`);
} }
} }

View file

@ -170,20 +170,25 @@ impl Engine {
pub async fn analyze_repo_dependencies( pub async fn analyze_repo_dependencies(
&self, &self,
repo_path: RepoPath, repo_path: RepoPath,
sub_path: &Option<String>,
) -> Result<AnalyzeDependenciesOutcome, Error> { ) -> Result<AnalyzeDependenciesOutcome, Error> {
let start = Instant::now(); let start = Instant::now();
let entry_point = RelativePath::new("/").to_relative_path_buf(); let mut entry_point = RelativePath::new("/").to_relative_path_buf();
if let Some(inner_path) = sub_path {
entry_point.push(inner_path);
}
let engine = self.clone(); let engine = self.clone();
let manifest_output = crawl_manifest(self.clone(), repo_path.clone(), entry_point).await?; let manifest_output = crawl_manifest(self.clone(), repo_path.clone(), entry_point).await?;
let engine_for_analyze = engine.clone();
let futures = manifest_output let futures = manifest_output
.crates .crates
.into_iter() .into_iter()
.map(|(crate_name, deps)| async { .map(|(crate_name, deps)| async {
let analyzed_deps = analyze_dependencies(engine_for_analyze.clone(), deps).await?; let analyzed_deps = analyze_dependencies(engine.clone(), deps).await?;
Ok::<_, Error>((crate_name, analyzed_deps)) Ok::<_, Error>((crate_name, analyzed_deps))
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();

View file

@ -186,7 +186,7 @@ impl App {
let qual = params.find("qual").expect("route param 'qual' not found"); let qual = params.find("qual").expect("route param 'qual' not found");
let name = params.find("name").expect("route param 'name' not found"); let name = params.find("name").expect("route param 'name' not found");
let badge_knobs = BadgeKnobs::from_query_string(req.uri().query()); let extra_knobs = ExtraConfig::from_query_string(req.uri().query());
let repo_path_result = RepoPath::from_parts(site, qual, name); let repo_path_result = RepoPath::from_parts(site, qual, name);
@ -204,7 +204,7 @@ impl App {
Ok(repo_path) => { Ok(repo_path) => {
let analyze_result = server let analyze_result = server
.engine .engine
.analyze_repo_dependencies(repo_path.clone()) .analyze_repo_dependencies(repo_path.clone(), &extra_knobs.path)
.await; .await;
match analyze_result { match analyze_result {
@ -214,7 +214,7 @@ impl App {
None, None,
format, format,
SubjectPath::Repo(repo_path), SubjectPath::Repo(repo_path),
badge_knobs, extra_knobs,
); );
Ok(response) Ok(response)
} }
@ -223,7 +223,7 @@ impl App {
Some(analysis_outcome), Some(analysis_outcome),
format, format,
SubjectPath::Repo(repo_path), SubjectPath::Repo(repo_path),
badge_knobs, extra_knobs,
); );
Ok(response) Ok(response)
} }
@ -345,7 +345,7 @@ impl App {
}; };
let crate_path_result = CratePath::from_parts(name, &version); let crate_path_result = CratePath::from_parts(name, &version);
let badge_knobs = BadgeKnobs::from_query_string(req.uri().query()); let badge_knobs = ExtraConfig::from_query_string(req.uri().query());
match crate_path_result { match crate_path_result {
Err(err) => { Err(err) => {
@ -393,11 +393,13 @@ impl App {
analysis_outcome: Option<AnalyzeDependenciesOutcome>, analysis_outcome: Option<AnalyzeDependenciesOutcome>,
format: StatusFormat, format: StatusFormat,
subject_path: SubjectPath, subject_path: SubjectPath,
badge_knobs: BadgeKnobs, badge_knobs: ExtraConfig,
) -> Response<Body> { ) -> Response<Body> {
match format { match format {
StatusFormat::Svg => views::badge::response(analysis_outcome.as_ref(), badge_knobs), StatusFormat::Svg => views::badge::response(analysis_outcome.as_ref(), badge_knobs),
StatusFormat::Html => views::html::status::render(analysis_outcome, subject_path), StatusFormat::Html => {
views::html::status::render(analysis_outcome, subject_path, badge_knobs)
}
} }
} }
@ -430,27 +432,34 @@ fn not_found() -> Response<Body> {
static SELF_BASE_URL: Lazy<String> = static SELF_BASE_URL: Lazy<String> =
Lazy::new(|| env::var("BASE_URL").unwrap_or_else(|_| "http://localhost:8080".to_string())); Lazy::new(|| env::var("BASE_URL").unwrap_or_else(|_| "http://localhost:8080".to_string()));
/// Configuration options supplied through Get Parameters
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct BadgeKnobs { pub struct ExtraConfig {
/// Badge style to show
style: BadgeStyle, style: BadgeStyle,
/// Whether the inscription _"dependencies"_ should be abbreviated as _"deps"_ in the badge.
compact: bool, compact: bool,
/// Path in which the crate resides within the repository
path: Option<String>,
} }
impl BadgeKnobs { impl ExtraConfig {
fn from_query_string(qs: Option<&str>) -> Self { fn from_query_string(qs: Option<&str>) -> Self {
#[derive(Debug, Clone, Default, Deserialize)] #[derive(Debug, Clone, Default, Deserialize)]
struct BadgeKnobsPartial { struct ExtraConfigPartial {
style: Option<BadgeStyle>, style: Option<BadgeStyle>,
compact: Option<bool>, compact: Option<bool>,
path: Option<String>,
} }
let badge_knobs = qs let extra_config = qs
.and_then(|qs| serde_urlencoded::from_str::<BadgeKnobsPartial>(qs).ok()) .and_then(|qs| serde_urlencoded::from_str::<ExtraConfigPartial>(qs).ok())
.unwrap_or_default(); .unwrap_or_default();
Self { Self {
style: badge_knobs.style.unwrap_or_default(), style: extra_config.style.unwrap_or_default(),
compact: badge_knobs.compact.unwrap_or_default(), compact: extra_config.compact.unwrap_or_default(),
path: extra_config.path,
} }
} }
} }

View file

@ -3,11 +3,11 @@ use hyper::header::CONTENT_TYPE;
use hyper::{Body, Response}; use hyper::{Body, Response};
use crate::engine::AnalyzeDependenciesOutcome; use crate::engine::AnalyzeDependenciesOutcome;
use crate::server::BadgeKnobs; use crate::server::ExtraConfig;
pub fn badge( pub fn badge(
analysis_outcome: Option<&AnalyzeDependenciesOutcome>, analysis_outcome: Option<&AnalyzeDependenciesOutcome>,
badge_knobs: BadgeKnobs, badge_knobs: ExtraConfig,
) -> Badge { ) -> Badge {
let subject = if badge_knobs.compact { let subject = if badge_knobs.compact {
"deps" "deps"
@ -74,7 +74,7 @@ pub fn badge(
pub fn response( pub fn response(
analysis_outcome: Option<&AnalyzeDependenciesOutcome>, analysis_outcome: Option<&AnalyzeDependenciesOutcome>,
badge_knobs: BadgeKnobs, badge_knobs: ExtraConfig,
) -> Response<Body> { ) -> Response<Body> {
let badge = badge(analysis_outcome, badge_knobs).to_svg(); let badge = badge(analysis_outcome, badge_knobs).to_svg();

View file

@ -57,6 +57,16 @@ fn link_forms() -> Markup {
p class="help" id="baseUrlHelp" { "Base URL of the Git instance the project is hosted on. Only relevant for Gitea Instances." } p class="help" id="baseUrlHelp" { "Base URL of the Git instance the project is hosted on. Only relevant for Gitea Instances." }
} }
div class="field" {
label class="label" { "Path in Repository" }
div class="control" {
input class="input" type="text" id="innerPath" placeholder="project1/rust-stuff";
}
p class="help" id="baseUrlHelp" { "Path within the repository where the " code { "Cargo.toml" } " file is located." }
}
input type="submit" class="button is-primary" value="Check" onclick="buildRepoLink();"; input type="submit" class="button is-primary" value="Check" onclick="buildRepoLink();";
} }
} }

View file

@ -11,7 +11,7 @@ use crate::models::crates::{AnalyzedDependencies, AnalyzedDependency, CrateName}
use crate::models::repo::RepoSite; use crate::models::repo::RepoSite;
use crate::models::SubjectPath; use crate::models::SubjectPath;
use crate::server::views::badge; use crate::server::views::badge;
use crate::server::BadgeKnobs; use crate::server::ExtraConfig;
fn get_crates_url(name: impl AsRef<str>) -> String { fn get_crates_url(name: impl AsRef<str>) -> String {
format!("https://crates.io/crates/{}", name.as_ref()) format!("https://crates.io/crates/{}", name.as_ref())
@ -161,6 +161,23 @@ fn render_title(subject_path: &SubjectPath) -> Markup {
} }
} }
/// Renders a path within a repository as HTML.
///
/// Panics, when the string is empty.
fn render_path(inner_path: &str) -> Markup {
let path_icon = PreEscaped(fa(FaType::Regular, "folder-open").unwrap());
let mut splitted = inner_path.trim_matches('/').split('/');
let init = splitted.next().unwrap().to_string();
let path_spaced = splitted.fold(init, |b, val| b + " / " + val);
html! {
{ (path_icon) }
" / "
(path_spaced)
}
}
fn dependencies_pluralized(count: usize) -> &'static str { fn dependencies_pluralized(count: usize) -> &'static str {
if count == 1 { if count == 1 {
"dependency" "dependency"
@ -332,6 +349,7 @@ fn render_failure(subject_path: SubjectPath) -> Markup {
fn render_success( fn render_success(
analysis_outcome: AnalyzeDependenciesOutcome, analysis_outcome: AnalyzeDependenciesOutcome,
subject_path: SubjectPath, subject_path: SubjectPath,
extra_config: ExtraConfig,
) -> Markup { ) -> Markup {
let self_path = match subject_path { let self_path = match subject_path {
SubjectPath::Repo(ref repo_path) => format!( SubjectPath::Repo(ref repo_path) => format!(
@ -347,7 +365,7 @@ fn render_success(
let status_base_url = format!("{}/{}", &super::SELF_BASE_URL as &str, self_path); let status_base_url = format!("{}/{}", &super::SELF_BASE_URL as &str, self_path);
let status_data_uri = let status_data_uri =
badge::badge(Some(&analysis_outcome), BadgeKnobs::default()).to_svg_data_uri(); badge::badge(Some(&analysis_outcome), extra_config.clone()).to_svg_data_uri();
let hero_class = if analysis_outcome.any_always_insecure() { let hero_class = if analysis_outcome.any_always_insecure() {
"is-danger" "is-danger"
@ -357,6 +375,15 @@ fn render_success(
"is-success" "is-success"
}; };
// NOTE(feliix42): While we could encode the whole `ExtraConfig` struct here, I've decided
// against doing so as this would always append the defaults for badge style and compactness
// settings to the URL, bloating it unnecessarily, we can do that once it's needed.
let options = serde_urlencoded::to_string([(
"path",
extra_config.path.clone().unwrap_or_default().as_str(),
)])
.unwrap();
html! { html! {
section class=(format!("hero {}", hero_class)) { section class=(format!("hero {}", hero_class)) {
div class="hero-head" { (super::render_navbar()) } div class="hero-head" { (super::render_navbar()) }
@ -366,13 +393,23 @@ fn render_success(
(render_title(&subject_path)) (render_title(&subject_path))
} }
@if let Some(ref path) = extra_config.path {
p class="subtitle" {
(render_path(path))
}
}
img src=(status_data_uri); img src=(status_data_uri);
} }
} }
div class="hero-footer" { div class="hero-footer" {
div class="container" { div class="container" {
pre class="is-size-7" { pre class="is-size-7" {
(format!("[![dependency status]({}/status.svg)]({})", status_base_url, status_base_url)) @if extra_config.path.is_some() {
(format!("[![dependency status]({}/status.svg?{opt})]({}?{opt})", status_base_url, status_base_url, opt = options))
} @else {
(format!("[![dependency status]({}/status.svg)]({})", status_base_url, status_base_url))
}
} }
} }
} }
@ -416,6 +453,7 @@ fn render_success(
pub fn render( pub fn render(
analysis_outcome: Option<AnalyzeDependenciesOutcome>, analysis_outcome: Option<AnalyzeDependenciesOutcome>,
subject_path: SubjectPath, subject_path: SubjectPath,
extra_config: ExtraConfig,
) -> Response<Body> { ) -> Response<Body> {
let title = match subject_path { let title = match subject_path {
SubjectPath::Repo(ref repo_path) => { SubjectPath::Repo(ref repo_path) => {
@ -427,7 +465,7 @@ pub fn render(
}; };
if let Some(outcome) = analysis_outcome { if let Some(outcome) = analysis_outcome {
super::render_html(&title, render_success(outcome, subject_path)) super::render_html(&title, render_success(outcome, subject_path, extra_config))
} else { } else {
super::render_html(&title, render_failure(subject_path)) super::render_html(&title, render_failure(subject_path))
} }