use std::collections::HashMap;
use std::rc::Rc;
use crate::analyses::architecture_trend::import_graph_at_rev;
use crate::analyses::import_graph::{ImportGraph, tarjan_scc};
use crate::facts::FactsDb;
use crate::repo::Repo;
use crate::{Options, Result};
pub const MAX_CYCLES: usize = 10;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct CycleOriginRow {
pub size: u32,
pub formed_at_rev: String,
pub formed_at_date: String,
pub members: String,
}
#[tracing::instrument(name = "cycle-origins", skip_all)]
pub fn run_cycle_origins<R: Repo>(
db: &FactsDb,
repo: &R,
_opts: &Options,
) -> Result<Vec<CycleOriginRow>> {
let commits: Vec<(String, String, String)> = crate::analyses::query::query_map_collect(
db,
"SELECT rev, CAST(date AS TEXT), CAST(CAST(date AS DATE) AS TEXT) \
FROM commits ORDER BY date ASC, rowid ASC",
[],
"cycle-origins commits",
|r| {
Ok((
r.get::<_, String>(0)?,
r.get::<_, String>(1)?,
r.get::<_, String>(2)?,
))
},
)?;
let Some((head_rev, head_ts, _)) = commits.last() else {
return Ok(Vec::new());
};
let mut graph_cache: HashMap<String, Rc<ImportGraph>> = HashMap::new();
let head_graph = graph_at_rev_cached(db, repo, head_rev, head_ts, &mut graph_cache)?;
let mut cycles = cycle_member_sets(&head_graph);
cycles.sort_by(|a, b| b.len().cmp(&a.len()).then_with(|| a.cmp(b)));
cycles.truncate(MAX_CYCLES);
let mut rows = Vec::with_capacity(cycles.len());
for members in cycles {
let idx = bisect_formation(db, repo, &commits, &members, &mut graph_cache)?;
let (rev, _, date) = &commits[idx];
rows.push(CycleOriginRow {
size: u32::try_from(members.len()).unwrap_or(u32::MAX),
formed_at_rev: rev.chars().take(12).collect(),
formed_at_date: date.clone(),
members: members.join("; "),
});
}
Ok(rows)
}
fn graph_at_rev_cached<R: Repo>(
db: &FactsDb,
repo: &R,
rev: &str,
ts: &str,
cache: &mut HashMap<String, Rc<ImportGraph>>,
) -> Result<Rc<ImportGraph>> {
if let Some(g) = cache.get(rev) {
return Ok(Rc::clone(g));
}
let g = Rc::new(import_graph_at_rev(db, repo, rev, ts)?);
cache.insert(rev.to_string(), Rc::clone(&g));
Ok(g)
}
fn cycle_member_sets(graph: &ImportGraph) -> Vec<Vec<String>> {
tarjan_scc(&graph.adj)
.into_iter()
.filter(|comp| comp.len() >= 2)
.map(|comp| {
let mut paths: Vec<String> = comp
.into_iter()
.map(|id| graph.id_to_path[id].clone())
.collect();
paths.sort();
paths
})
.collect()
}
fn bisect_formation<R: Repo>(
db: &FactsDb,
repo: &R,
commits: &[(String, String, String)],
members: &[String],
cache: &mut HashMap<String, Rc<ImportGraph>>,
) -> Result<usize> {
let mut lo = 0usize;
let mut hi = commits.len() - 1; while lo < hi {
let mid = lo + (hi - lo) / 2;
let (rev, ts, _) = &commits[mid];
let graph = graph_at_rev_cached(db, repo, rev, ts, cache)?;
if members_form_one_cycle(&graph, members) {
hi = mid;
} else {
lo = mid + 1;
}
}
Ok(lo)
}
fn members_form_one_cycle(graph: &ImportGraph, members: &[String]) -> bool {
let mut ids = Vec::with_capacity(members.len());
for p in members {
match graph.path_to_id.get(p) {
Some(&id) => ids.push(id),
None => return false,
}
}
if ids.len() < 2 {
return false;
}
let sccs = tarjan_scc(&graph.adj);
let mut scc_of = vec![usize::MAX; graph.len()];
for (ci, comp) in sccs.iter().enumerate() {
for &n in comp {
scc_of[n] = ci;
}
}
let first = scc_of[ids[0]];
ids.iter().all(|&id| scc_of[id] == first)
}