#![allow(missing_docs)]
use std::fs;
use std::path::PathBuf;
use std::process::{Command, ExitCode};
fn main() -> ExitCode {
let project_root = match get_project_root() {
Ok(root) => root,
Err(e) => {
eprintln!("❌ ERROR: Failed to find project root: {}", e);
return ExitCode::FAILURE;
}
};
if let Err(e) = std::env::set_current_dir(&project_root) {
eprintln!("❌ ERROR: Failed to change to project root: {}", e);
return ExitCode::FAILURE;
}
println!("🚦 Pre-push validation (5 gates)...");
println!();
println!("Gate 1/5: Cargo check...");
match run_cargo_make("check") {
Ok(()) => println!("✅ Gate 1 passed"),
Err(e) => {
eprintln!("❌ ERROR: cargo make check failed");
eprintln!(" {}", e);
return ExitCode::FAILURE;
}
}
println!();
println!("Gate 2/5: Clippy (strict mode for production)...");
match run_cargo_make("lint") {
Ok(()) => println!("✅ Gate 2 passed"),
Err(e) => {
eprintln!("❌ ERROR: Clippy found warnings or errors in production code");
eprintln!(
" Test files are allowed to use expect() with #![allow(clippy::expect_used)]"
);
eprintln!(" {}", e);
return ExitCode::FAILURE;
}
}
println!();
println!("Gate 2.5/5: TODO & error handling check...");
match check_todo_and_error_handling() {
Ok(()) => println!("✅ Gate 2.5 passed"),
Err(e) => {
eprintln!("❌ ERROR: {}", e);
return ExitCode::FAILURE;
}
}
println!();
println!("Gate 3/5: Formatting check...");
match check_formatting() {
Ok(()) => println!("✅ Gate 3 passed"),
Err(e) => {
eprintln!("❌ ERROR: Code is not formatted");
eprintln!(" Run: cargo make fmt");
eprintln!(" {}", e);
return ExitCode::FAILURE;
}
}
println!();
println!("Gate 4/5: Fast tests (lib + bins)...");
match run_cargo_make("test") {
Ok(()) => println!("✅ Gate 4 passed"),
Err(e) => {
eprintln!("❌ ERROR: Tests failed");
eprintln!(" {}", e);
return ExitCode::FAILURE;
}
}
println!();
println!("Gate 5/5: Security audit...");
let audit_available = Command::new("cargo")
.arg("audit")
.arg("--version")
.output()
.is_ok();
if audit_available {
match run_cargo_make("audit") {
Ok(()) => println!("✅ Gate 5 passed"),
Err(_) => {
println!("⚠️ Security audit found issues (non-blocking)");
}
}
} else {
println!("⚠️ cargo-audit not installed (optional)");
println!(" Install: cargo install cargo-audit");
}
println!();
println!("✅ All gates passed - ready to push");
ExitCode::SUCCESS
}
fn get_project_root() -> Result<PathBuf, Box<dyn std::error::Error>> {
let output = Command::new("git")
.arg("rev-parse")
.arg("--show-toplevel")
.output()?;
if !output.status.success() {
return Err("Not a git repository".into());
}
let root = String::from_utf8(output.stdout)?.trim().to_string();
Ok(PathBuf::from(root))
}
fn run_cargo_make(task: &str) -> Result<(), Box<dyn std::error::Error>> {
let output = Command::new("cargo").arg("make").arg(task).output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("cargo make {} failed: {}", task, stderr).into());
}
Ok(())
}
fn check_formatting() -> Result<(), Box<dyn std::error::Error>> {
let output = Command::new("cargo")
.arg("fmt")
.arg("--all")
.arg("--")
.arg("--check")
.output()?;
if !output.status.success() {
return Err("Formatting check failed".into());
}
Ok(())
}
fn check_todo_and_error_handling() -> Result<(), Box<dyn std::error::Error>> {
let todo_count = count_todos_in_production()?;
if todo_count > 0 {
return Err(format!("{} TODO comments found in production code. Policy: Zero TODOs in production (use FUTURE: for planned enhancements)", todo_count).into());
}
let unwrap_count = count_unwrap_in_production()?;
if unwrap_count > 0 {
return Err(format!("Found {} unwrap() calls in production code. Policy: Zero unwrap() unless documented with allow attribute", unwrap_count).into());
}
let expect_count = count_expect_in_production()?;
if expect_count > 0 {
return Err(format!("Found {} expect() calls in production code. Policy: Zero expect() unless documented with allow attribute. Note: CLI code (crates/ggen-cli) can use expect() for user-facing errors", expect_count).into());
}
Ok(())
}
fn count_todos_in_production() -> Result<usize, Box<dyn std::error::Error>> {
let output = Command::new("find")
.arg("crates/ggen-*/src")
.arg("-name")
.arg("*.rs")
.arg("-type")
.arg("f")
.output()?;
if !output.status.success() {
return Ok(0);
}
let files: Vec<PathBuf> = String::from_utf8(output.stdout)?
.lines()
.filter(|line| {
let path = line.trim();
!path.contains("/tests/")
&& !path.contains("/test/")
&& !path.contains("/example")
&& !path.contains("build.rs")
})
.map(PathBuf::from)
.collect();
let mut count = 0;
for file in files {
if let Ok(content) = fs::read_to_string(&file) {
let todos: usize = content
.lines()
.filter(|line| {
let has_todo = line.contains("// T") && line.contains("ODO:");
let has_future = line.contains("// F") && line.contains("UTURE:");
has_todo && !has_future
})
.count();
count += todos;
}
}
Ok(count)
}
fn count_unwrap_in_production() -> Result<usize, Box<dyn std::error::Error>> {
let output = Command::new("find")
.arg("crates/ggen-*/src")
.arg("-name")
.arg("*.rs")
.arg("-type")
.arg("f")
.output()?;
if !output.status.success() {
return Ok(0);
}
let files: Vec<PathBuf> = String::from_utf8(output.stdout)?
.lines()
.filter(|line| {
let path = line.trim();
!path.contains("/tests/")
&& !path.contains("/test/")
&& !path.contains("/example")
&& !path.contains("build.rs")
})
.map(PathBuf::from)
.collect();
let mut count = 0;
for file in files {
if file.to_string_lossy().contains("crates/ggen-cli/") {
continue;
}
if has_allow_attribute(&file, "unwrap_used") {
continue;
}
if has_test_modules(&file) {
continue;
}
if let Ok(content) = fs::read_to_string(&file) {
let unwraps: usize = content
.lines()
.filter(|line| line.contains(".unwrap()"))
.count();
count += unwraps;
}
}
Ok(count)
}
fn count_expect_in_production() -> Result<usize, Box<dyn std::error::Error>> {
let output = Command::new("find")
.arg("crates/ggen-*/src")
.arg("-name")
.arg("*.rs")
.arg("-type")
.arg("f")
.output()?;
if !output.status.success() {
return Ok(0);
}
let files: Vec<PathBuf> = String::from_utf8(output.stdout)?
.lines()
.filter(|line| {
let path = line.trim();
!path.contains("/tests/")
&& !path.contains("/test/")
&& !path.contains("/example")
&& !path.contains("build.rs")
})
.map(PathBuf::from)
.collect();
let mut count = 0;
for file in files {
if file.to_string_lossy().contains("crates/ggen-cli/") {
continue;
}
if has_allow_attribute(&file, "expect_used") {
continue;
}
if has_test_modules(&file) {
continue;
}
if let Ok(content) = fs::read_to_string(&file) {
let expects: usize = content
.lines()
.filter(|line| line.contains(".expect("))
.count();
count += expects;
}
}
Ok(count)
}
fn has_allow_attribute(file: &PathBuf, lint: &str) -> bool {
if let Ok(content) = fs::read_to_string(file) {
let pattern = format!("#!?\\[allow\\(clippy::{}\\s*\\)\\]", lint);
if let Ok(re) = regex::Regex::new(&pattern) {
return re.is_match(&content);
}
}
false
}
fn has_test_modules(file: &PathBuf) -> bool {
if let Ok(content) = fs::read_to_string(file) {
return content.contains("#[cfg(test)]");
}
false
}