omni_dev/cli/
git.rs

1//! Git-related CLI commands
2
3use anyhow::{Context, Result};
4use clap::{Parser, Subcommand};
5use tracing::debug;
6
7/// Git operations
8#[derive(Parser)]
9pub struct GitCommand {
10    /// Git subcommand to execute
11    #[command(subcommand)]
12    pub command: GitSubcommands,
13}
14
15/// Git subcommands
16#[derive(Subcommand)]
17pub enum GitSubcommands {
18    /// Commit-related operations
19    Commit(CommitCommand),
20    /// Branch-related operations
21    Branch(BranchCommand),
22}
23
24/// Commit operations
25#[derive(Parser)]
26pub struct CommitCommand {
27    /// Commit subcommand to execute
28    #[command(subcommand)]
29    pub command: CommitSubcommands,
30}
31
32/// Commit subcommands
33#[derive(Subcommand)]
34pub enum CommitSubcommands {
35    /// Commit message operations
36    Message(MessageCommand),
37}
38
39/// Message operations
40#[derive(Parser)]
41pub struct MessageCommand {
42    /// Message subcommand to execute
43    #[command(subcommand)]
44    pub command: MessageSubcommands,
45}
46
47/// Message subcommands
48#[derive(Subcommand)]
49pub enum MessageSubcommands {
50    /// Analyze commits and output repository information in YAML format
51    View(ViewCommand),
52    /// Amend commit messages based on a YAML configuration file
53    Amend(AmendCommand),
54    /// AI-powered commit message improvement using Claude
55    Twiddle(TwiddleCommand),
56}
57
58/// View command options
59#[derive(Parser)]
60pub struct ViewCommand {
61    /// Commit range to analyze (e.g., HEAD~3..HEAD, abc123..def456)
62    #[arg(value_name = "COMMIT_RANGE")]
63    pub commit_range: Option<String>,
64}
65
66/// Amend command options  
67#[derive(Parser)]
68pub struct AmendCommand {
69    /// YAML file containing commit amendments
70    #[arg(value_name = "YAML_FILE")]
71    pub yaml_file: String,
72}
73
74/// Twiddle command options
75#[derive(Parser)]
76pub struct TwiddleCommand {
77    /// Commit range to analyze and improve (e.g., HEAD~3..HEAD, abc123..def456)
78    #[arg(value_name = "COMMIT_RANGE")]
79    pub commit_range: Option<String>,
80
81    /// Claude API model to use (if not specified, uses settings or default)
82    #[arg(long)]
83    pub model: Option<String>,
84
85    /// Skip confirmation prompt and apply amendments automatically
86    #[arg(long)]
87    pub auto_apply: bool,
88
89    /// Save generated amendments to file without applying
90    #[arg(long, value_name = "FILE")]
91    pub save_only: Option<String>,
92
93    /// Use additional project context for better suggestions (Phase 3)
94    #[arg(long, default_value = "true")]
95    pub use_context: bool,
96
97    /// Path to custom context directory (defaults to .omni-dev/)
98    #[arg(long)]
99    pub context_dir: Option<std::path::PathBuf>,
100
101    /// Specify work context (e.g., "feature: user authentication")
102    #[arg(long)]
103    pub work_context: Option<String>,
104
105    /// Override detected branch context
106    #[arg(long)]
107    pub branch_context: Option<String>,
108
109    /// Disable contextual analysis (use basic prompting only)
110    #[arg(long)]
111    pub no_context: bool,
112
113    /// Maximum number of commits to process in a single batch (default: 4)
114    #[arg(long, default_value = "4")]
115    pub batch_size: usize,
116}
117
118/// Branch operations
119#[derive(Parser)]
120pub struct BranchCommand {
121    /// Branch subcommand to execute
122    #[command(subcommand)]
123    pub command: BranchSubcommands,
124}
125
126/// Branch subcommands
127#[derive(Subcommand)]
128pub enum BranchSubcommands {
129    /// Analyze branch commits and output repository information in YAML format
130    Info(InfoCommand),
131}
132
133/// Info command options
134#[derive(Parser)]
135pub struct InfoCommand {
136    /// Base branch to compare against (defaults to main/master)
137    #[arg(value_name = "BASE_BRANCH")]
138    pub base_branch: Option<String>,
139}
140
141impl GitCommand {
142    /// Execute git command
143    pub fn execute(self) -> Result<()> {
144        match self.command {
145            GitSubcommands::Commit(commit_cmd) => commit_cmd.execute(),
146            GitSubcommands::Branch(branch_cmd) => branch_cmd.execute(),
147        }
148    }
149}
150
151impl CommitCommand {
152    /// Execute commit command
153    pub fn execute(self) -> Result<()> {
154        match self.command {
155            CommitSubcommands::Message(message_cmd) => message_cmd.execute(),
156        }
157    }
158}
159
160impl MessageCommand {
161    /// Execute message command
162    pub fn execute(self) -> Result<()> {
163        match self.command {
164            MessageSubcommands::View(view_cmd) => view_cmd.execute(),
165            MessageSubcommands::Amend(amend_cmd) => amend_cmd.execute(),
166            MessageSubcommands::Twiddle(twiddle_cmd) => {
167                // Use tokio runtime for async execution
168                let rt =
169                    tokio::runtime::Runtime::new().context("Failed to create tokio runtime")?;
170                rt.block_on(twiddle_cmd.execute())
171            }
172        }
173    }
174}
175
176impl ViewCommand {
177    /// Execute view command
178    pub fn execute(self) -> Result<()> {
179        use crate::data::{
180            AiInfo, FieldExplanation, FileStatusInfo, RepositoryView, VersionInfo,
181            WorkingDirectoryInfo,
182        };
183        use crate::git::{GitRepository, RemoteInfo};
184        use crate::utils::ai_scratch;
185
186        let commit_range = self.commit_range.as_deref().unwrap_or("HEAD");
187
188        // Open git repository
189        let repo = GitRepository::open()
190            .context("Failed to open git repository. Make sure you're in a git repository.")?;
191
192        // Get working directory status
193        let wd_status = repo.get_working_directory_status()?;
194        let working_directory = WorkingDirectoryInfo {
195            clean: wd_status.clean,
196            untracked_changes: wd_status
197                .untracked_changes
198                .into_iter()
199                .map(|fs| FileStatusInfo {
200                    status: fs.status,
201                    file: fs.file,
202                })
203                .collect(),
204        };
205
206        // Get remote information
207        let remotes = RemoteInfo::get_all_remotes(repo.repository())?;
208
209        // Parse commit range and get commits
210        let commits = repo.get_commits_in_range(commit_range)?;
211
212        // Create version information
213        let versions = Some(VersionInfo {
214            omni_dev: env!("CARGO_PKG_VERSION").to_string(),
215        });
216
217        // Get AI scratch directory
218        let ai_scratch_path =
219            ai_scratch::get_ai_scratch_dir().context("Failed to determine AI scratch directory")?;
220        let ai_info = AiInfo {
221            scratch: ai_scratch_path.to_string_lossy().to_string(),
222        };
223
224        // Build repository view
225        let mut repo_view = RepositoryView {
226            versions,
227            explanation: FieldExplanation::default(),
228            working_directory,
229            remotes,
230            ai: ai_info,
231            branch_info: None,
232            pr_template: None,
233            branch_prs: None,
234            commits,
235        };
236
237        // Update field presence based on actual data
238        repo_view.update_field_presence();
239
240        // Output as YAML
241        let yaml_output = crate::data::to_yaml(&repo_view)?;
242        println!("{}", yaml_output);
243
244        Ok(())
245    }
246}
247
248impl AmendCommand {
249    /// Execute amend command
250    pub fn execute(self) -> Result<()> {
251        use crate::git::AmendmentHandler;
252
253        println!("๐Ÿ”„ Starting commit amendment process...");
254        println!("๐Ÿ“„ Loading amendments from: {}", self.yaml_file);
255
256        // Create amendment handler and apply amendments
257        let handler = AmendmentHandler::new().context("Failed to initialize amendment handler")?;
258
259        handler
260            .apply_amendments(&self.yaml_file)
261            .context("Failed to apply amendments")?;
262
263        Ok(())
264    }
265}
266
267impl TwiddleCommand {
268    /// Execute twiddle command with contextual intelligence
269    pub async fn execute(self) -> Result<()> {
270        // Determine if contextual analysis should be used
271        let use_contextual = self.use_context && !self.no_context;
272
273        if use_contextual {
274            println!(
275                "๐Ÿช„ Starting AI-powered commit message improvement with contextual intelligence..."
276            );
277        } else {
278            println!("๐Ÿช„ Starting AI-powered commit message improvement...");
279        }
280
281        // 1. Generate repository view to get all commits
282        let full_repo_view = self.generate_repository_view().await?;
283
284        // 2. Check if batching is needed
285        if full_repo_view.commits.len() > self.batch_size {
286            println!(
287                "๐Ÿ“ฆ Processing {} commits in batches of {} to ensure reliable analysis...",
288                full_repo_view.commits.len(),
289                self.batch_size
290            );
291            return self
292                .execute_with_batching(use_contextual, full_repo_view)
293                .await;
294        }
295
296        // 3. Collect contextual information (Phase 3)
297        let context = if use_contextual {
298            Some(self.collect_context(&full_repo_view).await?)
299        } else {
300            None
301        };
302
303        // 4. Show context summary if available
304        if let Some(ref ctx) = context {
305            self.show_context_summary(ctx)?;
306        }
307
308        // 5. Initialize Claude client
309        let claude_client = crate::claude::create_default_claude_client(self.model.clone())?;
310
311        // 6. Generate amendments via Claude API with context
312        if use_contextual && context.is_some() {
313            println!("๐Ÿค– Analyzing commits with enhanced contextual intelligence...");
314        } else {
315            println!("๐Ÿค– Analyzing commits with Claude AI...");
316        }
317
318        let amendments = if let Some(ctx) = context {
319            claude_client
320                .generate_contextual_amendments(&full_repo_view, &ctx)
321                .await?
322        } else {
323            claude_client.generate_amendments(&full_repo_view).await?
324        };
325
326        // 6. Handle different output modes
327        if let Some(save_path) = self.save_only {
328            amendments.save_to_file(save_path)?;
329            println!("๐Ÿ’พ Amendments saved to file");
330            return Ok(());
331        }
332
333        // 7. Handle amendments
334        if !amendments.amendments.is_empty() {
335            // Create temporary file for amendments
336            let temp_dir = tempfile::tempdir()?;
337            let amendments_file = temp_dir.path().join("twiddle_amendments.yaml");
338            amendments.save_to_file(&amendments_file)?;
339
340            // Show file path and get user choice
341            if !self.auto_apply && !self.handle_amendments_file(&amendments_file, &amendments)? {
342                println!("โŒ Amendment cancelled by user");
343                return Ok(());
344            }
345
346            // 8. Apply amendments
347            self.apply_amendments(amendments).await?;
348            println!("โœ… Commit messages improved successfully!");
349        } else {
350            println!("โœจ All commit messages are already well-formatted!");
351        }
352
353        Ok(())
354    }
355
356    /// Execute twiddle command with automatic batching for large commit ranges
357    async fn execute_with_batching(
358        &self,
359        use_contextual: bool,
360        full_repo_view: crate::data::RepositoryView,
361    ) -> Result<()> {
362        use crate::data::amendments::AmendmentFile;
363
364        // Initialize Claude client
365        let claude_client = crate::claude::create_default_claude_client(self.model.clone())?;
366
367        // Split commits into batches
368        let commit_batches: Vec<_> = full_repo_view.commits.chunks(self.batch_size).collect();
369
370        let total_batches = commit_batches.len();
371        let mut all_amendments = AmendmentFile {
372            amendments: Vec::new(),
373        };
374
375        println!("๐Ÿ“Š Processing {} batches...", total_batches);
376
377        for (batch_num, commit_batch) in commit_batches.into_iter().enumerate() {
378            println!(
379                "๐Ÿ”„ Processing batch {}/{} ({} commits)...",
380                batch_num + 1,
381                total_batches,
382                commit_batch.len()
383            );
384
385            // Create a repository view for just this batch
386            let batch_repo_view = crate::data::RepositoryView {
387                versions: full_repo_view.versions.clone(),
388                explanation: full_repo_view.explanation.clone(),
389                working_directory: full_repo_view.working_directory.clone(),
390                remotes: full_repo_view.remotes.clone(),
391                ai: full_repo_view.ai.clone(),
392                branch_info: full_repo_view.branch_info.clone(),
393                pr_template: full_repo_view.pr_template.clone(),
394                branch_prs: full_repo_view.branch_prs.clone(),
395                commits: commit_batch.to_vec(),
396            };
397
398            // Collect context for this batch if needed
399            let batch_context = if use_contextual {
400                Some(self.collect_context(&batch_repo_view).await?)
401            } else {
402                None
403            };
404
405            // Generate amendments for this batch
406            let batch_amendments = if let Some(ctx) = batch_context {
407                claude_client
408                    .generate_contextual_amendments(&batch_repo_view, &ctx)
409                    .await?
410            } else {
411                claude_client.generate_amendments(&batch_repo_view).await?
412            };
413
414            // Merge amendments from this batch
415            all_amendments
416                .amendments
417                .extend(batch_amendments.amendments);
418
419            if batch_num + 1 < total_batches {
420                println!("   โœ… Batch {}/{} completed", batch_num + 1, total_batches);
421                // Small delay between batches to be respectful to the API
422                tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
423            }
424        }
425
426        println!(
427            "โœ… All batches completed! Found {} commits to improve.",
428            all_amendments.amendments.len()
429        );
430
431        // Handle different output modes
432        if let Some(save_path) = &self.save_only {
433            all_amendments.save_to_file(save_path)?;
434            println!("๐Ÿ’พ Amendments saved to file");
435            return Ok(());
436        }
437
438        // Handle amendments
439        if !all_amendments.amendments.is_empty() {
440            // Create temporary file for amendments
441            let temp_dir = tempfile::tempdir()?;
442            let amendments_file = temp_dir.path().join("twiddle_amendments.yaml");
443            all_amendments.save_to_file(&amendments_file)?;
444
445            // Show file path and get user choice
446            if !self.auto_apply
447                && !self.handle_amendments_file(&amendments_file, &all_amendments)?
448            {
449                println!("โŒ Amendment cancelled by user");
450                return Ok(());
451            }
452
453            // Apply all amendments
454            self.apply_amendments(all_amendments).await?;
455            println!("โœ… Commit messages improved successfully!");
456        } else {
457            println!("โœจ All commit messages are already well-formatted!");
458        }
459
460        Ok(())
461    }
462
463    /// Generate repository view (reuse ViewCommand logic)
464    async fn generate_repository_view(&self) -> Result<crate::data::RepositoryView> {
465        use crate::data::{
466            AiInfo, FieldExplanation, FileStatusInfo, RepositoryView, VersionInfo,
467            WorkingDirectoryInfo,
468        };
469        use crate::git::{GitRepository, RemoteInfo};
470        use crate::utils::ai_scratch;
471
472        let commit_range = self.commit_range.as_deref().unwrap_or("HEAD~5..HEAD");
473
474        // Open git repository
475        let repo = GitRepository::open()
476            .context("Failed to open git repository. Make sure you're in a git repository.")?;
477
478        // Get working directory status
479        let wd_status = repo.get_working_directory_status()?;
480        let working_directory = WorkingDirectoryInfo {
481            clean: wd_status.clean,
482            untracked_changes: wd_status
483                .untracked_changes
484                .into_iter()
485                .map(|fs| FileStatusInfo {
486                    status: fs.status,
487                    file: fs.file,
488                })
489                .collect(),
490        };
491
492        // Get remote information
493        let remotes = RemoteInfo::get_all_remotes(repo.repository())?;
494
495        // Parse commit range and get commits
496        let commits = repo.get_commits_in_range(commit_range)?;
497
498        // Create version information
499        let versions = Some(VersionInfo {
500            omni_dev: env!("CARGO_PKG_VERSION").to_string(),
501        });
502
503        // Get AI scratch directory
504        let ai_scratch_path =
505            ai_scratch::get_ai_scratch_dir().context("Failed to determine AI scratch directory")?;
506        let ai_info = AiInfo {
507            scratch: ai_scratch_path.to_string_lossy().to_string(),
508        };
509
510        // Build repository view
511        let mut repo_view = RepositoryView {
512            versions,
513            explanation: FieldExplanation::default(),
514            working_directory,
515            remotes,
516            ai: ai_info,
517            branch_info: None,
518            pr_template: None,
519            branch_prs: None,
520            commits,
521        };
522
523        // Update field presence based on actual data
524        repo_view.update_field_presence();
525
526        Ok(repo_view)
527    }
528
529    /// Handle amendments file - show path and get user choice
530    fn handle_amendments_file(
531        &self,
532        amendments_file: &std::path::Path,
533        amendments: &crate::data::amendments::AmendmentFile,
534    ) -> Result<bool> {
535        use std::io::{self, Write};
536
537        println!(
538            "\n๐Ÿ“ Found {} commits that could be improved.",
539            amendments.amendments.len()
540        );
541        println!("๐Ÿ’พ Amendments saved to: {}", amendments_file.display());
542        println!();
543
544        loop {
545            print!("โ“ [A]pply amendments, [S]how file, or [Q]uit? [A/s/q] ");
546            io::stdout().flush()?;
547
548            let mut input = String::new();
549            io::stdin().read_line(&mut input)?;
550
551            match input.trim().to_lowercase().as_str() {
552                "a" | "apply" | "" => return Ok(true),
553                "s" | "show" => {
554                    self.show_amendments_file(amendments_file)?;
555                    println!();
556                }
557                "q" | "quit" => return Ok(false),
558                _ => {
559                    println!(
560                        "Invalid choice. Please enter 'a' to apply, 's' to show, or 'q' to quit."
561                    );
562                }
563            }
564        }
565    }
566
567    /// Show the contents of the amendments file
568    fn show_amendments_file(&self, amendments_file: &std::path::Path) -> Result<()> {
569        use std::fs;
570
571        println!("\n๐Ÿ“„ Amendments file contents:");
572        println!("โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€");
573
574        let contents =
575            fs::read_to_string(amendments_file).context("Failed to read amendments file")?;
576
577        println!("{}", contents);
578        println!("โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€");
579
580        Ok(())
581    }
582
583    /// Apply amendments using existing AmendmentHandler logic
584    async fn apply_amendments(
585        &self,
586        amendments: crate::data::amendments::AmendmentFile,
587    ) -> Result<()> {
588        use crate::git::AmendmentHandler;
589
590        // Create temporary file for amendments
591        let temp_dir = tempfile::tempdir()?;
592        let temp_file = temp_dir.path().join("twiddle_amendments.yaml");
593        amendments.save_to_file(&temp_file)?;
594
595        // Use AmendmentHandler to apply amendments
596        let handler = AmendmentHandler::new().context("Failed to initialize amendment handler")?;
597        handler
598            .apply_amendments(&temp_file.to_string_lossy())
599            .context("Failed to apply amendments")?;
600
601        Ok(())
602    }
603
604    /// Collect contextual information for enhanced commit message generation
605    async fn collect_context(
606        &self,
607        repo_view: &crate::data::RepositoryView,
608    ) -> Result<crate::data::context::CommitContext> {
609        use crate::claude::context::{BranchAnalyzer, ProjectDiscovery, WorkPatternAnalyzer};
610        use crate::data::context::CommitContext;
611        use crate::git::GitRepository;
612
613        let mut context = CommitContext::new();
614
615        // 1. Discover project context
616        let context_dir = self
617            .context_dir
618            .as_ref()
619            .cloned()
620            .unwrap_or_else(|| std::path::PathBuf::from(".omni-dev"));
621
622        // ProjectDiscovery takes repo root and context directory
623        let repo_root = std::path::PathBuf::from(".");
624        let discovery = ProjectDiscovery::new(repo_root, context_dir.clone());
625        debug!(context_dir = ?context_dir, "Using context directory");
626        match discovery.discover() {
627            Ok(project_context) => {
628                debug!("Discovery successful");
629                context.project = project_context;
630            }
631            Err(e) => {
632                debug!(error = %e, "Discovery failed");
633                context.project = Default::default();
634            }
635        }
636
637        // 2. Analyze current branch
638        let repo = GitRepository::open()?;
639        let current_branch = repo
640            .get_current_branch()
641            .unwrap_or_else(|_| "HEAD".to_string());
642        context.branch = BranchAnalyzer::analyze(&current_branch).unwrap_or_default();
643
644        // 3. Analyze commit range patterns
645        if !repo_view.commits.is_empty() {
646            context.range = WorkPatternAnalyzer::analyze_commit_range(&repo_view.commits);
647        }
648
649        // 4. Apply user-provided context overrides
650        if let Some(ref work_ctx) = self.work_context {
651            context.user_provided = Some(work_ctx.clone());
652        }
653
654        if let Some(ref branch_ctx) = self.branch_context {
655            context.branch.description = branch_ctx.clone();
656        }
657
658        Ok(context)
659    }
660
661    /// Show context summary to user
662    fn show_context_summary(&self, context: &crate::data::context::CommitContext) -> Result<()> {
663        use crate::data::context::{VerbosityLevel, WorkPattern};
664
665        println!("๐Ÿ” Context Analysis:");
666
667        // Project context
668        if !context.project.valid_scopes.is_empty() {
669            let scope_names: Vec<&str> = context
670                .project
671                .valid_scopes
672                .iter()
673                .map(|s| s.name.as_str())
674                .collect();
675            println!("   ๐Ÿ“ Valid scopes: {}", scope_names.join(", "));
676        }
677
678        // Branch context
679        if context.branch.is_feature_branch {
680            println!(
681                "   ๐ŸŒฟ Branch: {} ({})",
682                context.branch.description, context.branch.work_type
683            );
684            if let Some(ref ticket) = context.branch.ticket_id {
685                println!("   ๐ŸŽซ Ticket: {}", ticket);
686            }
687        }
688
689        // Work pattern
690        match context.range.work_pattern {
691            WorkPattern::Sequential => println!("   ๐Ÿ”„ Pattern: Sequential development"),
692            WorkPattern::Refactoring => println!("   ๐Ÿงน Pattern: Refactoring work"),
693            WorkPattern::BugHunt => println!("   ๐Ÿ› Pattern: Bug investigation"),
694            WorkPattern::Documentation => println!("   ๐Ÿ“– Pattern: Documentation updates"),
695            WorkPattern::Configuration => println!("   โš™๏ธ  Pattern: Configuration changes"),
696            WorkPattern::Unknown => {}
697        }
698
699        // Verbosity level
700        match context.suggested_verbosity() {
701            VerbosityLevel::Comprehensive => {
702                println!("   ๐Ÿ“ Detail level: Comprehensive (significant changes detected)")
703            }
704            VerbosityLevel::Detailed => println!("   ๐Ÿ“ Detail level: Detailed"),
705            VerbosityLevel::Concise => println!("   ๐Ÿ“ Detail level: Concise"),
706        }
707
708        // User context
709        if let Some(ref user_ctx) = context.user_provided {
710            println!("   ๐Ÿ‘ค User context: {}", user_ctx);
711        }
712
713        println!();
714        Ok(())
715    }
716}
717
718impl BranchCommand {
719    /// Execute branch command
720    pub fn execute(self) -> Result<()> {
721        match self.command {
722            BranchSubcommands::Info(info_cmd) => info_cmd.execute(),
723        }
724    }
725}
726
727impl InfoCommand {
728    /// Execute info command
729    pub fn execute(self) -> Result<()> {
730        use crate::data::{
731            AiInfo, BranchInfo, FieldExplanation, FileStatusInfo, RepositoryView, VersionInfo,
732            WorkingDirectoryInfo,
733        };
734        use crate::git::{GitRepository, RemoteInfo};
735        use crate::utils::ai_scratch;
736
737        // Open git repository
738        let repo = GitRepository::open()
739            .context("Failed to open git repository. Make sure you're in a git repository.")?;
740
741        // Get current branch name
742        let current_branch = repo.get_current_branch().context(
743            "Failed to get current branch. Make sure you're not in detached HEAD state.",
744        )?;
745
746        // Determine base branch
747        let base_branch = match self.base_branch {
748            Some(branch) => {
749                // Validate that the specified base branch exists
750                if !repo.branch_exists(&branch)? {
751                    anyhow::bail!("Base branch '{}' does not exist", branch);
752                }
753                branch
754            }
755            None => {
756                // Default to main or master
757                if repo.branch_exists("main")? {
758                    "main".to_string()
759                } else if repo.branch_exists("master")? {
760                    "master".to_string()
761                } else {
762                    anyhow::bail!("No default base branch found (main or master)");
763                }
764            }
765        };
766
767        // Calculate commit range: [base_branch]..HEAD
768        let commit_range = format!("{}..HEAD", base_branch);
769
770        // Get working directory status
771        let wd_status = repo.get_working_directory_status()?;
772        let working_directory = WorkingDirectoryInfo {
773            clean: wd_status.clean,
774            untracked_changes: wd_status
775                .untracked_changes
776                .into_iter()
777                .map(|fs| FileStatusInfo {
778                    status: fs.status,
779                    file: fs.file,
780                })
781                .collect(),
782        };
783
784        // Get remote information
785        let remotes = RemoteInfo::get_all_remotes(repo.repository())?;
786
787        // Parse commit range and get commits
788        let commits = repo.get_commits_in_range(&commit_range)?;
789
790        // Check for PR template
791        let pr_template = Self::read_pr_template().ok();
792
793        // Get PRs for current branch
794        let branch_prs = Self::get_branch_prs(&current_branch)
795            .ok()
796            .filter(|prs| !prs.is_empty());
797
798        // Create version information
799        let versions = Some(VersionInfo {
800            omni_dev: env!("CARGO_PKG_VERSION").to_string(),
801        });
802
803        // Get AI scratch directory
804        let ai_scratch_path =
805            ai_scratch::get_ai_scratch_dir().context("Failed to determine AI scratch directory")?;
806        let ai_info = AiInfo {
807            scratch: ai_scratch_path.to_string_lossy().to_string(),
808        };
809
810        // Build repository view with branch info
811        let mut repo_view = RepositoryView {
812            versions,
813            explanation: FieldExplanation::default(),
814            working_directory,
815            remotes,
816            ai: ai_info,
817            branch_info: Some(BranchInfo {
818                branch: current_branch,
819            }),
820            pr_template,
821            branch_prs,
822            commits,
823        };
824
825        // Update field presence based on actual data
826        repo_view.update_field_presence();
827
828        // Output as YAML
829        let yaml_output = crate::data::to_yaml(&repo_view)?;
830        println!("{}", yaml_output);
831
832        Ok(())
833    }
834
835    /// Read PR template file if it exists
836    fn read_pr_template() -> Result<String> {
837        use std::fs;
838        use std::path::Path;
839
840        let template_path = Path::new(".github/pull_request_template.md");
841        if template_path.exists() {
842            fs::read_to_string(template_path)
843                .context("Failed to read .github/pull_request_template.md")
844        } else {
845            anyhow::bail!("PR template file does not exist")
846        }
847    }
848
849    /// Get pull requests for the current branch using gh CLI
850    fn get_branch_prs(branch_name: &str) -> Result<Vec<crate::data::PullRequest>> {
851        use serde_json::Value;
852        use std::process::Command;
853
854        // Use gh CLI to get PRs for the branch
855        let output = Command::new("gh")
856            .args([
857                "pr",
858                "list",
859                "--head",
860                branch_name,
861                "--json",
862                "number,title,state,url,body",
863                "--limit",
864                "50",
865            ])
866            .output()
867            .context("Failed to execute gh command")?;
868
869        if !output.status.success() {
870            anyhow::bail!(
871                "gh command failed: {}",
872                String::from_utf8_lossy(&output.stderr)
873            );
874        }
875
876        let json_str = String::from_utf8_lossy(&output.stdout);
877        let prs_json: Value =
878            serde_json::from_str(&json_str).context("Failed to parse PR JSON from gh")?;
879
880        let mut prs = Vec::new();
881        if let Some(prs_array) = prs_json.as_array() {
882            for pr_json in prs_array {
883                if let (Some(number), Some(title), Some(state), Some(url), Some(body)) = (
884                    pr_json.get("number").and_then(|n| n.as_u64()),
885                    pr_json.get("title").and_then(|t| t.as_str()),
886                    pr_json.get("state").and_then(|s| s.as_str()),
887                    pr_json.get("url").and_then(|u| u.as_str()),
888                    pr_json.get("body").and_then(|b| b.as_str()),
889                ) {
890                    prs.push(crate::data::PullRequest {
891                        number,
892                        title: title.to_string(),
893                        state: state.to_string(),
894                        url: url.to_string(),
895                        body: body.to_string(),
896                    });
897                }
898            }
899        }
900
901        Ok(prs)
902    }
903}