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 crate::utils::check_git_repository()?;
26
27 let repo = GitRepository::open()
29 .context("Failed to open git repository. Make sure you're in a git repository.")?;
30
31 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 let base_branch = match self.base_branch {
38 Some(branch) => {
39 if !repo.branch_exists(&branch)? {
41 anyhow::bail!("Base branch '{branch}' does not exist");
42 }
43 branch
44 }
45 None => {
46 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 let commit_range = format!("{base_branch}..HEAD");
59
60 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 let remotes = RemoteInfo::get_all_remotes(repo.repository())?;
76
77 let commits = repo.get_commits_in_range(&commit_range)?;
79
80 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 let branch_prs = Self::get_branch_prs(¤t_branch)
89 .ok()
90 .filter(|prs| !prs.is_empty());
91
92 let versions = Some(VersionInfo {
94 omni_dev: env!("CARGO_PKG_VERSION").to_string(),
95 });
96
97 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 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 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 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 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}