use anyhow::{anyhow, Context, Result};
use chrono::{DateTime, Utc};
use serde_json::{Map, Value};
use std::collections::{BTreeMap, BTreeSet, HashMap};
use crate::cli::MergeArgs;
use crate::model::{Hazard, Link, Project, Requirement, SafetyFunction, SafetyRequirement};
use crate::storage::{load_with_options, to_canonical_json};
const COUNTER_KEYS: &[&str] = &["next_id", "next_haz_id", "next_sf_id", "next_sr_id"];
pub fn run(args: MergeArgs) -> Result<()> {
let base = load_with_options(&args.base, true)
.with_context(|| format!("load merge base {}", args.base.display()))?;
let ours = load_with_options(&args.ours, true)
.with_context(|| format!("load our version {}", args.ours.display()))?;
let mut theirs = load_with_options(&args.theirs, true)
.with_context(|| format!("load their version {}", args.theirs.display()))?;
let renamed = deconflict_added_ids(&base, &ours, &mut theirs);
let bv = serde_json::to_value(&base).context("serialize base")?;
let ov = serde_json::to_value(&ours).context("serialize ours")?;
let tv = serde_json::to_value(&theirs).context("serialize theirs")?;
let mut conflicts: Vec<String> = Vec::new();
let merged_ours = merge3(
Some(&bv),
Some(&ov),
Some(&tv),
"",
Side::Ours,
&mut conflicts,
true,
);
let mut sink: Vec<String> = Vec::new();
let merged_theirs = merge3(
Some(&bv),
Some(&ov),
Some(&tv),
"",
Side::Theirs,
&mut sink,
false,
);
let output = args.output.clone().unwrap_or_else(|| args.ours.clone());
if conflicts.is_empty() {
let project: Project = serde_json::from_value(
merged_ours.ok_or_else(|| anyhow!("merge produced no document"))?,
)
.context("deserialize merged project")?;
crate::storage::save(&output, &project).context("write merged project")?;
let renamed_note = if renamed > 0 {
format!(
" ({} colliding addition(s) on their side renumbered)",
renamed
)
} else {
String::new()
};
eprintln!(
"req merge: clean 3-way merge — {} requirement(s), {} hazard(s), {} safety function(s), {} safety requirement(s){}.",
project.requirements.len(),
project.hazards.len(),
project.safety_functions.len(),
project.safety_requirements.len(),
renamed_note
);
return Ok(());
}
let ours_doc = render_side(merged_ours, "ours")?;
let theirs_doc = render_side(merged_theirs, "theirs")?;
let body = format!(
"<<<<<<< ours (auto-merged; {} conflict(s) below need manual resolution)\n\
{}\n\
=======\n\
{}\n\
>>>>>>> theirs\n",
conflicts.len(),
ours_doc.trim_end(),
theirs_doc.trim_end(),
);
std::fs::write(&output, body).with_context(|| format!("write {}", output.display()))?;
eprintln!(
"req merge: {} unresolvable conflict(s) — left for human resolution (no side dropped):",
conflicts.len()
);
for c in &conflicts {
eprintln!(" CONFLICT: {}", c);
}
eprintln!(
"Resolve {}: keep one block, hand-merge the conflicting item(s), delete the\n\
conflict markers, then `git add` it. (Run `req repair --confirm-direct-edit`\n\
if you hand-edit and the integrity hash no longer matches.)",
output.display()
);
std::process::exit(1);
}
fn render_side(v: Option<Value>, which: &str) -> Result<String> {
let v = v.ok_or_else(|| anyhow!("merge produced no {} document", which))?;
let project: Project = serde_json::from_value(v)
.with_context(|| format!("deserialize {} side of merge", which))?;
to_canonical_json(&project)
}
#[derive(Clone, Copy, PartialEq)]
enum Side {
Ours,
Theirs,
}
fn merge3(
base: Option<&Value>,
ours: Option<&Value>,
theirs: Option<&Value>,
path: &str,
prefer: Side,
conflicts: &mut Vec<String>,
record: bool,
) -> Option<Value> {
if ours == theirs {
return ours.cloned();
}
if ours == base {
return theirs.cloned();
}
if theirs == base {
return ours.cloned();
}
match (ours, theirs) {
(Some(Value::Object(o)), Some(Value::Object(t))) => {
let bo = base.and_then(|v| v.as_object());
Some(merge_object(bo, o, t, path, prefer, conflicts, record))
}
_ => {
if record {
conflicts.push(describe(path, ours, theirs));
}
match prefer {
Side::Ours => ours.cloned(),
Side::Theirs => theirs.cloned(),
}
}
}
}
fn merge_object(
base: Option<&Map<String, Value>>,
ours: &Map<String, Value>,
theirs: &Map<String, Value>,
path: &str,
prefer: Side,
conflicts: &mut Vec<String>,
record: bool,
) -> Value {
let mut keys: BTreeSet<&String> = BTreeSet::new();
keys.extend(ours.keys());
keys.extend(theirs.keys());
if let Some(b) = base {
keys.extend(b.keys());
}
let mut out = Map::new();
let at_root = path.is_empty();
for k in keys {
if at_root && COUNTER_KEYS.contains(&k.as_str()) {
out.insert(k.clone(), max_counter(base, ours, theirs, k));
continue;
}
if k == "updated" {
out.insert(k.clone(), pick_extreme(ours, theirs, k, true));
continue;
}
if k == "created" {
out.insert(k.clone(), pick_extreme(ours, theirs, k, false));
continue;
}
if k == "history" {
if let Some(v) = merge_history(base.and_then(|b| b.get(k)), ours.get(k), theirs.get(k))
{
out.insert(k.clone(), v);
continue;
}
}
let child = if path.is_empty() {
k.clone()
} else {
format!("{}/{}", path, k)
};
let merged = merge3(
base.and_then(|b| b.get(k)),
ours.get(k),
theirs.get(k),
&child,
prefer,
conflicts,
record,
);
if let Some(v) = merged {
out.insert(k.clone(), v);
}
}
Value::Object(out)
}
fn max_counter(
base: Option<&Map<String, Value>>,
ours: &Map<String, Value>,
theirs: &Map<String, Value>,
key: &str,
) -> Value {
let get = |m: Option<&Map<String, Value>>| {
m.and_then(|m| m.get(key))
.and_then(|v| v.as_u64())
.unwrap_or(1)
};
let n = get(Some(ours)).max(get(Some(theirs))).max(get(base));
Value::Number(n.into())
}
fn pick_extreme(
ours: &Map<String, Value>,
theirs: &Map<String, Value>,
key: &str,
latest: bool,
) -> Value {
let o = ours.get(key).cloned().unwrap_or(Value::Null);
let t = theirs.get(key).cloned().unwrap_or(Value::Null);
let (os, ts) = (o.as_str().unwrap_or(""), t.as_str().unwrap_or(""));
let keep_ours = if latest { os >= ts } else { os <= ts };
if keep_ours {
o
} else {
t
}
}
fn merge_history(
base: Option<&Value>,
ours: Option<&Value>,
theirs: Option<&Value>,
) -> Option<Value> {
let o = ours.and_then(|v| v.as_array())?;
let t = theirs.and_then(|v| v.as_array())?;
let _ = base; let mut out: Vec<Value> = Vec::new();
for e in o.iter().chain(t.iter()) {
if !out.contains(e) {
out.push(e.clone());
}
}
let at_of = |v: &Value| {
v.get("at")
.and_then(|a| a.as_str())
.unwrap_or("")
.to_string()
};
out.sort_by_key(&at_of);
Some(Value::Array(out))
}
fn describe(path: &str, ours: Option<&Value>, theirs: Option<&Value>) -> String {
let kind = match (ours, theirs) {
(None, Some(_)) => "deleted on our side, changed on theirs",
(Some(_), None) => "changed on our side, deleted on theirs",
_ => "changed differently on both sides",
};
let at = if path.is_empty() { "<root>" } else { path };
format!("{} — {}", at, kind)
}
trait MergeArtifact {
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_merge_artifact {
($t:ty) => {
impl MergeArtifact 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 collision with the other side)",
old
),
None,
));
}
fn links_mut(&mut self) -> &mut Vec<Link> {
&mut self.links
}
}
};
}
impl_merge_artifact!(Requirement);
impl_merge_artifact!(Hazard);
impl_merge_artifact!(SafetyFunction);
impl_merge_artifact!(SafetyRequirement);
fn deconflict_added_ids(base: &Project, ours: &Project, theirs: &mut Project) -> usize {
let mut next_req = base.next_id.max(ours.next_id).max(theirs.next_id);
let mut next_haz = base
.next_haz_id
.max(ours.next_haz_id)
.max(theirs.next_haz_id);
let mut next_sf = base.next_sf_id.max(ours.next_sf_id).max(theirs.next_sf_id);
let mut next_sr = base.next_sr_id.max(ours.next_sr_id).max(theirs.next_sr_id);
let mut renames: Vec<(String, String)> = Vec::new();
plan_collisions(
&base.requirements,
&ours.requirements,
&theirs.requirements,
"REQ",
&mut next_req,
&mut renames,
);
plan_collisions(
&base.hazards,
&ours.hazards,
&theirs.hazards,
"HAZ",
&mut next_haz,
&mut renames,
);
plan_collisions(
&base.safety_functions,
&ours.safety_functions,
&theirs.safety_functions,
"SF",
&mut next_sf,
&mut renames,
);
plan_collisions(
&base.safety_requirements,
&ours.safety_requirements,
&theirs.safety_requirements,
"SR",
&mut next_sr,
&mut renames,
);
if renames.is_empty() {
return 0;
}
rekey(&mut theirs.requirements, &renames);
rekey(&mut theirs.hazards, &renames);
rekey(&mut theirs.safety_functions, &renames);
rekey(&mut theirs.safety_requirements, &renames);
let map: HashMap<String, String> = renames.iter().cloned().collect();
rewrite_links(theirs.requirements.values_mut(), &map);
rewrite_links(theirs.hazards.values_mut(), &map);
rewrite_links(theirs.safety_functions.values_mut(), &map);
rewrite_links(theirs.safety_requirements.values_mut(), &map);
theirs.next_id = next_req;
theirs.next_haz_id = next_haz;
theirs.next_sf_id = next_sf;
theirs.next_sr_id = next_sr;
renames.len()
}
fn plan_collisions<T: MergeArtifact>(
base: &BTreeMap<String, T>,
ours: &BTreeMap<String, T>,
theirs: &BTreeMap<String, T>,
prefix: &str,
next: &mut u32,
out: &mut Vec<(String, String)>,
) {
let mut ids: Vec<&String> = theirs.keys().collect();
ids.sort();
for id in ids {
if base.contains_key(id) {
continue; }
let (Some(t), Some(o)) = (theirs.get(id), ours.get(id)) else {
continue; };
if t.created() != o.created() || t.title() != o.title() {
let new_id = format!("{}-{:04}", prefix, *next);
*next += 1;
out.push((id.clone(), new_id));
}
}
}
fn rekey<T: MergeArtifact>(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: MergeArtifact + '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();
}
}
}
}