use std::path::PathBuf;
use gen_cargo::fleet_migrate::{self, MigrateOpts, MigrateReport};
use serde::{Deserialize, Serialize};
use tatara_lisp::{compile_typed, DeriveTataraDomain};
#[derive(DeriveTataraDomain, Clone, Debug, Serialize, Deserialize)]
#[tatara(keyword = "fleet-migration-plan")]
pub struct FleetMigrationPlan {
pub workspace_root: String,
#[serde(default)]
pub repos: Vec<String>,
#[serde(default)]
pub exclude: Vec<String>,
#[serde(default)]
pub push: bool,
#[serde(default)]
pub bot_identity: bool,
}
impl FleetMigrationPlan {
pub fn from_source(src: &str) -> Result<Self, String> {
compile_typed::<FleetMigrationPlan>(src)
.map_err(|e| e.to_string())?
.into_iter()
.next()
.ok_or_else(|| "no (fleet-migration-plan …) form found".to_string())
}
pub fn resolve_repos(&self) -> Result<Vec<PathBuf>, String> {
let root = expand_tilde(&self.workspace_root);
if !self.repos.is_empty() {
return Ok(self
.repos
.iter()
.filter(|n| !self.exclude.contains(n))
.map(|n| root.join(n))
.collect());
}
let mut dirs: Vec<PathBuf> = std::fs::read_dir(&root)
.map_err(|e| format!("read_dir {}: {e}", root.display()))?
.filter_map(|e| e.ok().map(|e| e.path()))
.filter(|p| p.is_dir())
.collect();
dirs.sort();
Ok(dirs
.into_iter()
.filter(|d| {
let name = d
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default();
!name.starts_with('.')
&& !self.exclude.contains(&name)
&& d.join("Cargo.toml").exists()
&& d.join(".git").exists()
&& (d.join("Cargo.build-spec.json").exists() || d.join("Cargo.gen.lock").exists())
})
.collect())
}
pub fn opts(&self, force_no_push: bool) -> MigrateOpts {
MigrateOpts {
push: self.push && !force_no_push,
bot_identity: self.bot_identity,
}
}
}
fn expand_tilde(p: &str) -> PathBuf {
if let Some(rest) = p.strip_prefix("~/") {
if let Some(home) = std::env::var_os("HOME") {
return PathBuf::from(home).join(rest);
}
}
PathBuf::from(p)
}
pub fn run_plan(src: &str, force_no_push: bool) -> Result<MigrateReport, String> {
let plan = FleetMigrationPlan::from_source(src)?;
let repos = plan.resolve_repos()?;
fleet_migrate::run(&repos, plan.opts(force_no_push)).map_err(|e| e.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_minimal_plan() {
let p = FleetMigrationPlan::from_source(
r#"(fleet-migration-plan :workspace-root "/tmp/ws")"#,
)
.expect("parse");
assert_eq!(p.workspace_root, "/tmp/ws");
assert!(p.repos.is_empty());
assert!(!p.push);
assert!(!p.bot_identity);
}
#[test]
fn parses_full_plan_with_lists_and_bools() {
let p = FleetMigrationPlan::from_source(
r#"(fleet-migration-plan
:workspace-root "~/code/github/pleme-io"
:repos ("seibi" "frost")
:exclude ("ishou")
:push #t
:bot-identity #f)"#,
)
.expect("parse");
assert_eq!(p.repos, vec!["seibi", "frost"]);
assert_eq!(p.exclude, vec!["ishou"]);
assert!(p.push);
assert!(!p.bot_identity);
}
#[test]
fn explicit_repos_honor_excludes() {
let p = FleetMigrationPlan::from_source(
r#"(fleet-migration-plan :workspace-root "/tmp/ws" :repos ("a" "b" "c") :exclude ("b"))"#,
)
.unwrap();
let repos = p.resolve_repos().unwrap();
let names: Vec<_> = repos
.iter()
.map(|p| p.file_name().unwrap().to_string_lossy().into_owned())
.collect();
assert_eq!(names, vec!["a", "c"]);
}
#[test]
fn no_push_override() {
let p = FleetMigrationPlan::from_source(
r#"(fleet-migration-plan :workspace-root "/tmp" :push #t)"#,
)
.unwrap();
assert!(p.opts(false).push);
assert!(!p.opts(true).push); }
}