diff --git a/.sqlx/query-4f0ac4223f5bcedc2f382bdd76fdbcb415496435223f87cc40583b82c3ea2bbd.json b/.sqlx/query-4f0ac4223f5bcedc2f382bdd76fdbcb415496435223f87cc40583b82c3ea2bbd.json new file mode 100644 index 0000000..0ff8d74 --- /dev/null +++ b/.sqlx/query-4f0ac4223f5bcedc2f382bdd76fdbcb415496435223f87cc40583b82c3ea2bbd.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n DELETE FROM reports\n WHERE project_id = ? AND git_commit = ? COLLATE NOCASE\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "4f0ac4223f5bcedc2f382bdd76fdbcb415496435223f87cc40583b82c3ea2bbd" +} diff --git a/crates/db/src/lib.rs b/crates/db/src/lib.rs index 3936711..dca7187 100644 --- a/crates/db/src/lib.rs +++ b/crates/db/src/lib.rs @@ -1106,6 +1106,35 @@ impl Database { } Ok(()) } + + pub async fn delete_reports_by_commit( + &self, + project_id: u64, + commit_sha: &str, + ) -> Result { + let mut conn = self.pool.acquire().await?; + let project_id_db = project_id as i64; + let deleted_count = sqlx::query!( + r#" + DELETE FROM reports + WHERE project_id = ? AND git_commit = ? COLLATE NOCASE + "#, + project_id_db, + commit_sha, + ) + .execute(&mut *conn) + .await? + .rows_affected(); + if deleted_count > 0 { + tracing::info!( + "Deleted {} reports for project ID {} commit {}", + deleted_count, + project_id, + commit_sha + ); + } + Ok(deleted_count as usize) + } } thread_local! { diff --git a/crates/github/src/changes.rs b/crates/github/src/changes.rs index 0ccedba..b3ce1eb 100644 --- a/crates/github/src/changes.rs +++ b/crates/github/src/changes.rs @@ -166,7 +166,7 @@ fn measure_line_simple(name: &str, from: u64, to: u64) -> String { const MAX_CHANGE_LINES: usize = 30; // Note: The order the tables are printed in is determined by the order of the variants in this enum. -#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug)] +#[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Debug)] enum ChangeKind { NewMatch, BrokenMatch, @@ -174,6 +174,35 @@ enum ChangeKind { Regression, } +impl ChangeKind { + fn emoji(self) -> &'static str { + match self { + ChangeKind::NewMatch => "✅", + ChangeKind::BrokenMatch => "💔", + ChangeKind::Improvement => "📈", + ChangeKind::Regression => "📉", + } + } + + fn singular_description(self) -> &'static str { + match self { + ChangeKind::NewMatch => "new match", + ChangeKind::BrokenMatch => "broken match", + ChangeKind::Improvement => "improvement in unmatched functions", + ChangeKind::Regression => "regression in unmatched functions", + } + } + + fn plural_description(self) -> &'static str { + match self { + ChangeKind::NewMatch => "new matches", + ChangeKind::BrokenMatch => "broken matches", + ChangeKind::Improvement => "improvements in unmatched functions", + ChangeKind::Regression => "regressions in unmatched functions", + } + } +} + struct ChangeLine { kind: ChangeKind, unit_name: String, @@ -206,25 +235,25 @@ fn generate_changes_list(changes: Vec, out: &mut String) { changes_by_kind.entry(change.kind.clone()).or_insert(vec![]).push(change); } for (change_kind, mut changes) in changes_by_kind { - let (emoji, description) = match change_kind { - ChangeKind::NewMatch => ("✅", "new matches"), - ChangeKind::BrokenMatch => ("💔", "broken matches"), - ChangeKind::Improvement => ("📈", "improvements in unmatched functions"), - ChangeKind::Regression => ("📉", "regressions in unmatched functions"), - }; - let total_changes = changes.len(); - if total_changes == 0 { - out.push_str(&format!("No {description}.\n")); + let description = if total_changes == 0 { + out.push_str(&format!("No {}.\n", change_kind.plural_description())); continue; - } + } else if total_changes == 1 { + change_kind.singular_description() + } else { + change_kind.plural_description() + }; if change_kind == ChangeKind::BrokenMatch { out.push_str("
\n"); } else { out.push_str("
\n"); } - out.push_str(&format!("{emoji} {total_changes} {description}\n")); + out.push_str(&format!( + "{} {total_changes} {description}\n", + change_kind.emoji() + )); out.push('\n'); // Must include a blank line before a table out.push_str("| Unit | Function | Bytes | Before | After |\n"); out.push_str("| - | - | - | - | - |\n"); diff --git a/crates/web/src/handlers/manage.rs b/crates/web/src/handlers/manage.rs index 9c6aa08..7197f34 100644 --- a/crates/web/src/handlers/manage.rs +++ b/crates/web/src/handlers/manage.rs @@ -22,7 +22,8 @@ use decomp_dev_github::{ }; use itertools::Itertools; use maud::{DOCTYPE, Markup, html}; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; +use tower_sessions::Session; use crate::{ AppState, @@ -339,6 +340,7 @@ pub async fn manage_project( State(state): State, current_user: CurrentUser, ctx: TemplateContext, + session: Session, ) -> Result { let Some(info) = state.db.get_project_info(¶ms.owner, ¶ms.repo, None).await? else { return Err(AppError::Status(StatusCode::NOT_FOUND)); @@ -361,10 +363,12 @@ pub async fn manage_project( } else { None }; - render_manage_project(ctx, &state, &info, report.as_ref(), ¤t_user, Message::None).await + render_manage_project(ctx, &state, &info, report.as_ref(), ¤t_user, session).await } +#[derive(Debug, Serialize, Deserialize, Default)] enum Message { + #[default] None, Info(String), Error(String), @@ -388,7 +392,7 @@ async fn render_manage_project( project_info: &ProjectInfo, latest_report: Option<&CachedReportFile>, current_user: &CurrentUser, - message: Message, + session: Session, ) -> Result { let project_short_name = project_info.project.short_name(); let project_manage_path = @@ -409,6 +413,11 @@ async fn render_manage_project( None }; + let message = session + .remove::(&format!("manage_{}_message", project_info.project.id)) + .await? + .unwrap_or_default(); + // Check if the project is hidden based on matched code percentage let visibility = project_visibility(&project_info.project, latest_report.map(|r| &r.report.measures)); @@ -635,10 +644,10 @@ pub async fn manage_project_save( } pub async fn manage_project_refresh( - ctx: TemplateContext, Path(params): Path, State(state): State, current_user: CurrentUser, + session: Session, ) -> Result { let Some(info) = state.db.get_project_info(¶ms.owner, ¶ms.repo, None).await? else { return Err(AppError::Status(StatusCode::NOT_FOUND)); @@ -658,20 +667,40 @@ pub async fn manage_project_refresh( Message::Error(format!("Failed to refresh project: {e}")) } }; + session.insert(&format!("manage_{}_message", info.project.id), message).await?; + let redirect_url = format!("/manage/{}/{}", params.owner, params.repo); + Ok(Redirect::to(&redirect_url).into_response()) +} - let report = if let (Some(version), Some(commit)) = (info.default_version(), &info.commit) { - state - .db - .get_report(&info.project.owner, &info.project.repo, &commit.sha, version) - .await - .with_context(|| { - format!( - "Failed to fetch report for {}/{} sha {} version {}", - info.project.owner, info.project.repo, commit.sha, version - ) - })? +#[derive(Deserialize)] +pub struct DeleteCommitParams { + #[serde(flatten)] + project: ProjectParams, + commit: String, +} + +pub async fn delete_commit( + Path(params): Path, + State(state): State, + current_user: CurrentUser, + session: Session, +) -> Result { + let Some(info) = + state.db.get_project_info(¶ms.project.owner, ¶ms.project.repo, None).await? + else { + return Err(AppError::Status(StatusCode::NOT_FOUND)); + }; + if !current_user.can_manage_repo(info.project.id) { + return Err(AppError::Status(StatusCode::FORBIDDEN)); + } + let num_reports_deleted = + state.db.delete_reports_by_commit(info.project.id, ¶ms.commit).await?; + let message = if num_reports_deleted > 0 { + Message::Info(format!("Deleted {num_reports_deleted} reports")) } else { - None + Message::Error("No reports found. Is the commit SHA correct?".to_string()) }; - render_manage_project(ctx, &state, &info, report.as_ref(), ¤t_user, message).await + session.insert(&format!("manage_{}_message", info.project.id), message).await?; + let redirect_url = format!("/manage/{}/{}", params.project.owner, params.project.repo); + Ok(Redirect::to(&redirect_url).into_response()) } diff --git a/crates/web/src/handlers/mod.rs b/crates/web/src/handlers/mod.rs index 89f1256..8a980a9 100644 --- a/crates/web/src/handlers/mod.rs +++ b/crates/web/src/handlers/mod.rs @@ -5,7 +5,7 @@ use axum::{ extract::{DefaultBodyLimit, Request}, http::{HeaderMap, HeaderValue, header, header::Entry}, response::Response, - routing::{get, post}, + routing::{delete, get, post}, }; use decomp_dev_images::image_mime_from_ext; use mime::Mime; @@ -69,6 +69,7 @@ pub fn build_router() -> Router { .layer(DefaultBodyLimit::max(50 * 1000 * 1000 /* 50MB */)), ) .route("/manage/{owner}/{repo}/refresh", post(manage::manage_project_refresh)) + .route("/manage/{owner}/{repo}/commit/{commit}", delete(manage::delete_commit)) .route("/og.png", get(decomp_dev_images::get_og)) .route("/", get(project::get_projects)) .route("/projects", get(project::get_projects))