use crate::db::GraphDb;
use crate::repograph::facts::{
author_counts, author_name, commits_of, int_prop, label_of, neighbors, newest_commit_ts,
owner_key, rank, score_of, CommitFact,
};
use crate::repograph::render::{quarter_index, quarter_label, sanitize};
use crate::Direction;
use core_storage::fs::Fs;
use serde::Serialize;
use std::collections::BTreeMap;
pub const QUARTERS: i64 = 4;
const MAX_KNOWS: usize = 4;
pub(super) const SHA_LEN: usize = 7;
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct OwnersReport {
pub path: String,
pub top: Option<(String, String, f64)>,
pub knows: Vec<(String, f64)>,
pub last_touch: Option<(String, i64, String)>,
pub by_quarter: Vec<(String, String, usize)>,
}
#[must_use]
pub fn owners<F: Fs>(db: &GraphDb<F>, path: &str, now_ts: Option<i64>) -> Option<OwnersReport> {
if label_of(db, path)? != "File" {
return None;
}
let commits = commits_of(db, path);
Some(OwnersReport {
path: sanitize(path),
top: top_author(db, path),
knows: knows(db, path),
last_touch: commits.first().map(|c| {
(
c.sha.chars().take(SHA_LEN).collect(),
c.ts,
sanitize(&c.subject),
)
}),
by_quarter: by_quarter(db, &commits, now_ts),
})
}
fn top_author<F: Fs>(db: &GraphDb<F>, path: &str) -> Option<(String, String, f64)> {
let key = owner_key(db, path)?;
let recorded = author_counts(db, path);
let total = int_prop(db, path, "n_commits").unwrap_or(0).max(0) as usize;
let (mine, all) = if recorded.is_empty() {
let counted = commit_authors(db, path);
let all: usize = counted.values().sum();
(counted.get(&key).copied().unwrap_or(0), all)
} else {
let mine = recorded
.iter()
.find(|(k, _)| *k == key)
.map_or(0, |(_, n)| *n);
let all = if total > 0 {
total
} else {
recorded.iter().map(|(_, n)| *n).sum()
};
(mine, all)
};
let share = if all == 0 {
0.0
} else {
mine as f64 / all as f64
};
Some((sanitize(&author_name(db, &key)), sanitize(&key), share))
}
fn commit_authors<F: Fs>(db: &GraphDb<F>, path: &str) -> BTreeMap<String, usize> {
let mut out: BTreeMap<String, usize> = BTreeMap::new();
for commit in commits_of(db, path) {
for author in neighbors(db, &commit.sha, "AUTHOR", Direction::Out) {
*out.entry(author).or_default() += 1;
}
}
out
}
fn knows<F: Fs>(db: &GraphDb<F>, path: &str) -> Vec<(String, f64)> {
let mut out: Vec<(String, f64)> = neighbors(db, path, "KNOWS", Direction::In)
.into_iter()
.map(|author| {
let score = score_of(db, "KNOWS", &author, path).unwrap_or(0.0);
(sanitize(&author_name(db, &author)), score)
})
.collect();
rank(&mut out);
out.truncate(MAX_KNOWS);
out
}
fn by_quarter<F: Fs>(
db: &GraphDb<F>,
commits: &[CommitFact],
now_ts: Option<i64>,
) -> Vec<(String, String, usize)> {
let Some(now) = now_ts.or_else(|| newest_commit_ts(db)) else {
return Vec::new();
};
let last = quarter_index(now);
let first = last - (QUARTERS - 1);
let mut counts: BTreeMap<i64, BTreeMap<String, usize>> = BTreeMap::new();
for commit in commits {
let q = quarter_index(commit.ts);
if !(first..=last).contains(&q) {
continue;
}
for author in neighbors(db, &commit.sha, "AUTHOR", Direction::Out) {
*counts.entry(q).or_default().entry(author).or_default() += 1;
}
}
counts
.into_iter()
.map(|(q, by_author)| {
let total: usize = by_author.values().sum();
let top = by_author
.iter()
.max_by(|a, b| a.1.cmp(b.1).then(b.0.cmp(a.0)))
.map(|(key, _)| sanitize(&author_name(db, key)))
.unwrap_or_default();
(quarter_label(q), top, total)
})
.collect()
}