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:
@@ -10,6 +10,7 @@ use crate::core::db::create_connection;
|
||||
use crate::core::error::{LoreError, Result};
|
||||
use crate::core::lock::{AppLock, LockOptions};
|
||||
use crate::core::paths::get_db_path;
|
||||
use crate::core::project::resolve_project;
|
||||
use crate::gitlab::GitLabClient;
|
||||
use crate::ingestion::{
|
||||
IngestMrProjectResult, IngestProjectResult, ProgressEvent, ingest_project_issues_with_progress,
|
||||
@@ -40,6 +41,36 @@ pub struct IngestResult {
|
||||
pub notes_upserted: usize,
|
||||
}
|
||||
|
||||
/// Controls what interactive UI elements `run_ingest` displays.
|
||||
///
|
||||
/// Separates progress indicators (spinners, bars) from text output (headers,
|
||||
/// per-project summaries) so callers like `sync` can show progress without
|
||||
/// duplicating summary text.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct IngestDisplay {
|
||||
/// Show animated spinners and progress bars.
|
||||
pub show_progress: bool,
|
||||
/// Show text headers ("Ingesting...") and per-project summary lines.
|
||||
pub show_text: bool,
|
||||
}
|
||||
|
||||
impl IngestDisplay {
|
||||
/// Interactive mode: everything visible.
|
||||
pub fn interactive() -> Self {
|
||||
Self { show_progress: true, show_text: true }
|
||||
}
|
||||
|
||||
/// Robot/JSON mode: everything hidden.
|
||||
pub fn silent() -> Self {
|
||||
Self { show_progress: false, show_text: false }
|
||||
}
|
||||
|
||||
/// Progress only (used by sync in interactive mode).
|
||||
pub fn progress_only() -> Self {
|
||||
Self { show_progress: true, show_text: false }
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the ingest command.
|
||||
pub async fn run_ingest(
|
||||
config: &Config,
|
||||
@@ -47,7 +78,7 @@ pub async fn run_ingest(
|
||||
project_filter: Option<&str>,
|
||||
force: bool,
|
||||
full: bool,
|
||||
robot_mode: bool,
|
||||
display: IngestDisplay,
|
||||
) -> Result<IngestResult> {
|
||||
// Validate resource type early
|
||||
if resource_type != "issues" && resource_type != "mrs" {
|
||||
@@ -86,7 +117,7 @@ pub async fn run_ingest(
|
||||
|
||||
// If --full flag is set, reset sync cursors and discussion watermarks for a complete re-fetch
|
||||
if full {
|
||||
if !robot_mode {
|
||||
if display.show_text {
|
||||
println!(
|
||||
"{}",
|
||||
style("Full sync: resetting cursors to fetch all data...").yellow()
|
||||
@@ -139,7 +170,7 @@ pub async fn run_ingest(
|
||||
} else {
|
||||
"merge requests"
|
||||
};
|
||||
if !robot_mode {
|
||||
if display.show_text {
|
||||
println!("{}", style(format!("Ingesting {type_label}...")).blue());
|
||||
println!();
|
||||
}
|
||||
@@ -147,7 +178,7 @@ pub async fn run_ingest(
|
||||
// Sync each project
|
||||
for (local_project_id, gitlab_project_id, path) in &projects {
|
||||
// Show spinner while fetching (only in interactive mode)
|
||||
let spinner = if robot_mode {
|
||||
let spinner = if !display.show_progress {
|
||||
ProgressBar::hidden()
|
||||
} else {
|
||||
let s = ProgressBar::new_spinner();
|
||||
@@ -162,7 +193,7 @@ pub async fn run_ingest(
|
||||
};
|
||||
|
||||
// Progress bar for discussion sync (hidden until needed, or always hidden in robot mode)
|
||||
let disc_bar = if robot_mode {
|
||||
let disc_bar = if !display.show_progress {
|
||||
ProgressBar::hidden()
|
||||
} else {
|
||||
let b = ProgressBar::new(0);
|
||||
@@ -178,7 +209,7 @@ pub async fn run_ingest(
|
||||
// Create progress callback (no-op in robot mode)
|
||||
let spinner_clone = spinner.clone();
|
||||
let disc_bar_clone = disc_bar.clone();
|
||||
let progress_callback: crate::ingestion::ProgressCallback = if robot_mode {
|
||||
let progress_callback: crate::ingestion::ProgressCallback = if !display.show_progress {
|
||||
Box::new(|_| {})
|
||||
} else {
|
||||
Box::new(move |event: ProgressEvent| match event {
|
||||
@@ -225,7 +256,7 @@ pub async fn run_ingest(
|
||||
disc_bar.finish_and_clear();
|
||||
|
||||
// Print per-project summary (only in interactive mode)
|
||||
if !robot_mode {
|
||||
if display.show_text {
|
||||
print_issue_project_summary(path, &result);
|
||||
}
|
||||
|
||||
@@ -254,7 +285,7 @@ pub async fn run_ingest(
|
||||
disc_bar.finish_and_clear();
|
||||
|
||||
// Print per-project summary (only in interactive mode)
|
||||
if !robot_mode {
|
||||
if display.show_text {
|
||||
print_mr_project_summary(path, &result);
|
||||
}
|
||||
|
||||
@@ -283,16 +314,39 @@ fn get_projects_to_sync(
|
||||
configured_projects: &[crate::core::config::ProjectConfig],
|
||||
filter: Option<&str>,
|
||||
) -> Result<Vec<(i64, i64, String)>> {
|
||||
let mut projects = Vec::new();
|
||||
// If a filter is provided, resolve it to a specific project
|
||||
if let Some(filter_str) = filter {
|
||||
let project_id = resolve_project(conn, filter_str)?;
|
||||
|
||||
for project_config in configured_projects {
|
||||
if let Some(filter_path) = filter
|
||||
&& !project_config.path.contains(filter_path)
|
||||
{
|
||||
continue;
|
||||
// Verify the resolved project is in our config
|
||||
let row: Option<(i64, String)> = conn
|
||||
.query_row(
|
||||
"SELECT gitlab_project_id, path_with_namespace FROM projects WHERE id = ?1",
|
||||
[project_id],
|
||||
|row| Ok((row.get(0)?, row.get(1)?)),
|
||||
)
|
||||
.ok();
|
||||
|
||||
if let Some((gitlab_id, path)) = row {
|
||||
// Confirm it's a configured project
|
||||
if configured_projects.iter().any(|p| p.path == path) {
|
||||
return Ok(vec![(project_id, gitlab_id, path)]);
|
||||
}
|
||||
return Err(LoreError::Other(format!(
|
||||
"Project '{}' exists in database but is not in configuration",
|
||||
path
|
||||
)));
|
||||
}
|
||||
|
||||
// Get project from database
|
||||
return Err(LoreError::Other(format!(
|
||||
"Project '{}' not found in database",
|
||||
filter_str
|
||||
)));
|
||||
}
|
||||
|
||||
// No filter: return all configured projects
|
||||
let mut projects = Vec::new();
|
||||
for project_config in configured_projects {
|
||||
let result: Option<(i64, i64)> = conn
|
||||
.query_row(
|
||||
"SELECT id, gitlab_project_id FROM projects WHERE path_with_namespace = ?",
|
||||
|
||||
Reference in New Issue
Block a user