1use std::collections::HashMap;
2use std::time::{Duration, Instant};
3
4use colored::Colorize;
5use serde_json::Value;
6
7use crate::function_calling::{CommitFunctionArgs, FileChange};
8use crate::multi_step_analysis::{FileAnalysisResult, FileWithScore};
9
10#[derive(Clone)]
12pub struct FileAnalysisDebug {
13 pub file_path: String,
14 pub operation: String,
15 pub analysis: FileAnalysisResult,
16 pub api_duration: Duration,
17 pub api_payload: String
18}
19
20#[derive(Clone)]
22pub struct MultiStepDebug {
23 pub file_analyses: Vec<FileAnalysisDebug>,
24 pub score_result: Option<Vec<FileWithScore>>,
25 pub score_duration: Option<Duration>,
26 pub score_payload: Option<String>,
27 pub generate_result: Option<Value>,
28 pub generate_duration: Option<Duration>,
29 pub generate_payload: Option<String>,
30 pub final_message_duration: Option<Duration>,
31 pub candidates: Vec<String>,
32 pub reasoning: Option<String>
33}
34
35pub struct DebugSession {
37 start_time: Instant,
38 timings: HashMap<String, Duration>,
39 args: String,
40 build_type: String,
41 multi_step_error: Option<String>,
42 single_step_success: bool,
43 commit_message: Option<String>,
44 commit_reasoning: Option<String>,
45 files_analyzed: Option<CommitFunctionArgs>,
46 total_files_parsed: usize,
47 api_duration: Option<Duration>,
48 final_commit_hash: Option<String>,
49 final_commit_branch: Option<String>,
50 files_changed_count: Option<(usize, usize, usize)>, multi_step_debug: Option<MultiStepDebug> }
53
54impl DebugSession {
55 pub fn new(args: &str) -> Self {
56 Self {
57 start_time: Instant::now(),
58 timings: HashMap::new(),
59 args: args.to_string(),
60 build_type: if cfg!(debug_assertions) {
61 "Debug build with performance profiling enabled".to_string()
62 } else {
63 "Release build".to_string()
64 },
65 multi_step_error: None,
66 single_step_success: false,
67 commit_message: None,
68 commit_reasoning: None,
69 files_analyzed: None,
70 total_files_parsed: 0,
71 api_duration: None,
72 final_commit_hash: None,
73 final_commit_branch: None,
74 files_changed_count: None,
75 multi_step_debug: None
76 }
77 }
78
79 pub fn record_timing(&mut self, operation: &str, duration: Duration) {
80 self.timings.insert(operation.to_string(), duration);
81 }
82
83 pub fn set_multi_step_error(&mut self, error: String) {
84 self.multi_step_error = Some(error);
85 }
86
87 pub fn set_single_step_success(&mut self, success: bool) {
88 self.single_step_success = success;
89 }
90
91 pub fn set_commit_result(&mut self, message: String, reasoning: String) {
92 self.commit_message = Some(message);
93 self.commit_reasoning = Some(reasoning);
94 }
95
96 pub fn set_files_analyzed(&mut self, args: CommitFunctionArgs) {
97 self.files_analyzed = Some(args);
98 }
99
100 pub fn set_total_files_parsed(&mut self, count: usize) {
101 self.total_files_parsed = count;
102 }
103
104 pub fn set_api_duration(&mut self, duration: Duration) {
105 self.api_duration = Some(duration);
106 }
107
108 pub fn init_multi_step_debug(&mut self) {
109 self.multi_step_debug = Some(MultiStepDebug {
110 file_analyses: Vec::new(),
111 score_result: None,
112 score_duration: None,
113 score_payload: None,
114 generate_result: None,
115 generate_duration: None,
116 generate_payload: None,
117 final_message_duration: None,
118 candidates: Vec::new(),
119 reasoning: None
120 });
121 }
122
123 pub fn add_file_analysis_debug(
124 &mut self, file_path: String, operation: String, analysis: FileAnalysisResult, duration: Duration, payload: String
125 ) {
126 if let Some(ref mut multi_step) = self.multi_step_debug {
127 multi_step.file_analyses.push(FileAnalysisDebug {
128 file_path,
129 operation,
130 analysis,
131 api_duration: duration,
132 api_payload: payload
133 });
134 }
135 }
136
137 pub fn set_score_debug(&mut self, result: Vec<FileWithScore>, duration: Duration, payload: String) {
138 if let Some(ref mut multi_step) = self.multi_step_debug {
139 multi_step.score_result = Some(result);
140 multi_step.score_duration = Some(duration);
141 multi_step.score_payload = Some(payload);
142 }
143 }
144
145 pub fn set_generate_debug(&mut self, result: Value, duration: Duration, payload: String) {
146 if let Some(ref mut multi_step) = self.multi_step_debug {
147 let mut candidates_vec = Vec::new();
149 if let Some(candidates) = result.get("candidates") {
150 if let Some(candidates_array) = candidates.as_array() {
151 candidates_vec = candidates_array
152 .iter()
153 .filter_map(|v| v.as_str().map(|s| s.to_string()))
154 .collect();
155 }
156 }
157
158 let reasoning_str = result
160 .get("reasoning")
161 .and_then(|r| r.as_str())
162 .map(|s| s.to_string());
163
164 multi_step.generate_result = Some(result);
166 multi_step.generate_duration = Some(duration);
167 multi_step.generate_payload = Some(payload);
168 multi_step.candidates = candidates_vec;
169 multi_step.reasoning = reasoning_str;
170 }
171 }
172
173 pub fn set_final_message_debug(&mut self, duration: Duration) {
174 if let Some(ref mut multi_step) = self.multi_step_debug {
175 multi_step.final_message_duration = Some(duration);
176 }
177 }
178
179 pub fn set_final_commit_info(&mut self, branch: String, hash: String, files: usize, insertions: usize, deletions: usize) {
180 self.final_commit_branch = Some(branch);
181 self.final_commit_hash = Some(hash);
182 self.files_changed_count = Some((files, insertions, deletions));
183 }
184
185 pub fn print_debug_output(&self) {
186 eprintln!("\n{}", "=== GIT AI HOOK DEBUG SESSION ===".bright_cyan().bold());
187
188 eprintln!("\n{} {}", "📋".bright_yellow(), "INITIALIZATION".bright_white().bold());
190 eprintln!(" {} {}", "Args:".bright_white(), self.args);
191 eprintln!(" {} {}", "Build:".bright_white(), self.build_type);
192
193 eprintln!("\n{} {}", "⚙️ ".bright_yellow(), "SETUP & PREPARATION".bright_white().bold());
195 self.print_timing_line("Generate instruction template", "Generate instruction template", false);
196 self.print_timing_line("Count tokens", "Count tokens", false);
197 self.print_timing_line("Calculate instruction tokens", "Calculate instruction tokens", false);
198 self.print_timing_line("Get context size", "Get context size", true);
199
200 eprintln!("\n{} {}", "📝".bright_yellow(), "GIT DIFF PROCESSING".bright_white().bold());
202 self.print_timing_line("Git diff generation", "Git diff generation", false);
203 self.print_timing_line("Processing diff changes", "Processing diff changes", false);
204 self.print_timing_line("Repository patch generation", "Repository patch generation", false);
205
206 let files_status = if self.total_files_parsed == 0 {
207 format!("{} files {}", self.total_files_parsed, "⚠️".yellow())
208 } else {
209 format!("{} files ✓", self.total_files_parsed)
210 .green()
211 .to_string()
212 };
213 eprintln!(" └ Files parsed from diff {files_status}");
214
215 if self.total_files_parsed > 0 {
217 eprintln!("\n{} {}", "🔍".bright_yellow(), "DISCOVERED FILES".bright_white().bold());
218
219 if let Some(ref multi_step) = self.multi_step_debug {
220 for (files_shown, file) in multi_step.file_analyses.iter().enumerate() {
221 let change_type = match file.operation.as_str() {
222 "added" => "[added]".green(),
223 "deleted" => "[deleted]".red(),
224 "modified" => "[modified]".yellow(),
225 "renamed" => "[renamed]".blue(),
226 _ => format!("[{}]", file.operation).normal()
227 };
228
229 let lines_info = format!("{} lines", file.analysis.lines_added + file.analysis.lines_removed);
230 let prefix = if files_shown == multi_step.file_analyses.len() - 1 {
231 "└"
232 } else {
233 "│"
234 };
235 eprintln!(" {} {:<30} {:<12} {}", prefix, file.file_path.bright_cyan(), change_type, lines_info);
236 }
237 } else if let Some(ref files) = self.files_analyzed {
238 let mut file_list: Vec<(&String, &FileChange)> = files.files.iter().collect();
239 file_list.sort_by(|a, b| {
240 b.1
241 .impact_score
242 .partial_cmp(&a.1.impact_score)
243 .unwrap_or(std::cmp::Ordering::Equal)
244 });
245
246 let total_files = file_list.len();
247 for (files_shown, (path, change)) in file_list.iter().enumerate() {
248 let change_type = match change.change_type.as_str() {
249 "added" => "[added]".green(),
250 "deleted" => "[deleted]".red(),
251 "modified" => "[modified]".yellow(),
252 "renamed" => "[renamed]".blue(),
253 _ => format!("[{}]", change.change_type).normal()
254 };
255
256 let prefix = if files_shown == total_files - 1 {
257 "└"
258 } else {
259 "│"
260 };
261 eprintln!(
262 " {} {:<30} {:<12} {} lines",
263 prefix,
264 path.bright_cyan(),
265 change_type,
266 change.lines_changed
267 );
268 }
269 }
270 }
271
272 eprintln!("\n{} {}", "🤖".bright_yellow(), "AI PROCESSING".bright_white().bold());
274
275 if let Some(ref multi_step) = self.multi_step_debug {
276 eprintln!(
277 "\n {} {}",
278 "📋".bright_yellow(),
279 "STEP 1: INDIVIDUAL FILE ANALYSIS".bright_white().bold()
280 );
281
282 for (i, file) in multi_step.file_analyses.iter().enumerate() {
283 let file_num = i + 1;
284 let total_files = multi_step.file_analyses.len();
285
286 eprintln!(" ");
287 eprintln!(" 🔸 File {}/{}: {}", file_num, total_files, file.file_path.bright_cyan());
288 eprintln!(" │ OpenAI Request [analyze]:");
289 eprintln!(
290 " │ └ Payload: {{\"file_path\": \"{}\", \"operation_type\": \"{}\", \"diff_content\": \"...\"}}",
291 file.file_path, file.operation
292 );
293 eprintln!(
294 " │ API Response Time: {:<7} ✓",
295 format!("{:.2}s", file.api_duration.as_secs_f32())
296 );
297 eprintln!(" │ Results:");
298 eprintln!(" │ ├ Lines Added: {}", file.analysis.lines_added);
299 eprintln!(" │ ├ Lines Removed: {}", file.analysis.lines_removed);
300 eprintln!(" │ ├ File Category: {}", file.analysis.file_category);
301 eprintln!(" │ └ Summary: {}", file.analysis.summary);
302 }
303
304 eprintln!(
305 "\n {} {}",
306 "📊".bright_yellow(),
307 "STEP 2: IMPACT SCORE CALCULATION".bright_white().bold()
308 );
309
310 if let Some(ref score_result) = multi_step.score_result {
311 if let Some(score_duration) = multi_step.score_duration {
312 eprintln!(" │ OpenAI Request [score]:");
313 eprintln!(
314 " │ └ Payload: {{\"files_data\": [{{\"{}\", ...}}, ...]}}",
315 if !multi_step.file_analyses.is_empty() {
316 &multi_step.file_analyses[0].file_path
317 } else {
318 "no files"
319 }
320 );
321 eprintln!(
322 " │ API Response Time: {:<7} ✓",
323 format!("{:.2}s", score_duration.as_secs_f32())
324 );
325 eprintln!(" │ Results:");
326
327 let mut sorted_files = score_result.clone();
328 sorted_files.sort_by(|a, b| {
329 b.impact_score
330 .partial_cmp(&a.impact_score)
331 .unwrap_or(std::cmp::Ordering::Equal)
332 });
333
334 for (i, file) in sorted_files.iter().enumerate() {
335 let prefix = if i == sorted_files.len() - 1 {
336 "└"
337 } else {
338 "├"
339 };
340 eprintln!(
341 " │ {} {:<30} Impact Score {:.2} {}",
342 prefix,
343 file.file_path,
344 file.impact_score,
345 if i == 0 {
346 "(highest)".bright_green()
347 } else {
348 "".normal()
349 }
350 );
351 }
352 }
353 }
354
355 eprintln!(
356 "\n {} {}",
357 "💭".bright_yellow(),
358 "STEP 3: COMMIT MESSAGE GENERATION".bright_white().bold()
359 );
360
361 if let Some(generate_duration) = multi_step.generate_duration {
362 eprintln!(" │ OpenAI Request [generate]:");
363 eprintln!(" │ └ Payload: {{\"files_with_scores\": [...], \"max_length\": 72}}");
364 eprintln!(
365 " │ API Response Time: {:<7} ✓",
366 format!("{:.2}s", generate_duration.as_secs_f32())
367 );
368
369 if !multi_step.candidates.is_empty() {
370 eprintln!(" │ Candidates Generated:");
371
372 for (i, candidate) in multi_step.candidates.iter().enumerate() {
373 let prefix = if i == multi_step.candidates.len() - 1 {
374 "└"
375 } else {
376 "├"
377 };
378 eprintln!(" │ {} \"{}\"", prefix, candidate.bright_cyan());
379 }
380
381 if let Some(ref reasoning) = multi_step.reasoning {
382 eprintln!(" │ Reasoning: {reasoning}");
383 }
384 }
385 }
386 } else {
387 let multi_step_status = if self.multi_step_error.is_some() {
389 "FAILED".red().to_string()
390 } else if self.single_step_success {
391 "SKIPPED".yellow().to_string()
392 } else {
393 "SUCCESS".green().to_string()
394 };
395 eprintln!(" Multi-Step Attempt: {multi_step_status}");
396
397 if let Some(ref error) = self.multi_step_error {
398 eprintln!(" │ Creating score function tool ✓");
399 eprintln!(" │ OpenAI connection ✓");
400 eprintln!(
401 " └ Error: {} {} {}",
402 error.trim_end_matches('.'),
403 "✗".red(),
404 error.split(':').next_back().unwrap_or("").trim()
405 );
406 }
407
408 if self.single_step_success {
410 eprintln!("\n Single-Step Fallback: {}", "SUCCESS".green());
411 eprintln!(" │ Creating commit function tool ✓ max_length=72");
412 if let Some(duration) = self.api_duration {
413 eprintln!(
414 " │ OpenAI API call {:<7} ✓",
415 format!("{:.2}s", duration.as_secs_f32())
416 );
417 }
418 eprintln!(" └ Response parsing ✓");
419 }
420 }
421
422 if let Some(ref message) = self.commit_message {
424 eprintln!("\n{} {}", "📊".bright_yellow(), "ANALYSIS RESULTS".bright_white().bold());
425 eprintln!(" Selected Message: '{}'", message.bright_cyan());
426 eprintln!(" Message Length: {} characters (within 72 limit)", message.len());
427
428 if let Some(ref reasoning) = self.commit_reasoning {
429 eprintln!("\n Final Reasoning:");
430 let words: Vec<&str> = reasoning.split_whitespace().collect();
432 let mut line = String::new();
433 for word in words {
434 if line.len() + word.len() + 1 > 70 {
435 eprintln!(" {line}");
436 line = word.to_string();
437 } else {
438 if !line.is_empty() {
439 line.push(' ');
440 }
441 line.push_str(word);
442 }
443 }
444 if !line.is_empty() {
445 eprintln!(" {line}");
446 }
447 }
448 }
449
450 if let Some(ref files) = self.files_analyzed {
452 eprintln!("\n{} {}", "📁".bright_yellow(), "DETAILED FILE ANALYSIS".bright_white().bold());
453 eprintln!(" Total Files: {}", files.files.len());
454
455 let mut sorted_files: Vec<(&String, &FileChange)> = files.files.iter().collect();
457 sorted_files.sort_by(|a, b| {
458 b.1
459 .impact_score
460 .partial_cmp(&a.1.impact_score)
461 .unwrap_or(std::cmp::Ordering::Equal)
462 });
463
464 for (path, change) in sorted_files.iter() {
465 eprintln!();
466 eprintln!(" 🔸 {}", path.bright_cyan());
467 eprintln!(" │ Summary: {}", change.summary);
468 eprintln!(
469 " │ Impact Score: {:.2} {}",
470 change.impact_score,
471 if change.impact_score >= 0.9 {
472 "(highest - drives commit message)".bright_green()
473 } else if change.impact_score >= 0.8 {
474 "(high - mentioned in commit)".bright_yellow()
475 } else if change.impact_score >= 0.5 {
476 "(medium - supporting change)".normal()
477 } else {
478 "(low)".normal()
479 }
480 );
481
482 let _change_type_str = match change.change_type.as_str() {
484 "added" => "added",
485 "modified" => "modified",
486 "deleted" => "deleted",
487 "renamed" => "renamed",
488 _ => &change.change_type
489 };
490
491 eprintln!(
492 " │ Lines: +{}, -{} ({} total)",
493 change.lines_changed / 2, change.lines_changed / 2,
495 change.lines_changed
496 );
497 eprintln!(" │ Category: {}", change.file_category);
498 eprintln!(
499 " │ Significance: {}",
500 if change.impact_score >= 0.9 {
501 "Core functionality"
502 } else if change.impact_score >= 0.8 {
503 "Supporting infrastructure"
504 } else if change.impact_score >= 0.5 {
505 "Minor improvement"
506 } else {
507 "Peripheral change"
508 }
509 );
510
511 let weight_str = if change.impact_score >= 0.9 {
512 "Primary focus for commit message"
513 } else if change.impact_score >= 0.8 {
514 "Secondary mention in commit"
515 } else if change.impact_score >= 0.6 {
516 "Implicit support (not explicitly mentioned)"
517 } else {
518 "Not reflected in commit message"
519 };
520
521 eprintln!(" └ Weight: {weight_str}");
522 }
523 }
524
525 if let Some(ref files) = self.files_analyzed {
527 eprintln!("\n{} {}", "📈".bright_yellow(), "STATISTICS SUMMARY".bright_white().bold());
528
529 let total_lines: u32 = files.files.values().map(|f| f.lines_changed).sum();
530 let avg_impact: f32 = if files.files.is_empty() {
531 0.0
532 } else {
533 files.files.values().map(|f| f.impact_score).sum::<f32>() / files.files.len() as f32
534 };
535
536 eprintln!(" │ Total Lines Changed: {total_lines}");
537 eprintln!(" │ Average Impact Score: {avg_impact:.2}");
538 eprintln!(" │");
539
540 let mut category_counts: HashMap<&str, usize> = HashMap::new();
542 for change in files.files.values() {
543 *category_counts.entry(&change.file_category).or_insert(0) += 1;
544 }
545
546 eprintln!(" │ By Category:");
547 for (category, count) in category_counts {
548 eprintln!(" │ └ {category}: {count}");
549 }
550
551 eprintln!(" │");
552
553 let mut type_counts: HashMap<&str, usize> = HashMap::new();
555 for change in files.files.values() {
556 *type_counts.entry(&change.change_type).or_insert(0) += 1;
557 }
558
559 eprintln!(" │ By Change Type:");
560 for (change_type, count) in type_counts {
561 eprintln!(" │ └ {change_type}: {count}");
562 }
563 }
564
565 eprintln!("\n{} {}", "⏱️ ".bright_yellow(), "PERFORMANCE SUMMARY".bright_white().bold());
567
568 if let Some(ref multi_step) = self.multi_step_debug {
569 let mut total_file_analysis = Duration::default();
570 for file in &multi_step.file_analyses {
571 total_file_analysis += file.api_duration;
572 }
573
574 eprintln!(
575 " │ Individual file analysis: {:.2}s ({} files)",
576 total_file_analysis.as_secs_f32(),
577 multi_step.file_analyses.len()
578 );
579
580 if let Some(score_duration) = multi_step.score_duration {
581 eprintln!(" │ Impact score calculation: {:.2}s", score_duration.as_secs_f32());
582 }
583
584 if let Some(generate_duration) = multi_step.generate_duration {
585 eprintln!(" │ Commit message generation: {:.2}s", generate_duration.as_secs_f32());
586 }
587
588 eprintln!(" │ ─────────────────────────────────────────");
589
590 let total_ai_processing = total_file_analysis
591 + multi_step.score_duration.unwrap_or_default()
592 + multi_step.generate_duration.unwrap_or_default()
593 + multi_step.final_message_duration.unwrap_or_default();
594
595 eprintln!(" │ Total AI processing: {:.2}s", total_ai_processing.as_secs_f32());
596 } else if let Some(duration) = self.api_duration {
597 eprintln!(" │ OpenAI request/response: {:.2}s", duration.as_secs_f32());
598 }
599
600 let total_duration = self.start_time.elapsed();
601 eprintln!(" │ Total execution time: {:.2}s", total_duration.as_secs_f32());
602 eprintln!(" └ Status: {} ✓", "SUCCESS".green());
603
604 if let (Some(ref branch), Some(ref hash), Some(ref message)) =
606 (&self.final_commit_branch, &self.final_commit_hash, &self.commit_message)
607 {
608 eprintln!("\n{} {}", "🎯".bright_yellow(), "FINAL RESULT".bright_white().bold());
609
610 let short_hash = if hash.len() > 7 {
611 &hash[..7]
612 } else {
613 hash
614 };
615 eprintln!(" [{} {}] {}", branch.bright_green(), short_hash.bright_yellow(), message.bright_cyan());
616
617 if let Some((files, insertions, deletions)) = self.files_changed_count {
618 let files_text = if files == 1 {
619 "file"
620 } else {
621 "files"
622 };
623 let insertions_text = if insertions == 1 {
624 "insertion"
625 } else {
626 "insertions"
627 };
628 let deletions_text = if deletions == 1 {
629 "deletion"
630 } else {
631 "deletions"
632 };
633
634 eprintln!(
635 " {} {} changed, {} {}(+), {} {}(-)",
636 files,
637 files_text,
638 insertions.to_string().green(),
639 insertions_text,
640 deletions.to_string().red(),
641 deletions_text
642 );
643 }
644 }
645 }
646
647 fn print_timing_line(&self, key: &str, label: &str, last: bool) {
648 let prefix = if last {
649 "└"
650 } else {
651 "│"
652 };
653
654 if let Some(duration) = self.timings.get(key) {
655 let duration_str = format_duration(*duration);
656 eprintln!(" {prefix} {label:<35} {duration_str:<10} ✓");
657 } else {
658 eprintln!(" {} {:<35} {:<10} ✓", prefix, label, "0.00ms");
659 }
660 }
661}
662
663fn format_duration(duration: Duration) -> String {
664 let micros = duration.as_micros();
665 if micros < 1000 {
666 format!("{micros:.0}µs")
667 } else if micros < 1_000_000 {
668 format!("{:.2}ms", duration.as_secs_f32() * 1000.0)
669 } else {
670 format!("{:.2}s", duration.as_secs_f32())
671 }
672}
673
674pub static mut DEBUG_SESSION: Option<DebugSession> = None;
676
677pub fn init_debug_session(args: &str) {
679 unsafe {
680 DEBUG_SESSION = Some(DebugSession::new(args));
681 }
682}
683
684#[allow(static_mut_refs)]
686pub fn debug_session() -> Option<&'static mut DebugSession> {
687 unsafe { DEBUG_SESSION.as_mut() }
688}
689
690pub fn print_final_output() {
692 if let Some(session) = debug_session() {
693 session.print_debug_output();
694 }
695}
696
697pub fn record_timing(operation: &str, duration: Duration) {
699 if let Some(session) = debug_session() {
700 session.record_timing(operation, duration);
701 }
702}