Files
gitlore/src/core/project.rs
teernisse e6771709f1 refactor(core): extract path_resolver module, fix old_path matching in who
Extract shared path resolution logic from who.rs into a new
core::path_resolver module for cross-module reuse. Functions moved:
escape_like, normalize_repo_path, PathQuery, SuffixResult,
build_path_query, suffix_probe. Duplicate escape_like copies removed
from list.rs, project.rs, and filters.rs — all now import from
path_resolver.

Additionally fixes two bugs in query_expert_details() and
query_overlap() where only position_new_path was checked (missing
old_path matches for renamed files) and state filter excluded 'closed'
MRs despite the main scoring query including them with a decay
multiplier.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 13:50:14 -05:00

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;