use std::sync::mpsc;
use std::thread;
use anyhow::Result;
use gitstack::{
build_handoff_context, build_next_actions, build_review_pack, calculate_file_heatmap,
calculate_stats, get_commit_files, load_cached_review_pack, record_quick_action_usage,
save_cached_review_pack, verify_patch_risk, App, GitEvent, Language, QuickAction, ReviewPack,
MIN_COMMITS_FOR_COUPLING, MIN_COUPLING_RATIO,
};
use crate::key_handlers::set_error_status;
use crate::{AnalysisResult, BackgroundAnalysisState};
pub fn load_or_build_review_pack_for_tui(app: &App) -> Result<ReviewPack> {
let events: Vec<_> = app.events().collect();
let selected = app.selected_event().map(|e| e.short_hash.as_str());
if let Some(pack) = load_cached_review_pack(selected) {
return Ok(pack);
}
let pack = build_review_pack(None, &events, selected)?;
let _ = save_cached_review_pack(selected, &pack);
Ok(pack)
}
pub fn friendly_error_message(e: &anyhow::Error, lang: Language) -> String {
if let Some(io_err) = e.downcast_ref::<std::io::Error>() {
return match io_err.kind() {
std::io::ErrorKind::NotFound => lang.error_not_found().to_string(),
std::io::ErrorKind::PermissionDenied => lang.error_permission_denied().to_string(),
std::io::ErrorKind::ConnectionRefused => lang.error_connection_refused().to_string(),
std::io::ErrorKind::TimedOut => lang.error_connection_timeout().to_string(),
_ => e.to_string(),
};
}
if let Some(git_err) = e.downcast_ref::<git2::Error>() {
let msg = git_err.message();
let msg_lower = msg.to_lowercase();
if msg_lower.contains("authentication")
|| msg_lower.contains("credential")
|| msg_lower.contains("auth")
{
return lang.error_auth_failed().to_string();
}
if msg_lower.contains("conflict") {
return lang.error_merge_conflict().to_string();
}
if msg_lower.contains("uncommitted")
|| msg_lower.contains("dirty")
|| msg_lower.contains("overwritten by checkout")
{
return lang.error_dirty_worktree().to_string();
}
if msg_lower.contains("not found") && msg_lower.contains("branch") {
return lang.error_branch_not_found().to_string();
}
if msg_lower.contains("already exists") {
return lang.error_branch_already_exists().to_string();
}
if msg_lower.contains("not a git repository") {
return lang.error_not_a_repo().to_string();
}
return msg.to_string();
}
let msg = e.to_string();
let msg_lower = msg.to_lowercase();
if msg_lower.contains("permission denied") {
return lang.error_permission_denied().to_string();
}
if msg_lower.contains("no such file") || msg_lower.contains("not found") {
return lang.error_not_found().to_string();
}
if msg_lower.contains("timed out") || msg_lower.contains("timeout") {
return lang.error_connection_timeout().to_string();
}
if msg_lower.contains("connection refused") {
return lang.error_connection_refused().to_string();
}
msg
}
pub fn execute_quick_action(
app: &mut App,
action: QuickAction,
bg_analysis_state: &mut BackgroundAnalysisState,
) {
match action {
QuickAction::AuthorStats => {
let events: Vec<_> = app.events().collect();
let stats = calculate_stats(&events);
app.start_stats_view(stats);
return;
}
QuickAction::Heatmap => {
let events: Vec<_> = app.events().collect();
let heatmap = calculate_file_heatmap(&events, |hash| get_commit_files(hash).ok());
app.start_heatmap_view(heatmap);
return;
}
_ => {}
}
if execute_analysis_action(app, &action, bg_analysis_state) {
return;
}
execute_review_action(app, action);
}
fn execute_analysis_action(
app: &mut App,
action: &QuickAction,
bg_analysis_state: &mut BackgroundAnalysisState,
) -> bool {
use gitstack::{
calculate_activity_timeline, calculate_change_coupling, calculate_file_heatmap,
calculate_impact_scores, calculate_ownership, calculate_quality_scores,
get_commit_files_from_repo,
};
if bg_analysis_state.is_running() {
app.set_status_message("Analysis already in progress...".to_string());
return true;
}
let events: Vec<GitEvent> = app.events().cloned().collect();
let action = *action;
match action {
QuickAction::Timeline
| QuickAction::Ownership
| QuickAction::ImpactScore
| QuickAction::ChangeCoupling
| QuickAction::QualityScore => {
app.set_status_message("Calculating...".to_string());
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let repo = match git2::Repository::discover(".") {
Ok(r) => r,
Err(_) => return,
};
let event_refs: Vec<&GitEvent> = events.iter().collect();
let mut file_cache: std::collections::HashMap<String, Vec<String>> =
std::collections::HashMap::new();
if !matches!(action, QuickAction::Timeline) {
for event in &event_refs {
if !file_cache.contains_key(&event.short_hash) {
if let Ok(files) = get_commit_files_from_repo(&repo, &event.short_hash)
{
file_cache.insert(event.short_hash.clone(), files);
}
}
}
}
let file_cache_ref = &file_cache;
let get_files =
|hash: &str| -> Option<Vec<String>> { file_cache_ref.get(hash).cloned() };
let result = match action {
QuickAction::Timeline => {
AnalysisResult::Timeline(Box::new(calculate_activity_timeline(&event_refs)))
}
QuickAction::Ownership => {
AnalysisResult::Ownership(calculate_ownership(&event_refs, get_files))
}
QuickAction::ImpactScore => {
let heatmap = calculate_file_heatmap(&event_refs, get_files);
AnalysisResult::ImpactScore(calculate_impact_scores(
&event_refs,
get_files,
&heatmap,
))
}
QuickAction::ChangeCoupling => {
AnalysisResult::ChangeCoupling(calculate_change_coupling(
&event_refs,
get_files,
MIN_COMMITS_FOR_COUPLING,
MIN_COUPLING_RATIO,
))
}
QuickAction::QualityScore => {
let coupling = calculate_change_coupling(
&event_refs,
get_files,
MIN_COMMITS_FOR_COUPLING,
MIN_COUPLING_RATIO,
);
AnalysisResult::QualityScore(calculate_quality_scores(
&event_refs,
get_files,
&coupling,
))
}
_ => unreachable!(),
};
let _ = tx.send(result);
});
bg_analysis_state.result_rx = Some(rx);
true
}
_ => false,
}
}
fn execute_review_action(app: &mut App, action: QuickAction) {
let result = (|| -> Result<()> {
let pack = load_or_build_review_pack_for_tui(app)?;
match action {
QuickAction::RiskSummary | QuickAction::ReviewPack | QuickAction::Verify => {
let verdict = verify_patch_risk(&pack);
let msg = format!(
"Risk {:.2} / conf {:.2} ({})",
pack.risk_score,
pack.confidence,
verdict
.get("verdict")
.and_then(|v| v.as_str())
.unwrap_or("unknown")
);
app.set_status_message(format!("{}: {}", action.id(), msg));
app.start_review_pack_view(pack, Some(verdict));
}
QuickAction::NextActions => {
let actions = build_next_actions(&pack);
let first = actions.first().map(|a| a.title.as_str()).unwrap_or("none");
let msg = format!("Next actions: {} (top: {})", actions.len(), first);
app.set_status_message(format!("{}: {}", action.id(), msg));
app.start_next_actions_view(actions);
}
QuickAction::HandoffClaude => {
let h = build_handoff_context(&pack, "claude");
let msg = format!(
"Handoff ready for Claude ({} actions)",
h.next_actions.len()
);
app.set_status_message(format!("{}: {}", action.id(), msg));
app.start_handoff_view(h);
}
QuickAction::HandoffCodex => {
let h = build_handoff_context(&pack, "codex");
let msg = format!("Handoff ready for Codex ({} actions)", h.next_actions.len());
app.set_status_message(format!("{}: {}", action.id(), msg));
app.start_handoff_view(h);
}
QuickAction::HandoffCopilot => {
let h = build_handoff_context(&pack, "copilot");
let msg = format!(
"Handoff ready for Copilot ({} actions)",
h.next_actions.len()
);
app.set_status_message(format!("{}: {}", action.id(), msg));
app.start_handoff_view(h);
}
_ => unreachable!(),
}
Ok(())
})();
match result {
Ok(()) => {
let _ = record_quick_action_usage(action.id());
}
Err(e) => set_error_status(app, app.language.status_quick_action_failed(), &e),
}
}