332 lines
9.7 KiB
Rust
332 lines
9.7 KiB
Rust
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<IngestIssueStats>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
merge_requests: Option<IngestMrStats>,
|
|
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<StatusEnrichmentJson>,
|
|
status_enrichment_errors: usize,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct StatusEnrichmentJson {
|
|
mode: String,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
reason: Option<String>,
|
|
seen: usize,
|
|
enriched: usize,
|
|
cleared: usize,
|
|
without_widget: usize,
|
|
partial_errors: usize,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
first_partial_error: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
error: Option<String>,
|
|
}
|
|
|
|
#[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<StatusEnrichmentJson> = 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}"),
|
|
}
|
|
}
|