use crate::core::{self, build_import_graph, update_import_graph_for_files, Corpus, EdgeMap};
use crate::history::{mine_history, HistoryData};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
pub const CACHE_VERSION: i64 = 4;
pub const CACHE_DIRNAME: &str = ".roust";
const INDEX_FILENAME: &str = "rust-index.bin";
pub type Manifest = HashMap<String, (i64, u64)>;
#[derive(Serialize, Deserialize)]
struct CachePayload {
version: i64,
key: String,
corpus: Corpus,
edges: EdgeMap,
history: Option<HistoryData>,
manifest: Manifest,
}
#[derive(Serialize)]
struct CachePayloadRef<'a> {
version: i64,
key: &'a str,
corpus: &'a Corpus,
edges: &'a EdgeMap,
history: &'a Option<HistoryData>,
manifest: &'a Manifest,
}
fn git_head_sha(repo_path: &Path) -> String {
let output = std::process::Command::new("git")
.args(["rev-parse", "HEAD"])
.current_dir(repo_path)
.output();
match output {
Ok(o) if o.status.success() => {
let sha = String::from_utf8_lossy(&o.stdout).trim().to_string();
if sha.is_empty() {
"nogit".to_string()
} else {
sha
}
}
_ => "nogit".to_string(),
}
}
fn scan_manifest(repo_path: &Path, with_docs: bool) -> Manifest {
let mut exts: HashSet<&str> = core::CODE_EXTENSIONS.iter().copied().collect();
if core::cfamily_ext_enabled() {
exts.extend(core::CFAMILY_EXTENSIONS.iter().copied());
}
if with_docs {
exts.extend(core::DOCS_EXTENSIONS.iter().copied());
}
let mut manifest = Manifest::new();
for rel in core::walk_all_files(repo_path) {
if rel.starts_with(".git/") || rel.contains("/.git/") {
continue;
}
if rel.starts_with(&format!("{CACHE_DIRNAME}/")) || rel.contains(&format!("/{CACHE_DIRNAME}/")) {
continue;
}
if !exts.contains(core::suffix_of(&rel)) {
continue;
}
let full = repo_path.join(&rel);
let meta = match std::fs::metadata(&full) {
Ok(m) => m,
Err(_) => continue,
};
if !meta.is_file() {
continue;
}
let mtime_ns = meta
.modified()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_nanos() as i64)
.unwrap_or(0);
manifest.insert(rel, (mtime_ns, meta.len()));
}
manifest
}
enum Verdict {
Unchanged,
Modified,
Full,
}
fn classify_changes(repo_path: &Path, with_docs: bool, manifest: &Manifest) -> (Verdict, Manifest, Vec<String>) {
let current = scan_manifest(repo_path, with_docs);
let old_keys: HashSet<&String> = manifest.keys().collect();
let new_keys: HashSet<&String> = current.keys().collect();
if !old_keys.is_subset(&new_keys) || !new_keys.is_subset(&old_keys) {
return (Verdict::Full, current, Vec::new());
}
let modified: Vec<String> = current
.iter()
.filter(|(k, v)| manifest.get(*k) != Some(v))
.map(|(k, _)| k.clone())
.collect();
if modified.is_empty() {
return (Verdict::Unchanged, current, Vec::new());
}
(Verdict::Modified, current, modified)
}
fn cache_key(repo_path: &Path, with_history: bool, with_docs: bool) -> String {
let sha = git_head_sha(repo_path);
let cf = if core::cfamily_ext_enabled() { ":cf1" } else { "" };
let ip = if core::impl_prior_v2_enabled() { ":ipv2" } else { "" };
let sv = if core::symbols_v2_enabled() { ":sv2" } else { "" };
format!("{sha}:h{}:d{}{cf}{ip}{sv}", with_history as i32, with_docs as i32)
}
fn cache_path(repo_path: &Path) -> PathBuf {
repo_path.join(CACHE_DIRNAME).join(INDEX_FILENAME)
}
fn load(repo_path: &Path, key: &str) -> Option<CachePayload> {
let path = cache_path(repo_path);
if !path.exists() {
return None;
}
let file = std::fs::File::open(&path).ok()?;
let reader = std::io::BufReader::new(file);
let payload: CachePayload = serde_json::from_reader(reader).ok()?;
if payload.version != CACHE_VERSION || payload.key != key {
return None;
}
Some(payload)
}
fn save(repo_path: &Path, key: &str, corpus: &Corpus, edges: &EdgeMap, history: &Option<HistoryData>, manifest: &Manifest) {
let cache_dir = repo_path.join(CACHE_DIRNAME);
if std::fs::create_dir_all(&cache_dir).is_err() {
return;
}
let payload = CachePayloadRef { version: CACHE_VERSION, key, corpus, edges, history, manifest };
let final_path = cache_path(repo_path);
let tmp_path = cache_dir.join(format!("{INDEX_FILENAME}.{}.tmp", std::process::id()));
let write_result: std::io::Result<()> = (|| {
let file = std::fs::File::create(&tmp_path)?;
let writer = std::io::BufWriter::new(file);
serde_json::to_writer(writer, &payload).map_err(std::io::Error::other)?;
Ok(())
})();
match write_result {
Ok(()) => {
let _ = std::fs::rename(&tmp_path, &final_path);
}
Err(_) => {
let _ = std::fs::remove_file(&tmp_path);
}
}
}
fn collect_current_code_files(repo_path: &Path) -> HashSet<String> {
let mut files = HashSet::new();
for rel in core::walk_all_files(repo_path) {
if rel.starts_with(".git/") || rel.contains("/.git/") {
continue;
}
if rel.starts_with(&format!("{CACHE_DIRNAME}/")) || rel.contains(&format!("/{CACHE_DIRNAME}/")) {
continue;
}
if !core::is_code_file(&rel) {
continue;
}
let full = repo_path.join(&rel);
match std::fs::metadata(&full) {
Ok(m) if m.is_file() => {}
_ => continue,
}
files.insert(rel);
}
files
}
fn try_incremental_update(corpus: &mut Corpus, edges: &mut EdgeMap, modified: &[String]) -> bool {
let code_rels: Vec<String> = modified.iter().filter(|r| core::code_suffix_allowed(core::suffix_of(r))).cloned().collect();
let docs_rels: Vec<String> = modified.iter().filter(|r| core::DOCS_EXTENSIONS.contains(&core::suffix_of(r))).cloned().collect();
if code_rels.iter().any(|r| !corpus.text.contains_key(r)) {
return false;
}
if docs_rels.iter().any(|r| !corpus.docs_text.contains_key(r)) {
return false;
}
let old_text: HashMap<String, String> = code_rels.iter().map(|r| (r.clone(), corpus.text[r].clone())).collect();
if !code_rels.is_empty() && !corpus.update_files(&code_rels) {
return false;
}
if !docs_rels.is_empty() && !corpus.update_docs_files(&docs_rels) {
return false;
}
if !code_rels.is_empty() {
update_import_graph_for_files(corpus, edges, &old_text);
}
true
}
fn build_fresh(repo_path: &Path, with_history: bool, with_docs: bool) -> (Corpus, EdgeMap, Option<HistoryData>) {
let history = if with_history {
let current_files = collect_current_code_files(repo_path);
Some(mine_history(repo_path, 5000, Some(¤t_files)))
} else {
None
};
let history_msgs = history.as_ref().map(|h| &h.msgs);
let corpus = Corpus::build(repo_path, history_msgs, false, with_docs);
let edges = build_import_graph(&corpus);
(corpus, edges, history)
}
pub fn load_or_build_ex(
repo_path: &Path,
with_history: bool,
with_docs: bool,
use_cache: bool,
force_reindex: bool,
) -> (Corpus, EdgeMap, Option<HistoryData>, bool, &'static str) {
let key = cache_key(repo_path, with_history, with_docs);
if use_cache && !force_reindex {
if let Some(payload) = load(repo_path, &key) {
let CachePayload { mut corpus, mut edges, history, manifest, .. } = payload;
let (verdict, new_manifest, modified) = classify_changes(repo_path, with_docs, &manifest);
match verdict {
Verdict::Unchanged => return (corpus, edges, history, true, "unchanged"),
Verdict::Modified => {
if try_incremental_update(&mut corpus, &mut edges, &modified) {
save(repo_path, &key, &corpus, &edges, &history, &new_manifest);
return (corpus, edges, history, true, "incremental");
}
}
Verdict::Full => {}
}
}
}
let (corpus, edges, history) = build_fresh(repo_path, with_history, with_docs);
let manifest = scan_manifest(repo_path, with_docs);
if use_cache {
save(repo_path, &key, &corpus, &edges, &history, &manifest);
}
(corpus, edges, history, false, "full")
}
pub fn load_or_build(
repo_path: &Path,
with_history: bool,
with_docs: bool,
use_cache: bool,
force_reindex: bool,
) -> (Corpus, EdgeMap, Option<HistoryData>, bool) {
let (corpus, edges, history, cache_hit, _update_kind) = load_or_build_ex(repo_path, with_history, with_docs, use_cache, force_reindex);
(corpus, edges, history, cache_hit)
}