mirror of
https://github.com/encounter/decomp.dev.git
synced 2026-07-10 03:18:48 -07:00
API browser, overview API & more
This commit is contained in:
Generated
+99
-5
@@ -288,7 +288,7 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
"object",
|
"object 0.36.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"rustc-demangle",
|
"rustc-demangle",
|
||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
@@ -772,6 +772,7 @@ dependencies = [
|
|||||||
"decomp-dev-core",
|
"decomp-dev-core",
|
||||||
"image",
|
"image",
|
||||||
"mime",
|
"mime",
|
||||||
|
"num-format",
|
||||||
"objdiff-core",
|
"objdiff-core",
|
||||||
"palette",
|
"palette",
|
||||||
"resvg",
|
"resvg",
|
||||||
@@ -795,6 +796,7 @@ dependencies = [
|
|||||||
"decomp-dev-images",
|
"decomp-dev-images",
|
||||||
"image",
|
"image",
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
|
"lexicmp",
|
||||||
"libsystemd",
|
"libsystemd",
|
||||||
"maud",
|
"maud",
|
||||||
"mime",
|
"mime",
|
||||||
@@ -851,6 +853,12 @@ dependencies = [
|
|||||||
"syn 2.0.100",
|
"syn 2.0.100",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deunicode"
|
||||||
|
version = "1.6.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.10.7"
|
version = "0.10.7"
|
||||||
@@ -889,6 +897,15 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encoding_rs"
|
||||||
|
version = "0.8.35"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "english-to-cron"
|
name = "english-to-cron"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
@@ -982,12 +999,29 @@ dependencies = [
|
|||||||
"simd-adler32",
|
"simd-adler32",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "filetime"
|
||||||
|
version = "0.2.25"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"libredox",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fixedbitset"
|
name = "fixedbitset"
|
||||||
version = "0.5.7"
|
version = "0.5.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
|
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flagset"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = "git+https://github.com/enarx/flagset.git?rev=a1fe9369b3741e43fec45da1998e83b9d78966a2#a1fe9369b3741e43fec45da1998e83b9d78966a2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flate2"
|
name = "flate2"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
@@ -1856,6 +1890,15 @@ version = "0.5.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
|
checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lexicmp"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0e8f89da8fd95c4eb6274e914694bea90c7826523b26f2a2fd863d44b9d42c43"
|
||||||
|
dependencies = [
|
||||||
|
"deunicode",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.172"
|
version = "0.2.172"
|
||||||
@@ -1878,6 +1921,17 @@ version = "0.2.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
|
checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libredox"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.9.0",
|
||||||
|
"libc",
|
||||||
|
"redox_syscall",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libsqlite3-sys"
|
name = "libsqlite3-sys"
|
||||||
version = "0.30.1"
|
version = "0.30.1"
|
||||||
@@ -2210,6 +2264,16 @@ dependencies = [
|
|||||||
"syn 2.0.100",
|
"syn 2.0.100",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-format"
|
||||||
|
version = "0.4.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3"
|
||||||
|
dependencies = [
|
||||||
|
"arrayvec",
|
||||||
|
"itoa",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-integer"
|
name = "num-integer"
|
||||||
version = "0.1.46"
|
version = "0.1.46"
|
||||||
@@ -2253,18 +2317,26 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objdiff-core"
|
name = "objdiff-core"
|
||||||
version = "2.5.0"
|
version = "3.0.0-beta.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/encounter/objdiff?rev=0c48d711c7bd51f791b353d7d85ba948b277e2f2#0c48d711c7bd51f791b353d7d85ba948b277e2f2"
|
||||||
checksum = "b114e70a6ede280511c1690609afbe802f84f7aaf27c71486c0510df07e647b8"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"object",
|
"encoding_rs",
|
||||||
|
"filetime",
|
||||||
|
"flagset",
|
||||||
|
"itertools 0.14.0",
|
||||||
|
"log",
|
||||||
|
"memmap2",
|
||||||
|
"num-traits",
|
||||||
|
"object 0.36.7 (git+https://github.com/gimli-rs/object?rev=a74579249e21ab8fcd3a86be588de336f18297cb)",
|
||||||
"pbjson",
|
"pbjson",
|
||||||
"pbjson-build",
|
"pbjson-build",
|
||||||
"prost",
|
"prost",
|
||||||
"prost-build",
|
"prost-build",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"similar",
|
||||||
|
"typed-path",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2276,6 +2348,14 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "object"
|
||||||
|
version = "0.36.7"
|
||||||
|
source = "git+https://github.com/gimli-rs/object?rev=a74579249e21ab8fcd3a86be588de336f18297cb#a74579249e21ab8fcd3a86be588de336f18297cb"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "octocrab"
|
name = "octocrab"
|
||||||
version = "0.44.0"
|
version = "0.44.0"
|
||||||
@@ -3426,6 +3506,14 @@ dependencies = [
|
|||||||
"quote",
|
"quote",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "similar"
|
||||||
|
version = "2.7.0"
|
||||||
|
source = "git+https://github.com/encounter/similar.git?branch=no_std#3568aba9173fad9f66740344dbdc6bc4c1970aab"
|
||||||
|
dependencies = [
|
||||||
|
"hashbrown",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "simple_asn1"
|
name = "simple_asn1"
|
||||||
version = "0.6.3"
|
version = "0.6.3"
|
||||||
@@ -4351,6 +4439,12 @@ dependencies = [
|
|||||||
"core_maths",
|
"core_maths",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typed-path"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c462d18470a2857aa657d338af5fa67170bb48bcc80a296710ce3b0802a32566"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.18.0"
|
version = "1.18.0"
|
||||||
|
|||||||
+2
-2
@@ -21,8 +21,8 @@ hex = "0.4"
|
|||||||
image = "0.25"
|
image = "0.25"
|
||||||
maud = { version = "0.27", features = ["axum"] }
|
maud = { version = "0.27", features = ["axum"] }
|
||||||
mime = "0.3"
|
mime = "0.3"
|
||||||
objdiff-core = { version = "2.5", features = ["bindings"] }
|
objdiff-core = { git = "https://github.com/encounter/objdiff", rev = "0c48d711c7bd51f791b353d7d85ba948b277e2f2", features = ["bindings", "serde"] }
|
||||||
#objdiff-core = { path = "../objdiff/objdiff-core", features = ["bindings"] }
|
#objdiff-core = { path = "../objdiff/objdiff-core", features = ["bindings", "serde"] }
|
||||||
octocrab = { version = "0.44", features = ["stream"] }
|
octocrab = { version = "0.44", features = ["stream"] }
|
||||||
regex = "1.11"
|
regex = "1.11"
|
||||||
reqwest = { version = "0.12", default-features = false, features = ["json", "gzip", "zstd", "http2", "rustls-tls-native-roots"] }
|
reqwest = { version = "0.12", default-features = false, features = ["json", "gzip", "zstd", "http2", "rustls-tls-native-roots"] }
|
||||||
|
|||||||
Binary file not shown.
+8
-1
@@ -1,7 +1,14 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://biomejs.dev/schemas/1.8.0/schema.json",
|
"$schema": "https://biomejs.dev/schemas/1.8.0/schema.json",
|
||||||
"files": {
|
"files": {
|
||||||
"ignore": ["node_modules", "dist", "package.json", "package-lock.json"]
|
"ignore": [
|
||||||
|
".sqlx",
|
||||||
|
"crates/github/graphql/schema.graphql",
|
||||||
|
"dist",
|
||||||
|
"node_modules",
|
||||||
|
"package-lock.json",
|
||||||
|
"package.json"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"organizeImports": {
|
"organizeImports": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
|
|||||||
@@ -60,3 +60,16 @@ pub fn join_normalized(base: impl AsRef<Path>, path: impl AsRef<Path>) -> PathBu
|
|||||||
out.extend(path.as_ref().components().filter(|v| matches!(v, std::path::Component::Normal(_))));
|
out.extend(path.as_ref().components().filter(|v| matches!(v, std::path::Component::Normal(_))));
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Format a size in bytes to a human-readable string.
|
||||||
|
/// Uses SI (kilo = 1000) units, formatted to two decimal places.
|
||||||
|
pub fn size(value: u64) -> String {
|
||||||
|
let units = ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||||
|
let mut value = value as f64;
|
||||||
|
let mut unit = 0;
|
||||||
|
while value >= 1000.0 && unit < units.len() - 1 {
|
||||||
|
value /= 1000.0;
|
||||||
|
unit += 1;
|
||||||
|
}
|
||||||
|
format!("{:.2} {}", value, units[unit])
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ query ViewerQuery($after: String) {
|
|||||||
login
|
login
|
||||||
url
|
url
|
||||||
repositories(
|
repositories(
|
||||||
first: 100,
|
first: 100
|
||||||
after: $after,
|
after: $after
|
||||||
ownerAffiliations: [OWNER, COLLABORATOR, ORGANIZATION_MEMBER],
|
ownerAffiliations: [OWNER, COLLABORATOR, ORGANIZATION_MEMBER]
|
||||||
visibility: PUBLIC,
|
visibility: PUBLIC
|
||||||
) {
|
) {
|
||||||
nodes {
|
nodes {
|
||||||
...repositoryFields
|
...repositoryFields
|
||||||
|
|||||||
@@ -239,6 +239,14 @@ async fn handle_workflow_run_completed(
|
|||||||
if !project_info.project.enable_pr_comments {
|
if !project_info.project.enable_pr_comments {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
// Actions pull_request builds always merge with the latest commit on the base branch.
|
||||||
|
// We can't use the base commit from the workflow run or pull request APIs, those are
|
||||||
|
// both lies. For simplicity, we'll just always compare against the latest commit that
|
||||||
|
// we have stored.
|
||||||
|
let Some(base_commit) = project_info.commit else {
|
||||||
|
tracing::warn!("No base commit found for repository ID {}", repository_id);
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
// Fetch any associated pull requests
|
// Fetch any associated pull requests
|
||||||
if pull_requests.is_empty() {
|
if pull_requests.is_empty() {
|
||||||
let head = if let Some(head_owner) =
|
let head = if let Some(head_owner) =
|
||||||
@@ -262,6 +270,22 @@ async fn handle_workflow_run_completed(
|
|||||||
}
|
}
|
||||||
for pull_request in pull_requests {
|
for pull_request in pull_requests {
|
||||||
if pull_request.head.sha != workflow_run.head_sha {
|
if pull_request.head.sha != workflow_run.head_sha {
|
||||||
|
tracing::warn!(
|
||||||
|
"Pull request {} head SHA {} does not match workflow run head SHA {}",
|
||||||
|
pull_request.id,
|
||||||
|
pull_request.head.sha,
|
||||||
|
workflow_run.head_sha
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if repository.default_branch.as_ref().is_none_or(|b| pull_request.base.ref_field != *b)
|
||||||
|
{
|
||||||
|
tracing::warn!(
|
||||||
|
"Pull request {} base branch {} does not match default branch {}",
|
||||||
|
pull_request.id,
|
||||||
|
pull_request.base.ref_field,
|
||||||
|
repository.default_branch.as_deref().unwrap_or("[unknown]")
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
tracing::info!("Processing pull request {}", pull_request.id);
|
tracing::info!("Processing pull request {}", pull_request.id);
|
||||||
@@ -274,7 +298,7 @@ async fn handle_workflow_run_completed(
|
|||||||
.get_report(
|
.get_report(
|
||||||
&project_info.project.owner,
|
&project_info.project.owner,
|
||||||
&project_info.project.repo,
|
&project_info.project.repo,
|
||||||
&pull_request.base.sha,
|
&base_commit.sha,
|
||||||
&artifact.version,
|
&artifact.version,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
|
|||||||
@@ -6,15 +6,17 @@ publish = false
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
resvg = "0.45"
|
|
||||||
tracing.workspace = true
|
|
||||||
objdiff-core.workspace = true
|
|
||||||
image.workspace = true
|
|
||||||
serde.workspace = true
|
|
||||||
badge-maker = "0.3"
|
|
||||||
axum.workspace = true
|
axum.workspace = true
|
||||||
|
badge-maker = "0.3"
|
||||||
decomp-dev-core = { path = "../core" }
|
decomp-dev-core = { path = "../core" }
|
||||||
tokio.workspace = true
|
image.workspace = true
|
||||||
mime.workspace = true
|
mime.workspace = true
|
||||||
streemap = "0.1"
|
num-format = "0.4"
|
||||||
|
objdiff-core.workspace = true
|
||||||
palette = "0.7"
|
palette = "0.7"
|
||||||
|
resvg = "0.45"
|
||||||
|
serde.workspace = true
|
||||||
|
streemap = "0.1"
|
||||||
|
tokio.workspace = true
|
||||||
|
tracing.workspace = true
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use anyhow::{Result, anyhow};
|
use anyhow::{Result, anyhow};
|
||||||
|
use decomp_dev_core::util::size;
|
||||||
use image::ImageFormat;
|
use image::ImageFormat;
|
||||||
use objdiff_core::bindings::report::Measures;
|
use objdiff_core::bindings::report::Measures;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -29,6 +30,26 @@ pub struct ShieldResponse {
|
|||||||
label_color: Option<String>,
|
label_color: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn format_percent(value: f32) -> String { format!("{:.2}%", value) }
|
||||||
|
|
||||||
|
fn format_bytes(a: u64, b: u64) -> String {
|
||||||
|
let mut a_buf = num_format::Buffer::default();
|
||||||
|
let mut b_buf = num_format::Buffer::default();
|
||||||
|
a_buf.write_formatted(&a, &num_format::Locale::en);
|
||||||
|
b_buf.write_formatted(&b, &num_format::Locale::en);
|
||||||
|
format!("{} B / {} B", a_buf, b_buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_num(a: u32, b: u32) -> String {
|
||||||
|
let mut a_buf = num_format::Buffer::default();
|
||||||
|
let mut b_buf = num_format::Buffer::default();
|
||||||
|
a_buf.write_formatted(&a, &num_format::Locale::en);
|
||||||
|
b_buf.write_formatted(&b, &num_format::Locale::en);
|
||||||
|
format!("{} / {}", a_buf, b_buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_size(a: u64, b: u64) -> String { format!("{} / {}", size(a), size(b)) }
|
||||||
|
|
||||||
pub fn render(
|
pub fn render(
|
||||||
measures: &Measures,
|
measures: &Measures,
|
||||||
default_label: &str,
|
default_label: &str,
|
||||||
@@ -37,16 +58,44 @@ pub fn render(
|
|||||||
let label = params.label.clone().unwrap_or_else(|| default_label.to_string());
|
let label = params.label.clone().unwrap_or_else(|| default_label.to_string());
|
||||||
let message = if let Some(measure) = ¶ms.measure {
|
let message = if let Some(measure) = ¶ms.measure {
|
||||||
match measure.as_str() {
|
match measure.as_str() {
|
||||||
"code" => format!("{:.2}%", measures.matched_code_percent),
|
"fuzzy_match_percent" | "fuzzy_match" => format_percent(measures.fuzzy_match_percent),
|
||||||
"data" => format!("{:.2}%", measures.matched_data_percent),
|
"matched_code_percent" | "matched_code" | "code" => {
|
||||||
"functions" => format!("{}/{}", measures.matched_functions, measures.total_functions),
|
format_percent(measures.matched_code_percent)
|
||||||
"complete_code" => format!("{:.2}%", measures.complete_code_percent),
|
}
|
||||||
"complete_data" => format!("{:.2}%", measures.complete_data_percent),
|
"matched_code_bytes" => format_bytes(measures.matched_code, measures.total_code),
|
||||||
"complete_units" => format!("{}/{}", measures.complete_units, measures.total_units),
|
"matched_code_size" => format_size(measures.matched_code, measures.total_code),
|
||||||
|
"matched_data_percent" | "matched_data" | "data" => {
|
||||||
|
format_percent(measures.matched_data_percent)
|
||||||
|
}
|
||||||
|
"matched_data_bytes" => format_bytes(measures.matched_data, measures.total_data),
|
||||||
|
"matched_data_size" => format_size(measures.matched_data, measures.total_data),
|
||||||
|
"matched_functions" | "functions" => {
|
||||||
|
format_num(measures.matched_functions, measures.total_functions)
|
||||||
|
}
|
||||||
|
"matched_functions_percent" => format_percent(measures.matched_functions_percent),
|
||||||
|
"complete_code_percent" | "complete_code" => {
|
||||||
|
format_percent(measures.complete_code_percent)
|
||||||
|
}
|
||||||
|
"complete_code_bytes" => format_bytes(measures.complete_code, measures.total_code),
|
||||||
|
"complete_code_size" => format_size(measures.complete_code, measures.total_code),
|
||||||
|
"complete_data_percent" | "complete_data" => {
|
||||||
|
format_percent(measures.complete_data_percent)
|
||||||
|
}
|
||||||
|
"complete_data_bytes" => format_bytes(measures.complete_data, measures.total_data),
|
||||||
|
"complete_data_size" => format_size(measures.complete_data, measures.total_data),
|
||||||
|
"complete_units" => format_num(measures.complete_units, measures.total_units),
|
||||||
|
"complete_units_percent" => {
|
||||||
|
let percent = if measures.total_units == 0 {
|
||||||
|
100.0
|
||||||
|
} else {
|
||||||
|
measures.complete_units as f32 / measures.total_units as f32 * 100.0
|
||||||
|
};
|
||||||
|
format_percent(percent)
|
||||||
|
}
|
||||||
_ => return Err(anyhow!("Unknown measure")),
|
_ => return Err(anyhow!("Unknown measure")),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
format!("{:.2}%", measures.matched_code_percent)
|
format_percent(measures.matched_code_percent)
|
||||||
};
|
};
|
||||||
Ok(ShieldResponse {
|
Ok(ShieldResponse {
|
||||||
schema_version: 1,
|
schema_version: 1,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ decomp-dev-db = { path = "../db" }
|
|||||||
decomp-dev-github = { path = "../github" }
|
decomp-dev-github = { path = "../github" }
|
||||||
decomp-dev-images = { path = "../images" }
|
decomp-dev-images = { path = "../images" }
|
||||||
itertools = "0.14"
|
itertools = "0.14"
|
||||||
|
lexicmp = "0.2"
|
||||||
maud.workspace = true
|
maud.workspace = true
|
||||||
mime.workspace = true
|
mime.workspace = true
|
||||||
objdiff-core.workspace = true
|
objdiff-core.workspace = true
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
use axum::response::{IntoResponse, Response};
|
||||||
|
use decomp_dev_auth::CurrentUser;
|
||||||
|
use decomp_dev_core::AppError;
|
||||||
|
use maud::{DOCTYPE, html};
|
||||||
|
|
||||||
|
use crate::handlers::common::{Load, TemplateContext, nav_links};
|
||||||
|
|
||||||
|
pub async fn overview(
|
||||||
|
mut ctx: TemplateContext,
|
||||||
|
current_user: Option<CurrentUser>,
|
||||||
|
) -> Result<Response, AppError> {
|
||||||
|
let rendered = html! {
|
||||||
|
(DOCTYPE)
|
||||||
|
html lang="en" {
|
||||||
|
head {
|
||||||
|
meta charset="utf-8";
|
||||||
|
title { "API • decomp.dev" }
|
||||||
|
(ctx.header().await)
|
||||||
|
(ctx.chunks("main", Load::Deferred).await)
|
||||||
|
(ctx.chunks("api", Load::Deferred).await)
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
header {
|
||||||
|
nav {
|
||||||
|
ul {
|
||||||
|
li {
|
||||||
|
a href="/" { strong { "decomp.dev" } }
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
a href="/api" { "API" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(nav_links())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
h1 { "API" }
|
||||||
|
#root {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(ctx.footer(current_user.as_ref()))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok((ctx, rendered).into_response())
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ use axum::{
|
|||||||
response::{IntoResponseParts, ResponseParts},
|
response::{IntoResponseParts, ResponseParts},
|
||||||
};
|
};
|
||||||
use decomp_dev_auth::CurrentUser;
|
use decomp_dev_auth::CurrentUser;
|
||||||
use decomp_dev_core::AppError;
|
use decomp_dev_core::{AppError, util::size};
|
||||||
use maud::{Markup, PreEscaped, Render, html};
|
use maud::{Markup, PreEscaped, Render, html};
|
||||||
use objdiff_core::bindings::report::Measures;
|
use objdiff_core::bindings::report::Measures;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
@@ -120,12 +120,11 @@ impl TemplateContext {
|
|||||||
meta name="apple-mobile-web-app-title" content="decomp.dev";
|
meta name="apple-mobile-web-app-title" content="decomp.dev";
|
||||||
meta name="theme-color" content="#181c25" media="(prefers-color-scheme: dark)";
|
meta name="theme-color" content="#181c25" media="(prefers-color-scheme: dark)";
|
||||||
meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)";
|
meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)";
|
||||||
(self.chunks("entry", Load::Blocking).await)
|
link rel="icon" type="image/png" href="/favicon.png" sizes="96x96";
|
||||||
link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96";
|
|
||||||
link rel="icon" type="image/svg+xml" href="/favicon.svg";
|
link rel="icon" type="image/svg+xml" href="/favicon.svg";
|
||||||
link rel="shortcut icon" href="/favicon.ico";
|
|
||||||
link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180";
|
link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180";
|
||||||
link rel="manifest" href="/site.webmanifest";
|
link rel="manifest" href="/site.webmanifest";
|
||||||
|
(self.chunks("entry", Load::Blocking).await)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +162,7 @@ impl TemplateContext {
|
|||||||
Style,
|
Style,
|
||||||
Font,
|
Font,
|
||||||
}
|
}
|
||||||
let mut push = |kind: ResourceKind, path: &str, load: Load| {
|
let mut push = async |kind: ResourceKind, path: &str, load: Load| {
|
||||||
match load {
|
match load {
|
||||||
Load::Preload => {
|
Load::Preload => {
|
||||||
if self.added_resources.contains_key(path) {
|
if self.added_resources.contains_key(path) {
|
||||||
@@ -202,14 +201,34 @@ impl TemplateContext {
|
|||||||
ResourceKind::Script | ResourceKind::Style => self.nonce.as_deref(),
|
ResourceKind::Script | ResourceKind::Style => self.nonce.as_deref(),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
let can_inline = path.starts_with('/')
|
||||||
|
&& matches!(kind, ResourceKind::Script | ResourceKind::Style)
|
||||||
|
&& tokio::fs::metadata(format!("dist{path}"))
|
||||||
|
.await
|
||||||
|
.is_ok_and(|m| m.is_file() && m.len() <= 2048);
|
||||||
let rendered = match load {
|
let rendered = match load {
|
||||||
Load::Preload => html! {
|
Load::Preload if !can_inline => html! {
|
||||||
link rel="preload" href=(path) as=(match kind {
|
link rel="preload" href=(path) as=(match kind {
|
||||||
ResourceKind::Script => "script",
|
ResourceKind::Script => "script",
|
||||||
ResourceKind::Style => "style",
|
ResourceKind::Style => "style",
|
||||||
ResourceKind::Font => "font",
|
ResourceKind::Font => "font",
|
||||||
}) crossorigin[crossorigin] nonce=[nonce];
|
}) crossorigin[crossorigin] nonce=[nonce];
|
||||||
},
|
},
|
||||||
|
Load::Preload => Markup::default(),
|
||||||
|
Load::Blocking | Load::Deferred if can_inline => {
|
||||||
|
let content =
|
||||||
|
tokio::fs::read_to_string(format!("dist{path}")).await.unwrap_or_default();
|
||||||
|
let content = escape_script(&content);
|
||||||
|
match kind {
|
||||||
|
ResourceKind::Script => html! {
|
||||||
|
script type=[(load == Load::Deferred).then_some("module")] nonce=[nonce] { (content) }
|
||||||
|
},
|
||||||
|
ResourceKind::Style => html! {
|
||||||
|
style nonce=[nonce] { (content) }
|
||||||
|
},
|
||||||
|
ResourceKind::Font => Markup::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
Load::Blocking | Load::Deferred => match kind {
|
Load::Blocking | Load::Deferred => match kind {
|
||||||
ResourceKind::Script => html! {
|
ResourceKind::Script => html! {
|
||||||
script src=(path) defer[load == Load::Deferred] crossorigin[crossorigin] nonce=[nonce] {};
|
script src=(path) defer[load == Load::Deferred] crossorigin[crossorigin] nonce=[nonce] {};
|
||||||
@@ -223,14 +242,14 @@ impl TemplateContext {
|
|||||||
out.push_str(&rendered.0);
|
out.push_str(&rendered.0);
|
||||||
};
|
};
|
||||||
for path in &entry.initial.js {
|
for path in &entry.initial.js {
|
||||||
push(ResourceKind::Script, path, load);
|
push(ResourceKind::Script, path, load).await;
|
||||||
}
|
}
|
||||||
for path in &entry.initial.css {
|
for path in &entry.initial.css {
|
||||||
push(ResourceKind::Style, path, load);
|
push(ResourceKind::Style, path, load).await;
|
||||||
}
|
}
|
||||||
for path in &entry.assets {
|
for path in &entry.assets {
|
||||||
if path.ends_with(".woff2") {
|
if path.ends_with(".woff2") {
|
||||||
push(ResourceKind::Font, path, Load::Preload);
|
push(ResourceKind::Font, path, Load::Preload).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PreEscaped(out)
|
PreEscaped(out)
|
||||||
@@ -268,6 +287,10 @@ impl TemplateContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Analytics
|
||||||
|
script src="https://umami.decomp.dev/script.js" defer crossorigin
|
||||||
|
nonce=[self.nonce.as_deref()]
|
||||||
|
data-website-id="80d6109f-52cc-42a9-98c9-9820dc8c4435" {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,7 +401,7 @@ impl Render for ProgressSections {
|
|||||||
};
|
};
|
||||||
out.0.push_str(&rendered.0);
|
out.0.push_str(&rendered.0);
|
||||||
}
|
}
|
||||||
if self.rendered.0.is_empty() {
|
if self.kind.is_empty() {
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
html! {
|
html! {
|
||||||
@@ -404,19 +427,6 @@ pub fn nav_links() -> Markup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Format a size in bytes to a human-readable string.
|
|
||||||
/// Uses SI (kilo = 1000) units, formatted to two decimal places.
|
|
||||||
pub fn size(value: u64) -> String {
|
|
||||||
let units = ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
|
||||||
let mut value = value as f64;
|
|
||||||
let mut unit = 0;
|
|
||||||
while value >= 1000.0 && unit < units.len() - 1 {
|
|
||||||
value /= 1000.0;
|
|
||||||
unit += 1;
|
|
||||||
}
|
|
||||||
format!("{:.2} {}", value, units[unit])
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_robots() -> Result<String, AppError> {
|
pub async fn get_robots() -> Result<String, AppError> {
|
||||||
static ROBOTS_CACHE: LazyLock<std::sync::RwLock<Option<String>>> =
|
static ROBOTS_CACHE: LazyLock<std::sync::RwLock<Option<String>>> =
|
||||||
LazyLock::new(|| std::sync::RwLock::new(None));
|
LazyLock::new(|| std::sync::RwLock::new(None));
|
||||||
@@ -445,7 +455,7 @@ pub async fn get_robots() -> Result<String, AppError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn escape_script(text: &str) -> PreEscaped<Cow<str>> {
|
pub fn escape_script(text: &str) -> PreEscaped<Cow<str>> {
|
||||||
static REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new("<(?:!--|/?script)").unwrap());
|
static REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new("<(?i:!--|/?script)").unwrap());
|
||||||
PreEscaped(REGEX.replace_all(text, |caps: ®ex::Captures| format!("\\x3C{}", &caps[0][1..])))
|
PreEscaped(REGEX.replace_all(text, |caps: ®ex::Captures| format!("\\x3C{}", &caps[0][1..])))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,8 +465,8 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_escape_script() {
|
fn test_escape_script() {
|
||||||
let input = r#"<!--<script>alert('test');</script>-->"#;
|
let input = r#"<!--<SCRIPT>alert('test');</ScRiPt>-->"#;
|
||||||
let expected = "\\x3C!--\\x3Cscript>alert('test');\\x3C/script>-->";
|
let expected = "\\x3C!--\\x3CSCRIPT>alert('test');\\x3C/ScRiPt>-->";
|
||||||
assert_eq!(escape_script(input).0.as_ref(), expected);
|
assert_eq!(escape_script(input).0.as_ref(), expected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ pub async fn csp_middleware(mut req: Request, next: Next) -> Response {
|
|||||||
header.push(' ');
|
header.push(' ');
|
||||||
header.push_str(domain);
|
header.push_str(domain);
|
||||||
}
|
}
|
||||||
header.push_str(";connect-src 'self'");
|
header.push_str(";connect-src 'self' https://umami.decomp.dev");
|
||||||
for domain in &extra_domains {
|
for domain in &extra_domains {
|
||||||
header.push(' ');
|
header.push(' ');
|
||||||
header.push_str(domain);
|
header.push_str(domain);
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ pub async fn manage(
|
|||||||
.await?
|
.await?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|p| current_user.can_manage_repo(p.project.id))
|
.filter(|p| current_user.can_manage_repo(p.project.id))
|
||||||
.sorted_by(|a, b| a.project.name().cmp(&b.project.name()))
|
.sorted_by(|a, b| lexicmp::natural_lexical_cmp(&a.project.name(), &b.project.name()))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let rendered = html! {
|
let rendered = html! {
|
||||||
@@ -49,7 +49,7 @@ pub async fn manage(
|
|||||||
nav {
|
nav {
|
||||||
ul {
|
ul {
|
||||||
li {
|
li {
|
||||||
a href="https://decomp.dev" { strong { "decomp.dev" } }
|
a href="/" { strong { "decomp.dev" } }
|
||||||
}
|
}
|
||||||
li {
|
li {
|
||||||
a href="/manage" { "Manage" }
|
a href="/manage" { "Manage" }
|
||||||
@@ -116,6 +116,7 @@ async fn render_new(
|
|||||||
.map(|r| {
|
.map(|r| {
|
||||||
(r.id, format!("{}/{}", r.owner, r.name), projects.iter().any(|p| p.project.id == r.id))
|
(r.id, format!("{}/{}", r.owner, r.name), projects.iter().any(|p| p.project.id == r.id))
|
||||||
})
|
})
|
||||||
|
.sorted_by(|a, b| lexicmp::lexical_cmp(&a.1, &b.1))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let current_name = prefill.as_ref().and_then(|p| p.name.as_deref()).unwrap_or("");
|
let current_name = prefill.as_ref().and_then(|p| p.name.as_deref()).unwrap_or("");
|
||||||
@@ -149,7 +150,7 @@ async fn render_new(
|
|||||||
nav {
|
nav {
|
||||||
ul {
|
ul {
|
||||||
li {
|
li {
|
||||||
a href="https://decomp.dev" { strong { "decomp.dev" } }
|
a href="/" { strong { "decomp.dev" } }
|
||||||
}
|
}
|
||||||
li {
|
li {
|
||||||
a href="/manage" { "Manage" }
|
a href="/manage" { "Manage" }
|
||||||
@@ -366,7 +367,7 @@ async fn render_manage_project(
|
|||||||
nav {
|
nav {
|
||||||
ul {
|
ul {
|
||||||
li {
|
li {
|
||||||
a href="https://decomp.dev" { strong { "decomp.dev" } }
|
a href="/" { strong { "decomp.dev" } }
|
||||||
}
|
}
|
||||||
li {
|
li {
|
||||||
a href="/manage" { "Manage" }
|
a href="/manage" { "Manage" }
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ use std::str::FromStr;
|
|||||||
use axum::{
|
use axum::{
|
||||||
Router,
|
Router,
|
||||||
extract::Request,
|
extract::Request,
|
||||||
http::{HeaderMap, HeaderValue, header},
|
http::{HeaderMap, HeaderValue, header, header::Entry},
|
||||||
|
response::Response,
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
};
|
};
|
||||||
use decomp_dev_images::image_mime_from_ext;
|
use decomp_dev_images::image_mime_from_ext;
|
||||||
@@ -13,6 +14,7 @@ use tower_http::services::ServeDir;
|
|||||||
|
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
|
|
||||||
|
mod api;
|
||||||
mod auth;
|
mod auth;
|
||||||
mod common;
|
mod common;
|
||||||
pub mod csp;
|
pub mod csp;
|
||||||
@@ -28,9 +30,10 @@ pub fn build_router() -> Router<AppState> {
|
|||||||
<ServeDir as ServiceExt<Request>>::map_response(
|
<ServeDir as ServiceExt<Request>>::map_response(
|
||||||
ServeDir::new("dist/static"),
|
ServeDir::new("dist/static"),
|
||||||
|mut response| {
|
|mut response| {
|
||||||
|
add_charset(&mut response);
|
||||||
// Cache static (hashed) files for a year, mark immutable
|
// Cache static (hashed) files for a year, mark immutable
|
||||||
response.headers_mut().insert(
|
add_cache_control(
|
||||||
header::CACHE_CONTROL,
|
&mut response,
|
||||||
HeaderValue::from_static("public, max-age=31536000, immutable"),
|
HeaderValue::from_static("public, max-age=31536000, immutable"),
|
||||||
);
|
);
|
||||||
response
|
response
|
||||||
@@ -40,15 +43,17 @@ pub fn build_router() -> Router<AppState> {
|
|||||||
.fallback_service(<ServeDir as ServiceExt<Request>>::map_response(
|
.fallback_service(<ServeDir as ServiceExt<Request>>::map_response(
|
||||||
ServeDir::new("dist"),
|
ServeDir::new("dist"),
|
||||||
|mut response| {
|
|mut response| {
|
||||||
|
add_charset(&mut response);
|
||||||
// Cache non-hashed public files for a day, mark must-revalidate
|
// Cache non-hashed public files for a day, mark must-revalidate
|
||||||
response.headers_mut().insert(
|
add_cache_control(
|
||||||
header::CACHE_CONTROL,
|
&mut response,
|
||||||
HeaderValue::from_static("public, max-age=86400, must-revalidate"),
|
HeaderValue::from_static("public, max-age=86400, must-revalidate"),
|
||||||
);
|
);
|
||||||
response
|
response
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
.route("/robots.txt", get(common::get_robots))
|
.route("/robots.txt", get(common::get_robots))
|
||||||
|
.route("/api", get(api::overview))
|
||||||
.route("/api/github/webhook", post(decomp_dev_github::webhook::webhook))
|
.route("/api/github/webhook", post(decomp_dev_github::webhook::webhook))
|
||||||
.route("/api/github/oauth", get(decomp_dev_auth::oauth))
|
.route("/api/github/oauth", get(decomp_dev_auth::oauth))
|
||||||
.route("/login", get(auth::login))
|
.route("/login", get(auth::login))
|
||||||
@@ -61,11 +66,32 @@ pub fn build_router() -> Router<AppState> {
|
|||||||
.route("/manage/{owner}/{repo}/refresh", post(manage::manage_project_refresh))
|
.route("/manage/{owner}/{repo}/refresh", post(manage::manage_project_refresh))
|
||||||
.route("/og.png", get(decomp_dev_images::get_og))
|
.route("/og.png", get(decomp_dev_images::get_og))
|
||||||
.route("/", get(project::get_projects))
|
.route("/", get(project::get_projects))
|
||||||
|
.route("/projects", get(project::get_projects))
|
||||||
|
.route("/projects.json", get(project::get_projects))
|
||||||
|
.route("/projects/{id}", get(report::get_report))
|
||||||
.route("/{owner}/{repo}", get(report::get_report))
|
.route("/{owner}/{repo}", get(report::get_report))
|
||||||
.route("/{owner}/{repo}/{version}", get(report::get_report))
|
.route("/{owner}/{repo}/{version}", get(report::get_report))
|
||||||
.route("/{owner}/{repo}/{version}/{commit}", get(report::get_report))
|
.route("/{owner}/{repo}/{version}/{commit}", get(report::get_report))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Adds a charset to the Content-Type header if it is missing and the type is text/*.
|
||||||
|
fn add_charset<ResBody>(response: &mut Response<ResBody>) {
|
||||||
|
let Entry::Occupied(mut existing) = response.headers_mut().entry(header::CONTENT_TYPE) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(mime) = existing.get().to_str().ok().and_then(|v| Mime::from_str(v).ok()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if mime.type_() == mime::TEXT && mime.get_param("charset").is_none() {
|
||||||
|
existing.insert(format!("{};charset=utf-8", mime).parse().unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a Cache-Control header if it is missing.
|
||||||
|
fn add_cache_control<ResBody>(response: &mut Response<ResBody>, value: HeaderValue) {
|
||||||
|
response.headers_mut().entry(header::CACHE_CONTROL).or_insert(value);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn parse_accept(headers: &HeaderMap, ext: Option<&str>) -> Vec<Mime> {
|
pub fn parse_accept(headers: &HeaderMap, ext: Option<&str>) -> Vec<Mime> {
|
||||||
// Explicit extension takes precedence
|
// Explicit extension takes precedence
|
||||||
if let Some(ext) = ext {
|
if let Some(ext) = ext {
|
||||||
|
|||||||
@@ -2,31 +2,37 @@ use std::{str::FromStr, sync::Arc};
|
|||||||
|
|
||||||
use anyhow::{Context, anyhow};
|
use anyhow::{Context, anyhow};
|
||||||
use axum::{
|
use axum::{
|
||||||
|
Json,
|
||||||
extract::{Query, State},
|
extract::{Query, State},
|
||||||
http::StatusCode,
|
http::{HeaderMap, StatusCode, Uri},
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
};
|
};
|
||||||
use decomp_dev_auth::CurrentUser;
|
use decomp_dev_auth::CurrentUser;
|
||||||
use decomp_dev_core::{
|
use decomp_dev_core::{
|
||||||
AppError, FullUri,
|
AppError, FullUri,
|
||||||
models::{Commit, Platform, Project},
|
models::{CachedReportFile, Commit, Platform, ProjectInfo, ReportInner},
|
||||||
util::UrlExt,
|
util::{UrlExt, size},
|
||||||
};
|
};
|
||||||
use maud::{DOCTYPE, Markup, html};
|
use maud::{DOCTYPE, Markup, html};
|
||||||
use objdiff_core::bindings::report::Measures;
|
use objdiff_core::bindings::report::Measures;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use time::{UtcDateTime, format_description::well_known::Rfc3339};
|
||||||
use tokio::{sync::Semaphore, task::JoinSet};
|
use tokio::{sync::Semaphore, task::JoinSet};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AppState,
|
AppState,
|
||||||
handlers::common::{Load, ProgressSections, TemplateContext, date, nav_links, size, timeago},
|
handlers::{
|
||||||
|
common::{Load, ProgressSections, TemplateContext, date, nav_links, timeago},
|
||||||
|
parse_accept,
|
||||||
|
report::TemplateMeasures,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ProjectInfoContext {
|
struct ProjectInfoContext {
|
||||||
project: Project,
|
info: ProjectInfo,
|
||||||
commit: Commit,
|
|
||||||
measures: Measures,
|
measures: Measures,
|
||||||
|
report: Option<CachedReportFile>,
|
||||||
code_progress: ProgressSections,
|
code_progress: ProgressSections,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,25 +62,114 @@ pub struct ProgressSection {
|
|||||||
pub tooltip: String,
|
pub tooltip: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
pub struct ProjectsResponse {
|
||||||
|
pub projects: Vec<ProjectResponse>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
pub struct ProjectResponse {
|
||||||
|
pub id: u64,
|
||||||
|
pub owner: String,
|
||||||
|
pub repo: String,
|
||||||
|
pub repo_url: String,
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub short_name: Option<String>,
|
||||||
|
pub platform: Option<String>,
|
||||||
|
pub default_version: Option<String>,
|
||||||
|
pub default_category: Option<String>,
|
||||||
|
pub commit: Option<CommitResponse>,
|
||||||
|
pub measures: TemplateMeasures,
|
||||||
|
pub report_versions: Vec<String>,
|
||||||
|
pub report_categories: Vec<CategoryResponse>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProjectResponse {
|
||||||
|
pub fn new<U>(info: &ProjectInfo, measures: &Measures, report: &ReportInner<U>) -> Self {
|
||||||
|
ProjectResponse {
|
||||||
|
id: info.project.id,
|
||||||
|
owner: info.project.owner.clone(),
|
||||||
|
repo: info.project.repo.clone(),
|
||||||
|
repo_url: info.project.repo_url(),
|
||||||
|
name: info.project.name.clone(),
|
||||||
|
short_name: info.project.short_name.clone(),
|
||||||
|
platform: info.project.platform.clone(),
|
||||||
|
default_version: info.default_version().map(|v| v.to_string()),
|
||||||
|
default_category: info.project.default_category.clone(),
|
||||||
|
commit: info.commit.clone().map(CommitResponse::from),
|
||||||
|
measures: measures.into(),
|
||||||
|
report_versions: info.report_versions.clone(),
|
||||||
|
report_categories: report
|
||||||
|
.categories
|
||||||
|
.iter()
|
||||||
|
.map(|c| CategoryResponse {
|
||||||
|
id: c.id.clone(),
|
||||||
|
name: c.name.clone(),
|
||||||
|
measures: c.measures.as_ref().map(TemplateMeasures::from).unwrap_or_default(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
pub struct CategoryResponse {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub measures: TemplateMeasures,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
pub struct CommitResponse {
|
||||||
|
pub sha: String,
|
||||||
|
pub message: Option<String>,
|
||||||
|
pub timestamp: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Commit> for CommitResponse {
|
||||||
|
fn from(value: Commit) -> Self {
|
||||||
|
Self {
|
||||||
|
sha: value.sha,
|
||||||
|
message: value.message,
|
||||||
|
timestamp: value.timestamp.format(&Rfc3339).unwrap_or_else(|_| "[invalid]".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_extension(uri: &Uri) -> Option<String> {
|
||||||
|
let path = uri.path();
|
||||||
|
if let Some(pos) = path.rfind('.') {
|
||||||
|
let ext = &path[pos + 1..];
|
||||||
|
if !ext.is_empty() {
|
||||||
|
return Some(ext.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_projects(
|
pub async fn get_projects(
|
||||||
mut ctx: TemplateContext,
|
ctx: TemplateContext,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Query(query): Query<ProjectsQuery>,
|
Query(query): Query<ProjectsQuery>,
|
||||||
FullUri(uri): FullUri,
|
FullUri(uri): FullUri,
|
||||||
current_user: Option<CurrentUser>,
|
current_user: Option<CurrentUser>,
|
||||||
|
headers: HeaderMap,
|
||||||
) -> Result<Response, AppError> {
|
) -> Result<Response, AppError> {
|
||||||
|
let ext = extract_extension(&uri);
|
||||||
|
let acceptable = parse_accept(&headers, ext.as_deref());
|
||||||
|
if acceptable.is_empty() {
|
||||||
|
return Err(AppError::Status(StatusCode::NOT_ACCEPTABLE));
|
||||||
|
}
|
||||||
|
|
||||||
let projects = state.db.get_projects().await?;
|
let projects = state.db.get_projects().await?;
|
||||||
let mut out = projects
|
let mut out = projects
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|p| {
|
.map(|p| ProjectInfoContext {
|
||||||
let commit = p.commit.as_ref()?;
|
info: p.clone(),
|
||||||
Some(ProjectInfoContext {
|
|
||||||
project: p.project.clone(),
|
|
||||||
commit: commit.clone(),
|
|
||||||
measures: Default::default(),
|
measures: Default::default(),
|
||||||
|
report: None,
|
||||||
code_progress: Default::default(),
|
code_progress: Default::default(),
|
||||||
})
|
})
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
// Fetch latest report for each
|
// Fetch latest report for each
|
||||||
@@ -105,8 +200,9 @@ pub async fn get_projects(
|
|||||||
while let Some(result) = join_set.join_next().await {
|
while let Some(result) = join_set.join_next().await {
|
||||||
match result {
|
match result {
|
||||||
Ok((info, Ok(Some(file)))) => {
|
Ok((info, Ok(Some(file)))) => {
|
||||||
if let Some(c) = out.iter_mut().find(|i| i.project.id == info.project.id) {
|
if let Some(c) = out.iter_mut().find(|i| i.info.project.id == info.project.id) {
|
||||||
c.measures = *file.report.measures(info.project.default_category.as_deref());
|
c.measures = *file.report.measures(info.project.default_category.as_deref());
|
||||||
|
c.report = Some(file.clone());
|
||||||
c.code_progress = ctx.code_progress_sections(&c.measures);
|
c.code_progress = ctx.code_progress_sections(&c.measures);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,11 +218,6 @@ pub async fn get_projects(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut combined_styles = ProgressSections { nonce: ctx.nonce.clone(), ..Default::default() };
|
|
||||||
for info in &mut out {
|
|
||||||
combined_styles.width_classes.append(&mut info.code_progress.width_classes);
|
|
||||||
}
|
|
||||||
|
|
||||||
let current_sort_key = query.sort.as_deref().unwrap_or("updated");
|
let current_sort_key = query.sort.as_deref().unwrap_or("updated");
|
||||||
let current_sort = SORT_OPTIONS
|
let current_sort = SORT_OPTIONS
|
||||||
.iter()
|
.iter()
|
||||||
@@ -134,8 +225,16 @@ pub async fn get_projects(
|
|||||||
.copied()
|
.copied()
|
||||||
.ok_or(AppError::Status(StatusCode::BAD_REQUEST))?;
|
.ok_or(AppError::Status(StatusCode::BAD_REQUEST))?;
|
||||||
match current_sort.key {
|
match current_sort.key {
|
||||||
"name" => out.sort_by(|a, b| a.project.name().cmp(&b.project.name())),
|
"name" => out.sort_by(|a, b| {
|
||||||
"updated" => out.sort_by(|a, b| b.commit.timestamp.cmp(&a.commit.timestamp)),
|
lexicmp::natural_lexical_cmp(&a.info.project.name(), &b.info.project.name())
|
||||||
|
}),
|
||||||
|
"updated" => out.sort_by(|a, b| {
|
||||||
|
let a_ts =
|
||||||
|
a.report.as_ref().map(|r| r.commit.timestamp).unwrap_or(UtcDateTime::UNIX_EPOCH);
|
||||||
|
let b_ts =
|
||||||
|
b.report.as_ref().map(|r| r.commit.timestamp).unwrap_or(UtcDateTime::UNIX_EPOCH);
|
||||||
|
b_ts.cmp(&a_ts)
|
||||||
|
}),
|
||||||
"matched_code_percent" => out.sort_by(|a, b| {
|
"matched_code_percent" => out.sort_by(|a, b| {
|
||||||
b.measures
|
b.measures
|
||||||
.matched_code_percent
|
.matched_code_percent
|
||||||
@@ -157,8 +256,42 @@ pub async fn get_projects(
|
|||||||
_ => return Err(AppError::Status(StatusCode::BAD_REQUEST)),
|
_ => return Err(AppError::Status(StatusCode::BAD_REQUEST)),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for mime in acceptable {
|
||||||
|
if (mime.type_() == mime::STAR && mime.subtype() == mime::STAR)
|
||||||
|
|| (mime.type_() == mime::TEXT && mime.subtype() == mime::HTML)
|
||||||
|
{
|
||||||
|
return render_project(ctx, out, uri, current_sort, current_user.as_ref()).await;
|
||||||
|
} else if mime.type_() == mime::APPLICATION && mime.subtype() == mime::JSON {
|
||||||
|
let projects = out
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|info| {
|
||||||
|
Some(ProjectResponse::new(
|
||||||
|
&info.info,
|
||||||
|
&info.measures,
|
||||||
|
&info.report.as_ref()?.report,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
return Ok(Json(ProjectsResponse { projects }).into_response());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(AppError::Status(StatusCode::NOT_ACCEPTABLE))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn render_project(
|
||||||
|
mut ctx: TemplateContext,
|
||||||
|
mut out: Vec<ProjectInfoContext>,
|
||||||
|
uri: Uri,
|
||||||
|
current_sort: SortOption,
|
||||||
|
current_user: Option<&CurrentUser>,
|
||||||
|
) -> Result<Response, AppError> {
|
||||||
|
let mut combined_styles = ProgressSections { nonce: ctx.nonce.clone(), ..Default::default() };
|
||||||
|
for info in &mut out {
|
||||||
|
combined_styles.width_classes.append(&mut info.code_progress.width_classes);
|
||||||
|
}
|
||||||
|
|
||||||
let request_url = Url::parse(&uri.to_string()).context("Failed to parse URI")?;
|
let request_url = Url::parse(&uri.to_string()).context("Failed to parse URI")?;
|
||||||
let canonical_url = request_url.with_path("");
|
let canonical_url = request_url.with_path("/projects");
|
||||||
|
|
||||||
let rendered = html! {
|
let rendered = html! {
|
||||||
(DOCTYPE)
|
(DOCTYPE)
|
||||||
@@ -168,6 +301,7 @@ pub async fn get_projects(
|
|||||||
title { "Projects • decomp.dev" }
|
title { "Projects • decomp.dev" }
|
||||||
(ctx.header().await)
|
(ctx.header().await)
|
||||||
(ctx.chunks("main", Load::Deferred).await)
|
(ctx.chunks("main", Load::Deferred).await)
|
||||||
|
link rel="canonical" href=(canonical_url);
|
||||||
meta name="description" content="Decompilation progress reports";
|
meta name="description" content="Decompilation progress reports";
|
||||||
meta property="og:title" content="Decompilation progress reports";
|
meta property="og:title" content="Decompilation progress reports";
|
||||||
meta property="og:description" content="Progress reports for matching decompilation projects";
|
meta property="og:description" content="Progress reports for matching decompilation projects";
|
||||||
@@ -179,10 +313,10 @@ pub async fn get_projects(
|
|||||||
nav {
|
nav {
|
||||||
ul {
|
ul {
|
||||||
li {
|
li {
|
||||||
a href="https://decomp.dev" { strong { "decomp.dev" } }
|
a href="/" { strong { "decomp.dev" } }
|
||||||
}
|
}
|
||||||
li {
|
li {
|
||||||
a href="/" { "Projects" }
|
a href="/projects" { "Projects" }
|
||||||
}
|
}
|
||||||
li class="md" {
|
li class="md" {
|
||||||
details class="dropdown" {
|
details class="dropdown" {
|
||||||
@@ -231,7 +365,7 @@ pub async fn get_projects(
|
|||||||
(project_fragment(project, current_sort, &canonical_url))
|
(project_fragment(project, current_sort, &canonical_url))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(ctx.footer(current_user.as_ref()))
|
(ctx.footer(current_user))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -239,48 +373,49 @@ pub async fn get_projects(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn project_fragment(
|
fn project_fragment(
|
||||||
info: ProjectInfoContext,
|
ctx: ProjectInfoContext,
|
||||||
current_sort: SortOption,
|
current_sort: SortOption,
|
||||||
canonical_url: &Url,
|
canonical_url: &Url,
|
||||||
) -> Markup {
|
) -> Markup {
|
||||||
let project_path =
|
let project = &ctx.info.project;
|
||||||
canonical_url.with_path(&format!("/{}/{}", info.project.owner, info.project.repo));
|
let Some(commit) = ctx.report.as_ref().map(|r| r.commit.clone()) else {
|
||||||
let commit_url = format!(
|
return Markup::default();
|
||||||
"https://github.com/{}/{}/commit/{}",
|
};
|
||||||
info.project.owner, info.project.repo, info.commit.sha
|
let project_path = canonical_url.with_path(&format!("/{}/{}", project.owner, project.repo));
|
||||||
);
|
let commit_url =
|
||||||
|
format!("https://github.com/{}/{}/commit/{}", project.owner, project.repo, commit.sha);
|
||||||
html! {
|
html! {
|
||||||
article class="project" {
|
article class="project" {
|
||||||
div class="project-header" {
|
div class="project-header" {
|
||||||
h3 class="project-title" {
|
h3 class="project-title" {
|
||||||
a href=(project_path) { (info.project.name()) }
|
a href=(project_path) { (project.name()) }
|
||||||
}
|
}
|
||||||
@if let Some(platform) = &info.project.platform {
|
@if let Some(platform) = &project.platform {
|
||||||
@let platform_name = Platform::from_str(platform).map(|p| p.name()).unwrap_or(platform);
|
@let platform_name = Platform::from_str(platform).map(|p| p.name()).unwrap_or(platform);
|
||||||
span class=(format!("platform-icon icon-{platform}")) title=(platform_name) {}
|
span class=(format!("platform-icon icon-{platform}")) title=(platform_name) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
h6 {
|
h6 {
|
||||||
@if current_sort.key == "total_code" || current_sort.key == "matched_code" {
|
@if current_sort.key == "total_code" || current_sort.key == "matched_code" {
|
||||||
(size(info.measures.matched_code))
|
(size(ctx.measures.matched_code))
|
||||||
" matched code | "
|
" matched code | "
|
||||||
(size(info.measures.total_code))
|
(size(ctx.measures.total_code))
|
||||||
" total code"
|
" total code"
|
||||||
} @else {
|
} @else {
|
||||||
(format!("{:.2}%", info.measures.matched_code_percent))
|
(format!("{:.2}%", ctx.measures.matched_code_percent))
|
||||||
" decompiled"
|
" decompiled"
|
||||||
@if info.measures.complete_code_percent > 0.0 {
|
@if ctx.measures.complete_code_percent > 0.0 {
|
||||||
" | "
|
" | "
|
||||||
(format!("{:.2}%", info.measures.complete_code_percent))
|
(format!("{:.2}%", ctx.measures.complete_code_percent))
|
||||||
" fully linked"
|
" fully linked"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(info.code_progress)
|
(ctx.code_progress)
|
||||||
small class="muted" {
|
small class="muted" {
|
||||||
span title=(date(info.commit.timestamp)) { "Updated " (timeago(info.commit.timestamp)) }
|
span title=(date(commit.timestamp)) { "Updated " (timeago(commit.timestamp)) }
|
||||||
" in commit "
|
" in commit "
|
||||||
a href=(commit_url) target="_blank" { (info.commit.sha[..7]) }
|
a href=(commit_url) target="_blank" { (commit.sha[..7]) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use decomp_dev_auth::CurrentUser;
|
|||||||
use decomp_dev_core::{
|
use decomp_dev_core::{
|
||||||
AppError, FullUri,
|
AppError, FullUri,
|
||||||
models::{FullReportFile, ProjectInfo},
|
models::{FullReportFile, ProjectInfo},
|
||||||
util::UrlExt,
|
util::{UrlExt, size},
|
||||||
};
|
};
|
||||||
use decomp_dev_images::{
|
use decomp_dev_images::{
|
||||||
badge,
|
badge,
|
||||||
@@ -28,14 +28,18 @@ use url::Url;
|
|||||||
use super::{parse_accept, treemap};
|
use super::{parse_accept, treemap};
|
||||||
use crate::{
|
use crate::{
|
||||||
AppState,
|
AppState,
|
||||||
handlers::common::{Load, TemplateContext, escape_script, nav_links, size},
|
handlers::{
|
||||||
|
common::{Load, TemplateContext, escape_script, nav_links},
|
||||||
|
project::ProjectResponse,
|
||||||
|
},
|
||||||
proto::{PROTOBUF, Protobuf},
|
proto::{PROTOBUF, Protobuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct ReportParams {
|
pub struct ReportParams {
|
||||||
owner: String,
|
id: Option<String>,
|
||||||
repo: String,
|
owner: Option<String>,
|
||||||
|
repo: Option<String>,
|
||||||
version: Option<String>,
|
version: Option<String>,
|
||||||
commit: Option<String>,
|
commit: Option<String>,
|
||||||
}
|
}
|
||||||
@@ -47,7 +51,9 @@ const DEFAULT_IMAGE_HEIGHT: u32 = 475;
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ReportQuery {
|
pub struct ReportQuery {
|
||||||
mode: Option<String>,
|
mode: Option<String>,
|
||||||
|
version: Option<String>,
|
||||||
category: Option<String>,
|
category: Option<String>,
|
||||||
|
commit: Option<String>,
|
||||||
w: Option<u32>,
|
w: Option<u32>,
|
||||||
h: Option<u32>,
|
h: Option<u32>,
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
@@ -173,9 +179,23 @@ fn extract_extension(params: ReportParams) -> (ReportParams, Option<String>) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if let Some((repo, ext)) = params.repo.rsplit_once('.') {
|
} else if let Some(repo) = params.repo.as_deref() {
|
||||||
|
if let Some((repo, ext)) = repo.rsplit_once('.') {
|
||||||
if is_valid_extension(ext) {
|
if is_valid_extension(ext) {
|
||||||
return (ReportParams { repo: repo.to_string(), ..params }, Some(ext.to_string()));
|
return (
|
||||||
|
ReportParams { repo: Some(repo.to_string()), ..params },
|
||||||
|
Some(ext.to_string()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let Some(id) = params.id.as_deref() {
|
||||||
|
if let Some((id, ext)) = id.rsplit_once('.') {
|
||||||
|
if is_valid_extension(ext) {
|
||||||
|
return (
|
||||||
|
ReportParams { id: Some(id.to_string()), ..params },
|
||||||
|
Some(ext.to_string()),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(params, None)
|
(params, None)
|
||||||
@@ -196,45 +216,95 @@ pub async fn get_report(
|
|||||||
return Err(AppError::Status(StatusCode::NOT_ACCEPTABLE));
|
return Err(AppError::Status(StatusCode::NOT_ACCEPTABLE));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut commit = params.commit.as_deref();
|
let mut commit = query.commit.as_deref().or(params.commit.as_deref());
|
||||||
if matches!(commit, Some(c) if c.eq_ignore_ascii_case("latest")) {
|
if matches!(commit, Some(c) if c.eq_ignore_ascii_case("latest")) {
|
||||||
commit = None;
|
commit = None;
|
||||||
}
|
}
|
||||||
let Some(project_info) = state.db.get_project_info(¶ms.owner, ¶ms.repo, commit).await?
|
let Some(project_info) = (match (¶ms.id, ¶ms.owner, ¶ms.repo) {
|
||||||
else {
|
(Some(id), _, _) => {
|
||||||
|
let id: u64 = id.parse().map_err(|_| AppError::Status(StatusCode::BAD_REQUEST))?;
|
||||||
|
state.db.get_project_info_by_id(id, commit).await?
|
||||||
|
}
|
||||||
|
(_, Some(owner), Some(repo)) => state.db.get_project_info(owner, repo, commit).await?,
|
||||||
|
_ => return Err(AppError::Status(StatusCode::BAD_REQUEST)),
|
||||||
|
}) else {
|
||||||
return Err(AppError::Status(StatusCode::NOT_FOUND));
|
return Err(AppError::Status(StatusCode::NOT_FOUND));
|
||||||
};
|
};
|
||||||
let Some(commit) = project_info.commit.as_ref() else {
|
let Some(commit) = project_info.commit.as_ref() else {
|
||||||
return Err(AppError::Status(StatusCode::NOT_FOUND));
|
return Err(AppError::Status(StatusCode::NOT_FOUND));
|
||||||
};
|
};
|
||||||
let version = if let Some(version) = ¶ms.version {
|
let version = if let Some(version) = query.version.as_deref().or(params.version.as_deref()) {
|
||||||
if version.eq_ignore_ascii_case("default") {
|
if version.eq_ignore_ascii_case("default") {
|
||||||
project_info.default_version().ok_or(AppError::Status(StatusCode::NOT_FOUND))?
|
project_info.default_version().ok_or(AppError::Status(StatusCode::NOT_FOUND))?
|
||||||
} else {
|
} else {
|
||||||
version.as_str()
|
version
|
||||||
}
|
}
|
||||||
} else if let Some(default_version) = project_info.default_version() {
|
} else if let Some(default_version) = project_info.default_version() {
|
||||||
default_version
|
default_version
|
||||||
} else {
|
} else {
|
||||||
return Err(AppError::Status(StatusCode::NOT_FOUND));
|
return Err(AppError::Status(StatusCode::NOT_FOUND));
|
||||||
};
|
};
|
||||||
let Some(report) =
|
let Some(report) = state
|
||||||
state.db.get_report(¶ms.owner, ¶ms.repo, &commit.sha, version).await?
|
.db
|
||||||
|
.get_report(&project_info.project.owner, &project_info.project.repo, &commit.sha, version)
|
||||||
|
.await?
|
||||||
else {
|
else {
|
||||||
return Err(AppError::Status(StatusCode::NOT_FOUND));
|
return Err(AppError::Status(StatusCode::NOT_FOUND));
|
||||||
};
|
};
|
||||||
let report = state.db.upgrade_report(&report).await?;
|
let report = state.db.upgrade_report(&report).await?;
|
||||||
|
|
||||||
let scope = apply_scope(&report, &project_info, &query)?;
|
let scope = apply_scope(&report, &project_info, &query)?;
|
||||||
match query.mode.as_deref().unwrap_or("report").to_ascii_lowercase().as_str() {
|
match query.mode.as_deref().unwrap_or("overview").to_ascii_lowercase().as_str() {
|
||||||
"shield" => mode_shield(&scope, query, &acceptable),
|
|
||||||
"report" => mode_report(&scope, &state, uri, query, ctx, &acceptable, current_user).await,
|
|
||||||
"measures" => mode_measures(&scope, &acceptable),
|
|
||||||
"history" => mode_history(&scope, &state, uri, query, ctx, &acceptable, current_user).await,
|
"history" => mode_history(&scope, &state, uri, query, ctx, &acceptable, current_user).await,
|
||||||
|
"measures" => mode_measures(&scope, &acceptable),
|
||||||
|
"overview" => {
|
||||||
|
mode_overview(&scope, &state, uri, query, ctx, &acceptable, current_user).await
|
||||||
|
}
|
||||||
|
"report" => mode_report(&scope, &state, uri, query, ctx, &acceptable, current_user).await,
|
||||||
|
"shield" => mode_shield(&scope, query, &acceptable),
|
||||||
_ => Err(AppError::Status(StatusCode::BAD_REQUEST)),
|
_ => Err(AppError::Status(StatusCode::BAD_REQUEST)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
async fn mode_overview(
|
||||||
|
scope: &Scope<'_>,
|
||||||
|
state: &AppState,
|
||||||
|
uri: Uri,
|
||||||
|
query: ReportQuery,
|
||||||
|
ctx: TemplateContext,
|
||||||
|
acceptable: &[Mime],
|
||||||
|
current_user: Option<CurrentUser>,
|
||||||
|
) -> Result<Response, AppError> {
|
||||||
|
for mime in acceptable {
|
||||||
|
if (mime.type_() == mime::STAR && mime.subtype() == mime::STAR)
|
||||||
|
|| (mime.type_() == mime::TEXT && mime.subtype() == mime::HTML)
|
||||||
|
{
|
||||||
|
return render_report(scope, state, uri, current_user, ctx).await;
|
||||||
|
} else if mime.type_() == mime::APPLICATION && mime.subtype() == mime::JSON {
|
||||||
|
let result =
|
||||||
|
ProjectResponse::new(scope.project_info, scope.measures, &scope.report.report);
|
||||||
|
return Ok(Json(result).into_response());
|
||||||
|
} else if mime.type_() == mime::IMAGE && mime.subtype() == mime::SVG {
|
||||||
|
let (w, h) = query.size();
|
||||||
|
let svg = treemap::render_svg(&scope.units, w, h);
|
||||||
|
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 {
|
||||||
|
// Default to PNG
|
||||||
|
ImageFormat::Png
|
||||||
|
} else {
|
||||||
|
ImageFormat::from_mime_type(mime.essence_str())
|
||||||
|
.ok_or_else(|| AppError::Status(StatusCode::NOT_ACCEPTABLE))?
|
||||||
|
};
|
||||||
|
let (w, h) = query.size();
|
||||||
|
let data = treemap::render_image(&scope.units, w, h, format)?;
|
||||||
|
return Ok(([(header::CONTENT_TYPE, format.to_mime_type())], data).into_response());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(AppError::Status(StatusCode::NOT_ACCEPTABLE))
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
async fn mode_report(
|
async fn mode_report(
|
||||||
scope: &Scope<'_>,
|
scope: &Scope<'_>,
|
||||||
@@ -327,6 +397,7 @@ fn mode_measures(
|
|||||||
struct ReportHistoryEntry {
|
struct ReportHistoryEntry {
|
||||||
timestamp: String,
|
timestamp: String,
|
||||||
commit_sha: String,
|
commit_sha: String,
|
||||||
|
commit_message: Option<String>,
|
||||||
measures: TemplateMeasures,
|
measures: TemplateMeasures,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,6 +444,7 @@ async fn mode_history(
|
|||||||
.format(&Rfc3339)
|
.format(&Rfc3339)
|
||||||
.unwrap_or_else(|_| "[invalid]".to_string()),
|
.unwrap_or_else(|_| "[invalid]".to_string()),
|
||||||
commit_sha: report.commit.sha,
|
commit_sha: report.commit.sha,
|
||||||
|
commit_message: report.commit.message,
|
||||||
measures: TemplateMeasures::from(measures),
|
measures: TemplateMeasures::from(measures),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -576,7 +648,9 @@ async fn render_report(
|
|||||||
"/{}/{}/{}/{}",
|
"/{}/{}/{}/{}",
|
||||||
project_info.project.owner, project_info.project.repo, report.version, report.commit.sha
|
project_info.project.owner, project_info.project.repo, report.version, report.commit.sha
|
||||||
));
|
));
|
||||||
let image_url = canonical_url.with_path(&format!("{}.png", canonical_url.path()));
|
let image_url = canonical_url
|
||||||
|
.with_path(&format!("{}.png", canonical_url.path()))
|
||||||
|
.query_param("mode", Some("report"));
|
||||||
|
|
||||||
let versions = project_info
|
let versions = project_info
|
||||||
.report_versions
|
.report_versions
|
||||||
@@ -671,6 +745,7 @@ async fn render_report(
|
|||||||
(header)
|
(header)
|
||||||
(ctx.chunks("main", Load::Deferred).await)
|
(ctx.chunks("main", Load::Deferred).await)
|
||||||
(ctx.chunks("report", Load::Preload).await)
|
(ctx.chunks("report", Load::Preload).await)
|
||||||
|
link rel="canonical" href=(canonical_url);
|
||||||
meta name="description" content=(format!("Decompilation progress report for {project_name}"));
|
meta name="description" content=(format!("Decompilation progress report for {project_name}"));
|
||||||
meta property="og:title" content=(format!("{project_short_name_with_label} is {:.2}% decompiled", measures.matched_code_percent));
|
meta property="og:title" content=(format!("{project_short_name_with_label} is {:.2}% decompiled", measures.matched_code_percent));
|
||||||
meta property="og:description" content=(format!("Decompilation progress report for {project_name}"));
|
meta property="og:description" content=(format!("Decompilation progress report for {project_name}"));
|
||||||
@@ -682,10 +757,10 @@ async fn render_report(
|
|||||||
nav {
|
nav {
|
||||||
ul {
|
ul {
|
||||||
li {
|
li {
|
||||||
a href="https://decomp.dev" { strong { "decomp.dev" } }
|
a href="/" { strong { "decomp.dev" } }
|
||||||
}
|
}
|
||||||
li {
|
li {
|
||||||
a href="/" { "Projects" }
|
a href="/projects" { "Projects" }
|
||||||
}
|
}
|
||||||
li {
|
li {
|
||||||
a href=(project_base_path) { (project_short_name) }
|
a href=(project_base_path) { (project_short_name) }
|
||||||
@@ -711,6 +786,12 @@ async fn render_report(
|
|||||||
details class="dropdown" {
|
details class="dropdown" {
|
||||||
summary {}
|
summary {}
|
||||||
ul dir="rtl" {
|
ul dir="rtl" {
|
||||||
|
li {
|
||||||
|
a href=(format!("/api?project={}", project_info.project.id)) {
|
||||||
|
"API "
|
||||||
|
span .icon-code { " " }
|
||||||
|
}
|
||||||
|
}
|
||||||
li {
|
li {
|
||||||
a href=(project_history_path) {
|
a href=(project_history_path) {
|
||||||
"History "
|
"History "
|
||||||
@@ -865,7 +946,9 @@ async fn render_history(
|
|||||||
"/{}/{}/{}",
|
"/{}/{}/{}",
|
||||||
project_info.project.owner, project_info.project.repo, report.version
|
project_info.project.owner, project_info.project.repo, report.version
|
||||||
));
|
));
|
||||||
let image_url = canonical_url.with_path(&format!("{}.png", canonical_url.path()));
|
let image_url = canonical_url
|
||||||
|
.with_path(&format!("{}.png", canonical_url.path()))
|
||||||
|
.query_param("mode", Some("report"));
|
||||||
|
|
||||||
let versions = project_info
|
let versions = project_info
|
||||||
.report_versions
|
.report_versions
|
||||||
@@ -929,6 +1012,7 @@ async fn render_history(
|
|||||||
(header)
|
(header)
|
||||||
(ctx.chunks("main", Load::Deferred).await)
|
(ctx.chunks("main", Load::Deferred).await)
|
||||||
(ctx.chunks("history", Load::Preload).await)
|
(ctx.chunks("history", Load::Preload).await)
|
||||||
|
link rel="canonical" href=(canonical_url);
|
||||||
meta name="description" content=(format!("Decompilation progress history for {project_name}"));
|
meta name="description" content=(format!("Decompilation progress history for {project_name}"));
|
||||||
meta property="og:title" content=(format!("{project_short_name_with_label} is {:.2}% decompiled", measures.matched_code_percent));
|
meta property="og:title" content=(format!("{project_short_name_with_label} is {:.2}% decompiled", measures.matched_code_percent));
|
||||||
meta property="og:description" content=(format!("Decompilation progress history for {project_name}"));
|
meta property="og:description" content=(format!("Decompilation progress history for {project_name}"));
|
||||||
@@ -940,10 +1024,10 @@ async fn render_history(
|
|||||||
nav {
|
nav {
|
||||||
ul {
|
ul {
|
||||||
li {
|
li {
|
||||||
a href="https://decomp.dev" { strong { "decomp.dev" } }
|
a href="/" { strong { "decomp.dev" } }
|
||||||
}
|
}
|
||||||
li {
|
li {
|
||||||
a href="/" { "Projects" }
|
a href="/projects" { "Projects" }
|
||||||
}
|
}
|
||||||
li {
|
li {
|
||||||
a href=(project_base_path) { (project_short_name) }
|
a href=(project_base_path) { (project_short_name) }
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
@import url('~uplot/dist/uPlot.min.css');
|
@import url("~uplot/dist/uPlot.min.css");
|
||||||
|
|||||||
+4
-1
@@ -69,7 +69,7 @@ $breakpoints: (
|
|||||||
// Forms
|
// Forms
|
||||||
"forms/basics": true,
|
"forms/basics": true,
|
||||||
"forms/checkbox-radio-switch": true,
|
"forms/checkbox-radio-switch": true,
|
||||||
"forms/input-color": false,
|
"forms/input-color": true,
|
||||||
"forms/input-date": false,
|
"forms/input-date": false,
|
||||||
"forms/input-file": false,
|
"forms/input-file": false,
|
||||||
"forms/input-range": false,
|
"forms/input-range": false,
|
||||||
@@ -161,6 +161,9 @@ $breakpoints: (
|
|||||||
.icon-github-circled:before { content: '\e804'; }
|
.icon-github-circled:before { content: '\e804'; }
|
||||||
.icon-left-open:before { content: '\e805'; }
|
.icon-left-open:before { content: '\e805'; }
|
||||||
.icon-right-open:before { content: '\e806'; }
|
.icon-right-open:before { content: '\e806'; }
|
||||||
|
.icon-copy:before { content: '\e807'; }
|
||||||
|
.icon-cancel:before { content: '\e808'; }
|
||||||
|
.icon-code:before { content: '\e809'; }
|
||||||
|
|
||||||
.icon-gc:before { content: '\ea00'; }
|
.icon-gc:before { content: '\ea00'; }
|
||||||
.icon-wii:before { content: '\ea01'; }
|
.icon-wii:before { content: '\ea01'; }
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
.code-block {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.5em;
|
||||||
|
right: 0.5em;
|
||||||
|
padding: 0.5em;
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-container {
|
||||||
|
display: flex;
|
||||||
|
gap: calc(var(--pico-spacing) * 0.5);
|
||||||
|
padding-top: calc(var(--pico-spacing) * 0.25);
|
||||||
|
|
||||||
|
input {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user