//! CLI for checking documentation coverage on files changed since a base branch. //! //! Usage: `source-map-check [--worktree ] [--base ]` //! //! Exits with code 1 and prints LLM-friendly directions when public items are //! missing doc comments. Exits 0 (silently) when all changed files are fully //! documented or when there are no relevant changes to check. //! //! The file set is derived from all worktree states: committed changes since //! `base`, staged changes, unstaged changes, and untracked files. This ensures //! the result is independent of git index state. use source_map_gen::{CheckResult, check_files_ratcheted}; use std::collections::HashSet; use std::path::{Path, PathBuf}; use std::process::Command; fn main() { let args: Vec = std::env::args().collect(); let worktree = parse_arg(&args, "--worktree").unwrap_or_else(|| ".".to_string()); let base = parse_arg(&args, "--base").unwrap_or_else(|| "master".to_string()); let worktree_path = Path::new(&worktree); let changed = collect_changed_files(worktree_path, &base); if changed.is_empty() { return; } let file_refs: Vec<&Path> = changed.iter().map(PathBuf::as_path).collect(); match check_files_ratcheted(&file_refs, worktree_path, &base) { CheckResult::Ok => {} CheckResult::Failures(failures) => { eprintln!( "Doc coverage check failed. Add doc comments to the following items before committing:\n" ); for f in &failures { eprintln!(" {}", f.to_direction()); } eprintln!( "\nRe-run: cargo run -p source-map-gen --bin source-map-check -- --worktree . --base master" ); std::process::exit(1); } } } /// Collect all files that differ from `base` in any git state: committed, staged, /// unstaged, or untracked. Returns deduplicated absolute paths that exist on disk. fn collect_changed_files(worktree_path: &Path, base: &str) -> Vec { let mut names: HashSet = HashSet::new(); // Committed changes since base (three-dot diff handles divergent histories). run_git_name_list( worktree_path, &["diff", "--name-only", &format!("{base}...HEAD")], &mut names, ); // Staged changes not yet committed. run_git_name_list( worktree_path, &["diff", "--name-only", "--cached"], &mut names, ); // Unstaged changes to tracked files. run_git_name_list(worktree_path, &["diff", "--name-only"], &mut names); // Untracked files (new files not yet added to the index). run_git_name_list( worktree_path, &["ls-files", "--others", "--exclude-standard"], &mut names, ); names .into_iter() .map(|l| worktree_path.join(l)) .filter(|p| p.exists()) .collect() } /// Run a git command and collect each non-empty output line into `out`. /// /// Silently ignores git errors so a missing base branch or a fresh repo without /// any commits does not abort the check. fn run_git_name_list(worktree_path: &Path, args: &[&str], out: &mut HashSet) { let Ok(output) = Command::new("git") .args(args) .current_dir(worktree_path) .output() else { return; }; if !output.status.success() { return; } for line in String::from_utf8_lossy(&output.stdout).lines() { if !line.is_empty() { out.insert(line.to_string()); } } } /// Parse a flag value from an argument list (e.g. `--flag value`). fn parse_arg(args: &[String], flag: &str) -> Option { args.windows(2).find(|w| w[0] == flag).map(|w| w[1].clone()) }