feat(cli): add 'lore count references' command (bd-2ez)
Adds 'references' entity type to the count command with breakdowns by reference_type (closes/mentioned/related), source_method (api/note_parse/description_parse), and unresolved count. Includes human and robot output formatters, 2 unit tests.
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::cli::render::{self, Theme};
|
||||
use rusqlite::Connection;
|
||||
use serde::Serialize;
|
||||
@@ -211,6 +213,78 @@ pub fn run_count_events(config: &Config) -> Result<EventCounts> {
|
||||
events_db::count_events(&conn)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// References count
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ReferenceCountResult {
|
||||
pub total: i64,
|
||||
pub by_type: HashMap<String, i64>,
|
||||
pub by_method: HashMap<String, i64>,
|
||||
pub unresolved: i64,
|
||||
}
|
||||
|
||||
pub fn run_count_references(config: &Config) -> Result<ReferenceCountResult> {
|
||||
let db_path = get_db_path(config.storage.db_path.as_deref());
|
||||
let conn = create_connection(&db_path)?;
|
||||
count_references(&conn)
|
||||
}
|
||||
|
||||
fn count_references(conn: &Connection) -> Result<ReferenceCountResult> {
|
||||
let (total, closes, mentioned, related, api, note_parse, desc_parse, unresolved): (
|
||||
i64,
|
||||
i64,
|
||||
i64,
|
||||
i64,
|
||||
i64,
|
||||
i64,
|
||||
i64,
|
||||
i64,
|
||||
) = conn.query_row(
|
||||
"SELECT
|
||||
COUNT(*) AS total,
|
||||
COALESCE(SUM(CASE WHEN reference_type = 'closes' THEN 1 ELSE 0 END), 0),
|
||||
COALESCE(SUM(CASE WHEN reference_type = 'mentioned' THEN 1 ELSE 0 END), 0),
|
||||
COALESCE(SUM(CASE WHEN reference_type = 'related' THEN 1 ELSE 0 END), 0),
|
||||
COALESCE(SUM(CASE WHEN source_method = 'api' THEN 1 ELSE 0 END), 0),
|
||||
COALESCE(SUM(CASE WHEN source_method = 'note_parse' THEN 1 ELSE 0 END), 0),
|
||||
COALESCE(SUM(CASE WHEN source_method = 'description_parse' THEN 1 ELSE 0 END), 0),
|
||||
COALESCE(SUM(CASE WHEN target_entity_id IS NULL THEN 1 ELSE 0 END), 0)
|
||||
FROM entity_references",
|
||||
[],
|
||||
|row| {
|
||||
Ok((
|
||||
row.get(0)?,
|
||||
row.get(1)?,
|
||||
row.get(2)?,
|
||||
row.get(3)?,
|
||||
row.get(4)?,
|
||||
row.get(5)?,
|
||||
row.get(6)?,
|
||||
row.get(7)?,
|
||||
))
|
||||
},
|
||||
)?;
|
||||
|
||||
let mut by_type = HashMap::new();
|
||||
by_type.insert("closes".to_string(), closes);
|
||||
by_type.insert("mentioned".to_string(), mentioned);
|
||||
by_type.insert("related".to_string(), related);
|
||||
|
||||
let mut by_method = HashMap::new();
|
||||
by_method.insert("api".to_string(), api);
|
||||
by_method.insert("note_parse".to_string(), note_parse);
|
||||
by_method.insert("description_parse".to_string(), desc_parse);
|
||||
|
||||
Ok(ReferenceCountResult {
|
||||
total,
|
||||
by_type,
|
||||
by_method,
|
||||
unresolved,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct EventCountJsonOutput {
|
||||
ok: bool,
|
||||
@@ -363,6 +437,77 @@ pub fn print_count(result: &CountResult) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// References output
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub fn print_reference_count(result: &ReferenceCountResult) {
|
||||
println!(
|
||||
"{}: {:>10}",
|
||||
Theme::info().render("References"),
|
||||
Theme::bold().render(&render::format_number(result.total))
|
||||
);
|
||||
|
||||
println!(" By type:");
|
||||
for key in &["closes", "mentioned", "related"] {
|
||||
let val = result.by_type.get(*key).copied().unwrap_or(0);
|
||||
println!(" {:<20} {:>10}", key, render::format_number(val));
|
||||
}
|
||||
|
||||
println!(" By source:");
|
||||
for key in &["api", "note_parse", "description_parse"] {
|
||||
let val = result.by_method.get(*key).copied().unwrap_or(0);
|
||||
println!(" {:<20} {:>10}", key, render::format_number(val));
|
||||
}
|
||||
|
||||
let pct = if result.total > 0 {
|
||||
format!(
|
||||
" ({:.1}%)",
|
||||
result.unresolved as f64 / result.total as f64 * 100.0
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
println!(
|
||||
" Unresolved: {:>10}{}",
|
||||
render::format_number(result.unresolved),
|
||||
Theme::dim().render(&pct)
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct RefCountJsonOutput {
|
||||
ok: bool,
|
||||
data: RefCountJsonData,
|
||||
meta: RobotMeta,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct RefCountJsonData {
|
||||
entity: String,
|
||||
total: i64,
|
||||
by_type: HashMap<String, i64>,
|
||||
by_method: HashMap<String, i64>,
|
||||
unresolved: i64,
|
||||
}
|
||||
|
||||
pub fn print_reference_count_json(result: &ReferenceCountResult, elapsed_ms: u64) {
|
||||
let output = RefCountJsonOutput {
|
||||
ok: true,
|
||||
data: RefCountJsonData {
|
||||
entity: "references".to_string(),
|
||||
total: result.total,
|
||||
by_type: result.by_type.clone(),
|
||||
by_method: result.by_method.clone(),
|
||||
unresolved: result.unresolved,
|
||||
},
|
||||
meta: RobotMeta { elapsed_ms },
|
||||
};
|
||||
|
||||
println!("{}", serde_json::to_string(&output).unwrap_or_default());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::cli::render;
|
||||
@@ -381,4 +526,99 @@ mod tests {
|
||||
assert_eq!(render::format_number(12345), "12,345");
|
||||
assert_eq!(render::format_number(1234567), "1,234,567");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_count_references_query() {
|
||||
use std::path::Path;
|
||||
|
||||
use crate::core::db::{create_connection, run_migrations};
|
||||
|
||||
use super::count_references;
|
||||
|
||||
let conn = create_connection(Path::new(":memory:")).unwrap();
|
||||
run_migrations(&conn).unwrap();
|
||||
|
||||
// Insert 3 entity_references rows with varied types/methods.
|
||||
// First need a project row to satisfy FK.
|
||||
conn.execute(
|
||||
"INSERT INTO projects (id, gitlab_project_id, path_with_namespace, web_url)
|
||||
VALUES (1, 100, 'g/test', 'https://git.example.com/g/test')",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Need source entities for the FK.
|
||||
conn.execute(
|
||||
"INSERT INTO issues (id, gitlab_id, iid, project_id, title, state, created_at, updated_at, last_seen_at)
|
||||
VALUES (1, 200, 1, 1, 'Issue 1', 'opened', 0, 0, 0)",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Row 1: closes / api / resolved (target_entity_id = 1)
|
||||
conn.execute(
|
||||
"INSERT INTO entity_references
|
||||
(project_id, source_entity_type, source_entity_id, target_entity_type, target_entity_id,
|
||||
reference_type, source_method, created_at)
|
||||
VALUES (1, 'issue', 1, 'issue', 1, 'closes', 'api', 1000)",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Row 2: mentioned / note_parse / unresolved (target_entity_id = NULL)
|
||||
conn.execute(
|
||||
"INSERT INTO entity_references
|
||||
(project_id, source_entity_type, source_entity_id, target_entity_type, target_entity_id,
|
||||
target_project_path, target_entity_iid,
|
||||
reference_type, source_method, created_at)
|
||||
VALUES (1, 'issue', 1, 'merge_request', NULL, 'other/proj', 42, 'mentioned', 'note_parse', 2000)",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Row 3: related / api / unresolved (target_entity_id = NULL)
|
||||
conn.execute(
|
||||
"INSERT INTO entity_references
|
||||
(project_id, source_entity_type, source_entity_id, target_entity_type, target_entity_id,
|
||||
target_project_path, target_entity_iid,
|
||||
reference_type, source_method, created_at)
|
||||
VALUES (1, 'issue', 1, 'issue', NULL, 'other/proj2', 99, 'related', 'api', 3000)",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let result = count_references(&conn).unwrap();
|
||||
|
||||
assert_eq!(result.total, 3);
|
||||
assert_eq!(*result.by_type.get("closes").unwrap(), 1);
|
||||
assert_eq!(*result.by_type.get("mentioned").unwrap(), 1);
|
||||
assert_eq!(*result.by_type.get("related").unwrap(), 1);
|
||||
assert_eq!(*result.by_method.get("api").unwrap(), 2);
|
||||
assert_eq!(*result.by_method.get("note_parse").unwrap(), 1);
|
||||
assert_eq!(*result.by_method.get("description_parse").unwrap(), 0);
|
||||
assert_eq!(result.unresolved, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_count_references_empty_table() {
|
||||
use std::path::Path;
|
||||
|
||||
use crate::core::db::{create_connection, run_migrations};
|
||||
|
||||
use super::count_references;
|
||||
|
||||
let conn = create_connection(Path::new(":memory:")).unwrap();
|
||||
run_migrations(&conn).unwrap();
|
||||
|
||||
let result = count_references(&conn).unwrap();
|
||||
|
||||
assert_eq!(result.total, 0);
|
||||
assert_eq!(*result.by_type.get("closes").unwrap(), 0);
|
||||
assert_eq!(*result.by_type.get("mentioned").unwrap(), 0);
|
||||
assert_eq!(*result.by_type.get("related").unwrap(), 0);
|
||||
assert_eq!(*result.by_method.get("api").unwrap(), 0);
|
||||
assert_eq!(*result.by_method.get("note_parse").unwrap(), 0);
|
||||
assert_eq!(*result.by_method.get("description_parse").unwrap(), 0);
|
||||
assert_eq!(result.unresolved, 0);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user