use std::path::{Path, PathBuf};
use crate::config::Context;
use crate::db;
use crate::index::{hasher, indexer};
use crate::models::Symbol;
const INFLIGHT_ENV: &str = "GCODE_FRESHNESS_INFLIGHT";
pub enum FreshnessScope {
Project,
Files(Vec<PathBuf>),
}
pub fn ensure_fresh(ctx: &Context, scope: FreshnessScope) -> anyhow::Result<()> {
if std::env::var_os(INFLIGHT_ENV).is_some() {
return Ok(());
}
let _guard = FreshnessGuard::enter();
let mut conn = db::connect_readwrite(&ctx.database_url)?;
match scope {
FreshnessScope::Project => {
indexer::index_directory(
&mut conn,
&ctx.project_root,
&ctx.project_id,
true,
ctx.quiet,
)?;
}
FreshnessScope::Files(paths) => {
let files: Vec<String> = paths
.iter()
.map(|path| normalize_file_path(&ctx.project_root, path))
.collect();
if !files.is_empty() {
indexer::index_files(&mut conn, &ctx.project_root, &ctx.project_id, &files)?;
}
}
}
Ok(())
}
pub fn ensure_symbol_fresh(ctx: &Context, id: &str) -> anyhow::Result<()> {
if std::env::var_os(INFLIGHT_ENV).is_some() {
return Ok(());
}
let mut conn = db::connect_readonly(&ctx.database_url)?;
let columns = db::symbol_select_columns("");
let sym = conn
.query_opt(
&format!("SELECT {columns} FROM code_symbols WHERE id = $1 AND project_id = $2"),
&[&id, &ctx.project_id],
)?
.as_ref()
.and_then(|row| Symbol::from_row(row).ok());
drop(conn);
let Some(sym) = sym else {
return Ok(());
};
if symbol_slice_is_current(ctx, &sym) {
return Ok(());
}
ensure_fresh(
ctx,
FreshnessScope::Files(vec![PathBuf::from(&sym.file_path)]),
)
}
fn symbol_slice_is_current(ctx: &Context, sym: &Symbol) -> bool {
if sym.content_hash.is_empty() {
return false;
}
let file_path = ctx.project_root.join(&sym.file_path);
let source = match std::fs::read(file_path) {
Ok(source) => source,
Err(_) => return false,
};
hasher::symbol_content_hash(&source, sym.byte_start, sym.byte_end)
.map(|hash| hash == sym.content_hash)
.unwrap_or(false)
}
fn normalize_file_path(root: &Path, path: &Path) -> String {
let abs = if path.is_absolute() {
path.to_path_buf()
} else {
root.join(path)
};
abs.canonicalize()
.ok()
.and_then(|canonical| {
root.canonicalize().ok().and_then(|canonical_root| {
canonical
.strip_prefix(canonical_root)
.ok()
.map(Path::to_path_buf)
})
})
.unwrap_or_else(|| path.to_path_buf())
.to_string_lossy()
.to_string()
}
struct FreshnessGuard;
impl FreshnessGuard {
fn enter() -> Self {
unsafe { std::env::set_var(INFLIGHT_ENV, "1") };
Self
}
}
impl Drop for FreshnessGuard {
fn drop(&mut self) {
unsafe { std::env::remove_var(INFLIGHT_ENV) };
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::CODE_INDEX_UUID_NAMESPACE;
fn context_for(root: &Path) -> Context {
Context {
database_url: "postgresql://localhost/gobby-test".to_string(),
project_root: root.to_path_buf(),
project_id: "proj".to_string(),
quiet: true,
falkordb: None,
qdrant: None,
embedding: None,
daemon_url: None,
}
}
fn symbol_hash(source: &[u8], start: usize, end: usize) -> String {
hasher::symbol_content_hash(source, start, end).expect("symbol hash")
}
#[test]
#[serial_test::serial]
fn no_freshness_env_short_circuits_project_refresh() {
let tmp = tempfile::tempdir().expect("tempdir");
let ctx = context_for(tmp.path());
unsafe { std::env::set_var(INFLIGHT_ENV, "1") };
let result = ensure_fresh(&ctx, FreshnessScope::Project);
unsafe { std::env::remove_var(INFLIGHT_ENV) };
assert!(result.is_ok());
}
#[test]
#[serial_test::serial]
fn symbol_slice_check_uses_stored_byte_range_hash() {
let tmp = tempfile::tempdir().expect("tempdir");
let source = b"fn before() {}\nfn target() {}\n";
std::fs::write(tmp.path().join("lib.rs"), source).expect("write file");
let ctx = context_for(tmp.path());
let start = 15;
let end = source.len();
let sym = Symbol {
id: uuid::Uuid::new_v5(&CODE_INDEX_UUID_NAMESPACE, b"sym").to_string(),
project_id: "proj".to_string(),
file_path: "lib.rs".to_string(),
name: "target".to_string(),
qualified_name: "target".to_string(),
kind: "function".to_string(),
language: "rust".to_string(),
byte_start: start,
byte_end: end,
line_start: 2,
line_end: 2,
signature: None,
docstring: None,
parent_symbol_id: None,
content_hash: symbol_hash(source, start, end),
summary: None,
created_at: String::new(),
updated_at: String::new(),
};
assert!(symbol_slice_is_current(&ctx, &sym));
std::fs::write(
tmp.path().join("lib.rs"),
b"// shifted\nfn before() {}\nfn target() {}\n",
)
.expect("shift file");
assert!(!symbol_slice_is_current(&ctx, &sym));
}
}