pub mod query;
pub mod render;
pub mod watermark;
#[cfg(test)]
mod tests;
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct BannerContext {
pub cwd: String,
pub client_name: String,
}
const BANNER_BUDGET: Duration = Duration::from_millis(50);
const MAX_RULES_IN_BANNER: usize = 5;
pub async fn render_since_last_session_banner(ctx: &BannerContext) -> Option<String> {
let result: Result<Option<String>, tokio::time::error::Elapsed> =
tokio::time::timeout(BANNER_BUDGET, render_inner(ctx)).await;
result.unwrap_or_default()
}
async fn render_inner(ctx: &BannerContext) -> Option<String> {
let project_root = resolve_project_root(&ctx.cwd);
let project_hash = difflore_core::db::project_hash_from_root(&project_root);
let repo_aliases = repo_aliases_for(&project_root);
if repo_aliases.is_empty() {
return None;
}
let prev_ts = watermark::read_watermark(&project_hash).map(|w| w.ts_ms);
let Ok(db) = difflore_core::db::init_db().await else {
return None;
};
let rows = query::new_rules_since(&db, prev_ts, &repo_aliases, MAX_RULES_IN_BANNER)
.await
.ok()?;
let now_ms = chrono::Utc::now().timestamp_millis();
let _ = watermark::write_watermark(
&project_hash,
&watermark::Watermark {
ts_ms: now_ms,
client: ctx.client_name.clone(),
},
);
if rows.is_empty() {
return None;
}
let prev_label = prev_ts
.and_then(timestamp_to_rfc3339)
.unwrap_or_else(|| "the start of this repo".to_owned());
Some(render::format_banner(&rows, &prev_label))
}
fn resolve_project_root(cwd: &str) -> std::path::PathBuf {
if cwd.is_empty() {
return difflore_core::db::current_project_root();
}
let output = std::process::Command::new("git")
.args(["rev-parse", "--show-toplevel"])
.current_dir(cwd)
.output();
if let Ok(out) = output
&& out.status.success()
{
let s = String::from_utf8_lossy(&out.stdout).trim().to_owned();
if !s.is_empty() {
return std::path::PathBuf::from(s);
}
}
std::path::PathBuf::from(cwd)
}
fn repo_aliases_for(project_root: &std::path::Path) -> Vec<String> {
let raw = difflore_core::git::detect_github_repo_full_names(&project_root.to_string_lossy());
raw.into_iter()
.map(|r| r.trim().to_ascii_lowercase())
.filter(|r| !r.is_empty())
.collect()
}
fn timestamp_to_rfc3339(ts_ms: i64) -> Option<String> {
chrono::DateTime::<chrono::Utc>::from_timestamp_millis(ts_ms).map(|dt| dt.to_rfc3339())
}