#[cfg(not(target_arch = "wasm32"))]
mod build;
#[cfg(not(target_arch = "wasm32"))]
mod commit;
#[cfg(not(target_arch = "wasm32"))]
mod compact;
#[cfg(not(target_arch = "wasm32"))]
mod compact_plan;
pub(crate) mod encoding;
mod helpers;
pub(crate) mod io_util;
pub(crate) use io_util::open_readonly_nofollow;
#[cfg(any(unix, windows))]
pub(crate) use io_util::verify_fd_matches_stat;
#[cfg(not(target_arch = "wasm32"))]
pub mod manifest;
#[cfg(not(target_arch = "wasm32"))]
mod open;
pub mod overlay;
pub mod pending;
pub mod segment;
pub mod snapshot;
#[cfg(not(target_arch = "wasm32"))]
mod stats;
pub mod walk;
#[cfg(feature = "wasm")]
pub mod wasm_index;
#[cfg(not(target_arch = "wasm32"))]
pub use build::ExternalFileRecord;
pub use snapshot::{BaseSegments, IndexSnapshot};
pub(crate) use encoding::normalize_encoding;
pub use walk::is_binary;
#[cfg(not(target_arch = "wasm32"))]
use std::path::{Component, Path};
#[cfg(not(target_arch = "wasm32"))]
use std::sync::Arc;
#[cfg(not(target_arch = "wasm32"))]
use arc_swap::ArcSwap;
#[cfg(not(target_arch = "wasm32"))]
use crate::index::manifest::Manifest;
#[cfg(not(target_arch = "wasm32"))]
use crate::index::overlay::PendingEdits;
#[cfg(not(target_arch = "wasm32"))]
use crate::{Config, IndexError, IndexStats, SearchMatch, SearchOptions};
#[cfg(not(target_arch = "wasm32"))]
const OVERLAY_WARN_THRESHOLD: f64 = 0.30;
#[cfg(not(target_arch = "wasm32"))]
const MAX_TOTAL_DOCS: u32 = 50_000_000;
#[cfg(not(target_arch = "wasm32"))]
const OVERLAY_ENFORCE_THRESHOLD: f64 = 0.50;
#[cfg(not(target_arch = "wasm32"))]
pub struct Index {
pub config: Config,
snapshot: ArcSwap<snapshot::IndexSnapshot>,
pending: PendingEdits,
_dir_lock: std::fs::File,
pub canonical_root: std::path::PathBuf,
#[cfg(feature = "symbols")]
pub symbol_index: Option<std::sync::Arc<crate::symbol::SymbolIndex>>,
}
#[cfg(not(target_arch = "wasm32"))]
impl Index {
fn install_rebuilt_index(&self, rebuilt: &Index) -> Result<IndexStats, IndexError> {
self.snapshot.store(rebuilt.snapshot());
self.pending.reset();
#[cfg(feature = "symbols")]
if let Some(symbol_index) = &self.symbol_index {
symbol_index.reopen(&self.config.index_dir.join("symbols.db"))?;
}
Ok(self.stats())
}
fn rebuild_with(
&self,
build_fn: impl FnOnce(Config) -> Result<Index, IndexError>,
) -> Result<IndexStats, IndexError> {
self._dir_lock.unlock()?;
let rebuilt = match build_fn(self.config.clone()) {
Ok(rebuilt) => rebuilt,
Err(err) => {
self._dir_lock
.try_lock_shared()
.map_err(|_| IndexError::LockConflict(self.config.index_dir.clone()))?;
return Err(err);
}
};
self._dir_lock
.try_lock_shared()
.map_err(|_| IndexError::LockConflict(self.config.index_dir.clone()))?;
self.install_rebuilt_index(&rebuilt)
}
fn repo_relative_path(&self, path: &Path) -> Result<std::path::PathBuf, IndexError> {
let rel = path
.strip_prefix(&self.config.repo_root)
.map_err(|_| IndexError::PathOutsideRepo(path.to_path_buf()))?;
if rel.components().any(|component| {
matches!(
component,
Component::ParentDir | Component::RootDir | Component::Prefix(_)
)
}) {
return Err(IndexError::PathOutsideRepo(path.to_path_buf()));
}
self.normalize_repo_relative_path(rel)
}
fn normalize_repo_relative_path(&self, rel: &Path) -> Result<std::path::PathBuf, IndexError> {
if !self.path_has_intermediate_symlink(rel)? {
return Ok(rel.to_path_buf());
}
let abs = self.config.repo_root.join(rel);
let Some(parent) = abs.parent() else {
return Ok(rel.to_path_buf());
};
let canonical_parent = std::fs::canonicalize(parent)?;
if !canonical_parent.starts_with(&self.canonical_root) {
return Err(IndexError::PathOutsideRepo(abs));
}
let Some(file_name) = rel.file_name() else {
return Ok(rel.to_path_buf());
};
let normalized = canonical_parent.join(file_name);
normalized
.strip_prefix(&self.canonical_root)
.map(|p| p.to_path_buf())
.map_err(|_| IndexError::PathOutsideRepo(normalized))
}
fn path_has_intermediate_symlink(&self, rel: &Path) -> Result<bool, IndexError> {
let mut current = self.config.repo_root.clone();
let mut components = rel.components().peekable();
while let Some(component) = components.next() {
let Component::Normal(part) = component else {
continue;
};
if components.peek().is_none() {
break;
}
current.push(part);
match std::fs::symlink_metadata(¤t) {
Ok(meta) if meta.file_type().is_symlink() => return Ok(true),
Ok(_) => {}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(false),
Err(err) => return Err(IndexError::Io(err)),
}
}
Ok(false)
}
pub fn build(config: Config) -> Result<Self, IndexError> {
build::build_index(config)
}
pub fn build_from_file_records(
config: Config,
records: Vec<ExternalFileRecord>,
) -> Result<Self, IndexError> {
build::build_index_from_external_records(config, records)
}
pub fn stats(&self) -> IndexStats {
let snap = self.snapshot.load();
stats::compute_stats(
snap.as_ref(),
&self.config,
self.pending.uncommitted_count(),
)
}
pub fn search(
&self,
pattern: &str,
opts: &SearchOptions,
) -> Result<Vec<SearchMatch>, IndexError> {
#[cfg(feature = "symbols")]
if let Some((name, kind)) = crate::symbol::parse_symbol_prefix(pattern) {
if let Some(sym_idx) = &self.symbol_index {
return sym_idx.search(&name, kind);
}
}
crate::search::search(
self.snapshot(),
&self.config,
&self.canonical_root,
pattern,
opts,
)
}
pub fn snapshot(&self) -> Arc<IndexSnapshot> {
self.snapshot.load_full()
}
pub fn notify_change(&self, path: &Path) -> Result<(), IndexError> {
let rel = self.repo_relative_path(path)?;
self.pending.notify_change(&rel);
Ok(())
}
pub fn notify_delete(&self, path: &Path) -> Result<(), IndexError> {
let rel = self.repo_relative_path(path)?;
self.pending.notify_delete(&rel);
Ok(())
}
pub fn notify_change_immediate(&self, path: &Path) -> Result<(), IndexError> {
self.notify_change(path)?;
self.commit_batch()
}
pub fn maybe_compact(&self) -> Result<bool, IndexError> {
if self.pending.has_uncommitted() {
self.commit_batch()?;
}
let snapshot = self.snapshot();
if compact::plan(snapshot.as_ref(), &self.config).is_none() {
return Ok(false);
}
self.compact()?;
Ok(true)
}
pub fn compact(&self) -> Result<(), IndexError> {
if self.pending.has_uncommitted() {
self.commit_batch()?;
}
{
let snap = self.snapshot();
if compact::forced_plan(snap.as_ref(), &self.config).is_none() {
return Ok(());
}
}
let write_lock = helpers::acquire_writer_lock(&self.config.index_dir)?;
let snapshot = self.snapshot();
let Some(plan) = compact::forced_plan(snapshot.as_ref(), &self.config) else {
return Ok(());
};
self._dir_lock.unlock()?;
let rebuilt = match compact::compact_index(self.config.clone(), snapshot, plan, write_lock)
{
Ok(rebuilt) => rebuilt,
Err(err) => {
self._dir_lock
.try_lock_shared()
.map_err(|_| IndexError::LockConflict(self.config.index_dir.clone()))?;
return Err(err);
}
};
self._dir_lock
.try_lock_shared()
.map_err(|_| IndexError::LockConflict(self.config.index_dir.clone()))?;
self.install_rebuilt_index(&rebuilt)?;
Ok(())
}
pub fn rebuild_if_stale(&self) -> Result<Option<IndexStats>, IndexError> {
if self.pending.has_uncommitted() {
self.commit_batch()?;
}
let manifest = Manifest::load(&self.config.index_dir)?;
let current_head = helpers::current_repo_head(&self.config.repo_root)?;
if manifest.base_commit == current_head {
return Ok(None);
}
self.rebuild_with(build::build_index).map(Some)
}
}
#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests;