//! Tiny git layer: the product repo becomes a real version-controlled //! project — every work action is a commit by its author, reviews see //! diffs, the boss can revert, and the commit log is a readable transcript //! of the sim. //! //! All commands run with inline `-c user.name/-c user.email` so nothing //! touches the user's global git config. Every helper degrades gracefully //! when git is absent or the repo is bare. use std::path::Path; fn run_git(repo: &Path, identity: Option<(&str, &str)>, args: &[&str]) -> Option { // Resolve once: a relative repo path gets double-resolved by the child's // chdir + -C combo otherwise (fatal: cannot change to ''). let repo_abs = std::fs::canonicalize(repo).unwrap_or_else(|_| repo.to_path_buf()); let mut cmd = std::process::Command::new("git"); cmd.current_dir(&repo_abs).arg("-C").arg(&repo_abs); if let Some((name, email)) = identity { cmd.arg("-c").arg(format!("user.name={name}")); cmd.arg("-c").arg(format!("user.email={email}")); } cmd.args(args); let out = cmd.output().ok()?; let stdout = String::from_utf8_lossy(&out.stdout).into_owned(); let stderr = String::from_utf8_lossy(&out.stderr).into_owned(); if out.status.success() { Some(stdout) } else { // Surface the actual reason (e.g. "nothing to commit"). Some(format!("(git {}) {}", args.join(" "), stderr.lines().next().unwrap_or("").trim())) } } /// One-time: make `work/project/` a git repo and commit the seed scaffold. pub fn init(repo: &Path) { if !which_git() { eprintln!("[git] git is not on PATH — repo history disabled"); return; } let out = run_git(repo, Some(("system", "system@dojo")), &["init", "-q"]).unwrap_or_default(); if out.contains("fatal") || out.contains("error") || !repo.join(".git").exists() { eprintln!("[git] init said {} (has .git: {})", out.trim(), repo.join(".git").exists()); } run_git(repo, Some(("system", "system@dojo")), &["init", "-q"]); // Generated reports churn every round — keep them out of history. let ignore = repo.join(".gitignore"); if !ignore.exists() { std::fs::write(&ignore, "judge_report.md\nverify_report.md\n").ok(); } run_git( repo, Some(("system", "system@dojo")), &["add", "-A", "--", "."], ); run_git( repo, Some(("system", "system@dojo")), &["commit", "-q", "-m", "seed: day-1 scaffold (harness + contract)", "--allow-empty"], ); } fn which_git() -> bool { std::process::Command::new("git") .arg("--version") .output() .map(|o| o.status.success()) .unwrap_or(false) } /// Commit exactly `paths` under `repo` as `author`. Returns Some(text) when a /// commit actually landed, None when there was nothing new (or git missing). pub fn commit( repo: &Path, author: &str, email: &str, message: &str, paths: &[String], ) -> Option { if !which_git() || paths.is_empty() { return None; } let mut add_args: Vec = vec!["add".into()]; add_args.extend(paths.iter().cloned()); let add_ref: Vec<&str> = add_args.iter().map(String::as_str).collect(); run_git(repo, Some(("system", "system@dojo")), &add_ref)?; // Avoid empty commits: check if the index has anything staged above HEAD. let status = run_git(repo, None, &["diff", "--cached", "--name-only"])?; if status.trim().is_empty() { return None; } let msg: String = message.chars().take(110).collect(); run_git( repo, Some((author, email)), &["commit", "-q", "-m", &msg], ) } /// `git diff HEAD~1 HEAD --stat` + truncated patch — what a reviewer sees. pub fn show_last_diff(repo: &Path, max_chars: usize) -> Option { if !which_git() { return None; } let stat = run_git(repo, None, &["diff", "HEAD~1", "HEAD", "--stat"])?; let patch = run_git(repo, None, &["diff", "HEAD~1", "HEAD"])?; let patch: String = patch.chars().take(max_chars).collect(); let text = format!("{stat}\n\n{patch}"); if text.trim().is_empty() { None } else { Some(text) } } /// Recent commits for the panel/TUI: "hash author message". pub fn log(repo: &Path, limit: usize) -> Option> { if !which_git() { return None; } let n = limit.to_string(); let fmt = "--pretty=format:%h %an › %s"; run_git( repo, None, &["log", &format!("-{n}"), fmt], ) .map(|out| out.lines().map(str::to_string).collect()) } /// Full detail for one commit hash (TUI preview / panel). pub fn show(repo: &Path, hash: &str) -> Option { if !which_git() { return None; } run_git(repo, None, &["show", "--stat", "--format=short", hash]) } /// Boss convenience: revert the most recent commit (leaves a real scar). pub fn revert_head(repo: &Path, by: &str) -> Option { if !which_git() { return None; } run_git( repo, Some((by, format!("{}@dojo", slug_of(by)).as_str())), &[ "revert", "--no-edit", "-q", "HEAD", ], ) } fn slug_of(name: &str) -> String { name.to_lowercase() .chars() .map(|c| if c.is_alphanumeric() { c } else { '-' }) .collect() } /// Author email derived from the person slug. pub fn person_email(person: &str, company: &str) -> String { format!( "{}@{}", slug_of(&person.replace(' ', "")), slug_of(company) ) }