use crate::error::{GuardError, Result};
use crate::{parse_filename, MigrationFile};
use std::path::{Path, PathBuf};
use std::process::Command;
pub struct GitCmd {
pub program: String,
pub repo: PathBuf,
}
impl GitCmd {
pub fn new(repo: impl Into<PathBuf>) -> Self {
Self {
program: "git".to_string(),
repo: repo.into(),
}
}
fn run(&self, args: &[&str]) -> Result<String> {
let out = Command::new(&self.program)
.args(args)
.current_dir(&self.repo)
.output()?;
if !out.status.success() {
return Err(GuardError::Git {
cmd: format!("{} {}", self.program, args.join(" ")),
status: out.status.code(),
stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
});
}
Ok(String::from_utf8_lossy(&out.stdout).into_owned())
}
}
pub fn local_branches(git: &GitCmd) -> Result<Vec<String>> {
let out = git.run(&["for-each-ref", "--format=%(refname:short)", "refs/heads/"])?;
Ok(out
.lines()
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty())
.collect())
}
pub fn branch_migration_files(
git: &GitCmd,
branch: &str,
dir: &Path,
) -> Result<Vec<MigrationFile>> {
if git
.run(&[
"rev-parse",
"--verify",
"--quiet",
&format!("{branch}^{{commit}}"),
])
.is_err()
{
return Err(GuardError::Git {
cmd: format!("git rev-parse --verify {branch}"),
status: None,
stderr: format!("ref '{branch}' does not resolve to a commit"),
});
}
let dir_str = dir.to_string_lossy();
let out = git.run(&["ls-tree", "-r", "--name-only", branch, "--", &dir_str])?;
Ok(files_from_paths(&out, dir))
}
pub fn topic_new_files(git: &GitCmd, base_ref: &str, dir: &Path) -> Result<Vec<MigrationFile>> {
let base = branch_migration_files(git, base_ref, dir)?;
let base_names: std::collections::HashSet<_> =
base.iter().map(|f| f.filename.clone()).collect();
let working = crate::scan_dir("", &git.repo.join(dir))?; Ok(working
.files
.into_iter()
.filter(|f| !base_names.contains(&f.filename))
.collect())
}
fn files_from_paths(ls_tree_out: &str, dir: &Path) -> Vec<MigrationFile> {
let mut files = Vec::new();
for line in ls_tree_out.lines() {
let p = Path::new(line.trim());
let Some(name) = p.file_name().and_then(|n| n.to_str()) else {
continue;
};
if let Some((version, width, desc)) = parse_filename(name) {
files.push(MigrationFile {
version,
width,
description: desc.to_string(),
filename: name.to_string(),
path: dir.join(name),
});
}
}
files.sort_by_key(|f| f.version);
files
}