use std::collections::HashSet; use std::collections::VecDeque; use rusqlite::Connection; use super::error::Result; /// Resolves a file path through its rename history in `mr_file_changes`. /// /// BFS in both directions: forward (`old_path` -> `new_path`) and backward /// (`new_path` -> `old_path`). Returns all equivalent paths including the /// original, sorted for determinism. Cycles are detected via a visited set. /// /// `max_hops` limits the BFS depth (distance from the starting path). pub fn resolve_rename_chain( conn: &Connection, project_id: i64, path: &str, max_hops: usize, ) -> Result> { let mut visited: HashSet = HashSet::new(); visited.insert(path.to_string()); if max_hops == 0 { return Ok(vec![path.to_string()]); } let mut queue: VecDeque<(String, usize)> = VecDeque::new(); queue.push_back((path.to_string(), 0)); let forward_sql = "\ SELECT DISTINCT mfc.new_path FROM mr_file_changes mfc \ WHERE mfc.project_id = ?1 AND mfc.old_path = ?2 AND mfc.change_type = 'renamed'"; let backward_sql = "\ SELECT DISTINCT mfc.old_path FROM mr_file_changes mfc \ WHERE mfc.project_id = ?1 AND mfc.new_path = ?2 AND mfc.change_type = 'renamed'"; while let Some((current, depth)) = queue.pop_front() { if depth >= max_hops { continue; } // Forward: current was the old name -> discover new names let mut fwd_stmt = conn.prepare_cached(forward_sql)?; let forward: Vec = fwd_stmt .query_map(rusqlite::params![project_id, ¤t], |row| row.get(0))? .filter_map(std::result::Result::ok) .collect(); // Backward: current was the new name -> discover old names let mut bwd_stmt = conn.prepare_cached(backward_sql)?; let backward: Vec = bwd_stmt .query_map(rusqlite::params![project_id, ¤t], |row| row.get(0))? .filter_map(std::result::Result::ok) .collect(); for discovered in forward.into_iter().chain(backward) { if visited.insert(discovered.clone()) { queue.push_back((discovered, depth + 1)); } } } let mut paths: Vec = visited.into_iter().collect(); paths.sort(); Ok(paths) } #[cfg(test)] #[path = "file_history_tests.rs"] mod tests;