use std::collections::{HashMap, HashSet};
use std::path::Path;
use git2::Repository;
use crate::cli::commands::search::{collect_entries_from_chain, parse_trace_content};
use crate::core::{detect_version, StorageVersion};
use crate::domain::{IndexEntry, WrappedBlob, WrappedNeuralCommit};
use crate::error::Result;
use crate::search::{index_state, indexer};
use crate::storage::{GitObjectStore, GitRefStore, ObjectStore, RefStore};
pub fn index_new_commits(
repo_path: &Path,
agit_dir: &Path,
refs_before: &HashMap<String, String>,
refs_after: &HashMap<String, String>,
) -> Result<(usize, bool)> {
if !index_state::is_index_healthy(agit_dir) {
return full_rebuild(repo_path, agit_dir);
}
let repo = Repository::discover(repo_path)?;
let is_v2 = matches!(detect_version(agit_dir, &repo), StorageVersion::V2GitNative);
if !is_v2 {
return full_rebuild(repo_path, agit_dir);
}
let mut indexed_commits = index_state::load_indexed_commits(agit_dir)?;
let mut all_entries = Vec::new();
let mut newly_indexed = HashSet::new();
for (branch, new_hash) in refs_after {
let old_hash = refs_before.get(branch).map(|s| s.as_str());
collect_new_entries(
repo_path,
agit_dir,
is_v2,
new_hash,
old_hash,
&indexed_commits,
&mut all_entries,
&mut newly_indexed,
)?;
}
if all_entries.is_empty() {
return Ok((0, false));
}
indexer::index_entries(agit_dir, &all_entries)?;
indexed_commits.extend(newly_indexed);
index_state::save_indexed_commits(agit_dir, &indexed_commits)?;
Ok((all_entries.len(), false))
}
fn collect_new_entries(
repo_path: &Path,
_agit_dir: &Path,
_is_v2: bool,
start_hash: &str,
stop_at_hash: Option<&str>,
already_indexed: &HashSet<String>,
entries: &mut Vec<IndexEntry>,
newly_indexed: &mut HashSet<String>,
) -> Result<()> {
let mut current_hash = Some(start_hash.to_string());
while let Some(hash) = current_hash {
if let Some(stop) = stop_at_hash {
if hash == stop {
break;
}
}
if already_indexed.contains(&hash) {
break;
}
if newly_indexed.contains(&hash) {
break;
}
newly_indexed.insert(hash.clone());
let commit_data = GitObjectStore::new(repo_path).load(&hash)?;
let wrapped: WrappedNeuralCommit = serde_json::from_slice(&commit_data)?;
let commit = &wrapped.data;
let trace_result = GitObjectStore::new(repo_path).load(&commit.trace_hash);
if let Ok(trace_data) = trace_result {
if let Ok(trace_blob) = serde_json::from_slice::<WrappedBlob>(&trace_data) {
let parsed = parse_trace_content(&trace_blob.data.content, commit.created_at);
entries.extend(parsed);
}
}
current_hash = commit.first_parent().map(|s| s.to_string());
}
Ok(())
}
fn full_rebuild(repo_path: &Path, agit_dir: &Path) -> Result<(usize, bool)> {
let index_path = agit_dir.join("search_index");
if index_path.exists() {
std::fs::remove_dir_all(&index_path)?;
}
let is_v2 = match Repository::discover(repo_path) {
Ok(repo) => matches!(detect_version(agit_dir, &repo), StorageVersion::V2GitNative),
Err(_) => false,
};
let branches: Vec<String> = if is_v2 {
let ref_store = GitRefStore::new(repo_path);
ref_store.list()?
} else {
use crate::storage::FileRefStore;
let ref_store = FileRefStore::new(agit_dir);
ref_store.list()?
};
let mut all_entries = Vec::new();
let mut visited_commits = HashSet::new();
for branch in &branches {
let start_hash = if is_v2 {
let ref_store = GitRefStore::new(repo_path);
ref_store.get(branch)?
} else {
use crate::storage::FileRefStore;
let ref_store = FileRefStore::new(agit_dir);
ref_store.get(branch)?
};
if let Some(hash) = start_hash {
collect_entries_from_chain(
repo_path,
agit_dir,
is_v2,
&hash,
&mut all_entries,
&mut visited_commits,
)?;
}
}
if !all_entries.is_empty() {
indexer::index_entries(agit_dir, &all_entries)?;
}
index_state::save_indexed_commits(agit_dir, &visited_commits)?;
Ok((all_entries.len(), true))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_refs_no_work() {
let before: HashMap<String, String> = HashMap::new();
let after: HashMap<String, String> = HashMap::new();
assert!(before.is_empty());
assert!(after.is_empty());
}
}