use anyhow::{anyhow, Context, Result};
use chrono::{DateTime, Utc};
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::path::PathBuf;
use std::process::Command;
use crate::cli::RenumberArgs;
use crate::model::{Hazard, Link, Project, Requirement, SafetyFunction, SafetyRequirement};
use crate::storage::{self, load_with_options, resolve_path};
trait Artifact {
fn created(&self) -> DateTime<Utc>;
fn title(&self) -> &str;
fn set_id(&mut self, id: String);
fn note_renamed(&mut self, old: &str);
fn links_mut(&mut self) -> &mut Vec<Link>;
}
macro_rules! impl_artifact {
($t:ty) => {
impl Artifact for $t {
fn created(&self) -> DateTime<Utc> {
self.created
}
fn title(&self) -> &str {
&self.title
}
fn set_id(&mut self, id: String) {
self.id = id;
}
fn note_renamed(&mut self, old: &str) {
self.history.push(super::history(
format!("renumbered from {} (merge with base)", old),
None,
));
}
fn links_mut(&mut self) -> &mut Vec<Link> {
&mut self.links
}
}
};
}
impl_artifact!(Requirement);
impl_artifact!(Hazard);
impl_artifact!(SafetyFunction);
impl_artifact!(SafetyRequirement);
pub fn run(args: RenumberArgs, file: &Option<PathBuf>) -> Result<()> {
let path = resolve_path(file);
let _lock = crate::storage::acquire_lock(&path)?;
let mut current = load_with_options(&path, true)?;
let base = load_from_git_ref(&args.base, path.file_name().unwrap().to_str().unwrap())?;
let mut next_id = current.next_id.max(base.next_id);
let mut next_haz = current.next_haz_id.max(base.next_haz_id);
let mut next_sf = current.next_sf_id.max(base.next_sf_id);
let mut next_sr = current.next_sr_id.max(base.next_sr_id);
let req_renames = plan_renames(
¤t.requirements,
&base.requirements,
&mut next_id,
"REQ",
);
let haz_renames = plan_renames(¤t.hazards, &base.hazards, &mut next_haz, "HAZ");
let sf_renames = plan_renames(
¤t.safety_functions,
&base.safety_functions,
&mut next_sf,
"SF",
);
let sr_renames = plan_renames(
¤t.safety_requirements,
&base.safety_requirements,
&mut next_sr,
"SR",
);
let all_renames: Vec<(String, String)> = req_renames
.iter()
.chain(&haz_renames)
.chain(&sf_renames)
.chain(&sr_renames)
.cloned()
.collect();
if all_renames.is_empty() {
println!("No ID collisions against {}.", args.base);
return Ok(());
}
println!("Planned renames:");
for (old, new) in &all_renames {
println!(" {} -> {}", old, new);
}
if args.dry_run {
return Ok(());
}
rekey(&mut current.requirements, &req_renames);
rekey(&mut current.hazards, &haz_renames);
rekey(&mut current.safety_functions, &sf_renames);
rekey(&mut current.safety_requirements, &sr_renames);
let map: HashMap<String, String> = all_renames.iter().cloned().collect();
rewrite_links(current.requirements.values_mut(), &map);
rewrite_links(current.hazards.values_mut(), &map);
rewrite_links(current.safety_functions.values_mut(), &map);
rewrite_links(current.safety_requirements.values_mut(), &map);
current.next_id = next_id;
current.next_haz_id = next_haz;
current.next_sf_id = next_sf;
current.next_sr_id = next_sr;
storage::save(&path, ¤t)?;
println!(
"Renumbered {} artifact(s) and re-signed {}.",
all_renames.len(),
path.display()
);
Ok(())
}
fn plan_renames<T: Artifact>(
current: &BTreeMap<String, T>,
base: &BTreeMap<String, T>,
next: &mut u32,
prefix: &str,
) -> Vec<(String, String)> {
let mut renames = Vec::new();
let mut ids: Vec<&String> = current.keys().collect();
ids.sort();
for id in ids {
let (Some(a), Some(b)) = (current.get(id), base.get(id)) else {
continue;
};
let differs = a.created() != b.created() || a.title() != b.title();
if differs {
let new_id = format!("{}-{:04}", prefix, *next);
*next += 1;
renames.push((id.clone(), new_id));
}
}
renames
}
fn rekey<T: Artifact>(map: &mut BTreeMap<String, T>, renames: &[(String, String)]) {
for (old, new) in renames {
if let Some(mut a) = map.remove(old) {
a.set_id(new.clone());
a.note_renamed(old);
map.insert(new.clone(), a);
}
}
}
fn rewrite_links<'a, T: Artifact + 'a>(
artifacts: impl Iterator<Item = &'a mut T>,
map: &HashMap<String, String>,
) {
for a in artifacts {
for link in a.links_mut().iter_mut() {
if let Some(new) = map.get(&link.target) {
link.target = new.clone();
}
}
}
}
fn load_from_git_ref(reference: &str, filename: &str) -> Result<Project> {
let spec = format!("{}:{}", reference, filename);
let output = Command::new("git")
.args(["show", &spec])
.output()
.with_context(|| format!("run git show {}", spec))?;
if !output.status.success() {
return Err(anyhow!(
"git show {} failed: {}",
spec,
String::from_utf8_lossy(&output.stderr)
));
}
let tmp = std::env::temp_dir().join(format!("req-base-{}.req", std::process::id()));
std::fs::write(&tmp, &output.stdout)?;
let project = load_with_options(&tmp, true)?;
std::fs::remove_file(&tmp).ok();
Ok(project)
}