Extract path resolution logic into a new `src/core/path_resolver.rs` module to enable reuse across the codebase. Previously, `escape_like`, `normalize_repo_path`, `PathQuery`, `build_path_query`, `SuffixResult`, and `suffix_probe` were private to `who.rs`, making them inaccessible to other modules that needed the same path-matching logic. Key changes: - New `path_resolver.rs` with all path resolution functions made `pub` - New `path_resolver_tests.rs` with 15 tests covering: escape_like, normalize_repo_path, build_path_query heuristics, DB probes (exact, prefix, suffix), project scoping, and cross-source deduplication - Remove duplicate `escape_like` from `list.rs` (was `note_escape_like`) and `search/filters.rs` — both now import from `path_resolver` - `project.rs` imports `escape_like` from the new module The path resolution strategy (exact > prefix > suffix > heuristic fallback) and all DB probe logic are preserved exactly as extracted. `suffix_probe` checks both `new_path` and `old_path` columns to support querying renamed files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
113 lines
3.8 KiB
Rust
113 lines
3.8 KiB
Rust
use rusqlite::Connection;
|
|
|
|
use super::error::{LoreError, Result};
|
|
use super::path_resolver::escape_like;
|
|
|
|
pub fn resolve_project(conn: &Connection, project_str: &str) -> Result<i64> {
|
|
let exact = conn.query_row(
|
|
"SELECT id FROM projects WHERE path_with_namespace = ?1",
|
|
rusqlite::params![project_str],
|
|
|row| row.get::<_, i64>(0),
|
|
);
|
|
if let Ok(id) = exact {
|
|
return Ok(id);
|
|
}
|
|
|
|
let ci = conn.query_row(
|
|
"SELECT id FROM projects WHERE LOWER(path_with_namespace) = LOWER(?1)",
|
|
rusqlite::params![project_str],
|
|
|row| row.get::<_, i64>(0),
|
|
);
|
|
if let Ok(id) = ci {
|
|
return Ok(id);
|
|
}
|
|
|
|
let escaped = escape_like(project_str);
|
|
let mut suffix_stmt = conn.prepare(
|
|
"SELECT id, path_with_namespace FROM projects
|
|
WHERE path_with_namespace LIKE '%/' || ?1 ESCAPE '\\'
|
|
OR path_with_namespace = ?2",
|
|
)?;
|
|
let suffix_matches: Vec<(i64, String)> = suffix_stmt
|
|
.query_map(rusqlite::params![escaped, project_str], |row| {
|
|
Ok((row.get(0)?, row.get(1)?))
|
|
})?
|
|
.collect::<std::result::Result<Vec<_>, _>>()?;
|
|
|
|
match suffix_matches.len() {
|
|
1 => return Ok(suffix_matches[0].0),
|
|
n if n > 1 => {
|
|
let matching: Vec<String> = suffix_matches.iter().map(|(_, p)| p.clone()).collect();
|
|
return Err(LoreError::Ambiguous(format!(
|
|
"Project '{}' is ambiguous. Matching projects:\n{}\n\nHint: Use the full path, e.g., --project={}",
|
|
project_str,
|
|
matching
|
|
.iter()
|
|
.map(|p| format!(" {}", p))
|
|
.collect::<Vec<_>>()
|
|
.join("\n"),
|
|
matching[0]
|
|
)));
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
let mut substr_stmt = conn.prepare(
|
|
"SELECT id, path_with_namespace FROM projects
|
|
WHERE LOWER(path_with_namespace) LIKE '%' || LOWER(?1) || '%' ESCAPE '\\'",
|
|
)?;
|
|
let substr_matches: Vec<(i64, String)> = substr_stmt
|
|
.query_map(rusqlite::params![escaped], |row| {
|
|
Ok((row.get(0)?, row.get(1)?))
|
|
})?
|
|
.collect::<std::result::Result<Vec<_>, _>>()?;
|
|
|
|
match substr_matches.len() {
|
|
1 => return Ok(substr_matches[0].0),
|
|
n if n > 1 => {
|
|
let matching: Vec<String> = substr_matches.iter().map(|(_, p)| p.clone()).collect();
|
|
return Err(LoreError::Ambiguous(format!(
|
|
"Project '{}' is ambiguous. Matching projects:\n{}\n\nHint: Use the full path, e.g., --project={}",
|
|
project_str,
|
|
matching
|
|
.iter()
|
|
.map(|p| format!(" {}", p))
|
|
.collect::<Vec<_>>()
|
|
.join("\n"),
|
|
matching[0]
|
|
)));
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
let mut all_stmt =
|
|
conn.prepare("SELECT path_with_namespace FROM projects ORDER BY path_with_namespace")?;
|
|
let all_projects: Vec<String> = all_stmt
|
|
.query_map([], |row| row.get(0))?
|
|
.collect::<std::result::Result<Vec<_>, _>>()?;
|
|
|
|
if all_projects.is_empty() {
|
|
return Err(LoreError::Other(format!(
|
|
"Project '{}' not found. No projects have been synced yet.\n\nHint: Run 'lore ingest' first.",
|
|
project_str
|
|
)));
|
|
}
|
|
|
|
Err(LoreError::Other(format!(
|
|
"Project '{}' not found.\n\nAvailable projects:\n{}\n\nHint: Use the full path, e.g., --project={}",
|
|
project_str,
|
|
all_projects
|
|
.iter()
|
|
.map(|p| format!(" {}", p))
|
|
.collect::<Vec<_>>()
|
|
.join("\n"),
|
|
all_projects[0]
|
|
)))
|
|
}
|
|
|
|
/// Escape LIKE metacharacters so `%` and `_` in user input are treated as
|
|
/// literals. All queries using this must include `ESCAPE '\'`.
|
|
#[cfg(test)]
|
|
#[path = "project_tests.rs"]
|
|
mod tests;
|