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 (defaults to claude-3-5-sonnet-20241022)
82    #[arg(long, default_value = "claude-3-5-sonnet-20241022")]
83    pub model: 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        use crate::claude::ClaudeClient;
271
272        // Determine if contextual analysis should be used
273        let use_contextual = self.use_context && !self.no_context;
274
275        if use_contextual {
276            println!(
277                "๐Ÿช„ Starting AI-powered commit message improvement with contextual intelligence..."
278            );
279        } else {
280            println!("๐Ÿช„ Starting AI-powered commit message improvement...");
281        }
282
283        // 1. Generate repository view to get all commits
284        let full_repo_view = self.generate_repository_view().await?;
285
286        // 2. Check if batching is needed
287        if full_repo_view.commits.len() > self.batch_size {
288            println!(
289                "๐Ÿ“ฆ Processing {} commits in batches of {} to ensure reliable analysis...",
290                full_repo_view.commits.len(),
291                self.batch_size
292            );
293            return self
294                .execute_with_batching(use_contextual, full_repo_view)
295                .await;
296        }
297
298        // 3. Collect contextual information (Phase 3)
299        let context = if use_contextual {
300            Some(self.collect_context(&full_repo_view).await?)
301        } else {
302            None
303        };
304
305        // 4. Show context summary if available
306        if let Some(ref ctx) = context {
307            self.show_context_summary(ctx)?;
308        }
309
310        // 5. Initialize Claude client
311        let claude_client = ClaudeClient::new(self.model.clone())?;
312
313        // 6. Generate amendments via Claude API with context
314        if use_contextual && context.is_some() {
315            println!("๐Ÿค– Analyzing commits with enhanced contextual intelligence...");
316        } else {
317            println!("๐Ÿค– Analyzing commits with Claude AI...");
318        }
319
320        let amendments = if let Some(ctx) = context {
321            claude_client
322                .generate_contextual_amendments(&full_repo_view, &ctx)
323                .await?
324        } else {
325            claude_client.generate_amendments(&full_repo_view).await?
326        };
327
328        // 6. Handle different output modes
329        if let Some(save_path) = self.save_only {
330            amendments.save_to_file(save_path)?;
331            println!("๐Ÿ’พ Amendments saved to file");
332            return Ok(());
333        }
334
335        // 7. Handle amendments
336        if !amendments.amendments.is_empty() {
337            // Create temporary file for amendments
338            let temp_dir = tempfile::tempdir()?;
339            let amendments_file = temp_dir.path().join("twiddle_amendments.yaml");
340            amendments.save_to_file(&amendments_file)?;
341
342            // Show file path and get user choice
343            if !self.auto_apply && !self.handle_amendments_file(&amendments_file, &amendments)? {
344                println!("โŒ Amendment cancelled by user");
345                return Ok(());
346            }
347
348            // 8. Apply amendments
349            self.apply_amendments(amendments).await?;
350            println!("โœ… Commit messages improved successfully!");
351        } else {
352            println!("โœจ All commit messages are already well-formatted!");
353        }
354
355        Ok(())
356    }
357
358    /// Execute twiddle command with automatic batching for large commit ranges
359    async fn execute_with_batching(
360        &self,
361        use_contextual: bool,
362        full_repo_view: crate::data::RepositoryView,
363    ) -> Result<()> {
364        use crate::claude::ClaudeClient;
365        use crate::data::amendments::AmendmentFile;
366
367        // Initialize Claude client
368        let claude_client = ClaudeClient::new(self.model.clone())?;
369
370        // Split commits into batches
371        let commit_batches: Vec<_> = full_repo_view.commits.chunks(self.batch_size).collect();
372
373        let total_batches = commit_batches.len();
374        let mut all_amendments = AmendmentFile {
375            amendments: Vec::new(),
376        };
377
378        println!("๐Ÿ“Š Processing {} batches...", total_batches);
379
380        for (batch_num, commit_batch) in commit_batches.into_iter().enumerate() {
381            println!(
382                "๐Ÿ”„ Processing batch {}/{} ({} commits)...",
383                batch_num + 1,
384                total_batches,
385                commit_batch.len()
386            );
387
388            // Create a repository view for just this batch
389            let batch_repo_view = crate::data::RepositoryView {
390                versions: full_repo_view.versions.clone(),
391                explanation: full_repo_view.explanation.clone(),
392                working_directory: full_repo_view.working_directory.clone(),
393                remotes: full_repo_view.remotes.clone(),
394                ai: full_repo_view.ai.clone(),
395                branch_info: full_repo_view.branch_info.clone(),
396                pr_template: full_repo_view.pr_template.clone(),
397                branch_prs: full_repo_view.branch_prs.clone(),
398                commits: commit_batch.to_vec(),
399            };
400
401            // Collect context for this batch if needed
402            let batch_context = if use_contextual {
403                Some(self.collect_context(&batch_repo_view).await?)
404            } else {
405                None
406            };
407
408            // Generate amendments for this batch
409            let batch_amendments = if let Some(ctx) = batch_context {
410                claude_client
411                    .generate_contextual_amendments(&batch_repo_view, &ctx)
412                    .await?
413            } else {
414                claude_client.generate_amendments(&batch_repo_view).await?
415            };
416
417            // Merge amendments from this batch
418            all_amendments
419                .amendments
420                .extend(batch_amendments.amendments);
421
422            if batch_num + 1 < total_batches {
423                println!("   โœ… Batch {}/{} completed", batch_num + 1, total_batches);
424                // Small delay between batches to be respectful to the API
425                tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
426            }
427        }
428
429        println!(
430            "โœ… All batches completed! Found {} commits to improve.",
431            all_amendments.amendments.len()
432        );
433
434        // Handle different output modes
435        if let Some(save_path) = &self.save_only {
436            all_amendments.save_to_file(save_path)?;
437            println!("๐Ÿ’พ Amendments saved to file");
438            return Ok(());
439        }
440
441        // Handle amendments
442        if !all_amendments.amendments.is_empty() {
443            // Create temporary file for amendments
444            let temp_dir = tempfile::tempdir()?;
445            let amendments_file = temp_dir.path().join("twiddle_amendments.yaml");
446            all_amendments.save_to_file(&amendments_file)?;
447
448            // Show file path and get user choice
449            if !self.auto_apply
450                && !self.handle_amendments_file(&amendments_file, &all_amendments)?
451            {
452                println!("โŒ Amendment cancelled by user");
453                return Ok(());
454            }
455
456            // Apply all amendments
457            self.apply_amendments(all_amendments).await?;
458            println!("โœ… Commit messages improved successfully!");
459        } else {
460            println!("โœจ All commit messages are already well-formatted!");
461        }
462
463        Ok(())
464    }
465
466    /// Generate repository view (reuse ViewCommand logic)
467    async fn generate_repository_view(&self) -> Result<crate::data::RepositoryView> {
468        use crate::data::{
469            AiInfo, FieldExplanation, FileStatusInfo, RepositoryView, VersionInfo,
470            WorkingDirectoryInfo,
471        };
472        use crate::git::{GitRepository, RemoteInfo};
473        use crate::utils::ai_scratch;
474
475        let commit_range = self.commit_range.as_deref().unwrap_or("HEAD~5..HEAD");
476
477        // Open git repository
478        let repo = GitRepository::open()
479            .context("Failed to open git repository. Make sure you're in a git repository.")?;
480
481        // Get working directory status
482        let wd_status = repo.get_working_directory_status()?;
483        let working_directory = WorkingDirectoryInfo {
484            clean: wd_status.clean,
485            untracked_changes: wd_status
486                .untracked_changes
487                .into_iter()
488                .map(|fs| FileStatusInfo {
489                    status: fs.status,
490                    file: fs.file,
491                })
492                .collect(),
493        };
494
495        // Get remote information
496        let remotes = RemoteInfo::get_all_remotes(repo.repository())?;
497
498        // Parse commit range and get commits
499        let commits = repo.get_commits_in_range(commit_range)?;
500
501        // Create version information
502        let versions = Some(VersionInfo {
503            omni_dev: env!("CARGO_PKG_VERSION").to_string(),
504        });
505
506        // Get AI scratch directory
507        let ai_scratch_path =
508            ai_scratch::get_ai_scratch_dir().context("Failed to determine AI scratch directory")?;
509        let ai_info = AiInfo {
510            scratch: ai_scratch_path.to_string_lossy().to_string(),
511        };
512
513        // Build repository view
514        let mut repo_view = RepositoryView {
515            versions,
516            explanation: FieldExplanation::default(),
517            working_directory,
518            remotes,
519            ai: ai_info,
520            branch_info: None,
521            pr_template: None,
522            branch_prs: None,
523            commits,
524        };
525
526        // Update field presence based on actual data
527        repo_view.update_field_presence();
528
529        Ok(repo_view)
530    }
531
532    /// Handle amendments file - show path and get user choice
533    fn handle_amendments_file(
534        &self,
535        amendments_file: &std::path::Path,
536        amendments: &crate::data::amendments::AmendmentFile,
537    ) -> Result<bool> {
538        use std::io::{self, Write};
539
540        println!(
541            "\n๐Ÿ“ Found {} commits that could be improved.",
542            amendments.amendments.len()
543        );
544        println!("๐Ÿ’พ Amendments saved to: {}", amendments_file.display());
545        println!();
546
547        loop {
548            print!("โ“ [A]pply amendments, [S]how file, or [Q]uit? [A/s/q] ");
549            io::stdout().flush()?;
550
551            let mut input = String::new();
552            io::stdin().read_line(&mut input)?;
553
554            match input.trim().to_lowercase().as_str() {
555                "a" | "apply" | "" => return Ok(true),
556                "s" | "show" => {
557                    self.show_amendments_file(amendments_file)?;
558                    println!();
559                }
560                "q" | "quit" => return Ok(false),
561                _ => {
562                    println!(
563                        "Invalid choice. Please enter 'a' to apply, 's' to show, or 'q' to quit."
564                    );
565                }
566            }
567        }
568    }
569
570    /// Show the contents of the amendments file
571    fn show_amendments_file(&self, amendments_file: &std::path::Path) -> Result<()> {
572        use std::fs;
573
574        println!("\n๐Ÿ“„ Amendments file contents:");
575        println!("โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€");
576
577        let contents =
578            fs::read_to_string(amendments_file).context("Failed to read amendments file")?;
579
580        println!("{}", contents);
581        println!("โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€");
582
583        Ok(())
584    }
585
586    /// Apply amendments using existing AmendmentHandler logic
587    async fn apply_amendments(
588        &self,
589        amendments: crate::data::amendments::AmendmentFile,
590    ) -> Result<()> {
591        use crate::git::AmendmentHandler;
592
593        // Create temporary file for amendments
594        let temp_dir = tempfile::tempdir()?;
595        let temp_file = temp_dir.path().join("twiddle_amendments.yaml");
596        amendments.save_to_file(&temp_file)?;
597
598        // Use AmendmentHandler to apply amendments
599        let handler = AmendmentHandler::new().context("Failed to initialize amendment handler")?;
600        handler
601            .apply_amendments(&temp_file.to_string_lossy())
602            .context("Failed to apply amendments")?;
603
604        Ok(())
605    }
606
607    /// Collect contextual information for enhanced commit message generation
608    async fn collect_context(
609        &self,
610        repo_view: &crate::data::RepositoryView,
611    ) -> Result<crate::data::context::CommitContext> {
612        use crate::claude::context::{BranchAnalyzer, ProjectDiscovery, WorkPatternAnalyzer};
613        use crate::data::context::CommitContext;
614        use crate::git::GitRepository;
615
616        let mut context = CommitContext::new();
617
618        // 1. Discover project context
619        let context_dir = self
620            .context_dir
621            .as_ref()
622            .cloned()
623            .unwrap_or_else(|| std::path::PathBuf::from(".omni-dev"));
624
625        // ProjectDiscovery takes repo root and context directory
626        let repo_root = std::path::PathBuf::from(".");
627        let discovery = ProjectDiscovery::new(repo_root, context_dir.clone());
628        debug!(context_dir = ?context_dir, "Using context directory");
629        match discovery.discover() {
630            Ok(project_context) => {
631                debug!("Discovery successful");
632                context.project = project_context;
633            }
634            Err(e) => {
635                debug!(error = %e, "Discovery failed");
636                context.project = Default::default();
637            }
638        }
639
640        // 2. Analyze current branch
641        let repo = GitRepository::open()?;
642        let current_branch = repo
643            .get_current_branch()
644            .unwrap_or_else(|_| "HEAD".to_string());
645        context.branch = BranchAnalyzer::analyze(&current_branch).unwrap_or_default();
646
647        // 3. Analyze commit range patterns
648        if !repo_view.commits.is_empty() {
649            context.range = WorkPatternAnalyzer::analyze_commit_range(&repo_view.commits);
650        }
651
652        // 4. Apply user-provided context overrides
653        if let Some(ref work_ctx) = self.work_context {
654            context.user_provided = Some(work_ctx.clone());
655        }
656
657        if let Some(ref branch_ctx) = self.branch_context {
658            context.branch.description = branch_ctx.clone();
659        }
660
661        Ok(context)
662    }
663
664    /// Show context summary to user
665    fn show_context_summary(&self, context: &crate::data::context::CommitContext) -> Result<()> {
666        use crate::data::context::{VerbosityLevel, WorkPattern};
667
668        println!("๐Ÿ” Context Analysis:");
669
670        // Project context
671        if !context.project.valid_scopes.is_empty() {
672            let scope_names: Vec<&str> = context
673                .project
674                .valid_scopes
675                .iter()
676                .map(|s| s.name.as_str())
677                .collect();
678            println!("   ๐Ÿ“ Valid scopes: {}", scope_names.join(", "));
679        }
680
681        // Branch context
682        if context.branch.is_feature_branch {
683            println!(
684                "   ๐ŸŒฟ Branch: {} ({})",
685                context.branch.description, context.branch.work_type
686            );
687            if let Some(ref ticket) = context.branch.ticket_id {
688                println!("   ๐ŸŽซ Ticket: {}", ticket);
689            }
690        }
691
692        // Work pattern
693        match context.range.work_pattern {
694            WorkPattern::Sequential => println!("   ๐Ÿ”„ Pattern: Sequential development"),
695            WorkPattern::Refactoring => println!("   ๐Ÿงน Pattern: Refactoring work"),
696            WorkPattern::BugHunt => println!("   ๐Ÿ› Pattern: Bug investigation"),
697            WorkPattern::Documentation => println!("   ๐Ÿ“– Pattern: Documentation updates"),
698            WorkPattern::Configuration => println!("   โš™๏ธ  Pattern: Configuration changes"),
699            WorkPattern::Unknown => {}
700        }
701
702        // Verbosity level
703        match context.suggested_verbosity() {
704            VerbosityLevel::Comprehensive => {
705                println!("   ๐Ÿ“ Detail level: Comprehensive (significant changes detected)")
706            }
707            VerbosityLevel::Detailed => println!("   ๐Ÿ“ Detail level: Detailed"),
708            VerbosityLevel::Concise => println!("   ๐Ÿ“ Detail level: Concise"),
709        }
710
711        // User context
712        if let Some(ref user_ctx) = context.user_provided {
713            println!("   ๐Ÿ‘ค User context: {}", user_ctx);
714        }
715
716        println!();
717        Ok(())
718    }
719}
720
721impl BranchCommand {
722    /// Execute branch command
723    pub fn execute(self) -> Result<()> {
724        match self.command {
725            BranchSubcommands::Info(info_cmd) => info_cmd.execute(),
726        }
727    }
728}
729
730impl InfoCommand {
731    /// Execute info command
732    pub fn execute(self) -> Result<()> {
733        use crate::data::{
734            AiInfo, BranchInfo, FieldExplanation, FileStatusInfo, RepositoryView, VersionInfo,
735            WorkingDirectoryInfo,
736        };
737        use crate::git::{GitRepository, RemoteInfo};
738        use crate::utils::ai_scratch;
739
740        // Open git repository
741        let repo = GitRepository::open()
742            .context("Failed to open git repository. Make sure you're in a git repository.")?;
743
744        // Get current branch name
745        let current_branch = repo.get_current_branch().context(
746            "Failed to get current branch. Make sure you're not in detached HEAD state.",
747        )?;
748
749        // Determine base branch
750        let base_branch = match self.base_branch {
751            Some(branch) => {
752                // Validate that the specified base branch exists
753                if !repo.branch_exists(&branch)? {
754                    anyhow::bail!("Base branch '{}' does not exist", branch);
755                }
756                branch
757            }
758            None => {
759                // Default to main or master
760                if repo.branch_exists("main")? {
761                    "main".to_string()
762                } else if repo.branch_exists("master")? {
763                    "master".to_string()
764                } else {
765                    anyhow::bail!("No default base branch found (main or master)");
766                }
767            }
768        };
769
770        // Calculate commit range: [base_branch]..HEAD
771        let commit_range = format!("{}..HEAD", base_branch);
772
773        // Get working directory status
774        let wd_status = repo.get_working_directory_status()?;
775        let working_directory = WorkingDirectoryInfo {
776            clean: wd_status.clean,
777            untracked_changes: wd_status
778                .untracked_changes
779                .into_iter()
780                .map(|fs| FileStatusInfo {
781                    status: fs.status,
782                    file: fs.file,
783                })
784                .collect(),
785        };
786
787        // Get remote information
788        let remotes = RemoteInfo::get_all_remotes(repo.repository())?;
789
790        // Parse commit range and get commits
791        let commits = repo.get_commits_in_range(&commit_range)?;
792
793        // Check for PR template
794        let pr_template = Self::read_pr_template().ok();
795
796        // Get PRs for current branch
797        let branch_prs = Self::get_branch_prs(&current_branch)
798            .ok()
799            .filter(|prs| !prs.is_empty());
800
801        // Create version information
802        let versions = Some(VersionInfo {
803            omni_dev: env!("CARGO_PKG_VERSION").to_string(),
804        });
805
806        // Get AI scratch directory
807        let ai_scratch_path =
808            ai_scratch::get_ai_scratch_dir().context("Failed to determine AI scratch directory")?;
809        let ai_info = AiInfo {
810            scratch: ai_scratch_path.to_string_lossy().to_string(),
811        };
812
813        // Build repository view with branch info
814        let mut repo_view = RepositoryView {
815            versions,
816            explanation: FieldExplanation::default(),
817            working_directory,
818            remotes,
819            ai: ai_info,
820            branch_info: Some(BranchInfo {
821                branch: current_branch,
822            }),
823            pr_template,
824            branch_prs,
825            commits,
826        };
827
828        // Update field presence based on actual data
829        repo_view.update_field_presence();
830
831        // Output as YAML
832        let yaml_output = crate::data::to_yaml(&repo_view)?;
833        println!("{}", yaml_output);
834
835        Ok(())
836    }
837
838    /// Read PR template file if it exists
839    fn read_pr_template() -> Result<String> {
840        use std::fs;
841        use std::path::Path;
842
843        let template_path = Path::new(".github/pull_request_template.md");
844        if template_path.exists() {
845            fs::read_to_string(template_path)
846                .context("Failed to read .github/pull_request_template.md")
847        } else {
848            anyhow::bail!("PR template file does not exist")
849        }
850    }
851
852    /// Get pull requests for the current branch using gh CLI
853    fn get_branch_prs(branch_name: &str) -> Result<Vec<crate::data::PullRequest>> {
854        use serde_json::Value;
855        use std::process::Command;
856
857        // Use gh CLI to get PRs for the branch
858        let output = Command::new("gh")
859            .args([
860                "pr",
861                "list",
862                "--head",
863                branch_name,
864                "--json",
865                "number,title,state,url,body",
866                "--limit",
867                "50",
868            ])
869            .output()
870            .context("Failed to execute gh command")?;
871
872        if !output.status.success() {
873            anyhow::bail!(
874                "gh command failed: {}",
875                String::from_utf8_lossy(&output.stderr)
876            );
877        }
878
879        let json_str = String::from_utf8_lossy(&output.stdout);
880        let prs_json: Value =
881            serde_json::from_str(&json_str).context("Failed to parse PR JSON from gh")?;
882
883        let mut prs = Vec::new();
884        if let Some(prs_array) = prs_json.as_array() {
885            for pr_json in prs_array {
886                if let (Some(number), Some(title), Some(state), Some(url), Some(body)) = (
887                    pr_json.get("number").and_then(|n| n.as_u64()),
888                    pr_json.get("title").and_then(|t| t.as_str()),
889                    pr_json.get("state").and_then(|s| s.as_str()),
890                    pr_json.get("url").and_then(|u| u.as_str()),
891                    pr_json.get("body").and_then(|b| b.as_str()),
892                ) {
893                    prs.push(crate::data::PullRequest {
894                        number,
895                        title: title.to_string(),
896                        state: state.to_string(),
897                        url: url.to_string(),
898                        body: body.to_string(),
899                    });
900                }
901            }
902        }
903
904        Ok(prs)
905    }
906}