mirror of
https://github.com/encounter/decomp.dev.git
synced 2026-07-10 03:18:48 -07:00
Add unit view and commit info
This commit is contained in:
+1
-1
@@ -257,7 +257,7 @@ $unit-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
#graph {
|
||||
#treemap {
|
||||
width: 100%;
|
||||
aspect-ratio: 2 / 1;
|
||||
margin-bottom: var(--pico-spacing);
|
||||
|
||||
+52
-26
@@ -38,9 +38,9 @@ const drawTooltip = (ctx: CanvasRenderingContext2D, unit: Unit, width: number, h
|
||||
const bw = m.actualBoundingBoxRight + m.actualBoundingBoxLeft + PADDING_W * 2;
|
||||
const bh = m.fontBoundingBoxAscent + m.fontBoundingBoxDescent + PADDING_H * 2;
|
||||
const margin = isTouch ? MARGIN * 2 : MARGIN;
|
||||
let bx = x + (w - bw) / 2 + PADDING_W;
|
||||
let bx = x + (w - bw) / 2;
|
||||
let by = y - bh - margin;
|
||||
let invY = false;
|
||||
let ay = y;
|
||||
if (bx + bw > width) {
|
||||
bx = width - bw;
|
||||
}
|
||||
@@ -48,22 +48,30 @@ const drawTooltip = (ctx: CanvasRenderingContext2D, unit: Unit, width: number, h
|
||||
bx = 0;
|
||||
}
|
||||
if (by < 0) {
|
||||
// Draw below the box
|
||||
by = y + h + margin;
|
||||
invY = true;
|
||||
ay = y + h;
|
||||
}
|
||||
if (by + bh > height) {
|
||||
// Draw inside the box
|
||||
by = y + margin;
|
||||
ay = y;
|
||||
}
|
||||
ctx.fillStyle = tooltipBackground;
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(bx, by, bw, bh, BORDER_RADIUS);
|
||||
// Arrow
|
||||
const ax = x + w / 2;
|
||||
if (invY) {
|
||||
ctx.moveTo(ax, y + h);
|
||||
ctx.lineTo(ax + margin, y + h + margin);
|
||||
ctx.lineTo(ax - margin, y + h + margin);
|
||||
if (ay < by) {
|
||||
// Top
|
||||
ctx.moveTo(ax, ay);
|
||||
ctx.lineTo(ax + margin, by);
|
||||
ctx.lineTo(ax - margin, by);
|
||||
} else {
|
||||
ctx.moveTo(ax, y);
|
||||
ctx.lineTo(ax + margin, y - margin);
|
||||
ctx.lineTo(ax - margin, y - margin);
|
||||
// Bottom
|
||||
ctx.moveTo(ax, ay);
|
||||
ctx.lineTo(ax + margin, by + bh);
|
||||
ctx.lineTo(ax - margin, by + bh);
|
||||
}
|
||||
ctx.fill();
|
||||
ctx.fillStyle = tooltipColor;
|
||||
@@ -136,7 +144,20 @@ const draw = (canvas: HTMLCanvasElement, units: Unit[]) => {
|
||||
}
|
||||
};
|
||||
|
||||
const drawGraph = (id: string, units: Unit[]) => {
|
||||
const findUnit = (canvas: HTMLCanvasElement, units: Unit[], clientX: number, clientY: number): Unit | null => {
|
||||
const {width, height, left, top} = canvas.getBoundingClientRect();
|
||||
const mx = clientX - left;
|
||||
const my = clientY - top;
|
||||
for (const unit of units) {
|
||||
const {x, y, w, h} = unitBounds(unit, width, height);
|
||||
if (mx >= x && mx <= x + w && my >= y && my <= y + h) {
|
||||
return unit;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const drawTreemap = (id: string, clickable: boolean, units: Unit[]) => {
|
||||
const canvas = document.getElementById(id) as HTMLCanvasElement;
|
||||
if (!canvas || !canvas.getContext) {
|
||||
return;
|
||||
@@ -148,21 +169,14 @@ const drawGraph = (id: string, units: Unit[]) => {
|
||||
const resizeObserver = new ResizeObserver(queueDraw);
|
||||
resizeObserver.observe(canvas);
|
||||
const handleHover = ({clientX, clientY}: { clientX: number, clientY: number }) => {
|
||||
const {width, height, left, top} = canvas.getBoundingClientRect();
|
||||
const mx = clientX - left;
|
||||
const my = clientY - top;
|
||||
const prev = hovered;
|
||||
hovered = null;
|
||||
for (const unit of units) {
|
||||
const {x, y, w, h} = unitBounds(unit, width, height);
|
||||
if (mx >= x && mx <= x + w && my >= y && my <= y + h) {
|
||||
hovered = unit;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (prev === hovered) {
|
||||
const unit = findUnit(canvas, units, clientX, clientY);
|
||||
if (unit === hovered) {
|
||||
return;
|
||||
}
|
||||
if (clickable) {
|
||||
canvas.style.cursor = unit ? "pointer" : "default";
|
||||
}
|
||||
hovered = unit;
|
||||
dirty = true;
|
||||
queueDraw();
|
||||
}
|
||||
@@ -170,6 +184,9 @@ const drawGraph = (id: string, units: Unit[]) => {
|
||||
if (!hovered) {
|
||||
return;
|
||||
}
|
||||
if (clickable) {
|
||||
canvas.style.cursor = "default";
|
||||
}
|
||||
hovered = null;
|
||||
dirty = true;
|
||||
queueDraw();
|
||||
@@ -184,12 +201,21 @@ const drawGraph = (id: string, units: Unit[]) => {
|
||||
handleHover(e.touches[0]);
|
||||
});
|
||||
canvas.addEventListener("touchend", handleLeave);
|
||||
canvas.addEventListener("click", ({clientX, clientY}) => {
|
||||
const unit = findUnit(canvas, units, clientX, clientY);
|
||||
if (!unit || !unit.name || !clickable) {
|
||||
return;
|
||||
}
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set("unit", unit.name);
|
||||
window.location.href = url.toString();
|
||||
});
|
||||
draw(canvas, units);
|
||||
};
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
interface Window {
|
||||
drawGraph: (id: string, units: Unit[]) => void;
|
||||
drawTreemap: (id: string, clickable: boolean, units: Unit[]) => void;
|
||||
}
|
||||
|
||||
window.drawGraph = drawGraph;
|
||||
window.drawTreemap = drawTreemap;
|
||||
+1
-1
@@ -29,7 +29,7 @@ struct ReportKey {
|
||||
type UnitKey = [u8; 32];
|
||||
|
||||
impl Database {
|
||||
pub async fn open(config: &AppConfig) -> Result<Self> {
|
||||
pub async fn new(config: &AppConfig) -> Result<Self> {
|
||||
if !Sqlite::database_exists(&config.db_url).await.unwrap_or(false) {
|
||||
tracing::info!(db_url = %config.db_url, "Creating database");
|
||||
Sqlite::create_database(&config.db_url).await.context("Failed to create database")?;
|
||||
|
||||
+62
-15
@@ -5,11 +5,13 @@ use std::{
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use axum::http::StatusCode;
|
||||
use moka::future::Cache;
|
||||
use objdiff_core::bindings::report::Report;
|
||||
use octocrab::{
|
||||
models::{ArtifactId, RunId},
|
||||
models::{repos::RepoCommitPage, ArtifactId, Author, RunId},
|
||||
params::actions::ArchiveFormat,
|
||||
Octocrab,
|
||||
GitHubError, Octocrab,
|
||||
};
|
||||
use regex::Regex;
|
||||
use tokio::{sync::Semaphore, task::JoinSet};
|
||||
@@ -20,23 +22,64 @@ use crate::{
|
||||
AppState,
|
||||
};
|
||||
|
||||
pub type Client = Octocrab;
|
||||
#[derive(Clone)]
|
||||
pub struct GitHub {
|
||||
pub client: Octocrab,
|
||||
#[allow(dead_code)]
|
||||
pub profile: Author,
|
||||
commit_cache: Cache<GetCommit, Option<RepoCommitPage>>,
|
||||
}
|
||||
|
||||
pub async fn create(config: &AppConfig) -> Result<Octocrab> {
|
||||
let client = Octocrab::builder()
|
||||
.personal_token(config.github_token.clone())
|
||||
.build()
|
||||
.context("Failed to create GitHub client")?;
|
||||
octocrab::initialise(client.clone());
|
||||
let profile = client.current().user().await.context("Failed to fetch current user")?;
|
||||
tracing::info!("Logged in as {}", profile.login);
|
||||
Ok(client)
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct GetCommit {
|
||||
owner: String,
|
||||
repo: String,
|
||||
sha: String,
|
||||
}
|
||||
|
||||
impl GitHub {
|
||||
pub async fn new(config: &AppConfig) -> Result<Self> {
|
||||
let client = Octocrab::builder()
|
||||
.personal_token(config.github_token.clone())
|
||||
.build()
|
||||
.context("Failed to create GitHub client")?;
|
||||
octocrab::initialise(client.clone());
|
||||
let profile = client.current().user().await.context("Failed to fetch current user")?;
|
||||
tracing::info!("Logged in as {}", profile.login);
|
||||
let commit_cache = Cache::builder().max_capacity(100).build();
|
||||
Ok(Self { client, profile, commit_cache })
|
||||
}
|
||||
|
||||
pub async fn get_commit(
|
||||
&self,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
sha: &str,
|
||||
) -> Result<Option<RepoCommitPage>> {
|
||||
let key =
|
||||
GetCommit { owner: owner.to_string(), repo: repo.to_string(), sha: sha.to_string() };
|
||||
if let Some(commit) = self.commit_cache.get(&key).await {
|
||||
return Ok(commit);
|
||||
}
|
||||
let commit =
|
||||
match self.client.repos(owner, repo).list_commits().sha(sha).per_page(1).send().await {
|
||||
Ok(page) => page.items.into_iter().next().map(|c| c.commit),
|
||||
Err(octocrab::Error::GitHub {
|
||||
source: GitHubError { status_code: StatusCode::NOT_FOUND, .. },
|
||||
..
|
||||
}) => None,
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
self.commit_cache.insert(key, commit.clone()).await;
|
||||
Ok(commit)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(state: &mut AppState, owner: &str, repo: &str, stop_run_id: u64) -> Result<()> {
|
||||
tracing::info!("Refreshing project {}/{}", owner, repo);
|
||||
let existing = state.db.get_project_info(owner, repo, None).await?;
|
||||
let repo = state.github.repos(owner, repo).get().await.context("Failed to fetch repo")?;
|
||||
let repo =
|
||||
state.github.client.repos(owner, repo).get().await.context("Failed to fetch repo")?;
|
||||
let branch = repo.default_branch.as_deref().unwrap_or("main");
|
||||
let Some(owner) = repo.owner else {
|
||||
return Err(anyhow!("Repo has no owner"));
|
||||
@@ -56,6 +99,7 @@ pub async fn run(state: &mut AppState, owner: &str, repo: &str, stop_run_id: u64
|
||||
'outer: loop {
|
||||
let result = state
|
||||
.github
|
||||
.client
|
||||
.workflows(&project.owner, &project.repo)
|
||||
.list_runs("build.yml")
|
||||
.branch(branch)
|
||||
@@ -173,13 +217,15 @@ struct ProcessArtifactResult {
|
||||
}
|
||||
|
||||
async fn process_workflow_run(
|
||||
github: Client,
|
||||
github: GitHub,
|
||||
project: Project,
|
||||
run_id: RunId,
|
||||
) -> Result<ProcessWorkflowRunResult> {
|
||||
let artifacts = github
|
||||
.client
|
||||
.all_pages(
|
||||
github
|
||||
.client
|
||||
.actions()
|
||||
.list_workflow_run_artifacts(&project.owner, &project.repo, run_id)
|
||||
.send()
|
||||
@@ -264,12 +310,13 @@ async fn process_workflow_run(
|
||||
type DownloadArtifactResult = Result<Vec<(String, Arc<Report>)>>;
|
||||
|
||||
async fn download_artifact(
|
||||
github: Client,
|
||||
github: GitHub,
|
||||
project: Project,
|
||||
artifact_id: ArtifactId,
|
||||
version: String,
|
||||
) -> DownloadArtifactResult {
|
||||
let bytes = github
|
||||
.client
|
||||
.actions()
|
||||
.download_artifact(&project.owner, &project.repo, artifact_id, ArchiveFormat::Zip)
|
||||
.await?;
|
||||
|
||||
+10
-18
@@ -1,9 +1,9 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use image::ImageFormat;
|
||||
use objdiff_core::bindings::report::{Measures, ReportCategory};
|
||||
use objdiff_core::bindings::report::Measures;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{models::ReportFile, svg};
|
||||
use crate::svg;
|
||||
|
||||
#[derive(Deserialize, Default, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -30,25 +30,19 @@ pub struct ShieldResponse {
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
report: &ReportFile,
|
||||
measures: &Measures,
|
||||
current_category: Option<&ReportCategory>,
|
||||
default_label: &str,
|
||||
params: &ShieldParams,
|
||||
) -> Result<ShieldResponse> {
|
||||
let label = if let Some(label) = params.label.clone() {
|
||||
label
|
||||
} else if let Some(category) = current_category {
|
||||
category.name.clone()
|
||||
} else {
|
||||
report.project.short_name().to_string()
|
||||
};
|
||||
let label = params.label.clone().unwrap_or_else(|| default_label.to_string());
|
||||
let message = if let Some(measure) = ¶ms.measure {
|
||||
match measure.as_str() {
|
||||
"code" => format!("{:.2}%", measures.matched_code_percent),
|
||||
"data" => format!("{:.2}%", measures.matched_data_percent),
|
||||
"functions" => format!("{:.2}%", measures.matched_functions_percent),
|
||||
"functions" => format!("{}/{}", measures.matched_functions, measures.total_functions),
|
||||
"complete_code" => format!("{:.2}%", measures.complete_code_percent),
|
||||
"complete_data" => format!("{:.2}%", measures.complete_data_percent),
|
||||
"complete_units" => format!("{}/{}", measures.complete_units, measures.total_units),
|
||||
_ => return Err(anyhow!("Unknown measure")),
|
||||
}
|
||||
} else {
|
||||
@@ -65,12 +59,11 @@ pub fn render(
|
||||
}
|
||||
|
||||
pub fn render_svg(
|
||||
report: &ReportFile,
|
||||
measures: &Measures,
|
||||
current_category: Option<&ReportCategory>,
|
||||
default_label: &str,
|
||||
params: &ShieldParams,
|
||||
) -> Result<String> {
|
||||
let response = render(report, measures, current_category, params)?;
|
||||
let response = render(measures, default_label, params)?;
|
||||
let mut builder = badge_maker::BadgeBuilder::new();
|
||||
builder.label(&response.label).message(&response.message);
|
||||
if let Some(color) = &response.color {
|
||||
@@ -87,12 +80,11 @@ pub fn render_svg(
|
||||
}
|
||||
|
||||
pub fn render_image(
|
||||
report: &ReportFile,
|
||||
measures: &Measures,
|
||||
current_category: Option<&ReportCategory>,
|
||||
default_label: &str,
|
||||
params: &ShieldParams,
|
||||
format: ImageFormat,
|
||||
) -> Result<Vec<u8>> {
|
||||
let svg = render_svg(report, measures, current_category, params)?;
|
||||
let svg = render_svg(measures, default_label, params)?;
|
||||
svg::render_image(&svg, format)
|
||||
}
|
||||
|
||||
+26
-3
@@ -58,15 +58,38 @@ impl<E: Into<anyhow::Error>> From<E> for AppError {
|
||||
fn from(err: E) -> Self { Self::Internal(err.into()) }
|
||||
}
|
||||
|
||||
pub fn parse_accept(headers: &HeaderMap) -> Vec<Mime> {
|
||||
headers
|
||||
pub fn parse_accept(headers: &HeaderMap, ext: Option<&str>) -> Vec<Mime> {
|
||||
// Explicit extension takes precedence
|
||||
if let Some(ext) = ext {
|
||||
return match ext.to_ascii_lowercase().as_str() {
|
||||
"json" => vec![mime::APPLICATION_JSON],
|
||||
"binpb" | "proto" => vec![Mime::from_str("application/x-protobuf").unwrap()],
|
||||
"svg" => vec![mime::IMAGE_SVG],
|
||||
_ => {
|
||||
if let Some(format) = image::ImageFormat::from_extension(ext) {
|
||||
vec![Mime::from_str(format.to_mime_type()).unwrap()]
|
||||
} else {
|
||||
// An unknown extension should be NOT_ACCEPTABLE, not */*.
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
// Otherwise, parse the Accept header
|
||||
let result = headers
|
||||
.get(header::ACCEPT)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.iter()
|
||||
.flat_map(|s| s.split(','))
|
||||
.map(|s| s.trim())
|
||||
.filter_map(|s| Mime::from_str(s).ok())
|
||||
.collect()
|
||||
.collect::<Vec<_>>();
|
||||
if result.is_empty() {
|
||||
// If no Accept header is present, use */*
|
||||
vec![mime::STAR_STAR]
|
||||
} else {
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Protobuf<T: Message>(pub Arc<T>);
|
||||
|
||||
+205
-129
@@ -1,4 +1,4 @@
|
||||
use std::{iter, str::FromStr, time::Instant};
|
||||
use std::{borrow::Cow, iter, time::Instant};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use axum::{
|
||||
@@ -9,7 +9,7 @@ use axum::{
|
||||
};
|
||||
use image::ImageFormat;
|
||||
use mime::Mime;
|
||||
use objdiff_core::bindings::report::{Measures, ReportCategory};
|
||||
use objdiff_core::bindings::report::{Measures, ReportCategory, ReportUnit};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
@@ -41,6 +41,7 @@ pub struct ReportQuery {
|
||||
h: Option<u32>,
|
||||
#[serde(flatten)]
|
||||
shield: badge::ShieldParams,
|
||||
unit: Option<String>,
|
||||
}
|
||||
|
||||
impl ReportQuery {
|
||||
@@ -61,18 +62,25 @@ struct ReportTemplateContext<'a> {
|
||||
measures: TemplateMeasures,
|
||||
units: &'a [ReportTemplateUnit<'a>],
|
||||
versions: &'a [ReportTemplateVersion<'a>],
|
||||
prev_commit: Option<&'a str>,
|
||||
next_commit: Option<&'a str>,
|
||||
prev_commit_path: Option<&'a str>,
|
||||
next_commit_path: Option<&'a str>,
|
||||
latest_commit_path: Option<&'a str>,
|
||||
categories: &'a [ReportCategoryItem<'a>],
|
||||
current_category: &'a ReportCategoryItem<'a>,
|
||||
canonical_path: &'a str,
|
||||
canonical_url: &'a str,
|
||||
image_url: &'a str,
|
||||
current_unit: Option<&'a str>,
|
||||
units_path: &'a str,
|
||||
commit_message: Option<&'a str>,
|
||||
commit_url: &'a str,
|
||||
source_file_url: Option<&'a str>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ReportTemplateUnit<'a> {
|
||||
name: &'a str,
|
||||
total_code: u64,
|
||||
fuzzy_match_percent: f32,
|
||||
color: String,
|
||||
x: f32,
|
||||
@@ -187,6 +195,11 @@ pub async fn get_report(
|
||||
) -> Result<Response, AppError> {
|
||||
let start = Instant::now();
|
||||
let (params, ext) = extract_extension(params);
|
||||
let acceptable = parse_accept(&headers, ext.as_deref());
|
||||
if acceptable.is_empty() {
|
||||
return Err(AppError::Status(StatusCode::NOT_ACCEPTABLE));
|
||||
}
|
||||
|
||||
let mut commit = params.commit.as_deref();
|
||||
if matches!(commit, Some(c) if c.eq_ignore_ascii_case("latest")) {
|
||||
commit = None;
|
||||
@@ -212,70 +225,38 @@ pub async fn get_report(
|
||||
return Err(AppError::Status(StatusCode::NOT_FOUND));
|
||||
};
|
||||
|
||||
if let Some(mode) = query.mode.as_deref() {
|
||||
match mode.to_ascii_lowercase().as_str() {
|
||||
"shield" => mode_shield(report, query, headers, ext),
|
||||
"report" => mode_report(report, project_info, &state, uri, query, headers, start, ext),
|
||||
_ => Err(AppError::Status(StatusCode::BAD_REQUEST)),
|
||||
}
|
||||
} else {
|
||||
mode_report(report, project_info, &state, uri, query, headers, start, ext)
|
||||
let scope = apply_scope(&report, &project_info, &query)?;
|
||||
match query.mode.as_deref().unwrap_or("report").to_ascii_lowercase().as_str() {
|
||||
"shield" => mode_shield(&scope, query, &acceptable),
|
||||
"report" => mode_report(&scope, &state, uri, query, start, &acceptable).await,
|
||||
_ => Err(AppError::Status(StatusCode::BAD_REQUEST)),
|
||||
}
|
||||
}
|
||||
|
||||
fn mode_report(
|
||||
report: ReportFile,
|
||||
project_info: ProjectInfo,
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn mode_report(
|
||||
scope: &Scope<'_>,
|
||||
state: &AppState,
|
||||
uri: Uri,
|
||||
query: ReportQuery,
|
||||
headers: HeaderMap,
|
||||
start: Instant,
|
||||
ext: Option<String>,
|
||||
acceptable: &[Mime],
|
||||
) -> Result<Response, AppError> {
|
||||
let (measures, current_category, units) = apply_category(&report, &query)?;
|
||||
let acceptable = if let Some(ext) = ext {
|
||||
vec![match ext.to_ascii_lowercase().as_str() {
|
||||
"json" => mime::APPLICATION_JSON,
|
||||
"binpb" | "proto" => Mime::from_str("application/x-protobuf")?,
|
||||
"svg" => mime::IMAGE_SVG,
|
||||
_ => {
|
||||
if let Some(format) = ImageFormat::from_extension(ext) {
|
||||
Mime::from_str(format.to_mime_type())?
|
||||
} else {
|
||||
return Err(AppError::Status(StatusCode::NOT_ACCEPTABLE));
|
||||
}
|
||||
}
|
||||
}]
|
||||
} else {
|
||||
if !headers.contains_key(header::ACCEPT) {
|
||||
return Ok(Json(report.report).into_response());
|
||||
}
|
||||
parse_accept(&headers)
|
||||
};
|
||||
for mime in acceptable {
|
||||
if (mime.type_() == mime::STAR && mime.subtype() == mime::STAR)
|
||||
|| (mime.type_() == mime::TEXT && mime.subtype() == mime::HTML)
|
||||
{
|
||||
let mut rendered = render_template(
|
||||
&report,
|
||||
&project_info,
|
||||
measures,
|
||||
current_category,
|
||||
&units,
|
||||
&state,
|
||||
uri,
|
||||
)?;
|
||||
let mut rendered = render_template(scope, state, uri).await?;
|
||||
let elapsed = start.elapsed();
|
||||
rendered = rendered.replace("[[time]]", &format!("{}ms", elapsed.as_millis()));
|
||||
return Ok(Html(rendered).into_response());
|
||||
} else if mime.type_() == mime::APPLICATION && mime.subtype() == mime::JSON {
|
||||
return Ok(Json(report.report).into_response());
|
||||
return Ok(Json(scope.report.report.clone()).into_response());
|
||||
} else if mime.type_() == mime::APPLICATION && mime.subtype() == PROTOBUF {
|
||||
return Ok(Protobuf(report.report).into_response());
|
||||
return Ok(Protobuf(scope.report.report.clone()).into_response());
|
||||
} else if mime.type_() == mime::IMAGE && mime.subtype() == mime::SVG {
|
||||
let (w, h) = query.size();
|
||||
let svg = treemap::render_svg(&units, w, h, &state)?;
|
||||
let svg = treemap::render_svg(&scope.units, w, h, state)?;
|
||||
return Ok(([(header::CONTENT_TYPE, mime::IMAGE_SVG.as_ref())], svg).into_response());
|
||||
} else if mime.type_() == mime::IMAGE {
|
||||
let format = if mime.subtype() == mime::STAR {
|
||||
@@ -286,7 +267,7 @@ fn mode_report(
|
||||
.ok_or_else(|| AppError::Status(StatusCode::NOT_ACCEPTABLE))?
|
||||
};
|
||||
let (w, h) = query.size();
|
||||
let data = treemap::render_image(&units, w, h, &state, format)?;
|
||||
let data = treemap::render_image(&scope.units, w, h, state, format)?;
|
||||
return Ok(([(header::CONTENT_TYPE, format.to_mime_type())], data).into_response());
|
||||
}
|
||||
}
|
||||
@@ -294,39 +275,20 @@ fn mode_report(
|
||||
}
|
||||
|
||||
fn mode_shield(
|
||||
report: ReportFile,
|
||||
Scope { report, measures, label, .. }: &Scope<'_>,
|
||||
query: ReportQuery,
|
||||
headers: HeaderMap,
|
||||
ext: Option<String>,
|
||||
acceptable: &[Mime],
|
||||
) -> Result<Response, AppError> {
|
||||
let (measures, current_category, _units) = apply_category(&report, &query)?;
|
||||
let acceptable = if let Some(ext) = ext {
|
||||
vec![match ext.to_ascii_lowercase().as_str() {
|
||||
"json" => mime::APPLICATION_JSON,
|
||||
"svg" => mime::IMAGE_SVG,
|
||||
_ => {
|
||||
if let Some(format) = ImageFormat::from_extension(ext) {
|
||||
Mime::from_str(format.to_mime_type())?
|
||||
} else {
|
||||
return Err(AppError::Status(StatusCode::NOT_ACCEPTABLE));
|
||||
}
|
||||
}
|
||||
}]
|
||||
} else {
|
||||
if !headers.contains_key(header::ACCEPT) {
|
||||
return Ok(Json(report.report).into_response());
|
||||
}
|
||||
parse_accept(&headers)
|
||||
};
|
||||
let label = label.unwrap_or_else(|| report.project.short_name());
|
||||
for mime in acceptable {
|
||||
if (mime.type_() == mime::STAR && mime.subtype() == mime::STAR)
|
||||
|| (mime.type_() == mime::IMAGE && mime.subtype() == mime::SVG)
|
||||
|| (mime.type_() == mime::TEXT && mime.subtype() == mime::HTML)
|
||||
{
|
||||
let data = badge::render_svg(&report, measures, current_category, &query.shield)?;
|
||||
let data = badge::render_svg(measures, label, &query.shield)?;
|
||||
return Ok(([(header::CONTENT_TYPE, mime::IMAGE_SVG.as_ref())], data).into_response());
|
||||
} else if mime.type_() == mime::APPLICATION && mime.subtype() == mime::JSON {
|
||||
let data = badge::render(&report, measures, current_category, &query.shield)?;
|
||||
let data = badge::render(measures, label, &query.shield)?;
|
||||
return Ok(Json(data).into_response());
|
||||
} else if mime.type_() == mime::IMAGE {
|
||||
let format = if mime.subtype() == mime::STAR {
|
||||
@@ -336,8 +298,7 @@ fn mode_shield(
|
||||
ImageFormat::from_mime_type(mime.essence_str())
|
||||
.ok_or_else(|| AppError::Status(StatusCode::NOT_ACCEPTABLE))?
|
||||
};
|
||||
let data =
|
||||
badge::render_image(&report, measures, current_category, &query.shield, format)?;
|
||||
let data = badge::render_image(measures, label, &query.shield, format)?;
|
||||
return Ok(([(header::CONTENT_TYPE, format.to_mime_type())], data).into_response());
|
||||
}
|
||||
}
|
||||
@@ -363,10 +324,21 @@ const EMPTY_MEASURES: Measures = Measures {
|
||||
complete_units: 0,
|
||||
};
|
||||
|
||||
fn apply_category<'a>(
|
||||
struct Scope<'a> {
|
||||
report: &'a ReportFile,
|
||||
project_info: &'a ProjectInfo,
|
||||
measures: &'a Measures,
|
||||
current_category: Option<&'a ReportCategory>,
|
||||
current_unit: Option<&'a ReportUnit>,
|
||||
units: Vec<ReportTemplateUnit<'a>>,
|
||||
label: Option<&'a str>,
|
||||
}
|
||||
|
||||
fn apply_scope<'a>(
|
||||
report: &'a ReportFile,
|
||||
project_info: &'a ProjectInfo,
|
||||
query: &ReportQuery,
|
||||
) -> Result<(&'a Measures, Option<&'a ReportCategory>, Vec<ReportTemplateUnit<'a>>)> {
|
||||
) -> Result<Scope<'a>> {
|
||||
let mut measures = report.report.measures.as_ref().unwrap_or(&EMPTY_MEASURES);
|
||||
let mut current_category = None;
|
||||
let mut category_id_filter = None;
|
||||
@@ -377,53 +349,109 @@ fn apply_category<'a>(
|
||||
current_category = Some(category);
|
||||
category_id_filter = Some(category.id.clone());
|
||||
}
|
||||
let mut current_unit = None;
|
||||
if let Some(unit) = query
|
||||
.unit
|
||||
.as_ref()
|
||||
.and_then(|unit_name| report.report.units.iter().find(|u| u.name == *unit_name))
|
||||
{
|
||||
measures = unit.measures.as_ref().unwrap_or(&EMPTY_MEASURES);
|
||||
current_unit = Some(unit);
|
||||
}
|
||||
let (w, h) = query.size();
|
||||
let aspect = w as f32 / h as f32;
|
||||
let units = treemap::layout_units(&report.report, w, h, |item| {
|
||||
if let Some(category_id) = &category_id_filter {
|
||||
item.metadata
|
||||
.as_ref()
|
||||
.map_or(false, |m| m.progress_categories.iter().any(|c| c == category_id))
|
||||
let mut units =
|
||||
if let Some(unit) = current_unit {
|
||||
unit.functions
|
||||
.iter()
|
||||
.filter_map(|f| {
|
||||
if f.size == 0 {
|
||||
return None;
|
||||
}
|
||||
Some(ReportTemplateUnit {
|
||||
name: f
|
||||
.metadata
|
||||
.as_ref()
|
||||
.and_then(|m| m.demangled_name.as_deref())
|
||||
.unwrap_or(&f.name),
|
||||
total_code: f.size,
|
||||
fuzzy_match_percent: f.fuzzy_match_percent,
|
||||
color: treemap::unit_color(f.fuzzy_match_percent),
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
w: 0.0,
|
||||
h: 0.0,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
let unit = item.unit();
|
||||
let match_percent =
|
||||
unit.measures.as_ref().map(|m| m.fuzzy_match_percent).unwrap_or_default();
|
||||
let mut bounds = item.bounds;
|
||||
if aspect > 1.0 {
|
||||
bounds.y *= aspect;
|
||||
bounds.h *= aspect;
|
||||
} else {
|
||||
bounds.x /= aspect;
|
||||
bounds.w /= aspect;
|
||||
}
|
||||
ReportTemplateUnit {
|
||||
name: &unit.name,
|
||||
fuzzy_match_percent: match_percent,
|
||||
color: treemap::unit_color(match_percent),
|
||||
x: bounds.x,
|
||||
y: bounds.y,
|
||||
w: bounds.w,
|
||||
h: bounds.h,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Ok((measures, current_category, units))
|
||||
report
|
||||
.report
|
||||
.units
|
||||
.iter()
|
||||
.filter_map(|unit| {
|
||||
if let Some(category_id) = &category_id_filter {
|
||||
if !unit.metadata.as_ref().map_or(false, |m| {
|
||||
m.progress_categories.iter().any(|c| c == category_id)
|
||||
}) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let measures = unit.measures.as_ref()?;
|
||||
if measures.total_code == 0 {
|
||||
return None;
|
||||
}
|
||||
Some(ReportTemplateUnit {
|
||||
name: &unit.name,
|
||||
total_code: measures.total_code,
|
||||
fuzzy_match_percent: measures.fuzzy_match_percent,
|
||||
color: treemap::unit_color(measures.fuzzy_match_percent),
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
w: 0.0,
|
||||
h: 0.0,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
treemap::layout_units(
|
||||
&mut units,
|
||||
w as f32 / h as f32,
|
||||
|i| i.total_code as f32,
|
||||
|i, r| {
|
||||
i.x = r.x;
|
||||
i.y = r.y;
|
||||
i.w = r.w;
|
||||
i.h = r.h;
|
||||
},
|
||||
);
|
||||
let label = current_unit
|
||||
.as_ref()
|
||||
.map(|u| u.name.rsplit_once('/').map_or(u.name.as_str(), |(_, name)| name))
|
||||
.or_else(|| current_category.as_ref().map(|c| c.name.as_str()));
|
||||
Ok(Scope { report, project_info, measures, current_category, current_unit, units, label })
|
||||
}
|
||||
|
||||
fn render_template(
|
||||
report: &ReportFile,
|
||||
project_info: &ProjectInfo,
|
||||
measures: &Measures,
|
||||
current_category: Option<&ReportCategory>,
|
||||
units: &[ReportTemplateUnit],
|
||||
state: &AppState,
|
||||
uri: Uri,
|
||||
) -> Result<String> {
|
||||
async fn render_template(scope: &Scope<'_>, state: &AppState, uri: Uri) -> Result<String> {
|
||||
let Scope { report, project_info, measures, current_category, current_unit, units, label } =
|
||||
scope;
|
||||
let commit = match state
|
||||
.github
|
||||
.get_commit(&project_info.project.owner, &project_info.project.repo, &report.commit.sha)
|
||||
.await
|
||||
{
|
||||
Ok(commit) => commit,
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
"Failed to get commit {}/{}@{}: {}",
|
||||
project_info.project.owner,
|
||||
project_info.project.repo,
|
||||
report.commit.sha,
|
||||
e
|
||||
);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let request_url = Url::parse(&uri.to_string()).context("Failed to parse URI")?;
|
||||
let project_base_path =
|
||||
format!("/{}/{}", project_info.project.owner, project_info.project.repo);
|
||||
@@ -445,41 +473,89 @@ fn render_template(
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let all_url = canonical_url.set_query("category", None);
|
||||
let all_url = canonical_url.query_param("category", None);
|
||||
let all_category =
|
||||
ReportCategoryItem { id: "all", name: "All", path: all_url.path_and_query().to_string() };
|
||||
let current_category = current_category
|
||||
.map(|c| {
|
||||
let path =
|
||||
canonical_url.set_query("category", Some(&c.id)).path_and_query().to_string();
|
||||
canonical_url.query_param("category", Some(&c.id)).path_and_query().to_string();
|
||||
ReportCategoryItem { id: &c.id, name: &c.name, path }
|
||||
})
|
||||
.unwrap_or_else(|| all_category.clone());
|
||||
let categories = iter::once(all_category)
|
||||
.chain(report.report.categories.iter().map(|c| {
|
||||
let path =
|
||||
canonical_url.set_query("category", Some(&c.id)).path_and_query().to_string();
|
||||
canonical_url.query_param("category", Some(&c.id)).path_and_query().to_string();
|
||||
ReportCategoryItem { id: &c.id, name: &c.name, path }
|
||||
}))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let prev_commit_path = project_info.prev_commit.as_deref().map(|commit| {
|
||||
let url = request_url.with_path(&format!(
|
||||
"/{}/{}/{}/{}",
|
||||
project_info.project.owner, project_info.project.repo, report.version, commit
|
||||
));
|
||||
url.path_and_query().to_string()
|
||||
});
|
||||
let next_commit_path = project_info.next_commit.as_deref().map(|commit| {
|
||||
let url = request_url.with_path(&format!(
|
||||
"/{}/{}/{}/{}",
|
||||
project_info.project.owner, project_info.project.repo, report.version, commit
|
||||
));
|
||||
url.path_and_query().to_string()
|
||||
});
|
||||
let latest_commit_path = project_info.next_commit.as_deref().map(|_| {
|
||||
let url = request_url.with_path(&format!(
|
||||
"/{}/{}/{}",
|
||||
project_info.project.owner, project_info.project.repo, report.version
|
||||
));
|
||||
url.path_and_query().to_string()
|
||||
});
|
||||
|
||||
let units_path = canonical_url.query_param("unit", None).path_and_query().to_string();
|
||||
let commit_message = commit.as_ref().and_then(|c| c.message.lines().next());
|
||||
let commit_url = format!("{}/commit/{}", project_info.project.repo_url(), report.commit.sha);
|
||||
let source_file_url = current_unit
|
||||
.and_then(|u| u.metadata.as_ref())
|
||||
.and_then(|m| m.source_path.as_deref())
|
||||
.map(|path| {
|
||||
format!("{}/blob/{}/{}", project_info.project.repo_url(), report.commit.sha, path)
|
||||
});
|
||||
let project_name = if let Some(label) = label {
|
||||
Cow::Owned(format!("{} ({})", project_info.project.name(), label))
|
||||
} else {
|
||||
project_info.project.name()
|
||||
};
|
||||
let project_short_name = if let Some(label) = label {
|
||||
Cow::Owned(format!("{} ({})", project_info.project.short_name(), label))
|
||||
} else {
|
||||
Cow::Borrowed(project_info.project.short_name())
|
||||
};
|
||||
|
||||
render(&state.templates, "report.html", ReportTemplateContext {
|
||||
project: &report.project,
|
||||
project_name: &report.project.name(),
|
||||
project_short_name: report.project.short_name(),
|
||||
project_name: project_name.as_ref(),
|
||||
project_short_name: project_short_name.as_ref(),
|
||||
project_url: &report.project.repo_url(),
|
||||
project_path: &project_base_path,
|
||||
commit: &report.commit.sha,
|
||||
version: &report.version,
|
||||
measures: TemplateMeasures::from(measures),
|
||||
measures: TemplateMeasures::from(*measures),
|
||||
units,
|
||||
versions: &versions,
|
||||
prev_commit: project_info.prev_commit.as_deref(),
|
||||
next_commit: project_info.next_commit.as_deref(),
|
||||
prev_commit_path: prev_commit_path.as_deref(),
|
||||
next_commit_path: next_commit_path.as_deref(),
|
||||
latest_commit_path: latest_commit_path.as_deref(),
|
||||
categories: &categories,
|
||||
current_category: ¤t_category,
|
||||
canonical_path: canonical_url.path_and_query(),
|
||||
canonical_url: canonical_url.as_ref(),
|
||||
image_url: image_url.as_ref(),
|
||||
current_unit: current_unit.map(|u| u.name.as_str()),
|
||||
units_path: &units_path,
|
||||
commit_message,
|
||||
commit_url: &commit_url,
|
||||
source_file_url: source_file_url.as_deref(),
|
||||
})
|
||||
}
|
||||
|
||||
+15
-42
@@ -1,58 +1,31 @@
|
||||
use anyhow::Result;
|
||||
use image::ImageFormat;
|
||||
use objdiff_core::bindings::report::{Report, ReportUnit};
|
||||
use palette::{Mix, Srgb};
|
||||
use serde::Serialize;
|
||||
use streemap::Rect;
|
||||
|
||||
use crate::{handlers::report::ReportTemplateUnit, svg, templates::render, AppState};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ReportUnitItem<'report> {
|
||||
pub unit: &'report ReportUnit,
|
||||
pub size: f32,
|
||||
pub bounds: Rect<f32>,
|
||||
}
|
||||
|
||||
impl<'report> ReportUnitItem<'report> {
|
||||
pub fn with_size(unit: &'report ReportUnit, size: f32) -> ReportUnitItem<'report> {
|
||||
ReportUnitItem { unit, size, bounds: Rect::from_size(0.0, 0.0) }
|
||||
}
|
||||
|
||||
pub fn unit(&self) -> &'report ReportUnit { self.unit }
|
||||
}
|
||||
|
||||
pub fn layout_units(
|
||||
report: &Report,
|
||||
w: u32,
|
||||
h: u32,
|
||||
mut predicate: impl FnMut(&ReportUnit) -> bool,
|
||||
) -> Vec<ReportUnitItem> {
|
||||
let aspect = w as f32 / h as f32;
|
||||
pub fn layout_units<T, S, R>(items: &mut [T], aspect: f32, size_fn: S, mut set_rect_fn: R)
|
||||
where
|
||||
S: Fn(&T) -> f32,
|
||||
R: FnMut(&mut T, Rect<f32>),
|
||||
{
|
||||
let rect = if aspect > 1.0 {
|
||||
Rect::from_size(1.0, 1.0 / aspect)
|
||||
} else {
|
||||
Rect::from_size(aspect, 1.0)
|
||||
};
|
||||
let mut items = report
|
||||
.units
|
||||
.iter()
|
||||
.filter_map(|unit| {
|
||||
if !predicate(unit) {
|
||||
return None;
|
||||
}
|
||||
let total_code = unit.measures.as_ref().unwrap().total_code;
|
||||
if total_code == 0 {
|
||||
return None;
|
||||
}
|
||||
Some(ReportUnitItem::with_size(
|
||||
unit,
|
||||
total_code as f32 / report.measures.as_ref().unwrap().total_code as f32,
|
||||
))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
streemap::ordered_pivot_by_middle(rect, &mut items, |i| i.size, |i, s| i.bounds = s);
|
||||
items
|
||||
streemap::ordered_pivot_by_middle(rect, items, size_fn, |item, mut rect| {
|
||||
if aspect > 1.0 {
|
||||
rect.y *= aspect;
|
||||
rect.h *= aspect;
|
||||
} else {
|
||||
rect.x /= aspect;
|
||||
rect.w /= aspect;
|
||||
}
|
||||
set_rect_fn(item, rect);
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
||||
+25
-7
@@ -19,28 +19,42 @@ use std::{
|
||||
use axum::{http::header, Router};
|
||||
use tokio::{net::TcpListener, signal};
|
||||
use tower::ServiceBuilder;
|
||||
use tower_http::{timeout::TimeoutLayer, ServiceBuilderExt};
|
||||
use tower_http::{
|
||||
timeout::TimeoutLayer,
|
||||
trace::{DefaultMakeSpan, DefaultOnResponse, TraceLayer},
|
||||
ServiceBuilderExt,
|
||||
};
|
||||
use tracing_subscriber::{filter::LevelFilter, EnvFilter};
|
||||
|
||||
use crate::{config::Config, db::Database, handlers::build_router, templates::Templates};
|
||||
use crate::{
|
||||
config::Config, db::Database, github::GitHub, handlers::build_router, templates::Templates,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
config: Config,
|
||||
db: Database,
|
||||
github: github::Client,
|
||||
github: GitHub,
|
||||
templates: Templates,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt::init();
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
EnvFilter::builder()
|
||||
// Default to info level
|
||||
.with_default_directive(LevelFilter::INFO.into())
|
||||
.from_env_lossy(),
|
||||
)
|
||||
.init();
|
||||
|
||||
let config: Config = {
|
||||
let file = BufReader::new(File::open("config.yml").expect("Failed to open config file"));
|
||||
serde_yaml::from_reader(file).expect("Failed to parse config file")
|
||||
};
|
||||
let db = Database::open(&config.app).await.expect("Failed to open database");
|
||||
let github = github::create(&config.app).await.expect("Failed to create GitHub client");
|
||||
let db = Database::new(&config.app).await.expect("Failed to open database");
|
||||
let github = GitHub::new(&config.app).await.expect("Failed to create GitHub client");
|
||||
let templates = templates::create("templates");
|
||||
let state = AppState { config, db: db.clone(), github, templates };
|
||||
|
||||
@@ -71,7 +85,11 @@ fn app(state: AppState) -> Router {
|
||||
let middleware = ServiceBuilder::new()
|
||||
.sensitive_request_headers(sensitive_headers.clone())
|
||||
.sensitive_response_headers(sensitive_headers)
|
||||
.trace_for_http()
|
||||
.layer(
|
||||
TraceLayer::new_for_http()
|
||||
.make_span_with(DefaultMakeSpan::new().level(tracing::Level::INFO))
|
||||
.on_response(DefaultOnResponse::new().level(tracing::Level::INFO)),
|
||||
)
|
||||
.layer(TimeoutLayer::new(Duration::from_secs(10)))
|
||||
.compression();
|
||||
build_router().layer(middleware).with_state(state)
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ pub fn render_image(svg: &str, format: ImageFormat) -> Result<Vec<u8>> {
|
||||
})
|
||||
.clone();
|
||||
let opt = Options { fontdb, ..Default::default() };
|
||||
let tree = Tree::from_str(&svg, &opt).context("Failed to parse SVG")?;
|
||||
let tree = Tree::from_str(svg, &opt).context("Failed to parse SVG")?;
|
||||
let rect = tree.size().to_int_size();
|
||||
let w = rect.width().clamp(1, 2048);
|
||||
let h = rect.height().clamp(1, 2048);
|
||||
|
||||
+5
-2
@@ -3,14 +3,14 @@ use std::path::{Path, PathBuf};
|
||||
use url::Url;
|
||||
|
||||
pub trait UrlExt {
|
||||
fn set_query(&self, key: &str, value: Option<&str>) -> Url;
|
||||
fn query_param(&self, key: &str, value: Option<&str>) -> Url;
|
||||
fn with_path(&self, path: &str) -> Url;
|
||||
fn path_and_query(&self) -> &str;
|
||||
}
|
||||
|
||||
impl UrlExt for Url {
|
||||
#[inline]
|
||||
fn set_query(&self, key: &str, value: Option<&str>) -> Url {
|
||||
fn query_param(&self, key: &str, value: Option<&str>) -> Url {
|
||||
let mut out = self.clone();
|
||||
let mut pairs = out.query_pairs_mut();
|
||||
pairs.clear();
|
||||
@@ -37,6 +37,9 @@ impl UrlExt for Url {
|
||||
}
|
||||
}
|
||||
drop(pairs);
|
||||
if out.query() == Some("") {
|
||||
out.set_query(None);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="darkreader-lock">
|
||||
<meta name="description" content="Decompilation progress reports">
|
||||
<link rel="stylesheet" href="/css/main.min.css?5">
|
||||
<link rel="stylesheet" href="/css/main.min.css?1">
|
||||
<title>Projects • decomp.dev</title>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav data-hx-boost="true">
|
||||
<nav>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://decomp.dev">
|
||||
@@ -22,7 +22,7 @@
|
||||
<a href="/">Projects</a>
|
||||
</li>
|
||||
<li>
|
||||
<details class="dropdown" data-hx-boost="true">
|
||||
<details class="dropdown">
|
||||
<summary>{{ current_sort.name }}</summary>
|
||||
<ul>
|
||||
{% for sort in sort_options %}
|
||||
@@ -45,7 +45,7 @@
|
||||
<article class="project">
|
||||
<div class="project-header">
|
||||
<h3 class="project-title">
|
||||
<a href="/{{ project.owner }}/{{ project.repo }}" data-hx-boost="true">
|
||||
<a href="/{{ project.owner }}/{{ project.repo }}">
|
||||
{{ project.name }}
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
+47
-30
@@ -5,21 +5,18 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="darkreader-lock">
|
||||
<meta name="description" content="Decompilation progress report for {{ project_name }}{% if current_category.id != 'all' %} ({{ current_category.name }}){% endif %}">
|
||||
<title>{{ project_short_name }}{% if current_category.id != "all" %} ({{ current_category.name }}){% endif %} • Progress Report</title>
|
||||
<link rel="stylesheet" href="/css/main.min.css?5">
|
||||
<script src="https://unpkg.com/htmx.org@2.0.2"
|
||||
integrity="sha384-Y7hw+L/jvKeWIRRkqWYfPcvVxHzVzn5REgzbawhxAuQGwX1XWe70vji+VSeHOThJ"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="/js/graph.min.js?6"></script>
|
||||
<meta property="og:title" content="{{ project_short_name }}{% if current_category.id != 'all' %} ({{ current_category.name }}){% endif %} is {{ measures.matched_code_percent | round(2) }}% decompiled">
|
||||
<meta property="og:description" content="Decompilation progress report for {{ project_name }}{% if current_category.id != 'all' %} ({{ current_category.name }}){% endif %}">
|
||||
<meta name="description" content="Decompilation progress report for {{ project_name }}">
|
||||
<title>{{ project_short_name }} • Progress Report</title>
|
||||
<link rel="stylesheet" href="/css/main.min.css?1">
|
||||
<script src="/js/treemap.min.js?1"></script>
|
||||
<meta property="og:title" content="{{ project_short_name }} is {{ measures.matched_code_percent | round(2) }}% decompiled">
|
||||
<meta property="og:description" content="Decompilation progress report for {{ project_name }}">
|
||||
<meta property="og:image" content="{{ image_url | safe }}">
|
||||
<meta property="og:url" content="{{ canonical_url | safe }}">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav data-hx-boost="false">
|
||||
<nav>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://decomp.dev">
|
||||
@@ -51,10 +48,13 @@
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<h3>
|
||||
{{ project_short_name }}{% if current_category.id != "all" %} ({{ current_category.name }}){% endif %} is {{ measures.matched_code_percent | round(2) }}% decompiled
|
||||
</h3>
|
||||
<h3>{{ project_short_name }} is {{ measures.matched_code_percent | round(2) }}% decompiled</h3>
|
||||
{% if not current_unit %}
|
||||
<h4 class="muted">{{ measures.complete_code_percent | round(2) }}% fully linked</h4>
|
||||
{% endif %}
|
||||
{% if source_file_url %}
|
||||
<h4 class="muted"><a href="{{ source_file_url | safe }}" target="_blank">View source file</a></h4>
|
||||
{% endif %}
|
||||
<h6 class="report-header">Code</h6>
|
||||
<div class="progress-root code">
|
||||
<div class="progress-section" style="width: {{ measures.complete_code_percent }}%"></div>
|
||||
@@ -88,9 +88,38 @@
|
||||
style="width: {{ measures.matched_data_percent - measures.complete_data_percent }}%"
|
||||
data-tooltip="{{ measures.matched_data_percent | round(2) }}% perfect match"></div>
|
||||
</div>
|
||||
<h6 class="report-header">Commit</h6>
|
||||
<div>
|
||||
{% if commit_message %}
|
||||
<pre><a href="{{ commit_url | safe }}" target="_blank">{{ commit[:7] }}</a> | {{ commit_message }}</pre>
|
||||
{% endif %}
|
||||
<div role="group">
|
||||
{% if prev_commit_path %}
|
||||
<a role="button" href="{{ prev_commit_path | safe }}" class="outline secondary">Previous</a>
|
||||
{% else %}
|
||||
<button class="outline secondary" disabled>Previous</button>
|
||||
{% endif %}
|
||||
{% if next_commit_path %}
|
||||
<a role="button" href="{{ next_commit_path | safe }}" class="outline secondary">Next</a>
|
||||
{% else %}
|
||||
<button class="outline secondary" disabled>Next</button>
|
||||
{% endif %}
|
||||
{% if latest_commit_path %}
|
||||
<a role="button" href="{{ latest_commit_path | safe }}" class="primary">Latest</a>
|
||||
{% else %}
|
||||
<button class="primary" disabled>Latest</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if current_unit %}
|
||||
<h6 class="report-header">Functions</h6>
|
||||
<div role="group">
|
||||
<a role="button" href="{{ units_path | safe }}">Back to units</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<h6 class="report-header">Units</h6>
|
||||
{% if categories | length > 1 %}
|
||||
<details class="dropdown" data-hx-boost="false">
|
||||
<details class="dropdown">
|
||||
<summary>{{ current_category.name }}</summary>
|
||||
<ul>
|
||||
{% for category in categories %}
|
||||
@@ -99,26 +128,14 @@
|
||||
</ul>
|
||||
</details>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<script>
|
||||
document.write('<canvas id="graph" width="100%" data-hx-preserve="true"></canvas>');
|
||||
drawGraph("graph", {{ units | tojson }});
|
||||
document.write('<canvas id="treemap" width="100%"></canvas>');
|
||||
drawTreemap("treemap", {{ not current_unit }}, {{ units | tojson }});
|
||||
</script>
|
||||
<noscript>
|
||||
<img id="graph" src="{{ image_url | safe }}" alt="Progress graph">
|
||||
<img id="treemap" src="{{ image_url | safe }}" alt="Progress graph">
|
||||
</noscript>
|
||||
<h6 class="report-header">Links</h6>
|
||||
<div>
|
||||
<p>
|
||||
<a href="{{ project_url | safe }}" target="_blank">GitHub</a> (<a
|
||||
href="{{ project_url | safe }}/commit/{{ commit }}" target="_blank">{{ commit[:7] }}</a>)
|
||||
</p>
|
||||
{% if prev_commit %}
|
||||
<p><a href="{{ project_path | safe }}/{{ version }}/{{ prev_commit }}">Previous commit</a></p>
|
||||
{% endif %}
|
||||
{% if next_commit %}
|
||||
<p><a href="{{ project_path | safe }}/{{ version }}/{{ next_commit }}">Next commit</a></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</main>
|
||||
{% include 'fragments/footer.html' %}
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user