Cleanup & add commit delete endpoint

This commit is contained in:
Luke Street
2025-08-12 14:48:34 -06:00
parent 413795fd01
commit a0b9598048
5 changed files with 130 additions and 30 deletions
+29
View File
@@ -1106,6 +1106,35 @@ impl Database {
}
Ok(())
}
pub async fn delete_reports_by_commit(
&self,
project_id: u64,
commit_sha: &str,
) -> Result<usize> {
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! {
+41 -12
View File
@@ -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<ChangeLine>, 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("<details open>\n");
} else {
out.push_str("<details>\n");
}
out.push_str(&format!("<summary>{emoji} {total_changes} {description}</summary>\n"));
out.push_str(&format!(
"<summary>{} {total_changes} {description}</summary>\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");
+46 -17
View File
@@ -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<AppState>,
current_user: CurrentUser,
ctx: TemplateContext,
session: Session,
) -> Result<Response, AppError> {
let Some(info) = state.db.get_project_info(&params.owner, &params.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(), &current_user, Message::None).await
render_manage_project(ctx, &state, &info, report.as_ref(), &current_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<Response, AppError> {
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::<Message>(&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<ProjectParams>,
State(state): State<AppState>,
current_user: CurrentUser,
session: Session,
) -> Result<Response, AppError> {
let Some(info) = state.db.get_project_info(&params.owner, &params.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<DeleteCommitParams>,
State(state): State<AppState>,
current_user: CurrentUser,
session: Session,
) -> Result<Response, AppError> {
let Some(info) =
state.db.get_project_info(&params.project.owner, &params.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, &params.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(), &current_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())
}
+2 -1
View File
@@ -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<AppState> {
.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))