mirror of
https://github.com/encounter/decomp.dev.git
synced 2026-07-10 03:18:48 -07:00
Split project into crates, migrate to maud
This commit is contained in:
+9
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT owner, repo, name, short_name, default_category, default_version, platform, workflow_id\n FROM projects\n WHERE id = ?\n ",
|
||||
"query": "\n SELECT owner, repo, name, short_name, default_category, default_version, platform, workflow_id, enable_pr_comments\n FROM projects\n WHERE id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -42,6 +42,11 @@
|
||||
"name": "workflow_id",
|
||||
"ordinal": 7,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "enable_pr_comments",
|
||||
"ordinal": 8,
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -55,8 +60,9 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "269859ed391626ddbc2af37381a13c36976f48e6a89aaaa03da8ea66baccddff"
|
||||
"hash": "23a6be61c86478402eb00bb9fd64642188eec62eec0846a4a25a8110459997f1"
|
||||
}
|
||||
+13
-7
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT\n projects.id AS \"project_id!\",\n owner AS \"owner!\",\n repo AS \"repo!\",\n name,\n short_name,\n default_category,\n default_version,\n platform,\n workflow_id,\n git_commit,\n git_commit_message,\n MAX(timestamp) AS \"timestamp: time::OffsetDateTime\",\n JSON_GROUP_ARRAY(version ORDER BY version)\n FILTER (WHERE version IS NOT NULL) AS versions\n FROM projects LEFT JOIN reports ON (\n reports.project_id = projects.id\n AND reports.timestamp = (\n SELECT MAX(timestamp)\n FROM reports\n WHERE project_id = projects.id\n )\n )\n GROUP BY projects.id\n ORDER BY MAX(timestamp) DESC\n ",
|
||||
"query": "\n SELECT\n projects.id AS \"project_id!\",\n owner AS \"owner!\",\n repo AS \"repo!\",\n name,\n short_name,\n default_category,\n default_version,\n platform,\n workflow_id,\n enable_pr_comments AS \"enable_pr_comments!\",\n git_commit,\n git_commit_message,\n MAX(timestamp) AS \"timestamp: time::OffsetDateTime\",\n JSON_GROUP_ARRAY(version ORDER BY version)\n FILTER (WHERE version IS NOT NULL) AS versions\n FROM projects LEFT JOIN reports ON (\n reports.project_id = projects.id\n AND reports.timestamp = (\n SELECT MAX(timestamp)\n FROM reports\n WHERE project_id = projects.id\n )\n )\n GROUP BY projects.id\n ORDER BY MAX(timestamp) DESC\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -49,24 +49,29 @@
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "git_commit",
|
||||
"name": "enable_pr_comments!",
|
||||
"ordinal": 9,
|
||||
"type_info": "Text"
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "git_commit_message",
|
||||
"name": "git_commit",
|
||||
"ordinal": 10,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "timestamp: time::OffsetDateTime",
|
||||
"name": "git_commit_message",
|
||||
"ordinal": 11,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "versions",
|
||||
"name": "timestamp: time::OffsetDateTime",
|
||||
"ordinal": 12,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "versions",
|
||||
"ordinal": 13,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -85,8 +90,9 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "b492f0562501ae39110cf2df7a545cd0c676b438b91d63b915988c17d5bb29b6"
|
||||
"hash": "912ad9086ec8b4caa4acf3e6642cfce34ebcff88a5fe321b31a56af31e8eda83"
|
||||
}
|
||||
+9
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT id AS \"id!\", owner, repo, name, short_name, default_category, default_version, platform, workflow_id\n FROM projects\n WHERE owner = ? COLLATE NOCASE AND repo = ? COLLATE NOCASE\n ",
|
||||
"query": "\n SELECT id AS \"id!\", owner, repo, name, short_name, default_category, default_version, platform, workflow_id, enable_pr_comments\n FROM projects\n WHERE owner = ? COLLATE NOCASE AND repo = ? COLLATE NOCASE\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -47,6 +47,11 @@
|
||||
"name": "workflow_id",
|
||||
"ordinal": 8,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "enable_pr_comments",
|
||||
"ordinal": 9,
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -61,8 +66,9 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "60a01415ef25ea6a2fc7f3287e0e6972404e351401f8aeaedbc74b4c934c777c"
|
||||
"hash": "ef595aa7d32d7d21d9721a794295a7d10d0edc60d25c1809bb94935e468873fd"
|
||||
}
|
||||
Generated
+992
-870
File diff suppressed because it is too large
Load Diff
+23
-42
@@ -1,56 +1,37 @@
|
||||
[package]
|
||||
name = "decompal-rs"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = [
|
||||
"crates/auth",
|
||||
"crates/core",
|
||||
"crates/db",
|
||||
"crates/github",
|
||||
"crates/images",
|
||||
"crates/scripts",
|
||||
"crates/web",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
[workspace.package]
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0"
|
||||
ariadne = { version = "0.4", features = ["auto-color"] }
|
||||
axum = { version = "0.8", features = ["macros"] }
|
||||
badge-maker = "0.3"
|
||||
blake3 = "1.5"
|
||||
bytes = "1.7"
|
||||
futures-util = "0.3.31"
|
||||
grass = "0.13"
|
||||
hex = "0.4"
|
||||
human-size = "0.4"
|
||||
image = "0.25"
|
||||
itertools = "0.14"
|
||||
lightningcss = "1.0.0-alpha.65"
|
||||
mime = "0.3"
|
||||
minijinja = { version = "2.9", features = ["loader", "json"] }
|
||||
minijinja-autoreload = "2.9"
|
||||
moka = { version = "0.12", features = ["future"] }
|
||||
objdiff-core = { version = "2.5", features = ["bindings"] }
|
||||
#objdiff-core = { path = "../objdiff/objdiff-core", features = ["bindings"] }
|
||||
octocrab = { version = "0.44", features = ["stream"] }
|
||||
oxc = { version = "0.31", features = ["codegen", "minifier", "transformer", "semantic"] }
|
||||
palette = "0.7"
|
||||
prost = "0.13"
|
||||
regex = "1.11"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "gzip", "zstd", "charset", "http2", "rustls-tls-native-roots"] }
|
||||
resvg = "0.45"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "gzip", "zstd", "http2", "rustls-tls-native-roots"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde_yaml = "0.9"
|
||||
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite", "chrono"] }
|
||||
streemap = "0.1"
|
||||
timeago = { version = "0.4.2", default-features = false, features = ["chrono"] }
|
||||
time = { version = "0.3", features = ["serde", "macros"] }
|
||||
tokio = { version = "1.40", features = ["full"] }
|
||||
tokio-cron-scheduler = "0.13"
|
||||
tower = { version = "0.5", features = ["full"] }
|
||||
tower-http = { version = "0.6", features = ["full"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
url = { version = "2.5", features = ["serde"] }
|
||||
zip = { version = "2.6", default-features = false, features = ["flate2", "deflate-flate2"] }
|
||||
zstd = "0.13"
|
||||
jsonwebtoken = "9.3"
|
||||
hmac = "0.12"
|
||||
sha2 = "0.10"
|
||||
async-openai = "0.28"
|
||||
tower-sessions = "0.14"
|
||||
tower-sessions-sqlx-store = { version = "0.15", features = ["sqlite"] }
|
||||
time = { version = "0.3", features = ["local-offset"] }
|
||||
rand = "0.9"
|
||||
base64 = "0.22"
|
||||
tracing = "0.1"
|
||||
url = { version = "2.5", features = ["serde"] }
|
||||
|
||||
[profile.dev.package.sqlx-macros]
|
||||
opt-level = 3
|
||||
|
||||
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.2 KiB |
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "decomp-dev-auth"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
axum.workspace = true
|
||||
base64 = "0.22"
|
||||
rand = "0.9"
|
||||
decomp-dev-core = { path = "../core" }
|
||||
octocrab.workspace = true
|
||||
time.workspace = true
|
||||
serde.workspace = true
|
||||
tower-sessions.workspace = true
|
||||
tracing.workspace = true
|
||||
url.workspace = true
|
||||
@@ -1,17 +1,16 @@
|
||||
use anyhow::{anyhow, bail, Context};
|
||||
use anyhow::{Context, anyhow, bail};
|
||||
use axum::{
|
||||
extract::{FromRef, FromRequestParts, OptionalFromRequestParts, Query, State},
|
||||
http::{header::ACCEPT, request::Parts, StatusCode},
|
||||
http::{StatusCode, header::ACCEPT, request::Parts},
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
};
|
||||
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
|
||||
use octocrab::{models::Author, Octocrab};
|
||||
use rand::{rngs::OsRng, TryRngCore};
|
||||
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
|
||||
use decomp_dev_core::{AppError, config::GitHubConfig};
|
||||
use octocrab::{Octocrab, models::Author};
|
||||
use rand::{TryRngCore, rngs::OsRng};
|
||||
use time::{Duration, UtcDateTime};
|
||||
use tower_sessions::Session;
|
||||
|
||||
use crate::{config::GitHubConfig, handlers::AppError, AppState};
|
||||
|
||||
const GITHUB_OAUTH_STATE: &str = "github_oauth_state";
|
||||
const CURRENT_USER: &str = "current_user";
|
||||
|
||||
@@ -37,21 +36,23 @@ impl From<StoredOAuth> for octocrab::auth::OAuth {
|
||||
}
|
||||
}
|
||||
|
||||
pub type Profile = Author;
|
||||
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct CurrentUser {
|
||||
pub oauth: StoredOAuth,
|
||||
pub profile: Author,
|
||||
pub profile: Profile,
|
||||
}
|
||||
|
||||
pub async fn login(
|
||||
session: Session,
|
||||
State(state): State<AppState>,
|
||||
State(config): State<GitHubConfig>,
|
||||
current_user: Option<CurrentUser>,
|
||||
) -> Result<Response, AppError> {
|
||||
if current_user.is_some() {
|
||||
return Ok(Redirect::to("/").into_response());
|
||||
}
|
||||
let Some(config) = &state.config.github.oauth else {
|
||||
let Some(config) = &config.oauth else {
|
||||
tracing::warn!("No GitHub OAuth config found");
|
||||
return Ok((StatusCode::INTERNAL_SERVER_ERROR, "No GitHub OAuth config").into_response());
|
||||
};
|
||||
@@ -134,7 +135,7 @@ struct RefreshAccessToken<'a> {
|
||||
pub async fn oauth(
|
||||
session: Session,
|
||||
Query(OAuthQuery { code, state: oauth_state }): Query<OAuthQuery>,
|
||||
State(state): State<AppState>,
|
||||
State(config): State<GitHubConfig>,
|
||||
) -> Result<Response, AppError> {
|
||||
let existing_state = session.get::<String>(GITHUB_OAUTH_STATE).await?;
|
||||
let Some(existing_state) = existing_state else {
|
||||
@@ -147,7 +148,7 @@ pub async fn oauth(
|
||||
}
|
||||
session.remove_value(GITHUB_OAUTH_STATE).await?;
|
||||
|
||||
let current_user = fetch_access_token(&state.config.github, &code).await?;
|
||||
let current_user = fetch_access_token(&config, &code).await?;
|
||||
session.insert(CURRENT_USER, current_user).await?;
|
||||
|
||||
Ok(Redirect::to("/").into_response())
|
||||
@@ -214,7 +215,7 @@ async fn refresh_access_token(
|
||||
|
||||
impl<S> FromRequestParts<S> for CurrentUser
|
||||
where
|
||||
AppState: FromRef<S>,
|
||||
GitHubConfig: FromRef<S>,
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = (StatusCode, &'static str);
|
||||
@@ -228,7 +229,7 @@ where
|
||||
|
||||
impl<S> OptionalFromRequestParts<S> for CurrentUser
|
||||
where
|
||||
AppState: FromRef<S>,
|
||||
GitHubConfig: FromRef<S>,
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = (StatusCode, &'static str);
|
||||
@@ -238,7 +239,7 @@ where
|
||||
state: &S,
|
||||
) -> Result<Option<Self>, Self::Rejection> {
|
||||
let session = Session::from_request_parts(parts, state).await?;
|
||||
let app_state = AppState::from_ref(state);
|
||||
let config = GitHubConfig::from_ref(state);
|
||||
let Some(user) = session.get::<CurrentUser>(CURRENT_USER).await.ok().flatten() else {
|
||||
return Ok(None);
|
||||
};
|
||||
@@ -255,14 +256,13 @@ where
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
let current_user =
|
||||
match refresh_access_token(&app_state.config.github, refresh_token).await {
|
||||
Ok(current_user) => current_user,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to refresh access token: {:?}", e);
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
let current_user = match refresh_access_token(&config, refresh_token).await {
|
||||
Ok(current_user) => current_user,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to refresh access token: {:?}", e);
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
if let Err(e) = session.insert(CURRENT_USER, current_user.clone()).await {
|
||||
tracing::error!("Failed to insert user into session: {}", e);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "decomp-dev-core"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
objdiff-core.workspace = true
|
||||
serde.workspace = true
|
||||
time.workspace = true
|
||||
url.workspace = true
|
||||
axum.workspace = true
|
||||
mime.workspace = true
|
||||
tracing.workspace = true
|
||||
@@ -0,0 +1,83 @@
|
||||
pub mod config;
|
||||
pub mod models;
|
||||
pub mod util;
|
||||
|
||||
use std::{convert::Infallible, net::SocketAddr};
|
||||
|
||||
use axum::{
|
||||
Extension,
|
||||
extract::{ConnectInfo, FromRequestParts, OriginalUri},
|
||||
http::{StatusCode, Uri, header, request::Parts},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
|
||||
pub enum AppError {
|
||||
Status(StatusCode),
|
||||
Internal(anyhow::Error),
|
||||
}
|
||||
|
||||
impl IntoResponse for AppError {
|
||||
fn into_response(self) -> Response {
|
||||
match self {
|
||||
Self::Status(status) if status == StatusCode::NOT_FOUND => {
|
||||
(status, "Not found!!").into_response()
|
||||
}
|
||||
Self::Status(status) => status.into_response(),
|
||||
Self::Internal(err) => {
|
||||
tracing::error!("{:?}", err);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, format!("Something went wrong: {}", err))
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Into<anyhow::Error>> From<E> for AppError {
|
||||
fn from(err: E) -> Self { Self::Internal(err.into()) }
|
||||
}
|
||||
|
||||
/// Extractor for the full URI of the request, including the scheme and authority.
|
||||
/// Uses the `x-forwarded-proto` and `x-forwarded-host` headers if present.
|
||||
pub struct FullUri(pub Uri);
|
||||
|
||||
impl<S> FromRequestParts<S> for FullUri
|
||||
where S: Send + Sync
|
||||
{
|
||||
type Rejection = Infallible;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||
let uri = Extension::<OriginalUri>::from_request_parts(parts, state)
|
||||
.await
|
||||
.map_or_else(|_| parts.uri.clone(), |Extension(OriginalUri(uri))| uri);
|
||||
let mut builder = Uri::builder();
|
||||
if let Some(scheme) =
|
||||
parts.headers.get("x-forwarded-proto").and_then(|value| value.to_str().ok())
|
||||
{
|
||||
builder = builder.scheme(scheme);
|
||||
} else if let Some(scheme) = uri.scheme().cloned() {
|
||||
builder = builder.scheme(scheme);
|
||||
} else {
|
||||
// TODO: native https?
|
||||
builder = builder.scheme("http");
|
||||
}
|
||||
if let Some(host) =
|
||||
parts.headers.get("x-forwarded-host").and_then(|value| value.to_str().ok())
|
||||
{
|
||||
builder = builder.authority(host);
|
||||
} else if let Some(host) =
|
||||
parts.headers.get(header::HOST).and_then(|value| value.to_str().ok())
|
||||
{
|
||||
builder = builder.authority(host);
|
||||
} else if let Some(authority) = uri.authority().cloned() {
|
||||
builder = builder.authority(authority);
|
||||
} else if let Ok(ConnectInfo(socket_addr)) =
|
||||
ConnectInfo::<SocketAddr>::from_request_parts(parts, state).await
|
||||
{
|
||||
builder = builder.authority(socket_addr.to_string());
|
||||
}
|
||||
if let Some(path_and_query) = uri.path_and_query().cloned() {
|
||||
builder = builder.path_and_query(path_and_query);
|
||||
}
|
||||
Ok(FullUri(builder.build().unwrap_or(uri)))
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,6 @@ use objdiff_core::bindings::report::{Measures, Report, ReportCategory, ReportUni
|
||||
use serde::Serialize;
|
||||
use time::UtcDateTime;
|
||||
|
||||
use crate::db::UnitKey;
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
|
||||
pub struct Project {
|
||||
pub id: u64,
|
||||
@@ -64,19 +62,6 @@ pub struct Commit {
|
||||
pub timestamp: UtcDateTime,
|
||||
}
|
||||
|
||||
impl From<&octocrab::models::workflows::HeadCommit> for Commit {
|
||||
fn from(commit: &octocrab::models::workflows::HeadCommit) -> Self {
|
||||
Self {
|
||||
sha: commit.id.clone(),
|
||||
timestamp: UtcDateTime::from_unix_timestamp(
|
||||
commit.timestamp.to_utc().timestamp_millis(),
|
||||
)
|
||||
.unwrap_or_else(|_| UtcDateTime::now()),
|
||||
message: (!commit.message.is_empty()).then(|| commit.message.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ReportFile<R> {
|
||||
pub commit: Commit,
|
||||
@@ -95,6 +80,9 @@ pub struct ReportInner<U> {
|
||||
pub categories: Vec<ReportCategory>,
|
||||
}
|
||||
|
||||
// BLAKE3 hash of the unit data
|
||||
pub type UnitKey = [u8; 32];
|
||||
|
||||
pub type CachedReport = ReportInner<UnitKey>;
|
||||
pub type FullReport = ReportInner<Arc<ReportUnit>>;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "decomp-dev-db"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
blake3 = "1.5"
|
||||
decomp-dev-core = { path = "../core" }
|
||||
futures-util.workspace = true
|
||||
hex.workspace = true
|
||||
moka = { version = "0.12", features = ["future"] }
|
||||
objdiff-core.workspace = true
|
||||
prost = "0.13"
|
||||
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite", "time"] }
|
||||
time.workspace = true
|
||||
tracing.workspace = true
|
||||
zstd = "0.13"
|
||||
serde_json.workspace = true
|
||||
@@ -1,23 +1,22 @@
|
||||
use core::mem;
|
||||
use std::{borrow::Cow, cell::RefCell, collections::HashMap, sync::Arc};
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use futures_util::TryStreamExt;
|
||||
use moka::future::Cache;
|
||||
use objdiff_core::bindings::report::{Report, ReportUnit, REPORT_VERSION};
|
||||
use prost::Message;
|
||||
use sqlx::{
|
||||
migrate::MigrateDatabase, Connection, Executor, Pool, Row, Sqlite, SqliteConnection, SqlitePool,
|
||||
};
|
||||
use time::{macros::format_description, OffsetDateTime, UtcDateTime};
|
||||
|
||||
use crate::{
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use decomp_dev_core::{
|
||||
config::DbConfig,
|
||||
models::{
|
||||
CachedReport, CachedReportFile, Commit, FrogressMapping, FullReport, FullReportFile,
|
||||
Project, ProjectInfo,
|
||||
Project, ProjectInfo, UnitKey,
|
||||
},
|
||||
};
|
||||
use futures_util::TryStreamExt;
|
||||
use moka::future::Cache;
|
||||
use objdiff_core::bindings::report::{REPORT_VERSION, Report, ReportUnit};
|
||||
use prost::Message;
|
||||
use sqlx::{
|
||||
Connection, Executor, Pool, Row, Sqlite, SqliteConnection, SqlitePool, migrate::MigrateDatabase,
|
||||
};
|
||||
use time::{OffsetDateTime, UtcDateTime, macros::format_description};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Database {
|
||||
@@ -34,9 +33,6 @@ struct ReportKey {
|
||||
version: String,
|
||||
}
|
||||
|
||||
// BLAKE3 hash of the unit data
|
||||
pub type UnitKey = [u8; 32];
|
||||
|
||||
// Maximum number of bind parameters in a single query (SQLite limit)
|
||||
const BIND_LIMIT: usize = 32766;
|
||||
|
||||
@@ -49,7 +45,7 @@ impl Database {
|
||||
}
|
||||
let pool =
|
||||
SqlitePool::connect(&config.url).await.context("Failed to connect to database")?;
|
||||
sqlx::migrate!("./migrations")
|
||||
sqlx::migrate!("../../migrations")
|
||||
.run(&pool)
|
||||
.await
|
||||
.context("Failed to run database migrations")?;
|
||||
@@ -104,8 +100,8 @@ impl Database {
|
||||
project.default_category,
|
||||
project.default_version,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
report.migrate()?;
|
||||
let units = mem::take(&mut report.units);
|
||||
let data = compress(&report.encode_to_vec());
|
||||
@@ -126,10 +122,10 @@ impl Database {
|
||||
data,
|
||||
report.version,
|
||||
)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?
|
||||
.id;
|
||||
Self::insert_report_units(&mut *tx, &units, report_id).await?;
|
||||
.fetch_one(&mut *tx)
|
||||
.await?
|
||||
.id;
|
||||
Self::insert_report_units(&mut tx, &units, report_id).await?;
|
||||
tx.commit().await?;
|
||||
// self.report_cache
|
||||
// .insert(
|
||||
@@ -369,8 +365,8 @@ impl Database {
|
||||
owner,
|
||||
repo
|
||||
)
|
||||
.fetch_optional(&mut *conn)
|
||||
.await?
|
||||
.fetch_optional(&mut *conn)
|
||||
.await?
|
||||
{
|
||||
Some(row) => Project {
|
||||
id: row.id as u64,
|
||||
@@ -386,7 +382,7 @@ impl Database {
|
||||
},
|
||||
None => return Ok(None),
|
||||
};
|
||||
self.get_project_info_inner(&mut *conn, project, commit).await
|
||||
self.get_project_info_inner(&mut conn, project, commit).await
|
||||
}
|
||||
|
||||
pub async fn get_project_info_by_id(
|
||||
@@ -404,8 +400,8 @@ impl Database {
|
||||
"#,
|
||||
project_id_db
|
||||
)
|
||||
.fetch_optional(&mut *conn)
|
||||
.await?
|
||||
.fetch_optional(&mut *conn)
|
||||
.await?
|
||||
{
|
||||
Some(row) => Project {
|
||||
id: project_id,
|
||||
@@ -421,7 +417,7 @@ impl Database {
|
||||
},
|
||||
None => return Ok(None),
|
||||
};
|
||||
self.get_project_info_inner(&mut *conn, project, commit).await
|
||||
self.get_project_info_inner(&mut conn, project, commit).await
|
||||
}
|
||||
|
||||
async fn get_project_info_inner(
|
||||
@@ -761,7 +757,7 @@ impl Database {
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
// Insert updated report units
|
||||
Self::insert_report_units(&mut *tx, &units, report_id).await?;
|
||||
Self::insert_report_units(&mut tx, &units, report_id).await?;
|
||||
tx.commit().await?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "decomp-dev-github"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
axum.workspace = true
|
||||
decomp-dev-core = { path = "../core" }
|
||||
decomp-dev-db = { path = "../db" }
|
||||
futures-util.workspace = true
|
||||
hex = "0.4"
|
||||
hmac = "0.12"
|
||||
http = "1.3"
|
||||
jsonwebtoken = "9.3"
|
||||
objdiff-core.workspace = true
|
||||
octocrab.workspace = true
|
||||
regex = "1.11"
|
||||
serde.workspace = true
|
||||
sha2 = "0.10"
|
||||
time.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
zip = { version = "2.6", default-features = false, features = ["flate2", "deflate-flate2"] }
|
||||
serde_json.workspace = true
|
||||
@@ -0,0 +1,283 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use anyhow::Result;
|
||||
use decomp_dev_core::models::{Commit, FullReportFile};
|
||||
use objdiff_core::bindings::report::{
|
||||
ChangeItem, ChangeItemInfo, ChangeUnit, Changes, Report, ReportItem, ReportUnit,
|
||||
};
|
||||
|
||||
use crate::ProcessArtifactResult;
|
||||
|
||||
pub fn generate_changes(previous: &Report, current: &Report) -> Result<Changes> {
|
||||
let mut changes = Changes { from: previous.measures, to: current.measures, units: vec![] };
|
||||
for prev_unit in &previous.units {
|
||||
let curr_unit = current.units.iter().find(|u| u.name == prev_unit.name);
|
||||
let sections = process_items(prev_unit, curr_unit, |u| &u.sections);
|
||||
let functions = process_items(prev_unit, curr_unit, |u| &u.functions);
|
||||
|
||||
let prev_measures = prev_unit.measures;
|
||||
let curr_measures = curr_unit.and_then(|u| u.measures);
|
||||
if !functions.is_empty() || prev_measures != curr_measures {
|
||||
changes.units.push(ChangeUnit {
|
||||
name: prev_unit.name.clone(),
|
||||
from: prev_measures,
|
||||
to: curr_measures,
|
||||
sections,
|
||||
functions,
|
||||
metadata: curr_unit
|
||||
.as_ref()
|
||||
.and_then(|u| u.metadata.clone())
|
||||
.or_else(|| prev_unit.metadata.clone()),
|
||||
});
|
||||
}
|
||||
}
|
||||
for curr_unit in ¤t.units {
|
||||
if !previous.units.iter().any(|u| u.name == curr_unit.name) {
|
||||
changes.units.push(ChangeUnit {
|
||||
name: curr_unit.name.clone(),
|
||||
from: None,
|
||||
to: curr_unit.measures,
|
||||
sections: process_new_items(&curr_unit.sections),
|
||||
functions: process_new_items(&curr_unit.functions),
|
||||
metadata: curr_unit.metadata.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(changes)
|
||||
}
|
||||
|
||||
fn process_items<F: Fn(&ReportUnit) -> &Vec<ReportItem>>(
|
||||
prev_unit: &ReportUnit,
|
||||
curr_unit: Option<&ReportUnit>,
|
||||
getter: F,
|
||||
) -> Vec<ChangeItem> {
|
||||
let prev_items = getter(prev_unit);
|
||||
let mut items = vec![];
|
||||
if let Some(curr_unit) = curr_unit {
|
||||
let curr_items = getter(curr_unit);
|
||||
for prev_func in prev_items {
|
||||
let prev_func_info = ChangeItemInfo::from(prev_func);
|
||||
let prev_func_address = prev_func.metadata.as_ref().and_then(|m| m.virtual_address);
|
||||
let curr_func = curr_items.iter().find(|f| {
|
||||
f.name == prev_func.name
|
||||
|| prev_func_address.is_some_and(|a| {
|
||||
f.metadata.as_ref().and_then(|m| m.virtual_address).is_some_and(|b| a == b)
|
||||
})
|
||||
});
|
||||
if let Some(curr_func) = curr_func {
|
||||
let curr_func_info = ChangeItemInfo::from(curr_func);
|
||||
if prev_func_info != curr_func_info {
|
||||
items.push(ChangeItem {
|
||||
name: curr_func.name.clone(),
|
||||
from: Some(prev_func_info),
|
||||
to: Some(curr_func_info),
|
||||
metadata: curr_func.metadata.clone(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
items.push(ChangeItem {
|
||||
name: prev_func.name.clone(),
|
||||
from: Some(prev_func_info),
|
||||
to: None,
|
||||
metadata: prev_func.metadata.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
for curr_func in curr_items {
|
||||
let curr_func_address = curr_func.metadata.as_ref().and_then(|m| m.virtual_address);
|
||||
if !prev_items.iter().any(|f| {
|
||||
f.name == curr_func.name
|
||||
|| curr_func_address.is_some_and(|a| {
|
||||
f.metadata.as_ref().and_then(|m| m.virtual_address).is_some_and(|b| a == b)
|
||||
})
|
||||
}) {
|
||||
items.push(ChangeItem {
|
||||
name: curr_func.name.clone(),
|
||||
from: None,
|
||||
to: Some(ChangeItemInfo::from(curr_func)),
|
||||
metadata: curr_func.metadata.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for prev_func in prev_items {
|
||||
items.push(ChangeItem {
|
||||
name: prev_func.name.clone(),
|
||||
from: Some(ChangeItemInfo::from(prev_func)),
|
||||
to: None,
|
||||
metadata: prev_func.metadata.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
items
|
||||
}
|
||||
|
||||
fn process_new_items(items: &[ReportItem]) -> Vec<ChangeItem> {
|
||||
items
|
||||
.iter()
|
||||
.map(|item| ChangeItem {
|
||||
name: item.name.clone(),
|
||||
from: None,
|
||||
to: Some(ChangeItemInfo::from(item)),
|
||||
metadata: item.metadata.clone(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn measure_line_matched(
|
||||
name: &str,
|
||||
from: u64,
|
||||
from_percent: f32,
|
||||
to: u64,
|
||||
to_percent: f32,
|
||||
) -> String {
|
||||
let emoji = if to > from { "📈" } else { "📉" };
|
||||
let percent_diff = to_percent - from_percent;
|
||||
let percent_str = if percent_diff < 0.0 {
|
||||
format!("{percent_diff:.2}%")
|
||||
} else {
|
||||
format!("+{percent_diff:.2}%")
|
||||
};
|
||||
let bytes_diff = to as i64 - from as i64;
|
||||
let bytes_str = match bytes_diff.cmp(&0) {
|
||||
Ordering::Less => bytes_diff.to_string(),
|
||||
Ordering::Equal | Ordering::Greater => format!("+{bytes_diff}"),
|
||||
};
|
||||
format!("{emoji} **{name}**: {to_percent:.2}% ({percent_str}, {bytes_str} bytes)\n")
|
||||
}
|
||||
|
||||
fn measure_line_bytes(name: &str, from: u64, to: u64) -> String {
|
||||
let diff = to as i64 - from as i64;
|
||||
let diff_str = match diff.cmp(&0) {
|
||||
Ordering::Less => diff.to_string(),
|
||||
Ordering::Equal | Ordering::Greater => format!("+{diff}"),
|
||||
};
|
||||
format!("**{name}**: {to} bytes ({diff_str} bytes)\n")
|
||||
}
|
||||
|
||||
fn measure_line_simple(name: &str, from: u64, to: u64) -> String {
|
||||
let diff = to as i64 - from as i64;
|
||||
let diff_str = match diff.cmp(&0) {
|
||||
Ordering::Less => diff.to_string(),
|
||||
Ordering::Equal | Ordering::Greater => format!("+{diff}"),
|
||||
};
|
||||
format!("**{name}**: {to} ({diff_str})\n")
|
||||
}
|
||||
|
||||
pub fn generate_comment(
|
||||
from: &FullReportFile,
|
||||
to: &ProcessArtifactResult,
|
||||
to_commit: &Commit,
|
||||
changes: Changes,
|
||||
) -> String {
|
||||
let mut comment = format!(
|
||||
"### Report for {} ({} - {})\n\n",
|
||||
to.version,
|
||||
&from.commit.sha[..7],
|
||||
&to_commit.sha[..7]
|
||||
);
|
||||
let mut measure_written = false;
|
||||
let from_measures = from.report.measures;
|
||||
let to_measures = to.report.measures.unwrap_or_default();
|
||||
if from_measures.total_code != to_measures.total_code {
|
||||
comment.push_str(&measure_line_bytes(
|
||||
"Total code",
|
||||
from_measures.total_code,
|
||||
to_measures.total_code,
|
||||
));
|
||||
measure_written = true;
|
||||
}
|
||||
if from_measures.total_functions != to_measures.total_functions {
|
||||
comment.push_str(&measure_line_simple(
|
||||
"Total functions",
|
||||
from_measures.total_functions as u64,
|
||||
to_measures.total_functions as u64,
|
||||
));
|
||||
measure_written = true;
|
||||
}
|
||||
if from_measures.matched_code != to_measures.matched_code {
|
||||
comment.push_str(&measure_line_matched(
|
||||
"Matched code",
|
||||
from_measures.matched_code,
|
||||
from_measures.matched_code_percent,
|
||||
to_measures.matched_code,
|
||||
to_measures.matched_code_percent,
|
||||
));
|
||||
measure_written = true;
|
||||
}
|
||||
if from_measures.complete_code != to_measures.complete_code {
|
||||
comment.push_str(&measure_line_matched(
|
||||
"Linked code",
|
||||
from_measures.complete_code,
|
||||
from_measures.complete_code_percent,
|
||||
to_measures.complete_code,
|
||||
to_measures.complete_code_percent,
|
||||
));
|
||||
measure_written = true;
|
||||
}
|
||||
if measure_written {
|
||||
comment.push('\n');
|
||||
}
|
||||
let mut total_changes = 0;
|
||||
let mut iter = changes.units.into_iter().flat_map(|mut unit| {
|
||||
let functions = core::mem::take(&mut unit.functions);
|
||||
functions.into_iter().map(move |f| (unit.clone(), f))
|
||||
});
|
||||
for (unit, item) in iter.by_ref() {
|
||||
let (from, to) = match (item.from, item.to) {
|
||||
(Some(from), Some(to)) => (from, to),
|
||||
(None, Some(to)) => (ChangeItemInfo::default(), to),
|
||||
(Some(from), None) => (from, ChangeItemInfo::default()),
|
||||
(None, None) => continue,
|
||||
};
|
||||
let emoji = if to.fuzzy_match_percent == 100.0 {
|
||||
"✅"
|
||||
} else if to.fuzzy_match_percent > from.fuzzy_match_percent {
|
||||
"📈"
|
||||
} else {
|
||||
"📉"
|
||||
};
|
||||
let from_bytes = ((from.fuzzy_match_percent as f64 / 100.0) * from.size as f64) as u64;
|
||||
let to_bytes = ((to.fuzzy_match_percent as f64 / 100.0) * to.size as f64) as u64;
|
||||
let bytes_diff = to_bytes as i64 - from_bytes as i64;
|
||||
let bytes_str = match bytes_diff.cmp(&0) {
|
||||
Ordering::Less => bytes_diff.to_string(),
|
||||
Ordering::Equal => "0".to_string(),
|
||||
Ordering::Greater => format!("+{}", bytes_diff),
|
||||
};
|
||||
let name =
|
||||
item.metadata.as_ref().and_then(|m| m.demangled_name.as_deref()).unwrap_or(&item.name);
|
||||
comment.push_str(&format!(
|
||||
"{emoji} `{} | {}` {} bytes -> {:.2}%\n",
|
||||
unit.name, name, bytes_str, to.fuzzy_match_percent
|
||||
));
|
||||
total_changes += 1;
|
||||
if total_changes >= 30 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let remaining = iter.count();
|
||||
if remaining > 0 {
|
||||
comment.push_str(&format!("...and {} more items\n", remaining));
|
||||
} else if total_changes == 0 {
|
||||
comment.push_str("No changes\n");
|
||||
}
|
||||
comment
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
fn platform_name(platform: &str) -> &str {
|
||||
match platform {
|
||||
"gc" => "GameCube",
|
||||
"wii" => "Wii",
|
||||
"n64" => "Nintendo 64",
|
||||
"switch" => "Nintendo Switch",
|
||||
"3ds" => "Nintendo 3DS",
|
||||
"nds" => "Nintendo DS",
|
||||
"gba" => "Game Boy Advance",
|
||||
"gbc" => "Game Boy Color",
|
||||
"ps" => "PlayStation",
|
||||
"ps2" => "PlayStation 2",
|
||||
_ => platform,
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
pub mod changes;
|
||||
pub mod webhook;
|
||||
|
||||
use std::{
|
||||
collections::{hash_map::Entry, HashMap},
|
||||
collections::{HashMap, hash_map::Entry},
|
||||
ffi::OsStr,
|
||||
io::{Cursor, Read},
|
||||
pin::pin,
|
||||
@@ -7,26 +10,26 @@ use std::{
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use axum::http::StatusCode;
|
||||
use decomp_dev_core::{
|
||||
config::GitHubConfig,
|
||||
models::{Commit, Project},
|
||||
};
|
||||
use decomp_dev_db::Database;
|
||||
use futures_util::TryStreamExt;
|
||||
use http::StatusCode;
|
||||
use objdiff_core::bindings::report::Report;
|
||||
use octocrab::{
|
||||
models::{repos::RepoCommitPage, ArtifactId, InstallationId, RunId},
|
||||
params::actions::ArchiveFormat,
|
||||
GitHubError, Octocrab,
|
||||
models::{ArtifactId, InstallationId, RunId, repos::RepoCommitPage, workflows::HeadCommit},
|
||||
params::actions::ArchiveFormat,
|
||||
};
|
||||
use regex::Regex;
|
||||
use time::UtcDateTime;
|
||||
use tokio::{
|
||||
sync::{Mutex, Semaphore},
|
||||
task::JoinSet,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
config::GitHubConfig,
|
||||
models::{Commit, Project},
|
||||
AppState,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GitHub {
|
||||
pub client: Octocrab,
|
||||
@@ -153,15 +156,13 @@ impl GitHub {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(state: &AppState, repo_id: u64, stop_run_id: u64) -> Result<()> {
|
||||
let mut project_info = state
|
||||
.db
|
||||
pub async fn run(github: &GitHub, db: &Database, repo_id: u64, stop_run_id: u64) -> Result<()> {
|
||||
let mut project_info = db
|
||||
.get_project_info_by_id(repo_id, None)
|
||||
.await
|
||||
.context("Failed to fetch project info")?
|
||||
.with_context(|| format!("Failed to fetch project info for ID {}", repo_id))?;
|
||||
let repo = state
|
||||
.github
|
||||
let repo = github
|
||||
.client
|
||||
.repos_by_id(project_info.project.id)
|
||||
.get()
|
||||
@@ -178,12 +179,8 @@ pub async fn run(state: &AppState, repo_id: u64, stop_run_id: u64) -> Result<()>
|
||||
owner.login,
|
||||
repo.name
|
||||
);
|
||||
state
|
||||
.db
|
||||
.update_project_owner_repo(project_info.project.id, &owner.login, &repo.name)
|
||||
.await?;
|
||||
project_info = state
|
||||
.db
|
||||
db.update_project_owner_repo(project_info.project.id, &owner.login, &repo.name).await?;
|
||||
project_info = db
|
||||
.get_project_info_by_id(repo_id, None)
|
||||
.await
|
||||
.context("Failed to fetch project info")?
|
||||
@@ -192,7 +189,7 @@ pub async fn run(state: &AppState, repo_id: u64, stop_run_id: u64) -> Result<()>
|
||||
|
||||
let project = &project_info.project;
|
||||
tracing::debug!("Refreshing project {}/{}", project.owner, project.repo);
|
||||
let client = state.github.client_for(&project.owner).await?;
|
||||
let client = github.client_for(&project.owner).await?;
|
||||
|
||||
let workflow_ids = if let Some(workflow_id) = &project.workflow_id {
|
||||
vec![workflow_id.clone()]
|
||||
@@ -234,7 +231,7 @@ pub async fn run(state: &AppState, repo_id: u64, stop_run_id: u64) -> Result<()>
|
||||
..
|
||||
}) =>
|
||||
{
|
||||
break
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(e)
|
||||
@@ -274,9 +271,9 @@ pub async fn run(state: &AppState, repo_id: u64, stop_run_id: u64) -> Result<()>
|
||||
let sem = sem.clone();
|
||||
let project = project.clone();
|
||||
let client = client.clone();
|
||||
let db = state.db.clone();
|
||||
let db = db.clone();
|
||||
let run_id = run.id;
|
||||
let commit = Commit::from(&run.head_commit);
|
||||
let commit = commit_from_head_commit(&run.head_commit);
|
||||
set.spawn(async move {
|
||||
let _permit = sem.acquire().await.unwrap();
|
||||
match db.report_exists(project.id, &commit.sha).await {
|
||||
@@ -310,9 +307,7 @@ pub async fn run(state: &AppState, repo_id: u64, stop_run_id: u64) -> Result<()>
|
||||
);
|
||||
for artifact in artifacts {
|
||||
let start = std::time::Instant::now();
|
||||
state
|
||||
.db
|
||||
.insert_report(&project, &commit, &artifact.version, *artifact.report)
|
||||
db.insert_report(project, &commit, &artifact.version, *artifact.report)
|
||||
.await?;
|
||||
let duration = start.elapsed();
|
||||
tracing::info!(
|
||||
@@ -340,7 +335,7 @@ pub async fn run(state: &AppState, repo_id: u64, stop_run_id: u64) -> Result<()>
|
||||
|
||||
if found_artifacts {
|
||||
if project.workflow_id.is_none() {
|
||||
state.db.update_project_workflow_id(project.id, workflow_id).await?;
|
||||
db.update_project_workflow_id(project.id, workflow_id).await?;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -485,3 +480,12 @@ async fn download_artifact(
|
||||
}
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
pub fn commit_from_head_commit(commit: &HeadCommit) -> Commit {
|
||||
Commit {
|
||||
sha: commit.id.clone(),
|
||||
timestamp: UtcDateTime::from_unix_timestamp(commit.timestamp.to_utc().timestamp_millis())
|
||||
.unwrap_or_else(|_| UtcDateTime::now()),
|
||||
message: (!commit.message.is_empty()).then(|| commit.message.clone()),
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{cmp::Ordering, fmt::Display, mem::take};
|
||||
use std::fmt::Display;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use axum::{
|
||||
@@ -7,33 +7,38 @@ use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use decomp_dev_core::{AppError, config::GitHubConfig};
|
||||
use decomp_dev_db::Database;
|
||||
use hmac::{Hmac, Mac};
|
||||
use objdiff_core::bindings::report::{
|
||||
ChangeItem, ChangeItemInfo, ChangeUnit, Changes, Report, ReportItem, ReportUnit,
|
||||
};
|
||||
use octocrab::{
|
||||
Octocrab,
|
||||
models::{
|
||||
pulls::PullRequest,
|
||||
webhook_events::{
|
||||
EventInstallation, WebhookEvent, WebhookEventPayload,
|
||||
payload::{
|
||||
InstallationWebhookEventAction, PullRequestWebhookEventAction,
|
||||
WorkflowRunWebhookEventAction,
|
||||
},
|
||||
EventInstallation, WebhookEvent, WebhookEventPayload,
|
||||
},
|
||||
workflows::{Run, WorkFlow},
|
||||
},
|
||||
Octocrab,
|
||||
};
|
||||
use sha2::Sha256;
|
||||
|
||||
use crate::{
|
||||
github::{process_workflow_run, ProcessArtifactResult, ProcessWorkflowRunResult},
|
||||
handlers::AppError,
|
||||
models::{Commit, FullReportFile},
|
||||
AppState,
|
||||
GitHub, ProcessWorkflowRunResult,
|
||||
changes::{generate_changes, generate_comment},
|
||||
commit_from_head_commit, process_workflow_run,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WebhookState {
|
||||
pub config: GitHubConfig,
|
||||
pub db: Database,
|
||||
pub github: GitHub,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
pub struct RunWithPullRequests {
|
||||
#[serde(flatten)]
|
||||
@@ -41,9 +46,7 @@ pub struct RunWithPullRequests {
|
||||
pub pull_requests: Vec<PullRequest>,
|
||||
}
|
||||
|
||||
pub async fn webhook(
|
||||
GitHubEvent { event, state }: GitHubEvent<AppState>,
|
||||
) -> Result<Response, AppError> {
|
||||
pub async fn webhook(GitHubEvent { event, state }: GitHubEvent) -> Result<Response, AppError> {
|
||||
let Some(installations) = &state.github.installations else {
|
||||
tracing::warn!("Received webhook event {:?} with no GitHub app config", event.kind);
|
||||
return Ok((StatusCode::OK, "No app config").into_response());
|
||||
@@ -160,7 +163,7 @@ pub async fn webhook(
|
||||
}
|
||||
|
||||
async fn handle_workflow_run_completed(
|
||||
state: &AppState,
|
||||
state: &WebhookState,
|
||||
client: Octocrab,
|
||||
_workflow: WorkFlow,
|
||||
workflow_run: RunWithPullRequests,
|
||||
@@ -185,7 +188,7 @@ async fn handle_workflow_run_completed(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let commit = Commit::from(&workflow_run.head_commit);
|
||||
let commit = commit_from_head_commit(&workflow_run.head_commit);
|
||||
if workflow_run.event == "push"
|
||||
&& match &repository.default_branch {
|
||||
Some(default_branch) => default_branch == &workflow_run.head_branch,
|
||||
@@ -261,7 +264,7 @@ async fn handle_workflow_run_completed(
|
||||
};
|
||||
let report_file = state.db.upgrade_report(&cached_report).await?;
|
||||
let report = report_file.report.flatten();
|
||||
let changes = changes(&report, &artifact.report)?;
|
||||
let changes = generate_changes(&report, &artifact.report)?;
|
||||
let comment_text = generate_comment(&report_file, artifact, &commit, changes);
|
||||
let existing_comment = existing_comments
|
||||
.items
|
||||
@@ -292,7 +295,7 @@ async fn handle_workflow_run_completed(
|
||||
}
|
||||
|
||||
async fn handle_pull_request_update(
|
||||
_state: &AppState,
|
||||
_state: &WebhookState,
|
||||
_client: Octocrab,
|
||||
_pull_request: PullRequest,
|
||||
) -> Result<()> {
|
||||
@@ -302,16 +305,16 @@ async fn handle_pull_request_update(
|
||||
}
|
||||
|
||||
/// Verify and extract GitHub Event Payload.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Clone)]
|
||||
#[must_use]
|
||||
pub struct GitHubEvent<S> {
|
||||
pub struct GitHubEvent {
|
||||
pub event: WebhookEvent,
|
||||
pub state: S,
|
||||
pub state: WebhookState,
|
||||
}
|
||||
|
||||
impl<S> FromRequest<S> for GitHubEvent<S>
|
||||
impl<S> FromRequest<S> for GitHubEvent
|
||||
where
|
||||
AppState: FromRef<S>,
|
||||
WebhookState: FromRef<S>,
|
||||
S: Send + Sync + Clone,
|
||||
{
|
||||
type Rejection = Response;
|
||||
@@ -327,8 +330,8 @@ where
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.ok_or_else(|| err("X-GitHub-Event header missing"))?
|
||||
.to_string();
|
||||
let app_state = AppState::from_ref(state);
|
||||
let body = if let Some(app_config) = &app_state.config.github.app {
|
||||
let inner = WebhookState::from_ref(state);
|
||||
let body = if let Some(app_config) = &inner.config.app {
|
||||
let signature_sha256 = req
|
||||
.headers()
|
||||
.get("X-Hub-Signature-256")
|
||||
@@ -352,280 +355,6 @@ where
|
||||
};
|
||||
let value = WebhookEvent::try_from_header_and_body(&event, &body)
|
||||
.map_err(|_| err("error parsing body"))?;
|
||||
Ok(GitHubEvent { event: value, state: state.clone() })
|
||||
}
|
||||
}
|
||||
|
||||
fn changes(previous: &Report, current: &Report) -> Result<Changes> {
|
||||
let mut changes = Changes { from: previous.measures, to: current.measures, units: vec![] };
|
||||
for prev_unit in &previous.units {
|
||||
let curr_unit = current.units.iter().find(|u| u.name == prev_unit.name);
|
||||
let sections = process_items(prev_unit, curr_unit, |u| &u.sections);
|
||||
let functions = process_items(prev_unit, curr_unit, |u| &u.functions);
|
||||
|
||||
let prev_measures = prev_unit.measures;
|
||||
let curr_measures = curr_unit.and_then(|u| u.measures);
|
||||
if !functions.is_empty() || prev_measures != curr_measures {
|
||||
changes.units.push(ChangeUnit {
|
||||
name: prev_unit.name.clone(),
|
||||
from: prev_measures,
|
||||
to: curr_measures,
|
||||
sections,
|
||||
functions,
|
||||
metadata: curr_unit
|
||||
.as_ref()
|
||||
.and_then(|u| u.metadata.clone())
|
||||
.or_else(|| prev_unit.metadata.clone()),
|
||||
});
|
||||
}
|
||||
}
|
||||
for curr_unit in ¤t.units {
|
||||
if !previous.units.iter().any(|u| u.name == curr_unit.name) {
|
||||
changes.units.push(ChangeUnit {
|
||||
name: curr_unit.name.clone(),
|
||||
from: None,
|
||||
to: curr_unit.measures,
|
||||
sections: process_new_items(&curr_unit.sections),
|
||||
functions: process_new_items(&curr_unit.functions),
|
||||
metadata: curr_unit.metadata.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(changes)
|
||||
}
|
||||
|
||||
fn process_items<F: Fn(&ReportUnit) -> &Vec<ReportItem>>(
|
||||
prev_unit: &ReportUnit,
|
||||
curr_unit: Option<&ReportUnit>,
|
||||
getter: F,
|
||||
) -> Vec<ChangeItem> {
|
||||
let prev_items = getter(prev_unit);
|
||||
let mut items = vec![];
|
||||
if let Some(curr_unit) = curr_unit {
|
||||
let curr_items = getter(curr_unit);
|
||||
for prev_func in prev_items {
|
||||
let prev_func_info = ChangeItemInfo::from(prev_func);
|
||||
let prev_func_address = prev_func.metadata.as_ref().and_then(|m| m.virtual_address);
|
||||
let curr_func = curr_items.iter().find(|f| {
|
||||
f.name == prev_func.name
|
||||
|| prev_func_address.is_some_and(|a| {
|
||||
f.metadata.as_ref().and_then(|m| m.virtual_address).is_some_and(|b| a == b)
|
||||
})
|
||||
});
|
||||
if let Some(curr_func) = curr_func {
|
||||
let curr_func_info = ChangeItemInfo::from(curr_func);
|
||||
if prev_func_info != curr_func_info {
|
||||
items.push(ChangeItem {
|
||||
name: curr_func.name.clone(),
|
||||
from: Some(prev_func_info),
|
||||
to: Some(curr_func_info),
|
||||
metadata: curr_func.metadata.clone(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
items.push(ChangeItem {
|
||||
name: prev_func.name.clone(),
|
||||
from: Some(prev_func_info),
|
||||
to: None,
|
||||
metadata: prev_func.metadata.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
for curr_func in curr_items {
|
||||
let curr_func_address = curr_func.metadata.as_ref().and_then(|m| m.virtual_address);
|
||||
if !prev_items.iter().any(|f| {
|
||||
f.name == curr_func.name
|
||||
|| curr_func_address.is_some_and(|a| {
|
||||
f.metadata.as_ref().and_then(|m| m.virtual_address).is_some_and(|b| a == b)
|
||||
})
|
||||
}) {
|
||||
items.push(ChangeItem {
|
||||
name: curr_func.name.clone(),
|
||||
from: None,
|
||||
to: Some(ChangeItemInfo::from(curr_func)),
|
||||
metadata: curr_func.metadata.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for prev_func in prev_items {
|
||||
items.push(ChangeItem {
|
||||
name: prev_func.name.clone(),
|
||||
from: Some(ChangeItemInfo::from(prev_func)),
|
||||
to: None,
|
||||
metadata: prev_func.metadata.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
items
|
||||
}
|
||||
|
||||
fn process_new_items(items: &[ReportItem]) -> Vec<ChangeItem> {
|
||||
items
|
||||
.iter()
|
||||
.map(|item| ChangeItem {
|
||||
name: item.name.clone(),
|
||||
from: None,
|
||||
to: Some(ChangeItemInfo::from(item)),
|
||||
metadata: item.metadata.clone(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn measure_line_matched(
|
||||
name: &str,
|
||||
from: u64,
|
||||
from_percent: f32,
|
||||
to: u64,
|
||||
to_percent: f32,
|
||||
) -> String {
|
||||
let emoji = if to > from { "📈" } else { "📉" };
|
||||
let percent_diff = to_percent - from_percent;
|
||||
let percent_str = if percent_diff < 0.0 {
|
||||
format!("{percent_diff:.2}%")
|
||||
} else {
|
||||
format!("+{percent_diff:.2}%")
|
||||
};
|
||||
let bytes_diff = to as i64 - from as i64;
|
||||
let bytes_str = match bytes_diff.cmp(&0) {
|
||||
Ordering::Less => bytes_diff.to_string(),
|
||||
Ordering::Equal | Ordering::Greater => format!("+{bytes_diff}"),
|
||||
};
|
||||
format!("{emoji} **{name}**: {to_percent:.2}% ({percent_str}, {bytes_str} bytes)\n")
|
||||
}
|
||||
|
||||
fn measure_line_bytes(name: &str, from: u64, to: u64) -> String {
|
||||
let diff = to as i64 - from as i64;
|
||||
let diff_str = match diff.cmp(&0) {
|
||||
Ordering::Less => diff.to_string(),
|
||||
Ordering::Equal | Ordering::Greater => format!("+{diff}"),
|
||||
};
|
||||
format!("**{name}**: {to} bytes ({diff_str} bytes)\n")
|
||||
}
|
||||
|
||||
fn measure_line_simple(name: &str, from: u64, to: u64) -> String {
|
||||
let diff = to as i64 - from as i64;
|
||||
let diff_str = match diff.cmp(&0) {
|
||||
Ordering::Less => diff.to_string(),
|
||||
Ordering::Equal | Ordering::Greater => format!("+{diff}"),
|
||||
};
|
||||
format!("**{name}**: {to} ({diff_str})\n")
|
||||
}
|
||||
|
||||
fn generate_comment(
|
||||
from: &FullReportFile,
|
||||
to: &ProcessArtifactResult,
|
||||
to_commit: &Commit,
|
||||
changes: Changes,
|
||||
) -> String {
|
||||
let mut comment = format!(
|
||||
"### Report for {} ({} - {})\n\n",
|
||||
to.version,
|
||||
&from.commit.sha[..7],
|
||||
&to_commit.sha[..7]
|
||||
);
|
||||
let mut measure_written = false;
|
||||
let from_measures = from.report.measures;
|
||||
let to_measures = to.report.measures.unwrap_or_default();
|
||||
if from_measures.total_code != to_measures.total_code {
|
||||
comment.push_str(&measure_line_bytes(
|
||||
"Total code",
|
||||
from_measures.total_code,
|
||||
to_measures.total_code,
|
||||
));
|
||||
measure_written = true;
|
||||
}
|
||||
if from_measures.total_functions != to_measures.total_functions {
|
||||
comment.push_str(&measure_line_simple(
|
||||
"Total functions",
|
||||
from_measures.total_functions as u64,
|
||||
to_measures.total_functions as u64,
|
||||
));
|
||||
measure_written = true;
|
||||
}
|
||||
if from_measures.matched_code != to_measures.matched_code {
|
||||
comment.push_str(&measure_line_matched(
|
||||
"Matched code",
|
||||
from_measures.matched_code,
|
||||
from_measures.matched_code_percent,
|
||||
to_measures.matched_code,
|
||||
to_measures.matched_code_percent,
|
||||
));
|
||||
measure_written = true;
|
||||
}
|
||||
if from_measures.complete_code != to_measures.complete_code {
|
||||
comment.push_str(&measure_line_matched(
|
||||
"Linked code",
|
||||
from_measures.complete_code,
|
||||
from_measures.complete_code_percent,
|
||||
to_measures.complete_code,
|
||||
to_measures.complete_code_percent,
|
||||
));
|
||||
measure_written = true;
|
||||
}
|
||||
if measure_written {
|
||||
comment.push('\n');
|
||||
}
|
||||
let mut total_changes = 0;
|
||||
let mut iter = changes.units.into_iter().flat_map(|mut unit| {
|
||||
let functions = take(&mut unit.functions);
|
||||
functions.into_iter().map(move |f| (unit.clone(), f))
|
||||
});
|
||||
for (unit, item) in iter.by_ref() {
|
||||
let (from, to) = match (item.from, item.to) {
|
||||
(Some(from), Some(to)) => (from, to),
|
||||
(None, Some(to)) => (ChangeItemInfo::default(), to),
|
||||
(Some(from), None) => (from, ChangeItemInfo::default()),
|
||||
(None, None) => continue,
|
||||
};
|
||||
let emoji = if to.fuzzy_match_percent == 100.0 {
|
||||
"✅"
|
||||
} else if to.fuzzy_match_percent > from.fuzzy_match_percent {
|
||||
"📈"
|
||||
} else {
|
||||
"📉"
|
||||
};
|
||||
let from_bytes = ((from.fuzzy_match_percent as f64 / 100.0) * from.size as f64) as u64;
|
||||
let to_bytes = ((to.fuzzy_match_percent as f64 / 100.0) * to.size as f64) as u64;
|
||||
let bytes_diff = to_bytes as i64 - from_bytes as i64;
|
||||
let bytes_str = match bytes_diff.cmp(&0) {
|
||||
Ordering::Less => bytes_diff.to_string(),
|
||||
Ordering::Equal => "0".to_string(),
|
||||
Ordering::Greater => format!("+{}", bytes_diff),
|
||||
};
|
||||
let name =
|
||||
item.metadata.as_ref().and_then(|m| m.demangled_name.as_deref()).unwrap_or(&item.name);
|
||||
comment.push_str(&format!(
|
||||
"{emoji} `{} | {}` {} bytes -> {:.2}%\n",
|
||||
unit.name, name, bytes_str, to.fuzzy_match_percent
|
||||
));
|
||||
total_changes += 1;
|
||||
if total_changes >= 30 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let remaining = iter.count();
|
||||
if remaining > 0 {
|
||||
comment.push_str(&format!("...and {} more items\n", remaining));
|
||||
} else if total_changes == 0 {
|
||||
comment.push_str("No changes\n");
|
||||
}
|
||||
comment
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
fn platform_name(platform: &str) -> &str {
|
||||
match platform {
|
||||
"gc" => "GameCube",
|
||||
"wii" => "Wii",
|
||||
"n64" => "Nintendo 64",
|
||||
"switch" => "Nintendo Switch",
|
||||
"3ds" => "Nintendo 3DS",
|
||||
"nds" => "Nintendo DS",
|
||||
"gba" => "Game Boy Advance",
|
||||
"gbc" => "Game Boy Color",
|
||||
"ps" => "PlayStation",
|
||||
"ps2" => "PlayStation 2",
|
||||
_ => platform,
|
||||
Ok(GitHubEvent { event: value, state: inner })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "decomp-dev-images"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
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
|
||||
decomp-dev-core = { path = "../core" }
|
||||
tokio.workspace = true
|
||||
mime.workspace = true
|
||||
streemap = "0.1"
|
||||
palette = "0.7"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user