use anyhow::{anyhow, Result};
use chrono::Utc;
use std::path::PathBuf;
use crate::cli::LinkArgs;
use crate::model::{Link, LinkKind};
use crate::storage::{self, load_for_mutation};
pub fn run(mut args: LinkArgs, file: &Option<PathBuf>) -> Result<()> {
let (path, mut project, _lock) = load_for_mutation(file)?;
args.from = super::resolve_id(&project, &args.from)?;
args.to = super::resolve_id(&project, &args.to)?;
if args.from == args.to {
return Err(anyhow!("cannot link a requirement to itself"));
}
let kind: LinkKind = args.kind.into();
let cycle_checked = matches!(
kind,
LinkKind::Parent | LinkKind::DependsOn | LinkKind::Refines | LinkKind::Verifies
);
if cycle_checked && !args.remove {
if let Some(path) = cycle_path(&project, &args.from, &args.to, kind) {
let chain = std::iter::once(args.from.clone())
.chain(path)
.collect::<Vec<_>>()
.join(" -> ");
return Err(anyhow!(
"linking {} -> {} {} would create a cycle: {}",
args.from,
kind.as_str(),
args.to,
chain
));
}
}
let r = project
.requirements
.get_mut(&args.from)
.ok_or_else(|| anyhow!("source {} does not exist", args.from))?;
if args.remove {
let before = r.links.len();
r.links.retain(|l| !(l.kind == kind && l.target == args.to));
if r.links.len() == before {
return Err(anyhow!("no such link to remove"));
}
r.history.push(super::history(
format!("removed {} link to {}", kind.as_str(), args.to),
None,
));
} else {
if r.links
.iter()
.any(|l| l.kind == kind && l.target == args.to)
{
return Err(anyhow!("link already exists"));
}
r.links.push(Link {
kind,
target: args.to.clone(),
});
r.history.push(super::history(
format!("added {} link to {}", kind.as_str(), args.to),
None,
));
}
r.updated = Utc::now();
project.updated = Utc::now();
storage::save(&path, &project)?;
if args.json {
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"from": args.from, "to": args.to,
"kind": kind.as_str(), "removed": args.remove
}))?
);
} else {
println!("OK");
}
Ok(())
}
fn cycle_path(
project: &crate::model::Project,
from: &str,
target: &str,
kind: LinkKind,
) -> Option<Vec<String>> {
use std::collections::HashSet;
let mut stack: Vec<(String, Vec<String>)> =
vec![(target.to_string(), vec![target.to_string()])];
let mut visited: HashSet<String> = HashSet::new();
while let Some((node, path)) = stack.pop() {
if node == from {
return Some(path);
}
if !visited.insert(node.clone()) {
continue;
}
if let Some(r) = project.requirements.get(&node) {
for l in r.links.iter().filter(|l| l.kind == kind) {
let mut next_path = path.clone();
next_path.push(l.target.clone());
stack.push((l.target.clone(), next_path));
}
}
}
None
}