feat(cli): Implement complete command-line interface
Provides a user-friendly CLI for all GitLab Inbox operations. src/cli/mod.rs - Clap command definitions: - Global --config flag for alternate config path - Subcommands: init, auth-test, doctor, version, backup, reset, migrate, sync-status, ingest, list, count, show - Ingest supports --type (issues/merge_requests), --project filter, --force lock override, --full resync - List supports rich filtering: --state, --author, --assignee, --label, --milestone, --since, --due-before, --has-due-date - List supports --sort (updated/created/iid), --order (asc/desc) - List supports --open to launch browser, --json for scripting src/cli/commands/ - Command implementations: init.rs: Interactive configuration wizard - Prompts for GitLab URL, token env var, projects to track - Creates config file and initializes database - Supports --force overwrite and --non-interactive mode auth_test.rs: Verify GitLab authentication - Calls /api/v4/user to validate token - Displays username and GitLab instance URL doctor.rs: Environment health check - Validates config file exists and parses correctly - Checks database connectivity and migration state - Verifies GitLab authentication - Reports token environment variable status - Supports --json output for CI integration ingest.rs: Data synchronization from GitLab - Acquires sync lock with stale detection - Shows progress bars for issues and discussions - Reports sync statistics on completion - Supports --full flag to reset cursors and refetch all data list.rs: Query local database - Formatted table output with comfy-table - Filters build dynamic SQL with parameterized queries - Username filters normalize @ prefix automatically - --open flag uses 'open' crate for cross-platform browser launch - --json outputs array of issue objects show.rs: Detailed entity view - Displays issue metadata in structured format - Shows full description with markdown - Lists labels, assignees, milestone - Shows discussion threads with notes count.rs: Entity statistics - Counts issues, discussions, or notes - Supports --type filter for discussions/notes sync_status.rs: Display sync watermarks - Shows last sync time per project - Displays cursor positions for debugging src/main.rs - Application entry point: - Initializes tracing subscriber with env-filter - Parses CLI arguments via clap - Dispatches to appropriate command handler - Consistent error formatting for all failure modes src/lib.rs - Library entry point: - Exports cli, core, gitlab, ingestion modules - Re-exports Config, GiError, Result for convenience Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
190
src/cli/commands/count.rs
Normal file
190
src/cli/commands/count.rs
Normal file
@@ -0,0 +1,190 @@
|
||||
//! Count command - display entity counts from local database.
|
||||
|
||||
use console::style;
|
||||
use rusqlite::Connection;
|
||||
|
||||
use crate::Config;
|
||||
use crate::core::db::create_connection;
|
||||
use crate::core::error::Result;
|
||||
use crate::core::paths::get_db_path;
|
||||
|
||||
/// Result of count query.
|
||||
pub struct CountResult {
|
||||
pub entity: String,
|
||||
pub count: i64,
|
||||
pub system_count: Option<i64>, // For notes only
|
||||
}
|
||||
|
||||
/// Run the count command.
|
||||
pub fn run_count(config: &Config, entity: &str, type_filter: Option<&str>) -> Result<CountResult> {
|
||||
let db_path = get_db_path(config.storage.db_path.as_deref());
|
||||
let conn = create_connection(&db_path)?;
|
||||
|
||||
match entity {
|
||||
"issues" => count_issues(&conn),
|
||||
"discussions" => count_discussions(&conn, type_filter),
|
||||
"notes" => count_notes(&conn, type_filter),
|
||||
"mrs" => {
|
||||
// Placeholder for CP2
|
||||
Ok(CountResult {
|
||||
entity: "Merge Requests".to_string(),
|
||||
count: 0,
|
||||
system_count: None,
|
||||
})
|
||||
}
|
||||
_ => Ok(CountResult {
|
||||
entity: entity.to_string(),
|
||||
count: 0,
|
||||
system_count: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Count issues.
|
||||
fn count_issues(conn: &Connection) -> Result<CountResult> {
|
||||
let count: i64 = conn.query_row("SELECT COUNT(*) FROM issues", [], |row| row.get(0))?;
|
||||
|
||||
Ok(CountResult {
|
||||
entity: "Issues".to_string(),
|
||||
count,
|
||||
system_count: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Count discussions with optional noteable type filter.
|
||||
fn count_discussions(conn: &Connection, type_filter: Option<&str>) -> Result<CountResult> {
|
||||
let (count, entity_name) = match type_filter {
|
||||
Some("issue") => {
|
||||
let count: i64 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM discussions WHERE noteable_type = 'Issue'",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
(count, "Issue Discussions")
|
||||
}
|
||||
Some("mr") => {
|
||||
let count: i64 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM discussions WHERE noteable_type = 'MergeRequest'",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
(count, "MR Discussions")
|
||||
}
|
||||
_ => {
|
||||
let count: i64 =
|
||||
conn.query_row("SELECT COUNT(*) FROM discussions", [], |row| row.get(0))?;
|
||||
(count, "Discussions")
|
||||
}
|
||||
};
|
||||
|
||||
Ok(CountResult {
|
||||
entity: entity_name.to_string(),
|
||||
count,
|
||||
system_count: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Count notes with optional noteable type filter.
|
||||
fn count_notes(conn: &Connection, type_filter: Option<&str>) -> Result<CountResult> {
|
||||
let (total, system_count, entity_name) = match type_filter {
|
||||
Some("issue") => {
|
||||
let (total, system): (i64, i64) = conn.query_row(
|
||||
"SELECT COUNT(*), COALESCE(SUM(n.is_system), 0)
|
||||
FROM notes n
|
||||
JOIN discussions d ON n.discussion_id = d.id
|
||||
WHERE d.noteable_type = 'Issue'",
|
||||
[],
|
||||
|row| Ok((row.get(0)?, row.get(1)?)),
|
||||
)?;
|
||||
(total, system, "Issue Notes")
|
||||
}
|
||||
Some("mr") => {
|
||||
let (total, system): (i64, i64) = conn.query_row(
|
||||
"SELECT COUNT(*), COALESCE(SUM(n.is_system), 0)
|
||||
FROM notes n
|
||||
JOIN discussions d ON n.discussion_id = d.id
|
||||
WHERE d.noteable_type = 'MergeRequest'",
|
||||
[],
|
||||
|row| Ok((row.get(0)?, row.get(1)?)),
|
||||
)?;
|
||||
(total, system, "MR Notes")
|
||||
}
|
||||
_ => {
|
||||
let (total, system): (i64, i64) = conn.query_row(
|
||||
"SELECT COUNT(*), COALESCE(SUM(is_system), 0) FROM notes",
|
||||
[],
|
||||
|row| Ok((row.get(0)?, row.get(1)?)),
|
||||
)?;
|
||||
(total, system, "Notes")
|
||||
}
|
||||
};
|
||||
|
||||
// Non-system notes count
|
||||
let non_system = total - system_count;
|
||||
|
||||
Ok(CountResult {
|
||||
entity: entity_name.to_string(),
|
||||
count: non_system,
|
||||
system_count: Some(system_count),
|
||||
})
|
||||
}
|
||||
|
||||
/// Format number with thousands separators.
|
||||
fn format_number(n: i64) -> String {
|
||||
let s = n.to_string();
|
||||
let chars: Vec<char> = s.chars().collect();
|
||||
let mut result = String::new();
|
||||
|
||||
for (i, c) in chars.iter().enumerate() {
|
||||
if i > 0 && (chars.len() - i).is_multiple_of(3) {
|
||||
result.push(',');
|
||||
}
|
||||
result.push(*c);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Print count result.
|
||||
pub fn print_count(result: &CountResult) {
|
||||
let count_str = format_number(result.count);
|
||||
|
||||
if let Some(system_count) = result.system_count {
|
||||
println!(
|
||||
"{}: {} {}",
|
||||
style(&result.entity).cyan(),
|
||||
style(count_str).bold(),
|
||||
style(format!(
|
||||
"(excluding {} system)",
|
||||
format_number(system_count)
|
||||
))
|
||||
.dim()
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"{}: {}",
|
||||
style(&result.entity).cyan(),
|
||||
style(count_str).bold()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn format_number_handles_small_numbers() {
|
||||
assert_eq!(format_number(0), "0");
|
||||
assert_eq!(format_number(1), "1");
|
||||
assert_eq!(format_number(100), "100");
|
||||
assert_eq!(format_number(999), "999");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_number_adds_thousands_separators() {
|
||||
assert_eq!(format_number(1000), "1,000");
|
||||
assert_eq!(format_number(12345), "12,345");
|
||||
assert_eq!(format_number(1234567), "1,234,567");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user