diff --git a/Cargo.lock b/Cargo.lock index ff15f63..5b1ed99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,21 +26,21 @@ dependencies = [ [[package]] name = "fs-err" -version = "3.1.1" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d7be93788013f265201256d58f04936a8079ad5dc898743aa20525f503b683" +checksum = "62d91fd049c123429b018c47887d3f75a265540dd3c30ba9cb7bae9197edb03a" dependencies = [ "autocfg", ] [[package]] name = "log" -version = "0.4.27" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "owo-colors" -version = "4.2.2" +version = "4.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e" +checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" diff --git a/Cargo.toml b/Cargo.toml index 063cd53..272a1dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,9 +9,9 @@ rust-version = "1.87" license = "MIT OR Apache-2.0" [dependencies] -fs-err = "3.1.0" -log = { version = "0.4.27", features = ["std"] } -owo-colors = "4.2.1" +fs-err = "3.2.0" +log = { version = "0.4.29", features = ["std"] } +owo-colors = "4.2.3" [dev-dependencies] cargo-husky = { version = "1.5.0", default-features = false, features = [ diff --git a/src/.github/workflows/test.yml b/src/.github/workflows/test.yml index f5af643..ec3eabd 100644 --- a/src/.github/workflows/test.yml +++ b/src/.github/workflows/test.yml @@ -16,7 +16,7 @@ jobs: container: image: ghcr.io/facet-rs/facet-ci:latest-amd64 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: Swatinem/rust-cache@v2 @@ -47,7 +47,7 @@ jobs: container: image: ghcr.io/facet-rs/facet-ci:latest-amd64 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: Swatinem/rust-cache@v2 @@ -68,7 +68,7 @@ jobs: container: image: ghcr.io/facet-rs/facet-ci:latest-amd64 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: Swatinem/rust-cache@v2 @@ -92,7 +92,7 @@ jobs: container: image: ghcr.io/facet-rs/facet-ci:latest-amd64 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: Swatinem/rust-cache@v2 @@ -111,7 +111,7 @@ jobs: container: image: ghcr.io/facet-rs/facet-ci:latest-amd64 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: Swatinem/rust-cache@v2 @@ -128,7 +128,7 @@ jobs: permissions: security-events: write # to upload sarif results steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: Swatinem/rust-cache@v2 @@ -145,7 +145,7 @@ jobs: continue-on-error: true - name: Upload SARIF results - uses: github/codeql-action/upload-sarif@v3 + uses: github/codeql-action/upload-sarif@v4 with: sarif_file: clippy-results.sarif wait-for-processing: true diff --git a/src/main.rs b/src/main.rs index 7a9bf31..21239ef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ use log::{Level, LevelFilter, Log, Metadata, Record, debug, error, warn}; use owo_colors::{OwoColorize, Style}; +#[cfg(unix)] use std::os::unix::fs::PermissionsExt; use std::sync::mpsc; use std::{ @@ -16,6 +17,7 @@ struct Job { path: PathBuf, old_content: Option>, new_content: Vec, + #[cfg(unix)] executable: bool, } @@ -26,15 +28,31 @@ impl Job { if &self.new_content != old { return false; } - // Check if executable bit would change - let current_executable = self - .path - .metadata() - .map(|m| m.permissions().mode() & 0o111 != 0) - .unwrap_or(false); - current_executable == self.executable + #[cfg(unix)] + { + // Check if executable bit would change + let current_executable = self + .path + .metadata() + .map(|m| m.permissions().mode() & 0o111 != 0) + .unwrap_or(false); + current_executable == self.executable + } + #[cfg(not(unix))] + { + true + } + } + None => { + #[cfg(unix)] + { + self.new_content.is_empty() && !self.executable + } + #[cfg(not(unix))] + { + self.new_content.is_empty() + } } - None => self.new_content.is_empty() && !self.executable, } } @@ -51,6 +69,7 @@ impl Job { fs::write(&self.path, &self.new_content)?; // Set executable bit if needed + #[cfg(unix)] if self.executable { let mut perms = fs::metadata(&self.path)?.permissions(); perms.set_mode(perms.mode() | 0o111); @@ -103,6 +122,7 @@ fn enqueue_readme_jobs(sender: std::sync::mpsc::Sender) { path: readme_path, old_content, new_content: readme_content.into_bytes(), + #[cfg(unix)] executable: false, }; @@ -283,6 +303,7 @@ fn enqueue_rustfmt_jobs(sender: std::sync::mpsc::Sender, staged_files: &Sta path: path.clone(), old_content: Some(original), new_content: formatted, + #[cfg(unix)] executable: false, }; if let Err(e) = sender.send(job) { @@ -312,6 +333,7 @@ fn enqueue_github_workflow_jobs(sender: std::sync::mpsc::Sender) { path: workflow_path.to_path_buf(), old_content, new_content, + #[cfg(unix)] executable: false, }; if let Err(e) = sender.send(job) { @@ -330,6 +352,7 @@ fn enqueue_github_funding_jobs(sender: std::sync::mpsc::Sender) { path: funding_path.to_path_buf(), old_content, new_content, + #[cfg(unix)] executable: false, }; if let Err(e) = sender.send(job) { @@ -373,6 +396,7 @@ fn enqueue_cargo_husky_precommit_hook_jobs(sender: std::sync::mpsc::Sender) path: hook_path.to_path_buf(), old_content, new_content, + #[cfg(unix)] executable: true, }; if let Err(e) = sender.send(job) { @@ -380,6 +404,205 @@ fn enqueue_cargo_husky_precommit_hook_jobs(sender: std::sync::mpsc::Sender) } } +fn run_pre_push() { + use std::collections::{BTreeSet, HashSet}; + + println!("{}", "Running pre-push checks...".cyan().bold()); + + // Find the merge base with origin/main + let merge_base_output = Command::new("git") + .args(["merge-base", "HEAD", "origin/main"]) + .output(); + + let merge_base = match merge_base_output { + Ok(output) if output.status.success() => { + String::from_utf8_lossy(&output.stdout).trim().to_string() + } + _ => { + warn!("Failed to find merge base with origin/main, using HEAD"); + "HEAD".to_string() + } + }; + + // Get the list of changed files + let diff_output = Command::new("git") + .args(["diff", "--name-only", &format!("{}...HEAD", merge_base)]) + .output(); + + let changed_files = match diff_output { + Ok(output) if output.status.success() => String::from_utf8_lossy(&output.stdout) + .lines() + .map(|s| s.to_string()) + .collect::>(), + Err(e) => { + error!("Failed to get changed files: {}", e); + std::process::exit(1); + } + Ok(output) => { + error!( + "git diff failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + std::process::exit(1); + } + }; + + if changed_files.is_empty() { + println!("{}", "No changes detected".green().bold()); + std::process::exit(0); + } + + // Find which crates are affected + let mut affected_crates = HashSet::new(); + + for file in &changed_files { + let path = Path::new(file); + + // Find the crate directory by looking for Cargo.toml + let mut current = path; + while let Some(parent) = current.parent() { + let cargo_toml = if parent.as_os_str().is_empty() { + PathBuf::from("Cargo.toml") + } else { + parent.join("Cargo.toml") + }; + + if cargo_toml.exists() { + // Read Cargo.toml to get the package name + if let Ok(content) = fs::read_to_string(&cargo_toml) { + // Simple parsing: look for [package] section and name field + let mut in_package = false; + for line in content.lines() { + let trimmed = line.trim(); + if trimmed == "[package]" { + in_package = true; + } else if trimmed.starts_with('[') { + in_package = false; + } else if in_package && trimmed.starts_with("name") { + if let Some(name_part) = trimmed.split('=').nth(1) { + let name = name_part.trim().trim_matches('"').trim_matches('\''); + affected_crates.insert(name.to_string()); + break; + } + } + } + } + break; + } + + if parent.as_os_str().is_empty() { + break; + } + current = parent; + } + } + + if affected_crates.is_empty() { + println!("{}", "No crates affected by changes".yellow()); + std::process::exit(0); + } + + // Sort for consistent output + let affected_crates: BTreeSet<_> = affected_crates.into_iter().collect(); + + println!( + "{} Affected crates: {}", + "๐Ÿ”".cyan(), + affected_crates + .iter() + .map(|s| s.as_str()) + .collect::>() + .join(", ") + .yellow() + ); + + let mut all_passed = true; + + for crate_name in &affected_crates { + println!( + "\n{} Checking crate: {}", + "๐Ÿ“ฆ".cyan(), + crate_name.yellow().bold() + ); + + // Run clippy + print!(" {} Running clippy... ", "๐Ÿ”".cyan()); + io::stdout().flush().unwrap(); + let clippy_status = Command::new("cargo") + .args(["clippy", "-p", crate_name, "--", "-D", "warnings"]) + .status(); + + match clippy_status { + Ok(status) if status.success() => { + println!("{}", "passed".green()); + } + _ => { + println!("{}", "failed".red()); + all_passed = false; + } + } + + // Run tests + print!(" {} Running tests... ", "๐Ÿงช".cyan()); + io::stdout().flush().unwrap(); + let test_status = Command::new("cargo") + .args(["test", "-p", crate_name]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + + match test_status { + Ok(status) if status.success() => { + println!("{}", "passed".green()); + } + _ => { + println!("{}", "failed".red()); + all_passed = false; + } + } + + // Run doc tests + print!(" {} Running doc tests... ", "๐Ÿ“š".cyan()); + io::stdout().flush().unwrap(); + let doctest_status = Command::new("cargo") + .args(["test", "--doc", "-p", crate_name]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + + match doctest_status { + Ok(status) if status.success() => { + println!("{}", "passed".green()); + } + Ok(status) if status.code() == Some(101) => { + // Exit code 101 often means "no tests to run" + println!("{}", "skipped (no lib)".yellow()); + } + _ => { + println!("{}", "failed".red()); + all_passed = false; + } + } + } + + println!(); + if all_passed { + println!( + "{} {}", + "โœ…".green(), + "All pre-push checks passed!".green().bold() + ); + std::process::exit(0); + } else { + println!( + "{} {}", + "โŒ".red(), + "Some pre-push checks failed".red().bold() + ); + std::process::exit(1); + } +} + fn show_and_apply_jobs(jobs: &mut [Job]) { use std::io::{self, Write}; @@ -456,6 +679,13 @@ fn main() { } } + // Parse CLI arguments + let args: Vec = std::env::args().collect(); + if args.len() > 1 && args[1] == "pre-push" { + run_pre_push(); + return; + } + let staged_files = match collect_staged_files() { Ok(sf) => sf, Err(e) => {