use crate::db::GraphDb;
use crate::repograph::facts::str_prop;
use crate::repograph::map::{file_pagerank, SYNC_KEY};
use crate::repograph::render::{basename, dir_components, sanitize, top_tokens};
use core_storage::fs::Fs;
use core_storage::Value;
use serde::Serialize;
use std::collections::{BTreeMap, BTreeSet};
use std::time::{Duration, Instant};
const SHORT_SHA: usize = 7;
const ROLE_TOKENS: usize = 2;
const RANK_BUDGET: Duration = Duration::from_secs(3);
const DEPENDENCY_EDGES: [&str; 2] = ["IMPORTS", "CALLS"];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BriefOptions {
pub max_files: usize,
pub max_symbols: usize,
}
impl Default for BriefOptions {
fn default() -> Self {
Self {
max_files: 25,
max_symbols: 25,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct BriefReport {
pub repo: String,
pub files: usize,
pub symbols: usize,
pub edges: usize,
pub last_sync: Option<String>,
pub key_files: Vec<(String, String)>,
pub key_symbols: Vec<(String, String)>,
}
#[must_use]
pub fn brief<F: Fs>(db: &GraphDb<F>, opts: &BriefOptions) -> BriefReport {
let mut file_keys: Vec<String> = db
.nodes_with_label("File")
.iter()
.map(|n| n.key().to_string())
.collect();
file_keys.sort();
let (ranked, _truncated) = file_pagerank(db, &file_keys, Some(Instant::now() + RANK_BUDGET));
let connected = connected_files(db);
let key_files = ranked
.iter()
.filter(|(k, _)| connected.contains(k.as_str()))
.take(opts.max_files)
.map(|(k, _)| (sanitize(k), role_of(db, k, &file_keys)))
.collect();
let mut callers: BTreeMap<String, usize> = BTreeMap::new();
for (_src, dst, _w) in db.weighted_edges("CALLS", None) {
*callers.entry(dst).or_default() += 1;
}
let symbols = db.nodes_with_label("Symbol");
let mut ranked_symbols: Vec<(String, usize, String)> = symbols
.iter()
.map(|n| {
let key = n.key().to_string();
let called = callers.get(&key).copied().unwrap_or(0);
(key, called, first_line(n.prop("signature")))
})
.collect();
ranked_symbols.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
let key_symbols = ranked_symbols
.into_iter()
.take(opts.max_symbols)
.map(|(k, _, sig)| (sanitize(&k), sig))
.collect();
BriefReport {
repo: str_prop(db, SYNC_KEY, "repo")
.map(|p| sanitize(basename(p.trim_end_matches('/'))))
.unwrap_or_default(),
files: file_keys.len(),
symbols: symbols.len(),
edges: usize::try_from(db.edge_count()).unwrap_or(usize::MAX),
last_sync: str_prop(db, SYNC_KEY, "sha")
.map(|s| sanitize(&s).chars().take(SHORT_SHA).collect()),
key_files,
key_symbols,
}
}
fn connected_files<F: Fs>(db: &GraphDb<F>) -> BTreeSet<String> {
let mut sym_file: BTreeMap<String, String> = BTreeMap::new();
for node in db.nodes_with_label("Symbol") {
if let Some(Value::Str(file)) = node.prop("file_id") {
sym_file.insert(node.key().to_string(), file);
}
}
let mut out = BTreeSet::new();
for edge_type in DEPENDENCY_EDGES {
for (src, dst, _w) in db.weighted_edges(edge_type, None) {
for end in [src, dst] {
match sym_file.get(&end) {
Some(file) => out.insert(file.clone()),
None => out.insert(end),
};
}
}
}
out
}
fn first_line(v: Option<Value>) -> String {
match v {
Some(Value::Str(s)) => sanitize(s.lines().next().unwrap_or_default().trim()),
_ => String::new(),
}
}
fn role_of<F: Fs>(db: &GraphDb<F>, key: &str, file_keys: &[String]) -> String {
if let Some(role) = str_prop(db, key, "role") {
let role = sanitize(role.trim());
if !role.is_empty() {
return role;
}
}
let dir = dir_components(key).join("/");
if dir.is_empty() {
return String::new(); }
let prefix = format!("{dir}/");
let neighbours: Vec<String> = file_keys
.iter()
.filter(|k| k.starts_with(&prefix))
.cloned()
.collect();
sanitize(&top_tokens(&neighbours, &dir, ROLE_TOKENS, true).join(", "))
}