fn print_issue_project_summary(path: &str, result: &IngestProjectResult) { let labels_str = if result.labels_created > 0 { format!(", {} new labels", result.labels_created) } else { String::new() }; println!( " {}: {} issues fetched{}", Theme::info().render(path), result.issues_upserted, labels_str ); if result.issues_synced_discussions > 0 { println!( " {} issues -> {} discussions, {} notes", result.issues_synced_discussions, result.discussions_fetched, result.notes_upserted ); } if result.issues_skipped_discussion_sync > 0 { println!( " {} unchanged issues (discussion sync skipped)", Theme::dim().render(&result.issues_skipped_discussion_sync.to_string()) ); } } fn print_mr_project_summary(path: &str, result: &IngestMrProjectResult) { let labels_str = if result.labels_created > 0 { format!(", {} new labels", result.labels_created) } else { String::new() }; let assignees_str = if result.assignees_linked > 0 || result.reviewers_linked > 0 { format!( ", {} assignees, {} reviewers", result.assignees_linked, result.reviewers_linked ) } else { String::new() }; println!( " {}: {} MRs fetched{}{}", Theme::info().render(path), result.mrs_upserted, labels_str, assignees_str ); if result.mrs_synced_discussions > 0 { let diffnotes_str = if result.diffnotes_count > 0 { format!(" ({} diff notes)", result.diffnotes_count) } else { String::new() }; println!( " {} MRs -> {} discussions, {} notes{}", result.mrs_synced_discussions, result.discussions_fetched, result.notes_upserted, diffnotes_str ); } if result.mrs_skipped_discussion_sync > 0 { println!( " {} unchanged MRs (discussion sync skipped)", Theme::dim().render(&result.mrs_skipped_discussion_sync.to_string()) ); } } #[derive(Serialize)] struct IngestJsonOutput { ok: bool, data: IngestJsonData, meta: RobotMeta, } #[derive(Serialize)] struct IngestJsonData { resource_type: String, projects_synced: usize, #[serde(skip_serializing_if = "Option::is_none")] issues: Option, #[serde(skip_serializing_if = "Option::is_none")] merge_requests: Option, labels_created: usize, discussions_fetched: usize, notes_upserted: usize, resource_events_fetched: usize, resource_events_failed: usize, #[serde(skip_serializing_if = "Vec::is_empty")] status_enrichment: Vec, status_enrichment_errors: usize, } #[derive(Serialize)] struct StatusEnrichmentJson { mode: String, #[serde(skip_serializing_if = "Option::is_none")] reason: Option, seen: usize, enriched: usize, cleared: usize, without_widget: usize, partial_errors: usize, #[serde(skip_serializing_if = "Option::is_none")] first_partial_error: Option, #[serde(skip_serializing_if = "Option::is_none")] error: Option, } #[derive(Serialize)] struct IngestIssueStats { fetched: usize, upserted: usize, synced_discussions: usize, skipped_discussion_sync: usize, } #[derive(Serialize)] struct IngestMrStats { fetched: usize, upserted: usize, synced_discussions: usize, skipped_discussion_sync: usize, assignees_linked: usize, reviewers_linked: usize, diffnotes_count: usize, } pub fn print_ingest_summary_json(result: &IngestResult, elapsed_ms: u64) { let (issues, merge_requests) = if result.resource_type == "issues" { ( Some(IngestIssueStats { fetched: result.issues_fetched, upserted: result.issues_upserted, synced_discussions: result.issues_synced_discussions, skipped_discussion_sync: result.issues_skipped_discussion_sync, }), None, ) } else { ( None, Some(IngestMrStats { fetched: result.mrs_fetched, upserted: result.mrs_upserted, synced_discussions: result.mrs_synced_discussions, skipped_discussion_sync: result.mrs_skipped_discussion_sync, assignees_linked: result.assignees_linked, reviewers_linked: result.reviewers_linked, diffnotes_count: result.diffnotes_count, }), ) }; let status_enrichment: Vec = result .status_enrichment_projects .iter() .map(|p| StatusEnrichmentJson { mode: p.mode.clone(), reason: p.reason.clone(), seen: p.seen, enriched: p.enriched, cleared: p.cleared, without_widget: p.without_widget, partial_errors: p.partial_errors, first_partial_error: p.first_partial_error.clone(), error: p.error.clone(), }) .collect(); let output = IngestJsonOutput { ok: true, data: IngestJsonData { resource_type: result.resource_type.clone(), projects_synced: result.projects_synced, issues, merge_requests, labels_created: result.labels_created, discussions_fetched: result.discussions_fetched, notes_upserted: result.notes_upserted, resource_events_fetched: result.resource_events_fetched, resource_events_failed: result.resource_events_failed, status_enrichment, status_enrichment_errors: result.status_enrichment_errors, }, meta: RobotMeta { elapsed_ms }, }; match serde_json::to_string(&output) { Ok(json) => println!("{json}"), Err(e) => eprintln!("Error serializing to JSON: {e}"), } } pub fn print_ingest_summary(result: &IngestResult) { println!(); if result.resource_type == "issues" { println!( "{}", Theme::success().render(&format!( "Total: {} issues, {} discussions, {} notes", result.issues_upserted, result.discussions_fetched, result.notes_upserted )) ); if result.issues_skipped_discussion_sync > 0 { println!( "{}", Theme::dim().render(&format!( "Skipped discussion sync for {} unchanged issues.", result.issues_skipped_discussion_sync )) ); } } else { let diffnotes_str = if result.diffnotes_count > 0 { format!(" ({} diff notes)", result.diffnotes_count) } else { String::new() }; println!( "{}", Theme::success().render(&format!( "Total: {} MRs, {} discussions, {} notes{}", result.mrs_upserted, result.discussions_fetched, result.notes_upserted, diffnotes_str )) ); if result.mrs_skipped_discussion_sync > 0 { println!( "{}", Theme::dim().render(&format!( "Skipped discussion sync for {} unchanged MRs.", result.mrs_skipped_discussion_sync )) ); } } if result.resource_events_fetched > 0 || result.resource_events_failed > 0 { println!( " Resource events: {} fetched{}", result.resource_events_fetched, if result.resource_events_failed > 0 { format!(", {} failed", result.resource_events_failed) } else { String::new() } ); } } pub fn print_dry_run_preview(preview: &DryRunPreview) { println!( "{} {}", Theme::info().bold().render("Dry Run Preview"), Theme::warning().render("(no changes will be made)") ); println!(); let type_label = if preview.resource_type == "issues" { "issues" } else { "merge requests" }; println!(" Resource type: {}", Theme::bold().render(type_label)); println!( " Sync mode: {}", if preview.sync_mode == "full" { Theme::warning().render("full (all data will be re-fetched)") } else { Theme::success().render("incremental (only changes since last sync)") } ); println!(" Projects: {}", preview.projects.len()); println!(); println!("{}", Theme::info().bold().render("Projects to sync:")); for project in &preview.projects { let sync_status = if !project.has_cursor { Theme::warning().render("initial sync") } else { Theme::success().render("incremental") }; println!( " {} ({})", Theme::bold().render(&project.path), sync_status ); println!(" Existing {}: {}", type_label, project.existing_count); if let Some(ref last_synced) = project.last_synced { println!(" Last synced: {}", last_synced); } } } #[derive(Serialize)] struct DryRunJsonOutput { ok: bool, dry_run: bool, data: DryRunPreview, } pub fn print_dry_run_preview_json(preview: &DryRunPreview) { let output = DryRunJsonOutput { ok: true, dry_run: true, data: preview.clone(), }; match serde_json::to_string(&output) { Ok(json) => println!("{json}"), Err(e) => eprintln!("Error serializing to JSON: {e}"), } }