use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use super::{OBJECT_CACHE_BYTES, current_unix_seconds, history, repo, walk_err};
use crate::vcs::HistoryIndex;
use crate::vcs::cache::{self, CacheConfig, CommitEvent, HistoryCache};
use crate::vcs::error::Error;
use crate::vcs::options::Options;
use crate::vcs::replay;
use crate::vcs::score::RISK_SCORE_VERSION;
use crate::vcs::stats::VCS_SCHEMA_VERSION;
pub(crate) fn build_cached(
root: &Path,
options: &Options,
config: &CacheConfig,
) -> Result<HistoryIndex, Error> {
let repo::OpenRepo {
mut repo,
workdir,
shallow,
} = repo::open(root)?;
repo.object_cache_size_if_unset(OBJECT_CACHE_BYTES);
let commit = repo::resolve_commit(&repo, &options.reference)?;
if let Some(as_of) = options.as_of {
let head_time = commit.time().map_err(walk_err)?.seconds;
if as_of < head_time {
return super::build(root, options);
}
}
let head_oid = commit.id;
let head_sha = head_oid.to_string();
let target_tree = commit.tree().map_err(walk_err)?;
let now = options.as_of.unwrap_or_else(current_unix_seconds);
let repo_dir = cache_dir(config, root, workdir.as_deref());
if config.clear
&& let Some(dir) = &repo_dir
{
cache::clear_repo(dir)?;
}
let seed = repo::enumerate_target_files(&repo, &target_tree, &options.file_types)?;
let Some(repo_dir) = repo_dir.filter(|_| config.enabled) else {
let (events, _) = history::collect_events(&repo, head_oid, options, now, &HashSet::new())?;
return Ok(assemble(seed, &events, options, now, workdir, shallow));
};
let fingerprint = cache::fingerprint(options);
let long_boundary = now - options.long_window_secs;
let exact_path = cache::entry_path(&repo_dir, &head_sha);
if let Some(entry) = cache::load(&exact_path).filter(|entry| {
entry.is_compatible(fingerprint, shallow) && entry.walk_long_boundary <= long_boundary
}) {
return Ok(assemble(
seed,
&entry.events,
options,
now,
workdir,
shallow,
));
}
let (events, superseded) = incremental_events(
&repo,
&repo_dir,
head_oid,
&head_sha,
fingerprint,
long_boundary,
options,
now,
shallow,
)?;
let index = assemble(seed, &events, options, now, workdir, shallow);
persist(
&exact_path,
&events,
fingerprint,
&head_sha,
long_boundary,
shallow,
);
if let Some(path) = superseded {
let _ = std::fs::remove_file(path);
}
Ok(index)
}
#[allow(clippy::too_many_arguments)]
fn incremental_events(
repo: &gix::Repository,
repo_dir: &Path,
head_oid: gix::ObjectId,
head_sha: &str,
fingerprint: u64,
long_boundary: i64,
options: &Options,
now: i64,
shallow: bool,
) -> Result<(Vec<CommitEvent>, Option<PathBuf>), Error> {
let candidates = load_candidates(
repo_dir,
fingerprint,
long_boundary,
options.full_history,
shallow,
);
let stop_oids: HashSet<gix::ObjectId> = candidates.keys().copied().collect();
let (new_events, stopped) = history::collect_events(repo, head_oid, options, now, &stop_oids)?;
let spliced = stopped.and_then(|oid| candidates.get(&oid));
let superseded = spliced
.filter(|candidate| candidate.cache.head_sha != head_sha)
.map(|candidate| candidate.path.clone());
let mut events = new_events;
if let Some(candidate) = spliced {
events.extend(candidate.cache.events.iter().cloned());
}
Ok((prune_and_dedup(events, now, long_boundary), superseded))
}
struct Candidate {
path: PathBuf,
cache: HistoryCache,
}
fn cache_dir(config: &CacheConfig, root: &Path, workdir: Option<&Path>) -> Option<PathBuf> {
let cache_root = config.dir.clone().or_else(cache::default_cache_dir)?;
let canonical = workdir.map_or_else(
|| root.canonicalize().unwrap_or_else(|_| root.to_path_buf()),
Path::to_path_buf,
);
Some(cache::repo_dir(&cache_root, &canonical))
}
fn load_candidates(
repo_dir: &Path,
fingerprint: u64,
long_boundary: i64,
full_history: bool,
shallow: bool,
) -> HashMap<gix::ObjectId, Candidate> {
if full_history {
return HashMap::new();
}
cache::load_compatible(repo_dir, fingerprint, shallow)
.into_iter()
.filter(|(_, cache)| cache.walk_long_boundary <= long_boundary)
.filter_map(|(path, cache)| {
let oid = gix::ObjectId::from_hex(cache.head_sha.as_bytes()).ok()?;
Some((oid, Candidate { path, cache }))
})
.collect()
}
fn assemble(
seed: HashMap<PathBuf, u64>,
events: &[CommitEvent],
options: &Options,
now: i64,
workdir: Option<PathBuf>,
shallow: bool,
) -> HistoryIndex {
let out = replay::replay(seed, events, options, now);
HistoryIndex::new(out.files, workdir, shallow).with_bus_factor(out.bus_factor)
}
fn prune_and_dedup(events: Vec<CommitEvent>, now: i64, long_boundary: i64) -> Vec<CommitEvent> {
let mut seen = HashSet::new();
events
.into_iter()
.filter(|event| event.time.min(now) >= long_boundary)
.filter(|event| seen.insert(event.oid.clone()))
.collect()
}
fn persist(
path: &Path,
events: &[CommitEvent],
fingerprint: u64,
head_sha: &str,
long_boundary: i64,
shallow: bool,
) {
let entry = HistoryCache {
cache_schema_version: cache::CACHE_SCHEMA_VERSION,
vcs_schema_version: VCS_SCHEMA_VERSION,
risk_score_version: RISK_SCORE_VERSION,
options_fingerprint: fingerprint,
head_sha: head_sha.to_owned(),
walk_long_boundary: long_boundary,
truncated_shallow_clone: shallow,
events: events.to_vec(),
};
if let Err(e) = cache::write_atomic(path, &entry) {
eprintln!("warning: failed to write VCS history cache: {e}");
}
}