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