1use anyhow::{Context, Result};
4use clap::Parser;
5
6#[derive(Parser)]
8pub struct InfoCommand {
9 #[arg(value_name = "BASE_BRANCH")]
11 pub base_branch: Option<String>,
12}
13
14impl InfoCommand {
15 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 let repo = GitRepository::open()
26 .context("Failed to open git repository. Make sure you're in a git repository.")?;
27
28 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 let base_branch = match self.base_branch {
35 Some(branch) => {
36 if !repo.branch_exists(&branch)? {
38 anyhow::bail!("Base branch '{}' does not exist", branch);
39 }
40 branch
41 }
42 None => {
43 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 let commit_range = format!("{}..HEAD", base_branch);
56
57 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 let remotes = RemoteInfo::get_all_remotes(repo.repository())?;
73
74 let commits = repo.get_commits_in_range(&commit_range)?;
76
77 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 let branch_prs = Self::get_branch_prs(¤t_branch)
86 .ok()
87 .filter(|prs| !prs.is_empty());
88
89 let versions = Some(VersionInfo {
91 omni_dev: env!("CARGO_PKG_VERSION").to_string(),
92 });
93
94 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 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 repo_view.update_field_presence();
119
120 let yaml_output = crate::data::to_yaml(&repo_view)?;
122 println!("{}", yaml_output);
123
124 Ok(())
125 }
126
127 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 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 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}