use clap::{Parser, Subcommand};
use migrate_guard::apply::{apply, ApplyMode};
use migrate_guard::config::Config;
use migrate_guard::git::{self, GitCmd};
use migrate_guard::model::{detect_collisions, Observation, Source};
use std::path::PathBuf;
use std::process::ExitCode;
#[derive(Parser)]
#[command(bin_name = "cargo migrate-guard")]
struct Cargo {
#[command(subcommand)]
inner: Inner,
}
#[derive(Subcommand)]
enum Inner {
MigrateGuard(Cli),
}
#[derive(Parser)]
struct Cli {
#[arg(long, default_value = ".")]
root: PathBuf,
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Subcommand)]
enum Cmd {
Check,
Renumber {
#[arg(long)]
base: Option<String>,
#[arg(long)]
apply: bool,
},
Max,
}
fn main() -> ExitCode {
let Cargo {
inner: Inner::MigrateGuard(cli),
} = Cargo::parse();
match run(cli) {
Ok(code) => code,
Err(e) => {
eprintln!("error: {e}");
ExitCode::from(2)
}
}
}
fn current_branch(git: &GitCmd) -> String {
std::process::Command::new(&git.program)
.args(["rev-parse", "--abbrev-ref", "HEAD"])
.current_dir(&git.repo)
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "HEAD".into())
}
fn run(cli: Cli) -> migrate_guard::error::Result<ExitCode> {
let cfg = Config::load(&cli.root)?;
let gitcmd = GitCmd {
program: cfg.git.clone(),
repo: cli.root.clone(),
};
let cur = current_branch(&gitcmd);
match cli.cmd {
Cmd::Max => {
for d in &cfg.dirs {
let files = migrate_guard::scan_dir(&d.role, &cli.root.join(&d.path))?;
println!(
"{}: {}",
d.role,
migrate_guard::model::max_version(&files.files)
);
}
Ok(ExitCode::SUCCESS)
}
Cmd::Check => {
let mut obs: Vec<Observation> = Vec::new();
let branches = git::local_branches(&gitcmd)?;
for d in &cfg.dirs {
let abs_dir = cli.root.join(&d.path);
for f in migrate_guard::scan_dir(&d.role, &abs_dir)?.files {
obs.push(Observation {
role: d.role.clone(),
version: f.version,
source: Source::WorkingTree,
file: f,
});
}
for b in branches.iter().filter(|b| **b != cur) {
for f in git::branch_migration_files(&gitcmd, b, &d.path)? {
obs.push(Observation {
role: d.role.clone(),
version: f.version,
source: Source::Branch(b.clone()),
file: f,
});
}
}
}
let collisions = detect_collisions(&obs);
if collisions.is_empty() {
println!("no migration version collisions");
Ok(ExitCode::SUCCESS)
} else {
for c in &collisions {
eprintln!("COLLISION [{}] version {}:", c.role, c.version);
for o in &c.occurrences {
eprintln!(" {:?} {}", o.source, o.path.display());
}
}
Ok(ExitCode::from(1))
}
}
Cmd::Renumber {
base,
apply: do_apply,
} => {
let mut cfg = cfg;
if let Some(b) = base {
cfg.base_ref = b;
}
let mode = if do_apply {
ApplyMode::Commit
} else {
ApplyMode::DryRun
};
let plans = apply(&cli.root, &cfg, &cur, mode)?;
let mut any = false;
for p in &plans {
for r in &p.renames {
any = true;
println!("[{}] {} -> {}", p.role, r.from.display(), r.to.display());
}
for (f, line) in &p.reference_hits {
println!(" ref {f}:{line}");
}
}
if !any {
println!("nothing to renumber");
} else if !do_apply {
println!("(dry run — pass --apply to write)");
}
Ok(ExitCode::SUCCESS)
}
}
}