refactor(commands): Add IngestDisplay, resolve_project, and color-aware tables
Ingest: - Introduce IngestDisplay struct with show_progress/show_text booleans to decouple progress bars from text output. Replaces the robot_mode bool parameter with explicit display control, enabling sync to show progress without duplicating summary text (progress_only mode). - Use resolve_project() for --project filtering instead of LIKE queries, providing proper error messages for ambiguous or missing projects. List: - Add colored_cell() helper that checks console::colors_enabled() before applying comfy-table foreground colors, bridging the gap between the console and comfy-table crates for --color flag support. - Use resolve_project() for project filtering (exact ID match). - Improve since filter to return explicit errors instead of silently ignoring invalid values. - Improve format_relative_time for proper singular/plural forms. Search: - Validate --after/--updated-after with explicit error messages. - Handle optional title field (Option<String>) in HydratedRow. Show: - Use resolve_project() for project disambiguation. Sync: - Thread robot_mode via SyncOptions for IngestDisplay selection. - Use IngestDisplay::progress_only() in interactive sync mode. GenerateDocs: - Use resolve_project() for --project filtering. Co-Authored-By: Claude (us.anthropic.claude-opus-4-5-20251101-v1:0) <noreply@anthropic.com>
This commit is contained in:
@@ -6,10 +6,21 @@ use serde::Serialize;
|
||||
|
||||
use crate::Config;
|
||||
use crate::core::db::create_connection;
|
||||
use crate::core::error::Result;
|
||||
use crate::core::error::{LoreError, Result};
|
||||
use crate::core::paths::get_db_path;
|
||||
use crate::core::project::resolve_project;
|
||||
use crate::core::time::{ms_to_iso, now_ms, parse_since};
|
||||
|
||||
/// Apply foreground color to a Cell only if colors are enabled.
|
||||
fn colored_cell(content: impl std::fmt::Display, color: Color) -> Cell {
|
||||
let cell = Cell::new(content);
|
||||
if console::colors_enabled() {
|
||||
cell.fg(color)
|
||||
} else {
|
||||
cell
|
||||
}
|
||||
}
|
||||
|
||||
/// Issue row for display.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct IssueListRow {
|
||||
@@ -232,11 +243,9 @@ fn query_issues(conn: &Connection, filters: &ListFilters) -> Result<ListResult>
|
||||
let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
|
||||
|
||||
if let Some(project) = filters.project {
|
||||
// Exact match or suffix match after '/' to avoid partial matches
|
||||
// e.g. "foo" matches "group/foo" but NOT "group/foobar"
|
||||
where_clauses.push("(p.path_with_namespace = ? OR p.path_with_namespace LIKE ?)");
|
||||
params.push(Box::new(project.to_string()));
|
||||
params.push(Box::new(format!("%/{project}")));
|
||||
let project_id = resolve_project(conn, project)?;
|
||||
where_clauses.push("i.project_id = ?");
|
||||
params.push(Box::new(project_id));
|
||||
}
|
||||
|
||||
if let Some(state) = filters.state
|
||||
@@ -264,9 +273,13 @@ fn query_issues(conn: &Connection, filters: &ListFilters) -> Result<ListResult>
|
||||
}
|
||||
|
||||
// Handle since filter
|
||||
if let Some(since_str) = filters.since
|
||||
&& let Some(cutoff_ms) = parse_since(since_str)
|
||||
{
|
||||
if let Some(since_str) = filters.since {
|
||||
let cutoff_ms = parse_since(since_str).ok_or_else(|| {
|
||||
LoreError::Other(format!(
|
||||
"Invalid --since value '{}'. Use relative (7d, 2w, 1m) or absolute (YYYY-MM-DD) format.",
|
||||
since_str
|
||||
))
|
||||
})?;
|
||||
where_clauses.push("i.updated_at >= ?");
|
||||
params.push(Box::new(cutoff_ms));
|
||||
}
|
||||
@@ -419,11 +432,9 @@ fn query_mrs(conn: &Connection, filters: &MrListFilters) -> Result<MrListResult>
|
||||
let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
|
||||
|
||||
if let Some(project) = filters.project {
|
||||
// Exact match or suffix match after '/' to avoid partial matches
|
||||
// e.g. "foo" matches "group/foo" but NOT "group/foobar"
|
||||
where_clauses.push("(p.path_with_namespace = ? OR p.path_with_namespace LIKE ?)");
|
||||
params.push(Box::new(project.to_string()));
|
||||
params.push(Box::new(format!("%/{project}")));
|
||||
let project_id = resolve_project(conn, project)?;
|
||||
where_clauses.push("m.project_id = ?");
|
||||
params.push(Box::new(project_id));
|
||||
}
|
||||
|
||||
if let Some(state) = filters.state
|
||||
@@ -461,9 +472,13 @@ fn query_mrs(conn: &Connection, filters: &MrListFilters) -> Result<MrListResult>
|
||||
}
|
||||
|
||||
// Handle since filter
|
||||
if let Some(since_str) = filters.since
|
||||
&& let Some(cutoff_ms) = parse_since(since_str)
|
||||
{
|
||||
if let Some(since_str) = filters.since {
|
||||
let cutoff_ms = parse_since(since_str).ok_or_else(|| {
|
||||
LoreError::Other(format!(
|
||||
"Invalid --since value '{}'. Use relative (7d, 2w, 1m) or absolute (YYYY-MM-DD) format.",
|
||||
since_str
|
||||
))
|
||||
})?;
|
||||
where_clauses.push("m.updated_at >= ?");
|
||||
params.push(Box::new(cutoff_ms));
|
||||
}
|
||||
@@ -628,10 +643,22 @@ fn format_relative_time(ms_epoch: i64) -> String {
|
||||
match diff {
|
||||
d if d < 60_000 => "just now".to_string(),
|
||||
d if d < 3_600_000 => format!("{} min ago", d / 60_000),
|
||||
d if d < 86_400_000 => format!("{} hours ago", d / 3_600_000),
|
||||
d if d < 604_800_000 => format!("{} days ago", d / 86_400_000),
|
||||
d if d < 2_592_000_000 => format!("{} weeks ago", d / 604_800_000),
|
||||
_ => format!("{} months ago", diff / 2_592_000_000),
|
||||
d if d < 86_400_000 => {
|
||||
let n = d / 3_600_000;
|
||||
format!("{n} {} ago", if n == 1 { "hour" } else { "hours" })
|
||||
}
|
||||
d if d < 604_800_000 => {
|
||||
let n = d / 86_400_000;
|
||||
format!("{n} {} ago", if n == 1 { "day" } else { "days" })
|
||||
}
|
||||
d if d < 2_592_000_000 => {
|
||||
let n = d / 604_800_000;
|
||||
format!("{n} {} ago", if n == 1 { "week" } else { "weeks" })
|
||||
}
|
||||
_ => {
|
||||
let n = diff / 2_592_000_000;
|
||||
format!("{n} {} ago", if n == 1 { "month" } else { "months" })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -735,19 +762,19 @@ pub fn print_list_issues(result: &ListResult) {
|
||||
let discussions = format_discussions(issue.discussion_count, issue.unresolved_count);
|
||||
|
||||
let state_cell = if issue.state == "opened" {
|
||||
Cell::new(&issue.state).fg(Color::Green)
|
||||
colored_cell(&issue.state, Color::Green)
|
||||
} else {
|
||||
Cell::new(&issue.state).fg(Color::DarkGrey)
|
||||
colored_cell(&issue.state, Color::DarkGrey)
|
||||
};
|
||||
|
||||
table.add_row(vec![
|
||||
Cell::new(format!("#{}", issue.iid)).fg(Color::Cyan),
|
||||
colored_cell(format!("#{}", issue.iid), Color::Cyan),
|
||||
Cell::new(title),
|
||||
state_cell,
|
||||
Cell::new(assignee).fg(Color::Magenta),
|
||||
Cell::new(labels).fg(Color::Yellow),
|
||||
colored_cell(assignee, Color::Magenta),
|
||||
colored_cell(labels, Color::Yellow),
|
||||
Cell::new(discussions),
|
||||
Cell::new(relative_time).fg(Color::DarkGrey),
|
||||
colored_cell(relative_time, Color::DarkGrey),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -807,7 +834,6 @@ pub fn print_list_mrs(result: &MrListResult) {
|
||||
]);
|
||||
|
||||
for mr in &result.mrs {
|
||||
// Add [DRAFT] prefix for draft MRs
|
||||
let title = if mr.draft {
|
||||
format!("[DRAFT] {}", truncate_with_ellipsis(&mr.title, 38))
|
||||
} else {
|
||||
@@ -819,25 +845,24 @@ pub fn print_list_mrs(result: &MrListResult) {
|
||||
let discussions = format_discussions(mr.discussion_count, mr.unresolved_count);
|
||||
|
||||
let state_cell = match mr.state.as_str() {
|
||||
"opened" => Cell::new(&mr.state).fg(Color::Green),
|
||||
"merged" => Cell::new(&mr.state).fg(Color::Magenta),
|
||||
"closed" => Cell::new(&mr.state).fg(Color::Red),
|
||||
"locked" => Cell::new(&mr.state).fg(Color::Yellow),
|
||||
_ => Cell::new(&mr.state).fg(Color::DarkGrey),
|
||||
"opened" => colored_cell(&mr.state, Color::Green),
|
||||
"merged" => colored_cell(&mr.state, Color::Magenta),
|
||||
"closed" => colored_cell(&mr.state, Color::Red),
|
||||
"locked" => colored_cell(&mr.state, Color::Yellow),
|
||||
_ => colored_cell(&mr.state, Color::DarkGrey),
|
||||
};
|
||||
|
||||
table.add_row(vec![
|
||||
Cell::new(format!("!{}", mr.iid)).fg(Color::Cyan),
|
||||
colored_cell(format!("!{}", mr.iid), Color::Cyan),
|
||||
Cell::new(title),
|
||||
state_cell,
|
||||
Cell::new(format!(
|
||||
"@{}",
|
||||
truncate_with_ellipsis(&mr.author_username, 12)
|
||||
))
|
||||
.fg(Color::Magenta),
|
||||
Cell::new(branches).fg(Color::Blue),
|
||||
colored_cell(
|
||||
format!("@{}", truncate_with_ellipsis(&mr.author_username, 12)),
|
||||
Color::Magenta,
|
||||
),
|
||||
colored_cell(branches, Color::Blue),
|
||||
Cell::new(discussions),
|
||||
Cell::new(relative_time).fg(Color::DarkGrey),
|
||||
colored_cell(relative_time, Color::DarkGrey),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user