diff --git a/src/cli/commands/embed.rs b/src/cli/commands/embed.rs index 32ccfd5..330c70d 100644 --- a/src/cli/commands/embed.rs +++ b/src/cli/commands/embed.rs @@ -3,12 +3,12 @@ use console::style; use serde::Serialize; +use crate::Config; use crate::core::db::create_connection; use crate::core::error::Result; use crate::core::paths::get_db_path; use crate::embedding::ollama::{OllamaClient, OllamaConfig}; use crate::embedding::pipeline::embed_documents; -use crate::Config; /// Result of the embed command. #[derive(Debug, Default, Serialize)] @@ -69,10 +69,7 @@ pub async fn run_embed( /// Print human-readable output. pub fn print_embed(result: &EmbedCommandResult) { - println!( - "{} Embedding complete", - style("done").green().bold(), - ); + println!("{} Embedding complete", style("done").green().bold(),); println!(" Embedded: {}", result.embedded); if result.failed > 0 { println!(" Failed: {}", style(result.failed).red()); diff --git a/src/cli/commands/generate_docs.rs b/src/cli/commands/generate_docs.rs index eb6aa3a..166dc27 100644 --- a/src/cli/commands/generate_docs.rs +++ b/src/cli/commands/generate_docs.rs @@ -5,12 +5,12 @@ use rusqlite::Connection; use serde::Serialize; use tracing::info; +use crate::Config; use crate::core::db::create_connection; use crate::core::error::Result; use crate::core::paths::get_db_path; use crate::core::project::resolve_project; use crate::documents::{SourceType, regenerate_dirty_documents}; -use crate::Config; const FULL_MODE_CHUNK_SIZE: i64 = 2000; @@ -134,7 +134,11 @@ fn seed_dirty( /// Print human-readable output. pub fn print_generate_docs(result: &GenerateDocsResult) { - let mode = if result.full_mode { "full" } else { "incremental" }; + let mode = if result.full_mode { + "full" + } else { + "incremental" + }; println!( "{} Document generation complete ({})", style("done").green().bold(), @@ -147,10 +151,7 @@ pub fn print_generate_docs(result: &GenerateDocsResult) { println!(" Regenerated: {}", result.regenerated); println!(" Unchanged: {}", result.unchanged); if result.errored > 0 { - println!( - " Errored: {}", - style(result.errored).red() - ); + println!(" Errored: {}", style(result.errored).red()); } } diff --git a/src/cli/commands/mod.rs b/src/cli/commands/mod.rs index 98ba1a4..8bb3735 100644 --- a/src/cli/commands/mod.rs +++ b/src/cli/commands/mod.rs @@ -22,19 +22,19 @@ pub use count::{ pub use doctor::{print_doctor_results, run_doctor}; pub use embed::{print_embed, print_embed_json, run_embed}; pub use generate_docs::{print_generate_docs, print_generate_docs_json, run_generate_docs}; -pub use stats::{print_stats, print_stats_json, run_stats}; -pub use search::{ - print_search_results, print_search_results_json, run_search, SearchCliFilters, SearchResponse, -}; pub use ingest::{IngestDisplay, print_ingest_summary, print_ingest_summary_json, run_ingest}; pub use init::{InitInputs, InitOptions, InitResult, run_init}; pub use list::{ ListFilters, MrListFilters, open_issue_in_browser, open_mr_in_browser, print_list_issues, print_list_issues_json, print_list_mrs, print_list_mrs_json, run_list_issues, run_list_mrs, }; -pub use sync::{print_sync, print_sync_json, run_sync, SyncOptions, SyncResult}; +pub use search::{ + SearchCliFilters, SearchResponse, print_search_results, print_search_results_json, run_search, +}; pub use show::{ print_show_issue, print_show_issue_json, print_show_mr, print_show_mr_json, run_show_issue, run_show_mr, }; +pub use stats::{print_stats, print_stats_json, run_stats}; +pub use sync::{SyncOptions, SyncResult, print_sync, print_sync_json, run_sync}; pub use sync_status::{print_sync_status, print_sync_status_json, run_sync_status}; diff --git a/src/cli/commands/search.rs b/src/cli/commands/search.rs index 2eb69c7..63004d5 100644 --- a/src/cli/commands/search.rs +++ b/src/cli/commands/search.rs @@ -3,6 +3,7 @@ use console::style; use serde::Serialize; +use crate::Config; use crate::core::db::create_connection; use crate::core::error::{LoreError, Result}; use crate::core::paths::get_db_path; @@ -10,10 +11,9 @@ use crate::core::project::resolve_project; use crate::core::time::{ms_to_iso, parse_since}; use crate::documents::SourceType; use crate::search::{ - apply_filters, get_result_snippet, rank_rrf, search_fts, FtsQueryMode, PathFilter, - SearchFilters, + FtsQueryMode, PathFilter, SearchFilters, apply_filters, get_result_snippet, rank_rrf, + search_fts, }; -use crate::Config; /// Display-ready search result with all fields hydrated. #[derive(Debug, Serialize)] @@ -86,9 +86,7 @@ pub fn run_search( mode: "lexical".to_string(), total_results: 0, results: vec![], - warnings: vec![ - "No documents indexed. Run 'lore generate-docs' first.".to_string() - ], + warnings: vec!["No documents indexed. Run 'lore generate-docs' first.".to_string()], }); } @@ -151,9 +149,9 @@ pub fn run_search( // Adaptive recall: wider initial fetch when filters applied let requested = filters.clamp_limit(); let top_k = if filters.has_any_filter() { - (requested * 50).max(200).min(1500) + (requested * 50).clamp(200, 1500) } else { - (requested * 10).max(50).min(1500) + (requested * 10).clamp(50, 1500) }; // FTS search @@ -190,10 +188,8 @@ pub fn run_search( let hydrated = hydrate_results(&conn, &filtered_ids)?; // Build display results preserving filter order - let rrf_map: std::collections::HashMap = ranked - .iter() - .map(|r| (r.document_id, r)) - .collect(); + let rrf_map: std::collections::HashMap = + ranked.iter().map(|r| (r.document_id, r)).collect(); let mut results: Vec = Vec::with_capacity(hydrated.len()); for row in &hydrated { @@ -256,16 +252,13 @@ struct HydratedRow { /// /// Uses json_each() to pass ranked IDs and preserve ordering via ORDER BY j.key. /// Labels and paths fetched via correlated json_group_array subqueries. -fn hydrate_results( - conn: &rusqlite::Connection, - document_ids: &[i64], -) -> Result> { +fn hydrate_results(conn: &rusqlite::Connection, document_ids: &[i64]) -> Result> { if document_ids.is_empty() { return Ok(Vec::new()); } - let ids_json = serde_json::to_string(document_ids) - .map_err(|e| LoreError::Other(e.to_string()))?; + let ids_json = + serde_json::to_string(document_ids).map_err(|e| LoreError::Other(e.to_string()))?; let sql = r#" SELECT d.id, d.source_type, d.title, d.url, d.author_username, @@ -325,10 +318,7 @@ pub fn print_search_results(response: &SearchResponse) { } if response.results.is_empty() { - println!( - "No results found for '{}'", - style(&response.query).bold() - ); + println!("No results found for '{}'", style(&response.query).bold()); return; } @@ -371,17 +361,11 @@ pub fn print_search_results(response: &SearchResponse) { ); if !result.labels.is_empty() { - println!( - " Labels: {}", - result.labels.join(", ") - ); + println!(" Labels: {}", result.labels.join(", ")); } // Strip HTML tags from snippet for terminal display - let clean_snippet = result - .snippet - .replace("", "") - .replace("", ""); + let clean_snippet = result.snippet.replace("", "").replace("", ""); println!(" {}", style(clean_snippet).dim()); if let Some(ref explain) = result.explain { diff --git a/src/cli/commands/show.rs b/src/cli/commands/show.rs index bbdff2b..8855b95 100644 --- a/src/cli/commands/show.rs +++ b/src/cli/commands/show.rs @@ -154,10 +154,7 @@ fn find_issue(conn: &Connection, iid: i64, project_filter: Option<&str>) -> Resu FROM issues i JOIN projects p ON i.project_id = p.id WHERE i.iid = ? AND i.project_id = ?", - vec![ - Box::new(iid), - Box::new(project_id), - ], + vec![Box::new(iid), Box::new(project_id)], ) } None => ( @@ -346,10 +343,7 @@ fn find_mr(conn: &Connection, iid: i64, project_filter: Option<&str>) -> Result< FROM merge_requests m JOIN projects p ON m.project_id = p.id WHERE m.iid = ? AND m.project_id = ?", - vec![ - Box::new(iid), - Box::new(project_id), - ], + vec![Box::new(iid), Box::new(project_id)], ) } None => ( diff --git a/src/cli/commands/stats.rs b/src/cli/commands/stats.rs index d16c1bc..2303ad4 100644 --- a/src/cli/commands/stats.rs +++ b/src/cli/commands/stats.rs @@ -4,10 +4,10 @@ use console::style; use rusqlite::Connection; use serde::Serialize; +use crate::Config; use crate::core::db::create_connection; use crate::core::error::Result; use crate::core::paths::get_db_path; -use crate::Config; /// Result of the stats command. #[derive(Debug, Default, Serialize)] @@ -75,11 +75,7 @@ pub struct RepairResult { } /// Run the stats command. -pub fn run_stats( - config: &Config, - check: bool, - repair: bool, -) -> Result { +pub fn run_stats(config: &Config, check: bool, repair: bool) -> Result { let db_path = get_db_path(config.storage.db_path.as_deref()); let conn = create_connection(&db_path)?; @@ -87,14 +83,22 @@ pub fn run_stats( // Document counts result.documents.total = count_query(&conn, "SELECT COUNT(*) FROM documents")?; - result.documents.issues = - count_query(&conn, "SELECT COUNT(*) FROM documents WHERE source_type = 'issue'")?; - result.documents.merge_requests = - count_query(&conn, "SELECT COUNT(*) FROM documents WHERE source_type = 'merge_request'")?; - result.documents.discussions = - count_query(&conn, "SELECT COUNT(*) FROM documents WHERE source_type = 'discussion'")?; - result.documents.truncated = - count_query(&conn, "SELECT COUNT(*) FROM documents WHERE is_truncated = 1")?; + result.documents.issues = count_query( + &conn, + "SELECT COUNT(*) FROM documents WHERE source_type = 'issue'", + )?; + result.documents.merge_requests = count_query( + &conn, + "SELECT COUNT(*) FROM documents WHERE source_type = 'merge_request'", + )?; + result.documents.discussions = count_query( + &conn, + "SELECT COUNT(*) FROM documents WHERE source_type = 'discussion'", + )?; + result.documents.truncated = count_query( + &conn, + "SELECT COUNT(*) FROM documents WHERE is_truncated = 1", + )?; // Embedding stats — skip gracefully if table doesn't exist (Gate A only) if table_exists(&conn, "embedding_metadata") { @@ -119,10 +123,14 @@ pub fn run_stats( result.fts.indexed = count_query(&conn, "SELECT COUNT(*) FROM documents_fts")?; // Queue stats - result.queues.dirty_sources = - count_query(&conn, "SELECT COUNT(*) FROM dirty_sources WHERE last_error IS NULL")?; - result.queues.dirty_sources_failed = - count_query(&conn, "SELECT COUNT(*) FROM dirty_sources WHERE last_error IS NOT NULL")?; + result.queues.dirty_sources = count_query( + &conn, + "SELECT COUNT(*) FROM dirty_sources WHERE last_error IS NULL", + )?; + result.queues.dirty_sources_failed = count_query( + &conn, + "SELECT COUNT(*) FROM dirty_sources WHERE last_error IS NOT NULL", + )?; if table_exists(&conn, "pending_discussion_fetches") { result.queues.pending_discussion_fetches = count_query( @@ -151,6 +159,7 @@ pub fn run_stats( } // Integrity check + #[allow(clippy::field_reassign_with_default)] if check { let mut integrity = IntegrityResult::default(); @@ -276,9 +285,7 @@ pub fn run_stats( } fn count_query(conn: &Connection, sql: &str) -> Result { - let count: i64 = conn - .query_row(sql, [], |row| row.get(0)) - .unwrap_or(0); + let count: i64 = conn.query_row(sql, [], |row| row.get(0)).unwrap_or(0); Ok(count) } @@ -300,7 +307,10 @@ pub fn print_stats(result: &StatsResult) { println!(" Merge Requests: {}", result.documents.merge_requests); println!(" Discussions: {}", result.documents.discussions); if result.documents.truncated > 0 { - println!(" Truncated: {}", style(result.documents.truncated).yellow()); + println!( + " Truncated: {}", + style(result.documents.truncated).yellow() + ); } println!(); @@ -318,13 +328,13 @@ pub fn print_stats(result: &StatsResult) { println!(); println!("{}", style("Queues").cyan().bold()); - println!(" Dirty sources: {} pending, {} failed", - result.queues.dirty_sources, - result.queues.dirty_sources_failed + println!( + " Dirty sources: {} pending, {} failed", + result.queues.dirty_sources, result.queues.dirty_sources_failed ); - println!(" Discussion fetch: {} pending, {} failed", - result.queues.pending_discussion_fetches, - result.queues.pending_discussion_fetches_failed + println!( + " Discussion fetch: {} pending, {} failed", + result.queues.pending_discussion_fetches, result.queues.pending_discussion_fetches_failed ); if result.queues.pending_dependent_fetches > 0 || result.queues.pending_dependent_fetches_failed > 0 @@ -431,10 +441,12 @@ pub fn print_stats_json(result: &StatsResult) { let output = StatsJsonOutput { ok: true, data: StatsResult { - documents: DocumentStats { ..*&result.documents }, - embeddings: EmbeddingStats { ..*&result.embeddings }, - fts: FtsStats { ..*&result.fts }, - queues: QueueStats { ..*&result.queues }, + documents: DocumentStats { ..result.documents }, + embeddings: EmbeddingStats { + ..result.embeddings + }, + fts: FtsStats { ..result.fts }, + queues: QueueStats { ..result.queues }, integrity: result.integrity.as_ref().map(|i| IntegrityResult { ok: i.ok, fts_doc_mismatch: i.fts_doc_mismatch, diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 8186d72..5e6e416 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -213,7 +213,12 @@ pub struct IssuesArgs { pub iid: Option, /// Maximum results - #[arg(short = 'n', long = "limit", default_value = "50", help_heading = "Output")] + #[arg( + short = 'n', + long = "limit", + default_value = "50", + help_heading = "Output" + )] pub limit: usize, /// Filter by state (opened, closed, all) @@ -249,7 +254,11 @@ pub struct IssuesArgs { pub due_before: Option, /// Show only issues with a due date - #[arg(long = "has-due", help_heading = "Filters", overrides_with = "no_has_due")] + #[arg( + long = "has-due", + help_heading = "Filters", + overrides_with = "no_has_due" + )] pub has_due: bool, #[arg(long = "no-has-due", hide = true, overrides_with = "has_due")] @@ -267,7 +276,12 @@ pub struct IssuesArgs { pub no_asc: bool, /// Open first matching item in browser - #[arg(short = 'o', long, help_heading = "Actions", overrides_with = "no_open")] + #[arg( + short = 'o', + long, + help_heading = "Actions", + overrides_with = "no_open" + )] pub open: bool, #[arg(long = "no-open", hide = true, overrides_with = "open")] @@ -281,7 +295,12 @@ pub struct MrsArgs { pub iid: Option, /// Maximum results - #[arg(short = 'n', long = "limit", default_value = "50", help_heading = "Output")] + #[arg( + short = 'n', + long = "limit", + default_value = "50", + help_heading = "Output" + )] pub limit: usize, /// Filter by state (opened, merged, closed, locked, all) @@ -313,11 +332,21 @@ pub struct MrsArgs { pub since: Option, /// Show only draft MRs - #[arg(short = 'd', long, conflicts_with = "no_draft", help_heading = "Filters")] + #[arg( + short = 'd', + long, + conflicts_with = "no_draft", + help_heading = "Filters" + )] pub draft: bool, /// Exclude draft MRs - #[arg(short = 'D', long = "no-draft", conflicts_with = "draft", help_heading = "Filters")] + #[arg( + short = 'D', + long = "no-draft", + conflicts_with = "draft", + help_heading = "Filters" + )] pub no_draft: bool, /// Filter by target branch @@ -340,7 +369,12 @@ pub struct MrsArgs { pub no_asc: bool, /// Open first matching item in browser - #[arg(short = 'o', long, help_heading = "Actions", overrides_with = "no_open")] + #[arg( + short = 'o', + long, + help_heading = "Actions", + overrides_with = "no_open" + )] pub open: bool, #[arg(long = "no-open", hide = true, overrides_with = "open")] @@ -427,7 +461,12 @@ pub struct SearchArgs { pub updated_after: Option, /// Maximum results (default 20, max 100) - #[arg(short = 'n', long = "limit", default_value = "20", help_heading = "Output")] + #[arg( + short = 'n', + long = "limit", + default_value = "20", + help_heading = "Output" + )] pub limit: usize, /// Show ranking explanation per result diff --git a/src/core/backoff.rs b/src/core/backoff.rs index 1ee035d..d1f0bd3 100644 --- a/src/core/backoff.rs +++ b/src/core/backoff.rs @@ -86,7 +86,10 @@ mod tests { let result = compute_next_attempt_at(now, 1); let delay = result - now; // attempt 1: base = 2000ms, with jitter: 1800-2200ms - assert!(delay >= 1800 && delay <= 2200, "first retry delay: {delay}ms"); + assert!( + (1800..=2200).contains(&delay), + "first retry delay: {delay}ms" + ); } #[test] diff --git a/src/core/db.rs b/src/core/db.rs index 2c6fc3c..bee0fa4 100644 --- a/src/core/db.rs +++ b/src/core/db.rs @@ -31,22 +31,10 @@ const MIGRATIONS: &[(&str, &str)] = &[ "006", include_str!("../../migrations/006_merge_requests.sql"), ), - ( - "007", - include_str!("../../migrations/007_documents.sql"), - ), - ( - "008", - include_str!("../../migrations/008_fts5.sql"), - ), - ( - "009", - include_str!("../../migrations/009_embeddings.sql"), - ), - ( - "010", - include_str!("../../migrations/010_chunk_config.sql"), - ), + ("007", include_str!("../../migrations/007_documents.sql")), + ("008", include_str!("../../migrations/008_fts5.sql")), + ("009", include_str!("../../migrations/009_embeddings.sql")), + ("010", include_str!("../../migrations/010_chunk_config.sql")), ( "011", include_str!("../../migrations/011_resource_events.sql"), diff --git a/src/core/dependent_queue.rs b/src/core/dependent_queue.rs index 0b8ea44..8ffad0e 100644 --- a/src/core/dependent_queue.rs +++ b/src/core/dependent_queue.rs @@ -40,7 +40,15 @@ pub fn enqueue_job( "INSERT OR IGNORE INTO pending_dependent_fetches (project_id, entity_type, entity_iid, entity_local_id, job_type, payload_json, enqueued_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", - rusqlite::params![project_id, entity_type, entity_iid, entity_local_id, job_type, payload_json, now], + rusqlite::params![ + project_id, + entity_type, + entity_iid, + entity_local_id, + job_type, + payload_json, + now + ], )?; Ok(changes > 0) @@ -69,21 +77,18 @@ pub fn claim_jobs(conn: &Connection, job_type: &str, batch_size: usize) -> Resul )?; let jobs: Vec = select_stmt - .query_map( - rusqlite::params![job_type, now, batch_size as i64], - |row| { - Ok(PendingJob { - id: row.get(0)?, - project_id: row.get(1)?, - entity_type: row.get(2)?, - entity_iid: row.get(3)?, - entity_local_id: row.get(4)?, - job_type: row.get(5)?, - payload_json: row.get(6)?, - attempts: row.get(7)?, - }) - }, - )? + .query_map(rusqlite::params![job_type, now, batch_size as i64], |row| { + Ok(PendingJob { + id: row.get(0)?, + project_id: row.get(1)?, + entity_type: row.get(2)?, + entity_iid: row.get(3)?, + entity_local_id: row.get(4)?, + job_type: row.get(5)?, + payload_json: row.get(6)?, + attempts: row.get(7)?, + }) + })? .collect::, _>>()?; // Lock the claimed jobs diff --git a/src/core/error.rs b/src/core/error.rs index 17795db..8e42e5f 100644 --- a/src/core/error.rs +++ b/src/core/error.rs @@ -222,9 +222,9 @@ impl LoreError { "Check database file permissions or reset with 'lore reset'.\n\n Example:\n lore doctor\n lore reset --yes", ), Self::Http(_) => Some("Check network connection"), - Self::NotFound(_) => Some( - "Verify the entity exists.\n\n Example:\n lore issues\n lore mrs", - ), + Self::NotFound(_) => { + Some("Verify the entity exists.\n\n Example:\n lore issues\n lore mrs") + } Self::Ambiguous(_) => Some( "Use -p to choose a specific project.\n\n Example:\n lore issues 42 -p group/project-a\n lore mrs 99 -p group/project-b", ), diff --git a/src/core/events_db.rs b/src/core/events_db.rs index cefd830..0e5ee5b 100644 --- a/src/core/events_db.rs +++ b/src/core/events_db.rs @@ -150,7 +150,10 @@ pub fn upsert_milestone_events( /// Resolve entity type string to (issue_id, merge_request_id) pair. /// Exactly one is Some, the other is None. -fn resolve_entity_ids(entity_type: &str, entity_local_id: i64) -> Result<(Option, Option)> { +fn resolve_entity_ids( + entity_type: &str, + entity_local_id: i64, +) -> Result<(Option, Option)> { match entity_type { "issue" => Ok((Some(entity_local_id), None)), "merge_request" => Ok((None, Some(entity_local_id))), diff --git a/src/core/project.rs b/src/core/project.rs index 033d050..828693f 100644 --- a/src/core/project.rs +++ b/src/core/project.rs @@ -33,7 +33,7 @@ pub fn resolve_project(conn: &Connection, project_str: &str) -> Result { let mut suffix_stmt = conn.prepare( "SELECT id, path_with_namespace FROM projects WHERE path_with_namespace LIKE '%/' || ?1 - OR path_with_namespace = ?1" + OR path_with_namespace = ?1", )?; let suffix_matches: Vec<(i64, String)> = suffix_stmt .query_map(rusqlite::params![project_str], |row| { @@ -48,7 +48,11 @@ pub fn resolve_project(conn: &Connection, project_str: &str) -> Result { 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::>().join("\n"), + matching + .iter() + .map(|p| format!(" {}", p)) + .collect::>() + .join("\n"), matching[0] ))); } @@ -58,7 +62,7 @@ pub fn resolve_project(conn: &Connection, project_str: &str) -> Result { // Step 4: Case-insensitive substring match (unambiguous) let mut substr_stmt = conn.prepare( "SELECT id, path_with_namespace FROM projects - WHERE LOWER(path_with_namespace) LIKE '%' || LOWER(?1) || '%'" + WHERE LOWER(path_with_namespace) LIKE '%' || LOWER(?1) || '%'", )?; let substr_matches: Vec<(i64, String)> = substr_stmt .query_map(rusqlite::params![project_str], |row| { @@ -73,7 +77,11 @@ pub fn resolve_project(conn: &Connection, project_str: &str) -> Result { 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::>().join("\n"), + matching + .iter() + .map(|p| format!(" {}", p)) + .collect::>() + .join("\n"), matching[0] ))); } @@ -81,9 +89,8 @@ pub fn resolve_project(conn: &Connection, project_str: &str) -> Result { } // Step 5: No match — list available projects - let mut all_stmt = conn.prepare( - "SELECT path_with_namespace FROM projects ORDER BY path_with_namespace" - )?; + let mut all_stmt = + conn.prepare("SELECT path_with_namespace FROM projects ORDER BY path_with_namespace")?; let all_projects: Vec = all_stmt .query_map([], |row| row.get(0))? .collect::, _>>()?; @@ -98,7 +105,11 @@ pub fn resolve_project(conn: &Connection, project_str: &str) -> Result { 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::>().join("\n"), + all_projects + .iter() + .map(|p| format!(" {}", p)) + .collect::>() + .join("\n"), all_projects[0] ))) } @@ -109,7 +120,8 @@ mod tests { fn setup_db() -> Connection { let conn = Connection::open_in_memory().unwrap(); - conn.execute_batch(" + conn.execute_batch( + " CREATE TABLE projects ( id INTEGER PRIMARY KEY, gitlab_project_id INTEGER UNIQUE NOT NULL, @@ -121,7 +133,9 @@ mod tests { raw_payload_id INTEGER ); CREATE INDEX idx_projects_path ON projects(path_with_namespace); - ").unwrap(); + ", + ) + .unwrap(); conn } @@ -129,7 +143,8 @@ mod tests { conn.execute( "INSERT INTO projects (id, gitlab_project_id, path_with_namespace) VALUES (?1, ?2, ?3)", rusqlite::params![id, id * 100, path], - ).unwrap(); + ) + .unwrap(); } #[test] @@ -164,7 +179,11 @@ mod tests { insert_project(&conn, 2, "frontend/auth-service"); let err = resolve_project(&conn, "auth-service").unwrap_err(); let msg = err.to_string(); - assert!(msg.contains("ambiguous"), "Expected ambiguous error, got: {}", msg); + assert!( + msg.contains("ambiguous"), + "Expected ambiguous error, got: {}", + msg + ); assert!(msg.contains("backend/auth-service")); assert!(msg.contains("frontend/auth-service")); } @@ -195,7 +214,11 @@ mod tests { // "code" matches both projects let err = resolve_project(&conn, "code").unwrap_err(); let msg = err.to_string(); - assert!(msg.contains("ambiguous"), "Expected ambiguous error, got: {}", msg); + assert!( + msg.contains("ambiguous"), + "Expected ambiguous error, got: {}", + msg + ); assert!(msg.contains("vs/python-code")); assert!(msg.contains("vs/typescript-code")); } @@ -217,7 +240,11 @@ mod tests { insert_project(&conn, 1, "backend/auth-service"); let err = resolve_project(&conn, "nonexistent").unwrap_err(); let msg = err.to_string(); - assert!(msg.contains("not found"), "Expected not found error, got: {}", msg); + assert!( + msg.contains("not found"), + "Expected not found error, got: {}", + msg + ); assert!(msg.contains("backend/auth-service")); } diff --git a/src/documents/extractor.rs b/src/documents/extractor.rs index 8b3b04e..14d9358 100644 --- a/src/documents/extractor.rs +++ b/src/documents/extractor.rs @@ -4,10 +4,10 @@ use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::collections::BTreeSet; -use crate::core::error::Result; use super::truncation::{ - truncate_discussion, truncate_hard_cap, NoteContent, MAX_DISCUSSION_BYTES, + MAX_DISCUSSION_BYTES, NoteContent, truncate_discussion, truncate_hard_cap, }; +use crate::core::error::Result; /// Source type for documents. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] @@ -98,22 +98,34 @@ pub fn extract_issue_document(conn: &Connection, issue_id: i64) -> Result