Skip to main content

omni_dev/cli/git/
info.rs

1//! Info command — analyzes branch commits and outputs repository information.
2
3use anyhow::{Context, Result};
4use clap::Parser;
5
6/// Info command options.
7#[derive(Parser)]
8pub struct InfoCommand {
9    /// Base branch to compare against (defaults to main/master).
10    #[arg(value_name = "BASE_BRANCH")]
11    pub base_branch: Option<String>,
12}
13
14impl InfoCommand {
15    /// Executes the info command.
16    pub fn execute(self) -> Result<()> {
17        use crate::data::{
18            AiInfo, BranchInfo, FieldExplanation, FileStatusInfo, RepositoryView, VersionInfo,
19            WorkingDirectoryInfo,
20        };
21        use crate::git::{GitRepository, RemoteInfo};
22        use crate::utils::ai_scratch;
23
24        // Open git repository
25        let repo = GitRepository::open()
26            .context("Failed to open git repository. Make sure you're in a git repository.")?;
27
28        // Get current branch name
29        let current_branch = repo.get_current_branch().context(
30            "Failed to get current branch. Make sure you're not in detached HEAD state.",
31        )?;
32
33        // Determine base branch
34        let base_branch = match self.base_branch {
35            Some(branch) => {
36                // Validate that the specified base branch exists
37                if !repo.branch_exists(&branch)? {
38                    anyhow::bail!("Base branch '{}' does not exist", branch);
39                }
40                branch
41            }
42            None => {
43                // Default to main or master
44                if repo.branch_exists("main")? {
45                    "main".to_string()
46                } else if repo.branch_exists("master")? {
47                    "master".to_string()
48                } else {
49                    anyhow::bail!("No default base branch found (main or master)");
50                }
51            }
52        };
53
54        // Calculate commit range: [base_branch]..HEAD
55        let commit_range = format!("{}..HEAD", base_branch);
56
57        // Get working directory status
58        let wd_status = repo.get_working_directory_status()?;
59        let working_directory = WorkingDirectoryInfo {
60            clean: wd_status.clean,
61            untracked_changes: wd_status
62                .untracked_changes
63                .into_iter()
64                .map(|fs| FileStatusInfo {
65                    status: fs.status,
66                    file: fs.file,
67                })
68                .collect(),
69        };
70
71        // Get remote information
72        let remotes = RemoteInfo::get_all_remotes(repo.repository())?;
73
74        // Parse commit range and get commits
75        let commits = repo.get_commits_in_range(&commit_range)?;
76
77        // Check for PR template
78        let pr_template_result = Self::read_pr_template().ok();
79        let (pr_template, pr_template_location) = match pr_template_result {
80            Some((content, location)) => (Some(content), Some(location)),
81            None => (None, None),
82        };
83
84        // Get PRs for current branch
85        let branch_prs = Self::get_branch_prs(&current_branch)
86            .ok()
87            .filter(|prs| !prs.is_empty());
88
89        // Create version information
90        let versions = Some(VersionInfo {
91            omni_dev: env!("CARGO_PKG_VERSION").to_string(),
92        });
93
94        // Get AI scratch directory
95        let ai_scratch_path =
96            ai_scratch::get_ai_scratch_dir().context("Failed to determine AI scratch directory")?;
97        let ai_info = AiInfo {
98            scratch: ai_scratch_path.to_string_lossy().to_string(),
99        };
100
101        // Build repository view with branch info
102        let mut repo_view = RepositoryView {
103            versions,
104            explanation: FieldExplanation::default(),
105            working_directory,
106            remotes,
107            ai: ai_info,
108            branch_info: Some(BranchInfo {
109                branch: current_branch,
110            }),
111            pr_template,
112            pr_template_location,
113            branch_prs,
114            commits,
115        };
116
117        // Update field presence based on actual data
118        repo_view.update_field_presence();
119
120        // Output as YAML
121        let yaml_output = crate::data::to_yaml(&repo_view)?;
122        println!("{}", yaml_output);
123
124        Ok(())
125    }
126
127    /// Reads the PR template file if it exists, returning both content and location.
128    pub(crate) fn read_pr_template() -> Result<(String, String)> {
129        use std::fs;
130        use std::path::Path;
131
132        let template_path = Path::new(".github/pull_request_template.md");
133        if template_path.exists() {
134            let content = fs::read_to_string(template_path)
135                .context("Failed to read .github/pull_request_template.md")?;
136            Ok((content, template_path.to_string_lossy().to_string()))
137        } else {
138            anyhow::bail!("PR template file does not exist")
139        }
140    }
141
142    /// Returns pull requests for the current branch using gh CLI.
143    pub(crate) fn get_branch_prs(branch_name: &str) -> Result<Vec<crate::data::PullRequest>> {
144        use serde_json::Value;
145        use std::process::Command;
146
147        // Use gh CLI to get PRs for the branch
148        let output = Command::new("gh")
149            .args([
150                "pr",
151                "list",
152                "--head",
153                branch_name,
154                "--json",
155                "number,title,state,url,body,baseRefName",
156                "--limit",
157                "50",
158            ])
159            .output()
160            .context("Failed to execute gh command")?;
161
162        if !output.status.success() {
163            anyhow::bail!(
164                "gh command failed: {}",
165                String::from_utf8_lossy(&output.stderr)
166            );
167        }
168
169        let json_str = String::from_utf8_lossy(&output.stdout);
170        let prs_json: Value =
171            serde_json::from_str(&json_str).context("Failed to parse PR JSON from gh")?;
172
173        let mut prs = Vec::new();
174        if let Some(prs_array) = prs_json.as_array() {
175            for pr_json in prs_array {
176                if let (Some(number), Some(title), Some(state), Some(url), Some(body)) = (
177                    pr_json.get("number").and_then(|n| n.as_u64()),
178                    pr_json.get("title").and_then(|t| t.as_str()),
179                    pr_json.get("state").and_then(|s| s.as_str()),
180                    pr_json.get("url").and_then(|u| u.as_str()),
181                    pr_json.get("body").and_then(|b| b.as_str()),
182                ) {
183                    let base = pr_json
184                        .get("baseRefName")
185                        .and_then(|b| b.as_str())
186                        .unwrap_or("")
187                        .to_string();
188                    prs.push(crate::data::PullRequest {
189                        number,
190                        title: title.to_string(),
191                        state: state.to_string(),
192                        url: url.to_string(),
193                        body: body.to_string(),
194                        base,
195                    });
196                }
197            }
198        }
199
200        Ok(prs)
201    }
202}