112 lines
3.7 KiB
Rust
112 lines
3.7 KiB
Rust
//! CLI for checking documentation coverage on files changed since a base branch.
|
|
//!
|
|
//! Usage: `source-map-check [--worktree <path>] [--base <branch>]`
|
|
//!
|
|
//! 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<String> = 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<PathBuf> {
|
|
let mut names: HashSet<String> = 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<String>) {
|
|
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<String> {
|
|
args.windows(2).find(|w| w[0] == flag).map(|w| w[1].clone())
|
|
}
|