treewide: format with nightly rustfmt; auto-fix Clippy lints

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I15d9215ab506b37954468d99746098326a6a6964
This commit is contained in:
raf 2026-02-08 02:15:06 +03:00
commit 73919f2f9e
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
30 changed files with 144 additions and 151 deletions

View file

@ -51,6 +51,7 @@ pub enum CiError {
} }
impl CiError { impl CiError {
#[must_use]
pub fn is_disk_full(&self) -> bool { pub fn is_disk_full(&self) -> bool {
let msg = self.to_string().to_lowercase(); let msg = self.to_string().to_lowercase();
msg.contains("no space left on device") msg.contains("no space left on device")
@ -78,7 +79,7 @@ pub fn check_disk_space(path: &std::path::Path) -> Result<DiskSpaceInfo> {
})?; })?;
let mut statfs: libc::statfs = unsafe { std::mem::zeroed() }; let mut statfs: libc::statfs = unsafe { std::mem::zeroed() };
if unsafe { libc::statfs(cpath.as_ptr(), &mut statfs) } != 0 { if unsafe { libc::statfs(cpath.as_ptr(), &raw mut statfs) } != 0 {
return Err(CiError::Io(std::io::Error::last_os_error())); return Err(CiError::Io(std::io::Error::last_os_error()));
} }
@ -185,16 +186,19 @@ pub struct DiskSpaceInfo {
impl DiskSpaceInfo { impl DiskSpaceInfo {
/// Check if disk space is critically low (less than 1GB available) /// Check if disk space is critically low (less than 1GB available)
#[must_use]
pub fn is_critical(&self) -> bool { pub fn is_critical(&self) -> bool {
self.available_gb < 1.0 self.available_gb < 1.0
} }
/// Check if disk space is low (less than 5GB available) /// Check if disk space is low (less than 5GB available)
#[must_use]
pub fn is_low(&self) -> bool { pub fn is_low(&self) -> bool {
self.available_gb < 5.0 self.available_gb < 5.0
} }
/// Get a human-readable summary /// Get a human-readable summary
#[must_use]
pub fn summary(&self) -> String { pub fn summary(&self) -> String {
format!( format!(
"Total: {:.1}GB, Free: {:.1}GB ({:.1}%), Available: {:.1}GB", "Total: {:.1}GB, Free: {:.1}GB ({:.1}%), Available: {:.1}GB",

View file

@ -8,7 +8,7 @@ use std::{
use tracing::{info, warn}; use tracing::{info, warn};
/// Remove GC root symlinks with mtime older than max_age. Returns count /// Remove GC root symlinks with mtime older than `max_age`. Returns count
/// removed. /// removed.
pub fn cleanup_old_roots( pub fn cleanup_old_roots(
roots_dir: &Path, roots_dir: &Path,

View file

@ -15,13 +15,15 @@ impl LogStorage {
} }
/// Returns the filesystem path where a build's log should be stored /// Returns the filesystem path where a build's log should be stored
#[must_use]
pub fn log_path(&self, build_id: &Uuid) -> PathBuf { pub fn log_path(&self, build_id: &Uuid) -> PathBuf {
self.log_dir.join(format!("{}.log", build_id)) self.log_dir.join(format!("{build_id}.log"))
} }
/// Returns the filesystem path for an active (in-progress) build log /// Returns the filesystem path for an active (in-progress) build log
#[must_use]
pub fn log_path_for_active(&self, build_id: &Uuid) -> PathBuf { pub fn log_path_for_active(&self, build_id: &Uuid) -> PathBuf {
self.log_dir.join(format!("{}.active.log", build_id)) self.log_dir.join(format!("{build_id}.active.log"))
} }
/// Write build log content to file /// Write build log content to file

View file

@ -96,7 +96,7 @@ pub async fn probe_flake(
}; };
let output = let output =
tokio::time::timeout(std::time::Duration::from_secs(60), async { tokio::time::timeout(std::time::Duration::from_mins(1), async {
tokio::process::Command::new("nix") tokio::process::Command::new("nix")
.args([ .args([
"--extra-experimental-features", "--extra-experimental-features",
@ -230,7 +230,7 @@ pub async fn probe_flake(
description: top description: top
.get("description") .get("description")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.map(|s| s.to_string()), .map(std::string::ToString::to_string),
url: Some(repo_url.to_string()), url: Some(repo_url.to_string()),
}; };

View file

@ -72,11 +72,11 @@ async fn run_command_notification(cmd: &str, build: &Build, project: &Project) {
match result { match result {
Ok(output) => { Ok(output) => {
if !output.status.success() { if output.status.success() {
info!(build_id = %build.id, "RunCommand completed successfully");
} else {
let stderr = String::from_utf8_lossy(&output.stderr); let stderr = String::from_utf8_lossy(&output.stderr);
warn!(build_id = %build.id, "RunCommand failed: {stderr}"); warn!(build_id = %build.id, "RunCommand failed: {stderr}");
} else {
info!(build_id = %build.id, "RunCommand completed successfully");
} }
}, },
Err(e) => error!(build_id = %build.id, "RunCommand execution failed: {e}"), Err(e) => error!(build_id = %build.id, "RunCommand execution failed: {e}"),
@ -90,12 +90,9 @@ async fn set_github_status(
build: &Build, build: &Build,
) { ) {
// Parse owner/repo from URL // Parse owner/repo from URL
let (owner, repo) = match parse_github_repo(repo_url) { let (owner, repo) = if let Some(v) = parse_github_repo(repo_url) { v } else {
Some(v) => v,
None => {
warn!("Cannot parse GitHub owner/repo from {repo_url}"); warn!("Cannot parse GitHub owner/repo from {repo_url}");
return; return;
},
}; };
let (state, description) = match build.status { let (state, description) = match build.status {
@ -125,12 +122,12 @@ async fn set_github_status(
.await .await
{ {
Ok(resp) => { Ok(resp) => {
if !resp.status().is_success() { if resp.status().is_success() {
info!(build_id = %build.id, "Set GitHub commit status: {state}");
} else {
let status = resp.status(); let status = resp.status();
let text = resp.text().await.unwrap_or_default(); let text = resp.text().await.unwrap_or_default();
warn!("GitHub status API returned {status}: {text}"); warn!("GitHub status API returned {status}: {text}");
} else {
info!(build_id = %build.id, "Set GitHub commit status: {state}");
} }
}, },
Err(e) => error!("GitHub status API request failed: {e}"), Err(e) => error!("GitHub status API request failed: {e}"),
@ -145,12 +142,9 @@ async fn set_gitea_status(
build: &Build, build: &Build,
) { ) {
// Parse owner/repo from URL (try to extract from the gitea URL) // Parse owner/repo from URL (try to extract from the gitea URL)
let (owner, repo) = match parse_gitea_repo(repo_url, base_url) { let (owner, repo) = if let Some(v) = parse_gitea_repo(repo_url, base_url) { v } else {
Some(v) => v,
None => {
warn!("Cannot parse Gitea owner/repo from {repo_url}"); warn!("Cannot parse Gitea owner/repo from {repo_url}");
return; return;
},
}; };
let (state, description) = match build.status { let (state, description) = match build.status {
@ -177,12 +171,12 @@ async fn set_gitea_status(
.await .await
{ {
Ok(resp) => { Ok(resp) => {
if !resp.status().is_success() { if resp.status().is_success() {
info!(build_id = %build.id, "Set Gitea commit status: {state}");
} else {
let status = resp.status(); let status = resp.status();
let text = resp.text().await.unwrap_or_default(); let text = resp.text().await.unwrap_or_default();
warn!("Gitea status API returned {status}: {text}"); warn!("Gitea status API returned {status}: {text}");
} else {
info!(build_id = %build.id, "Set Gitea commit status: {state}");
} }
}, },
Err(e) => error!("Gitea status API request failed: {e}"), Err(e) => error!("Gitea status API request failed: {e}"),
@ -197,12 +191,9 @@ async fn set_gitlab_status(
build: &Build, build: &Build,
) { ) {
// Parse project path from URL // Parse project path from URL
let project_path = match parse_gitlab_project(repo_url, base_url) { let project_path = if let Some(p) = parse_gitlab_project(repo_url, base_url) { p } else {
Some(p) => p,
None => {
warn!("Cannot parse GitLab project from {repo_url}"); warn!("Cannot parse GitLab project from {repo_url}");
return; return;
},
}; };
// GitLab uses different state names // GitLab uses different state names
@ -238,12 +229,12 @@ async fn set_gitlab_status(
.await .await
{ {
Ok(resp) => { Ok(resp) => {
if !resp.status().is_success() { if resp.status().is_success() {
info!(build_id = %build.id, "Set GitLab commit status: {state}");
} else {
let status = resp.status(); let status = resp.status();
let text = resp.text().await.unwrap_or_default(); let text = resp.text().await.unwrap_or_default();
warn!("GitLab status API returned {status}: {text}"); warn!("GitLab status API returned {status}: {text}");
} else {
info!(build_id = %build.id, "Set GitLab commit status: {state}");
} }
}, },
Err(e) => error!("GitLab status API request failed: {e}"), Err(e) => error!("GitLab status API request failed: {e}"),

View file

@ -45,7 +45,7 @@ pub async fn list_for_build(
} }
/// Batch check if all dependency builds are completed for multiple builds at /// Batch check if all dependency builds are completed for multiple builds at
/// once. Returns a map from build_id to whether all deps are completed. /// once. Returns a map from `build_id` to whether all deps are completed.
pub async fn check_deps_for_builds( pub async fn check_deps_for_builds(
pool: &PgPool, pool: &PgPool,
build_ids: &[Uuid], build_ids: &[Uuid],

View file

@ -179,7 +179,7 @@ pub async fn reset_orphaned(
Ok(result.rows_affected()) Ok(result.rows_affected())
} }
/// List builds with optional evaluation_id, status, system, and job_name /// List builds with optional `evaluation_id`, status, system, and `job_name`
/// filters, with pagination. /// filters, with pagination.
pub async fn list_filtered( pub async fn list_filtered(
pool: &PgPool, pool: &PgPool,
@ -305,7 +305,7 @@ pub async fn mark_signed(pool: &PgPool, id: Uuid) -> Result<()> {
} }
/// Batch-fetch completed builds by derivation paths. /// Batch-fetch completed builds by derivation paths.
/// Returns a map from drv_path to Build for deduplication. /// Returns a map from `drv_path` to Build for deduplication.
pub async fn get_completed_by_drv_paths( pub async fn get_completed_by_drv_paths(
pool: &PgPool, pool: &PgPool,
drv_paths: &[String], drv_paths: &[String],
@ -330,7 +330,7 @@ pub async fn get_completed_by_drv_paths(
) )
} }
/// Set the builder_id for a build. /// Set the `builder_id` for a build.
pub async fn set_builder( pub async fn set_builder(
pool: &PgPool, pool: &PgPool,
id: Uuid, id: Uuid,

View file

@ -58,7 +58,7 @@ pub async fn list_for_jobset(
.map_err(CiError::Database) .map_err(CiError::Database)
} }
/// List evaluations with optional jobset_id and status filters, with /// List evaluations with optional `jobset_id` and status filters, with
/// pagination. /// pagination.
pub async fn list_filtered( pub async fn list_filtered(
pool: &PgPool, pool: &PgPool,
@ -145,7 +145,7 @@ pub async fn set_inputs_hash(
Ok(()) Ok(())
} }
/// Check if an evaluation with the same inputs_hash already exists for this /// Check if an evaluation with the same `inputs_hash` already exists for this
/// jobset. /// jobset.
pub async fn get_by_inputs_hash( pub async fn get_by_inputs_hash(
pool: &PgPool, pool: &PgPool,

View file

@ -52,7 +52,7 @@ pub async fn get(pool: &PgPool, id: Uuid) -> Result<ProjectMember> {
.map_err(|e| { .map_err(|e| {
match e { match e {
sqlx::Error::RowNotFound => { sqlx::Error::RowNotFound => {
CiError::NotFound(format!("Project member {} not found", id)) CiError::NotFound(format!("Project member {id} not found"))
}, },
_ => CiError::Database(e), _ => CiError::Database(e),
} }
@ -123,7 +123,7 @@ pub async fn update(
.map_err(|e| { .map_err(|e| {
match e { match e {
sqlx::Error::RowNotFound => { sqlx::Error::RowNotFound => {
CiError::NotFound(format!("Project member {} not found", id)) CiError::NotFound(format!("Project member {id} not found"))
}, },
_ => CiError::Database(e), _ => CiError::Database(e),
} }
@ -141,8 +141,7 @@ pub async fn delete(pool: &PgPool, id: Uuid) -> Result<()> {
.await?; .await?;
if result.rows_affected() == 0 { if result.rows_affected() == 0 {
return Err(CiError::NotFound(format!( return Err(CiError::NotFound(format!(
"Project member {} not found", "Project member {id} not found"
id
))); )));
} }
Ok(()) Ok(())

View file

@ -9,7 +9,7 @@ use crate::{
}; };
/// Search entity types /// Search entity types
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SearchEntity { pub enum SearchEntity {
Projects, Projects,
Jobsets, Jobsets,
@ -18,14 +18,14 @@ pub enum SearchEntity {
} }
/// Sort order for search results /// Sort order for search results
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SortOrder { pub enum SortOrder {
Asc, Asc,
Desc, Desc,
} }
/// Sort field for builds /// Sort field for builds
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BuildSortField { pub enum BuildSortField {
CreatedAt, CreatedAt,
JobName, JobName,
@ -34,7 +34,7 @@ pub enum BuildSortField {
} }
/// Sort field for projects /// Sort field for projects
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProjectSortField { pub enum ProjectSortField {
Name, Name,
CreatedAt, CreatedAt,
@ -42,7 +42,7 @@ pub enum ProjectSortField {
} }
/// Build status filter /// Build status filter
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BuildStatusFilter { pub enum BuildStatusFilter {
Pending, Pending,
Running, Running,
@ -492,7 +492,7 @@ pub async fn quick_search(
query: &str, query: &str,
limit: i64, limit: i64,
) -> Result<(Vec<Project>, Vec<Build>)> { ) -> Result<(Vec<Project>, Vec<Build>)> {
let pattern = format!("%{}%", query); let pattern = format!("%{query}%");
let projects = sqlx::query_as::<_, Project>( let projects = sqlx::query_as::<_, Project>(
"SELECT * FROM projects WHERE name ILIKE $1 OR description ILIKE $1 ORDER \ "SELECT * FROM projects WHERE name ILIKE $1 OR description ILIKE $1 ORDER \

View file

@ -43,7 +43,7 @@ pub async fn get(pool: &PgPool, id: Uuid) -> Result<StarredJob> {
.map_err(|e| { .map_err(|e| {
match e { match e {
sqlx::Error::RowNotFound => { sqlx::Error::RowNotFound => {
CiError::NotFound(format!("Starred job {} not found", id)) CiError::NotFound(format!("Starred job {id} not found"))
}, },
_ => CiError::Database(e), _ => CiError::Database(e),
} }
@ -107,7 +107,7 @@ pub async fn delete(pool: &PgPool, id: Uuid) -> Result<()> {
.execute(pool) .execute(pool)
.await?; .await?;
if result.rows_affected() == 0 { if result.rows_affected() == 0 {
return Err(CiError::NotFound(format!("Starred job {} not found", id))); return Err(CiError::NotFound(format!("Starred job {id} not found")));
} }
Ok(()) Ok(())
} }

View file

@ -29,7 +29,7 @@ pub fn hash_password(password: &str) -> Result<String> {
argon2 argon2
.hash_password(password.as_bytes(), &salt) .hash_password(password.as_bytes(), &salt)
.map(|h| h.to_string()) .map(|h| h.to_string())
.map_err(|e| CiError::Internal(format!("Password hashing failed: {}", e))) .map_err(|e| CiError::Internal(format!("Password hashing failed: {e}")))
} }
/// Verify a password against a hash /// Verify a password against a hash
@ -37,7 +37,7 @@ pub fn verify_password(password: &str, hash: &str) -> Result<bool> {
use argon2::{Argon2, PasswordHash, PasswordVerifier}; use argon2::{Argon2, PasswordHash, PasswordVerifier};
let parsed_hash = PasswordHash::new(hash) let parsed_hash = PasswordHash::new(hash)
.map_err(|e| CiError::Internal(format!("Invalid password hash: {}", e)))?; .map_err(|e| CiError::Internal(format!("Invalid password hash: {e}")))?;
let argon2 = Argon2::default(); let argon2 = Argon2::default();
Ok( Ok(
argon2 argon2
@ -134,7 +134,7 @@ pub async fn get(pool: &PgPool, id: Uuid) -> Result<User> {
.map_err(|e| { .map_err(|e| {
match e { match e {
sqlx::Error::RowNotFound => { sqlx::Error::RowNotFound => {
CiError::NotFound(format!("User {} not found", id)) CiError::NotFound(format!("User {id} not found"))
}, },
_ => CiError::Database(e), _ => CiError::Database(e),
} }
@ -321,7 +321,7 @@ pub async fn delete(pool: &PgPool, id: Uuid) -> Result<()> {
.execute(pool) .execute(pool)
.await?; .await?;
if result.rows_affected() == 0 { if result.rows_affected() == 0 {
return Err(CiError::NotFound(format!("User {} not found", id))); return Err(CiError::NotFound(format!("User {id} not found")));
} }
Ok(()) Ok(())
} }
@ -335,7 +335,7 @@ pub async fn upsert_oauth_user(
oauth_provider_id: &str, oauth_provider_id: &str,
) -> Result<User> { ) -> Result<User> {
// Use provider ID in username to avoid collisions // Use provider ID in username to avoid collisions
let unique_username = format!("{}_{}", username, oauth_provider_id); let unique_username = format!("{username}_{oauth_provider_id}");
// Check if user exists by OAuth provider ID pattern // Check if user exists by OAuth provider ID pattern
let existing = let existing =
@ -381,7 +381,7 @@ pub async fn upsert_oauth_user(
VALUES ($1, $2, $3, NULL, 'read-only') RETURNING *", VALUES ($1, $2, $3, NULL, 'read-only') RETURNING *",
) )
.bind(&unique_username) .bind(&unique_username)
.bind(email.unwrap_or(&format!("{}@oauth.local", unique_username))) .bind(email.unwrap_or(&format!("{unique_username}@oauth.local")))
.bind(user_type_str) .bind(user_type_str)
.fetch_one(pool) .fetch_one(pool)
.await .await
@ -395,7 +395,7 @@ pub async fn upsert_oauth_user(
}) })
} }
/// Create a new session for a user. Returns (session_token, session_id). /// Create a new session for a user. Returns (`session_token`, `session_id`).
pub async fn create_session( pub async fn create_session(
pool: &PgPool, pool: &PgPool,
user_id: Uuid, user_id: Uuid,

View file

@ -49,16 +49,19 @@ pub const VALID_PROJECT_ROLES: &[&str] = &[
]; ];
/// Check if a global role is valid /// Check if a global role is valid
#[must_use]
pub fn is_valid_role(role: &str) -> bool { pub fn is_valid_role(role: &str) -> bool {
VALID_ROLES.contains(&role) VALID_ROLES.contains(&role)
} }
/// Check if a project role is valid /// Check if a project role is valid
#[must_use]
pub fn is_valid_project_role(role: &str) -> bool { pub fn is_valid_project_role(role: &str) -> bool {
VALID_PROJECT_ROLES.contains(&role) VALID_PROJECT_ROLES.contains(&role)
} }
/// Get the highest project role (for permission checks) /// Get the highest project role (for permission checks)
#[must_use]
pub fn project_role_level(role: &str) -> i32 { pub fn project_role_level(role: &str) -> i32 {
match role { match role {
PROJECT_ROLE_ADMIN => 3, PROJECT_ROLE_ADMIN => 3,
@ -70,6 +73,7 @@ pub fn project_role_level(role: &str) -> i32 {
/// Check if user has required project permission /// Check if user has required project permission
/// Higher level roles automatically have lower level permissions /// Higher level roles automatically have lower level permissions
#[must_use]
pub fn has_project_permission(user_role: &str, required: &str) -> bool { pub fn has_project_permission(user_role: &str, required: &str) -> bool {
let user_level = project_role_level(user_role); let user_level = project_role_level(user_role);
let required_level = project_role_level(required); let required_level = project_role_level(required);

View file

@ -160,7 +160,7 @@ pub fn validate_full_name(name: &str) -> Result<(), ValidationError> {
}); });
} }
if name.chars().any(|c| c.is_control()) { if name.chars().any(char::is_control) {
return Err(ValidationError { return Err(ValidationError {
field: "full_name".to_string(), field: "full_name".to_string(),
message: "Full name cannot contain control characters".to_string(), message: "Full name cannot contain control characters".to_string(),

View file

@ -3,7 +3,7 @@ use std::path::{Path, PathBuf};
use fc_common::error::Result; use fc_common::error::Result;
use git2::Repository; use git2::Repository;
/// Clone or fetch a repository. Returns (repo_path, commit_hash). /// Clone or fetch a repository. Returns (`repo_path`, `commit_hash`).
/// ///
/// If `branch` is `Some`, resolve `refs/remotes/origin/<branch>` instead of /// If `branch` is `Some`, resolve `refs/remotes/origin/<branch>` instead of
/// HEAD. /// HEAD.

View file

@ -72,8 +72,8 @@ pub fn parse_eval_output(stdout: &str) -> EvalResult {
} }
/// Evaluate nix expressions and return discovered jobs. /// Evaluate nix expressions and return discovered jobs.
/// If flake_mode is true, uses nix-eval-jobs with --flake flag. /// If `flake_mode` is true, uses nix-eval-jobs with --flake flag.
/// If flake_mode is false, evaluates a legacy expression file. /// If `flake_mode` is false, evaluates a legacy expression file.
#[tracing::instrument(skip(config, inputs), fields(flake_mode, nix_expression))] #[tracing::instrument(skip(config, inputs), fields(flake_mode, nix_expression))]
pub async fn evaluate( pub async fn evaluate(
repo_path: &Path, repo_path: &Path,
@ -286,7 +286,7 @@ async fn evaluate_with_nix_eval(
let system = drv_val let system = drv_val
.get("system") .get("system")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.map(|s| s.to_string()); .map(std::string::ToString::to_string);
jobs.push(NixJob { jobs.push(NixJob {
name: name.clone(), name: name.clone(),
drv_path: drv_path.clone(), drv_path: drv_path.clone(),

View file

@ -68,7 +68,7 @@ pub async fn run_nix_build_remote(
buf buf
}); });
let live_log_path_owned = live_log_path.map(|p| p.to_path_buf()); let live_log_path_owned = live_log_path.map(std::path::Path::to_path_buf);
let stderr_task = tokio::spawn(async move { let stderr_task = tokio::spawn(async move {
let mut buf = String::new(); let mut buf = String::new();
let steps: Vec<SubStep> = Vec::new(); let steps: Vec<SubStep> = Vec::new();
@ -146,6 +146,7 @@ pub struct SubStep {
/// Parse a single nix internal JSON log line (`@nix {...}`). /// Parse a single nix internal JSON log line (`@nix {...}`).
/// Returns `Some(action, drv_path)` if the line contains a derivation action. /// Returns `Some(action, drv_path)` if the line contains a derivation action.
#[must_use]
pub fn parse_nix_log_line(line: &str) -> Option<(&'static str, String)> { pub fn parse_nix_log_line(line: &str) -> Option<(&'static str, String)> {
let json_str = line.strip_prefix("@nix ")?.trim(); let json_str = line.strip_prefix("@nix ")?.trim();
let parsed: serde_json::Value = serde_json::from_str(json_str).ok()?; let parsed: serde_json::Value = serde_json::from_str(json_str).ok()?;
@ -209,7 +210,7 @@ pub async fn run_nix_build(
}); });
// Read stderr (logs + internal JSON) // Read stderr (logs + internal JSON)
let live_log_path_owned = live_log_path.map(|p| p.to_path_buf()); let live_log_path_owned = live_log_path.map(std::path::Path::to_path_buf);
let stderr_task = tokio::spawn(async move { let stderr_task = tokio::spawn(async move {
let mut buf = String::new(); let mut buf = String::new();
let mut steps: Vec<SubStep> = Vec::new(); let mut steps: Vec<SubStep> = Vec::new();

View file

@ -31,6 +31,7 @@ pub struct WorkerPool {
impl WorkerPool { impl WorkerPool {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
#[must_use]
pub fn new( pub fn new(
db_pool: PgPool, db_pool: PgPool,
workers: usize, workers: usize,
@ -348,7 +349,7 @@ async fn run_build(
match result { match result {
Ok(build_result) => { Ok(build_result) => {
// Complete the build step // Complete the build step
let exit_code = if build_result.success { 0 } else { 1 }; let exit_code = i32::from(!build_result.success);
repo::build_steps::complete( repo::build_steps::complete(
pool, pool,
step.id, step.id,
@ -366,7 +367,7 @@ async fn run_build(
command: format!("nix build {}", sub_step.drv_path), command: format!("nix build {}", sub_step.drv_path),
}) })
.await?; .await?;
let sub_exit = if sub_step.success { 0 } else { 1 }; let sub_exit = i32::from(!sub_step.success);
repo::build_steps::complete(pool, sub.id, sub_exit, None, None).await?; repo::build_steps::complete(pool, sub.id, sub_exit, None, None).await?;
} }
@ -476,7 +477,7 @@ async fn run_build(
} }
let primary_output = let primary_output =
build_result.output_paths.first().map(|s| s.as_str()); build_result.output_paths.first().map(std::string::String::as_str);
repo::builds::complete( repo::builds::complete(
pool, pool,

View file

@ -77,7 +77,7 @@ pub async fn require_api_key(
{ {
// Check session expiry (24 hours) // Check session expiry (24 hours)
if session.created_at.elapsed() if session.created_at.elapsed()
< std::time::Duration::from_secs(24 * 60 * 60) < std::time::Duration::from_hours(24)
{ {
// Insert both user and session data // Insert both user and session data
if let Some(ref user) = session.user { if let Some(ref user) = session.user {
@ -87,12 +87,11 @@ pub async fn require_api_key(
request.extensions_mut().insert(api_key.clone()); request.extensions_mut().insert(api_key.clone());
} }
return Ok(next.run(request).await); return Ok(next.run(request).await);
} else { }
// Expired, remove it // Expired, remove it
drop(session); drop(session);
state.sessions.remove(&session_id); state.sessions.remove(&session_id);
} }
}
// Try legacy API key session (fc_session cookie) // Try legacy API key session (fc_session cookie)
if let Some(session_id) = parse_cookie(cookie_header, "fc_session") if let Some(session_id) = parse_cookie(cookie_header, "fc_session")
@ -100,19 +99,18 @@ pub async fn require_api_key(
{ {
// Check session expiry (24 hours) // Check session expiry (24 hours)
if session.created_at.elapsed() if session.created_at.elapsed()
< std::time::Duration::from_secs(24 * 60 * 60) < std::time::Duration::from_hours(24)
{ {
if let Some(ref api_key) = session.api_key { if let Some(ref api_key) = session.api_key {
request.extensions_mut().insert(api_key.clone()); request.extensions_mut().insert(api_key.clone());
} }
return Ok(next.run(request).await); return Ok(next.run(request).await);
} else { }
// Expired, remove it // Expired, remove it
drop(session); drop(session);
state.sessions.remove(&session_id); state.sessions.remove(&session_id);
} }
} }
}
// No valid auth found // No valid auth found
if is_read { if is_read {
@ -138,7 +136,7 @@ impl FromRequestParts<AppState> for RequireAdmin {
&& user.role == "admin" && user.role == "admin"
{ {
// Create a synthetic API key for compatibility // Create a synthetic API key for compatibility
return Ok(RequireAdmin(ApiKey { return Ok(Self(ApiKey {
id: user.id, id: user.id,
name: user.username.clone(), name: user.username.clone(),
key_hash: String::new(), key_hash: String::new(),
@ -157,7 +155,7 @@ impl FromRequestParts<AppState> for RequireAdmin {
.ok_or(StatusCode::UNAUTHORIZED)?; .ok_or(StatusCode::UNAUTHORIZED)?;
if key.role == "admin" { if key.role == "admin" {
Ok(RequireAdmin(key)) Ok(Self(key))
} else { } else {
Err(StatusCode::FORBIDDEN) Err(StatusCode::FORBIDDEN)
} }
@ -216,7 +214,7 @@ pub async fn extract_session(
.headers() .headers()
.get("cookie") .get("cookie")
.and_then(|v| v.to_str().ok()) .and_then(|v| v.to_str().ok())
.map(|s| s.to_string()); .map(std::string::ToString::to_string);
if let Some(cookie_header) = cookie_header { if let Some(cookie_header) = cookie_header {
// Try user session first // Try user session first
@ -225,7 +223,7 @@ pub async fn extract_session(
{ {
// Check session expiry // Check session expiry
if session.created_at.elapsed() if session.created_at.elapsed()
< std::time::Duration::from_secs(24 * 60 * 60) < std::time::Duration::from_hours(24)
{ {
if let Some(ref user) = session.user { if let Some(ref user) = session.user {
request.extensions_mut().insert(user.clone()); request.extensions_mut().insert(user.clone());
@ -245,7 +243,7 @@ pub async fn extract_session(
{ {
// Check session expiry // Check session expiry
if session.created_at.elapsed() if session.created_at.elapsed()
< std::time::Duration::from_secs(24 * 60 * 60) < std::time::Duration::from_hours(24)
{ {
if let Some(ref api_key) = session.api_key { if let Some(ref api_key) = session.api_key {
request.extensions_mut().insert(api_key.clone()); request.extensions_mut().insert(api_key.clone());

View file

@ -9,7 +9,7 @@ pub struct ApiError(pub CiError);
impl From<CiError> for ApiError { impl From<CiError> for ApiError {
fn from(err: CiError) -> Self { fn from(err: CiError) -> Self {
ApiError(err) Self(err)
} }
} }
@ -45,12 +45,11 @@ impl IntoResponse for ApiError {
StatusCode::INSUFFICIENT_STORAGE, StatusCode::INSUFFICIENT_STORAGE,
"DISK_FULL", "DISK_FULL",
format!( format!(
"{}\n\nDISK SPACE ISSUE DETECTED:\nThe server has run out of \ "{msg}\n\nDISK SPACE ISSUE DETECTED:\nThe server has run out of \
disk space. Please free up space:\n- Run `nix-collect-garbage \ disk space. Please free up space:\n- Run `nix-collect-garbage \
-d` to clean the Nix store\n- Clear the evaluator work \ -d` to clean the Nix store\n- Clear the evaluator work \
directory: `rm -rf /tmp/fc-evaluator/*`\n- Clear build logs if \ directory: `rm -rf /tmp/fc-evaluator/*`\n- Clear build logs if \
configured", configured"
msg
), ),
) )
} else { } else {
@ -66,11 +65,10 @@ impl IntoResponse for ApiError {
StatusCode::INSUFFICIENT_STORAGE, StatusCode::INSUFFICIENT_STORAGE,
"DISK_FULL", "DISK_FULL",
format!( format!(
"{}\n\nDISK SPACE ISSUE:\nThe server is running low on disk \ "{msg}\n\nDISK SPACE ISSUE:\nThe server is running low on disk \
space. Please free up space:\n- Run `nix-collect-garbage -d` to \ space. Please free up space:\n- Run `nix-collect-garbage -d` to \
clean the Nix store\n- Clear the evaluator work directory\n- \ clean the Nix store\n- Clear the evaluator work directory\n- \
Clear build logs if configured", Clear build logs if configured"
msg
), ),
) )
}, },
@ -92,9 +90,8 @@ impl IntoResponse for ApiError {
StatusCode::INSUFFICIENT_STORAGE, StatusCode::INSUFFICIENT_STORAGE,
"DISK_FULL", "DISK_FULL",
format!( format!(
"Database error: {}\n\nDISK SPACE ISSUE:\nThe server is running \ "Database error: {e}\n\nDISK SPACE ISSUE:\nThe server is running \
low on disk space.", low on disk space."
e
), ),
) )
} else { } else {
@ -133,12 +130,11 @@ impl IntoResponse for ApiError {
StatusCode::INSUFFICIENT_STORAGE, StatusCode::INSUFFICIENT_STORAGE,
"DISK_FULL", "DISK_FULL",
format!( format!(
"IO error: {}\n\nDISK SPACE ISSUE DETECTED:\nThe server has run \ "IO error: {msg}\n\nDISK SPACE ISSUE DETECTED:\nThe server has run \
out of disk space. Please free up space:\n- Run \ out of disk space. Please free up space:\n- Run \
`nix-collect-garbage -d` to clean the Nix store\n- Clear the \ `nix-collect-garbage -d` to clean the Nix store\n- Clear the \
evaluator work directory: `rm -rf /tmp/fc-evaluator/*`\n- \ evaluator work directory: `rm -rf /tmp/fc-evaluator/*`\n- \
Clear build logs if configured", Clear build logs if configured"
msg
), ),
) )
} else { } else {
@ -160,9 +156,8 @@ impl IntoResponse for ApiError {
StatusCode::INSUFFICIENT_STORAGE, StatusCode::INSUFFICIENT_STORAGE,
"DISK_FULL", "DISK_FULL",
format!( format!(
"{}\n\nDISK SPACE ISSUE:\nThe server is running low on disk \ "{msg}\n\nDISK SPACE ISSUE:\nThe server is running low on disk \
space. Please free up space.", space. Please free up space."
msg
), ),
) )
} else { } else {

View file

@ -29,6 +29,7 @@ pub struct ApiKeyInfo {
pub last_used_at: Option<chrono::DateTime<chrono::Utc>>, pub last_used_at: Option<chrono::DateTime<chrono::Utc>>,
} }
#[must_use]
pub fn hash_api_key(key: &str) -> String { pub fn hash_api_key(key: &str) -> String {
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
hasher.update(key.as_bytes()); hasher.update(key.as_bytes());

View file

@ -10,7 +10,7 @@ use tokio::process::Command;
use crate::{error::ApiError, state::AppState}; use crate::{error::ApiError, state::AppState};
/// Serve NARInfo for a store path hash. /// Serve `NARInfo` for a store path hash.
/// GET /nix-cache/{hash}.narinfo /// GET /nix-cache/{hash}.narinfo
async fn narinfo( async fn narinfo(
State(state): State<AppState>, State(state): State<AppState>,
@ -68,7 +68,7 @@ async fn narinfo(
}; };
let nar_hash = entry.get("narHash").and_then(|v| v.as_str()).unwrap_or(""); let nar_hash = entry.get("narHash").and_then(|v| v.as_str()).unwrap_or("");
let nar_size = entry.get("narSize").and_then(|v| v.as_u64()).unwrap_or(0); let nar_size = entry.get("narSize").and_then(serde_json::Value::as_u64).unwrap_or(0);
let store_path = entry let store_path = entry
.get("path") .get("path")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())

View file

@ -186,7 +186,9 @@ async fn compare_evaluations(
// Jobs in both: compare derivation paths // Jobs in both: compare derivation paths
for (name, from_build) in &from_map { for (name, from_build) in &from_map {
if let Some(to_build) = to_map.get(name) { if let Some(to_build) = to_map.get(name) {
if from_build.drv_path != to_build.drv_path { if from_build.drv_path == to_build.drv_path {
unchanged_count += 1;
} else {
changed_jobs.push(JobChange { changed_jobs.push(JobChange {
job_name: name.to_string(), job_name: name.to_string(),
system: to_build.system.clone(), system: to_build.system.clone(),
@ -195,8 +197,6 @@ async fn compare_evaluations(
old_status: format!("{:?}", from_build.status), old_status: format!("{:?}", from_build.status),
new_status: format!("{:?}", to_build.status), new_status: format!("{:?}", to_build.status),
}); });
} else {
unchanged_count += 1;
} }
} }
} }

View file

@ -93,12 +93,9 @@ async fn stream_build_log(
if active_path.exists() { active_path.clone() } else { final_path.clone() } if active_path.exists() { active_path.clone() } else { final_path.clone() }
}; };
let file = match tokio::fs::File::open(&path).await { let file = if let Ok(f) = tokio::fs::File::open(&path).await { f } else {
Ok(f) => f,
Err(_) => {
yield Ok(Event::default().data("Failed to open log file")); yield Ok(Event::default().data("Failed to open log file"));
return; return;
}
}; };
let mut reader = BufReader::new(file); let mut reader = BufReader::new(file);

View file

@ -130,7 +130,7 @@ async fn prometheus_metrics(State(state): State<AppState>) -> Response {
output output
.push_str("\n# HELP fc_evaluations_total Total number of evaluations\n"); .push_str("\n# HELP fc_evaluations_total Total number of evaluations\n");
output.push_str("# TYPE fc_evaluations_total gauge\n"); output.push_str("# TYPE fc_evaluations_total gauge\n");
output.push_str(&format!("fc_evaluations_total {}\n", eval_count)); output.push_str(&format!("fc_evaluations_total {eval_count}\n"));
output.push_str("\n# HELP fc_evaluations_by_status Evaluations by status\n"); output.push_str("\n# HELP fc_evaluations_by_status Evaluations by status\n");
output.push_str("# TYPE fc_evaluations_by_status gauge\n"); output.push_str("# TYPE fc_evaluations_by_status gauge\n");

View file

@ -270,8 +270,7 @@ async fn github_callback(
"" ""
}; };
let clear_state = format!( let clear_state = format!(
"fc_oauth_state=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0{}", "fc_oauth_state=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0{secure_flag}"
secure_flag
); );
let session_cookie = format!( let session_cookie = format!(
"fc_user_session={}; HttpOnly; SameSite=Lax; Path=/; Max-Age={}{}", "fc_user_session={}; HttpOnly; SameSite=Lax; Path=/; Max-Age={}{}",

View file

@ -130,8 +130,8 @@ struct SearchRequest {
eval_before: Option<DateTime<Utc>>, eval_before: Option<DateTime<Utc>>,
// Sorting // Sorting
/// Sort builds by: created_at, job_name, status, priority (default: /// Sort builds by: `created_at`, `job_name`, status, priority (default:
/// created_at) /// `created_at`)
#[serde(rename = "build_sort")] #[serde(rename = "build_sort")]
build_sort: Option<String>, build_sort: Option<String>,
@ -139,12 +139,12 @@ struct SearchRequest {
#[serde(rename = "order")] #[serde(rename = "order")]
order: Option<String>, order: Option<String>,
/// Sort projects by: name, created_at (default: name) /// Sort projects by: name, `created_at` (default: name)
#[serde(rename = "project_sort")] #[serde(rename = "project_sort")]
project_sort: Option<String>, project_sort: Option<String>,
} }
fn default_limit() -> i64 { const fn default_limit() -> i64 {
20 20
} }

View file

@ -45,7 +45,7 @@ pub struct UserResponse {
impl From<User> for UserResponse { impl From<User> for UserResponse {
fn from(u: User) -> Self { fn from(u: User) -> Self {
UserResponse { Self {
id: u.id, id: u.id,
username: u.username, username: u.username,
email: u.email, email: u.email,

View file

@ -403,9 +403,7 @@ async fn handle_gitea_push(
.map_err(ApiError)?; .map_err(ApiError)?;
// Fall back to the other type if not found // Fall back to the other type if not found
let webhook_config = match webhook_config { let webhook_config = if let Some(c) = webhook_config { c } else {
Some(c) => c,
None => {
let alt = if forge_type == "gitea" { let alt = if forge_type == "gitea" {
"forgejo" "forgejo"
} else { } else {
@ -431,7 +429,6 @@ async fn handle_gitea_push(
)); ));
}, },
} }
},
}; };
// Verify signature if configured // Verify signature if configured

View file

@ -17,6 +17,7 @@ pub struct SessionData {
impl SessionData { impl SessionData {
/// Check if the session has admin role /// Check if the session has admin role
#[must_use]
pub fn is_admin(&self) -> bool { pub fn is_admin(&self) -> bool {
if let Some(ref user) = self.user { if let Some(ref user) = self.user {
user.role == "admin" user.role == "admin"
@ -28,6 +29,7 @@ impl SessionData {
} }
/// Check if the session has a specific role /// Check if the session has a specific role
#[must_use]
pub fn has_role(&self, role: &str) -> bool { pub fn has_role(&self, role: &str) -> bool {
if self.is_admin() { if self.is_admin() {
return true; return true;
@ -42,6 +44,7 @@ impl SessionData {
} }
/// Get the display name for the session (username or api key name) /// Get the display name for the session (username or api key name)
#[must_use]
pub fn display_name(&self) -> String { pub fn display_name(&self) -> String {
if let Some(ref user) = self.user { if let Some(ref user) = self.user {
user.username.clone() user.username.clone()
@ -53,7 +56,8 @@ impl SessionData {
} }
/// Check if this is a user session (not just API key) /// Check if this is a user session (not just API key)
pub fn is_user_session(&self) -> bool { #[must_use]
pub const fn is_user_session(&self) -> bool {
self.user.is_some() self.user.is_some()
} }
} }