use std::collections::HashSet;
use crate::analyses::import_graph::{build_import_graph_seeded, graph_metrics};
use crate::complexity::Tier1Language;
use crate::facts::FactsDb;
use crate::repo::Repo;
use crate::{Options, Result};
pub const SAMPLE_POINTS: usize = 12;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ArchitectureTrendRow {
pub date: String,
pub rev: String,
pub files: u32,
pub propagation_cost: f64,
pub cycle_count: u32,
pub largest_cycle: u32,
}
pub(crate) fn sampled_commits(db: &FactsDb) -> Result<Vec<(String, String)>> {
let commits: Vec<(String, String)> = crate::analyses::query::query_map_collect(
db,
"SELECT rev, CAST(date AS TEXT) FROM commits ORDER BY date ASC, rowid ASC",
[],
"sampled-commits",
|r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)),
)?;
if commits.is_empty() {
return Ok(Vec::new());
}
let picks = evenly_spaced_indices(commits.len(), SAMPLE_POINTS);
Ok(picks.into_iter().map(|i| commits[i].clone()).collect())
}
#[tracing::instrument(name = "architecture-trend", skip_all)]
pub fn run_architecture_trend<R: Repo>(
db: &FactsDb,
repo: &R,
_opts: &Options,
) -> Result<Vec<ArchitectureTrendRow>> {
let samples = sampled_commits(db)?;
let mut rows = Vec::with_capacity(samples.len());
for (rev, ts) in &samples {
let graph = import_graph_at_rev(db, repo, rev, ts)?;
let m = graph_metrics(&graph);
rows.push(ArchitectureTrendRow {
date: ts.get(..10).unwrap_or(ts).to_string(),
rev: rev.chars().take(12).collect(),
files: u32::try_from(m.n).unwrap_or(u32::MAX),
propagation_cost: m.propagation_cost,
cycle_count: m.cycle_count,
largest_cycle: m.largest_cycle,
});
}
Ok(rows)
}
pub(crate) fn import_graph_at_rev<R: Repo>(
db: &FactsDb,
repo: &R,
rev: &str,
ts: &str,
) -> Result<crate::analyses::import_graph::ImportGraph> {
let live = live_paths_at(db, ts)?;
Ok(import_graph_from_live_paths(repo, rev, &live))
}
pub(crate) fn import_graph_from_live_paths<R: Repo>(
repo: &R,
rev: &str,
live: &[String],
) -> crate::analyses::import_graph::ImportGraph {
let seeds: Vec<String> = live
.iter()
.filter(|p| Tier1Language::from_path(p.as_str()).is_some())
.cloned()
.collect();
let edges = resolve_imports_at_rev(repo, rev, live);
build_import_graph_seeded(&seeds, &edges)
}
pub(crate) fn evenly_spaced_indices(len: usize, k: usize) -> Vec<usize> {
if len == 0 {
return Vec::new();
}
if len <= k {
return (0..len).collect();
}
let mut out = Vec::with_capacity(k);
for i in 0..k {
out.push(i * (len - 1) / (k - 1));
}
out.dedup();
out
}
pub(crate) fn live_paths_at(db: &FactsDb, ts: &str) -> Result<Vec<String>> {
crate::analyses::query::query_map_collect(
db,
"SELECT path FROM ( \
SELECT c.path, \
arg_max(c.change_type, ROW(commits.date, -commits.rowid)) AS change_type \
FROM changes c \
INNER JOIN commits ON commits.rev = c.rev \
WHERE commits.date <= CAST(? AS TIMESTAMP) \
GROUP BY c.path \
) WHERE change_type != 'deleted'",
duckdb::params![ts],
"architecture-trend live-paths",
|r| r.get::<_, String>(0),
)
}
fn resolve_imports_at_rev<R: Repo>(
repo: &R,
rev: &str,
live_paths: &[String],
) -> Vec<(String, String)> {
use crate::imports::{ImportLanguage, extract_imports, resolve_by_extension};
use rayon::prelude::*;
let live_set: HashSet<String> = live_paths.iter().cloned().collect();
let candidates: Vec<(String, ImportLanguage)> = live_paths
.iter()
.filter_map(|rel| {
let lang = ImportLanguage::from_path(std::path::Path::new(rel))?;
Some((rel.clone(), lang))
})
.collect();
let edges: Vec<(String, String)> = candidates
.into_par_iter()
.flat_map_iter(|(rel, lang)| {
let mut out: Vec<(String, String)> = Vec::new();
let Ok(Some(code)) = repo.read_blob_at(rev, &rel) else {
return out.into_iter();
};
if code.len() > crate::constants::DEFAULT_MAX_AST_FILE_BYTES {
return out.into_iter();
}
let Ok(imports) = extract_imports(&code, lang) else {
return out.into_iter();
};
for imp in imports {
if let Some(target_path) = resolve_by_extension(&rel, &imp.target, &live_set) {
out.push((rel.clone(), target_path));
}
}
out.into_iter()
})
.collect();
edges
}