1use anyhow::{Context, Result};
4use clap::{Parser, Subcommand};
5
6#[derive(Parser)]
8pub struct GitCommand {
9 #[command(subcommand)]
11 pub command: GitSubcommands,
12}
13
14#[derive(Subcommand)]
16pub enum GitSubcommands {
17 Commit(CommitCommand),
19 Branch(BranchCommand),
21}
22
23#[derive(Parser)]
25pub struct CommitCommand {
26 #[command(subcommand)]
28 pub command: CommitSubcommands,
29}
30
31#[derive(Subcommand)]
33pub enum CommitSubcommands {
34 Message(MessageCommand),
36}
37
38#[derive(Parser)]
40pub struct MessageCommand {
41 #[command(subcommand)]
43 pub command: MessageSubcommands,
44}
45
46#[derive(Subcommand)]
48pub enum MessageSubcommands {
49 View(ViewCommand),
51 Amend(AmendCommand),
53}
54
55#[derive(Parser)]
57pub struct ViewCommand {
58 #[arg(value_name = "COMMIT_RANGE")]
60 pub commit_range: Option<String>,
61}
62
63#[derive(Parser)]
65pub struct AmendCommand {
66 #[arg(value_name = "YAML_FILE")]
68 pub yaml_file: String,
69}
70
71#[derive(Parser)]
73pub struct BranchCommand {
74 #[command(subcommand)]
76 pub command: BranchSubcommands,
77}
78
79#[derive(Subcommand)]
81pub enum BranchSubcommands {
82 Info(InfoCommand),
84}
85
86#[derive(Parser)]
88pub struct InfoCommand {
89 #[arg(value_name = "BASE_BRANCH")]
91 pub base_branch: Option<String>,
92}
93
94impl GitCommand {
95 pub fn execute(self) -> Result<()> {
97 match self.command {
98 GitSubcommands::Commit(commit_cmd) => commit_cmd.execute(),
99 GitSubcommands::Branch(branch_cmd) => branch_cmd.execute(),
100 }
101 }
102}
103
104impl CommitCommand {
105 pub fn execute(self) -> Result<()> {
107 match self.command {
108 CommitSubcommands::Message(message_cmd) => message_cmd.execute(),
109 }
110 }
111}
112
113impl MessageCommand {
114 pub fn execute(self) -> Result<()> {
116 match self.command {
117 MessageSubcommands::View(view_cmd) => view_cmd.execute(),
118 MessageSubcommands::Amend(amend_cmd) => amend_cmd.execute(),
119 }
120 }
121}
122
123impl ViewCommand {
124 pub fn execute(self) -> Result<()> {
126 use crate::data::{
127 AiInfo, FieldExplanation, FileStatusInfo, RepositoryView, VersionInfo,
128 WorkingDirectoryInfo,
129 };
130 use crate::git::{GitRepository, RemoteInfo};
131 use crate::utils::ai_scratch;
132
133 let commit_range = self.commit_range.as_deref().unwrap_or("HEAD");
134
135 let repo = GitRepository::open()
137 .context("Failed to open git repository. Make sure you're in a git repository.")?;
138
139 let wd_status = repo.get_working_directory_status()?;
141 let working_directory = WorkingDirectoryInfo {
142 clean: wd_status.clean,
143 untracked_changes: wd_status
144 .untracked_changes
145 .into_iter()
146 .map(|fs| FileStatusInfo {
147 status: fs.status,
148 file: fs.file,
149 })
150 .collect(),
151 };
152
153 let remotes = RemoteInfo::get_all_remotes(repo.repository())?;
155
156 let commits = repo.get_commits_in_range(commit_range)?;
158
159 let versions = Some(VersionInfo {
161 omni_dev: env!("CARGO_PKG_VERSION").to_string(),
162 });
163
164 let ai_scratch_path =
166 ai_scratch::get_ai_scratch_dir().context("Failed to determine AI scratch directory")?;
167 let ai_info = AiInfo {
168 scratch: ai_scratch_path.to_string_lossy().to_string(),
169 };
170
171 let mut repo_view = RepositoryView {
173 versions,
174 explanation: FieldExplanation::default(),
175 working_directory,
176 remotes,
177 ai: ai_info,
178 branch_info: None,
179 pr_template: None,
180 branch_prs: None,
181 commits,
182 };
183
184 repo_view.update_field_presence();
186
187 let yaml_output = crate::data::to_yaml(&repo_view)?;
189 println!("{}", yaml_output);
190
191 Ok(())
192 }
193}
194
195impl AmendCommand {
196 pub fn execute(self) -> Result<()> {
198 use crate::git::AmendmentHandler;
199
200 println!("🔄 Starting commit amendment process...");
201 println!("📄 Loading amendments from: {}", self.yaml_file);
202
203 let handler = AmendmentHandler::new().context("Failed to initialize amendment handler")?;
205
206 handler
207 .apply_amendments(&self.yaml_file)
208 .context("Failed to apply amendments")?;
209
210 Ok(())
211 }
212}
213
214impl BranchCommand {
215 pub fn execute(self) -> Result<()> {
217 match self.command {
218 BranchSubcommands::Info(info_cmd) => info_cmd.execute(),
219 }
220 }
221}
222
223impl InfoCommand {
224 pub fn execute(self) -> Result<()> {
226 use crate::data::{
227 AiInfo, BranchInfo, FieldExplanation, FileStatusInfo, RepositoryView, VersionInfo,
228 WorkingDirectoryInfo,
229 };
230 use crate::git::{GitRepository, RemoteInfo};
231 use crate::utils::ai_scratch;
232
233 let repo = GitRepository::open()
235 .context("Failed to open git repository. Make sure you're in a git repository.")?;
236
237 let current_branch = repo.get_current_branch().context(
239 "Failed to get current branch. Make sure you're not in detached HEAD state.",
240 )?;
241
242 let base_branch = match self.base_branch {
244 Some(branch) => {
245 if !repo.branch_exists(&branch)? {
247 anyhow::bail!("Base branch '{}' does not exist", branch);
248 }
249 branch
250 }
251 None => {
252 if repo.branch_exists("main")? {
254 "main".to_string()
255 } else if repo.branch_exists("master")? {
256 "master".to_string()
257 } else {
258 anyhow::bail!("No default base branch found (main or master)");
259 }
260 }
261 };
262
263 let commit_range = format!("{}..HEAD", base_branch);
265
266 let wd_status = repo.get_working_directory_status()?;
268 let working_directory = WorkingDirectoryInfo {
269 clean: wd_status.clean,
270 untracked_changes: wd_status
271 .untracked_changes
272 .into_iter()
273 .map(|fs| FileStatusInfo {
274 status: fs.status,
275 file: fs.file,
276 })
277 .collect(),
278 };
279
280 let remotes = RemoteInfo::get_all_remotes(repo.repository())?;
282
283 let commits = repo.get_commits_in_range(&commit_range)?;
285
286 let pr_template = Self::read_pr_template().ok();
288
289 let branch_prs = Self::get_branch_prs(¤t_branch)
291 .ok()
292 .filter(|prs| !prs.is_empty());
293
294 let versions = Some(VersionInfo {
296 omni_dev: env!("CARGO_PKG_VERSION").to_string(),
297 });
298
299 let ai_scratch_path =
301 ai_scratch::get_ai_scratch_dir().context("Failed to determine AI scratch directory")?;
302 let ai_info = AiInfo {
303 scratch: ai_scratch_path.to_string_lossy().to_string(),
304 };
305
306 let mut repo_view = RepositoryView {
308 versions,
309 explanation: FieldExplanation::default(),
310 working_directory,
311 remotes,
312 ai: ai_info,
313 branch_info: Some(BranchInfo {
314 branch: current_branch,
315 }),
316 pr_template,
317 branch_prs,
318 commits,
319 };
320
321 repo_view.update_field_presence();
323
324 let yaml_output = crate::data::to_yaml(&repo_view)?;
326 println!("{}", yaml_output);
327
328 Ok(())
329 }
330
331 fn read_pr_template() -> Result<String> {
333 use std::fs;
334 use std::path::Path;
335
336 let template_path = Path::new(".github/pull_request_template.md");
337 if template_path.exists() {
338 fs::read_to_string(template_path)
339 .context("Failed to read .github/pull_request_template.md")
340 } else {
341 anyhow::bail!("PR template file does not exist")
342 }
343 }
344
345 fn get_branch_prs(branch_name: &str) -> Result<Vec<crate::data::PullRequest>> {
347 use serde_json::Value;
348 use std::process::Command;
349
350 let output = Command::new("gh")
352 .args([
353 "pr",
354 "list",
355 "--head",
356 branch_name,
357 "--json",
358 "number,title,state,url,body",
359 "--limit",
360 "50",
361 ])
362 .output()
363 .context("Failed to execute gh command")?;
364
365 if !output.status.success() {
366 anyhow::bail!(
367 "gh command failed: {}",
368 String::from_utf8_lossy(&output.stderr)
369 );
370 }
371
372 let json_str = String::from_utf8_lossy(&output.stdout);
373 let prs_json: Value =
374 serde_json::from_str(&json_str).context("Failed to parse PR JSON from gh")?;
375
376 let mut prs = Vec::new();
377 if let Some(prs_array) = prs_json.as_array() {
378 for pr_json in prs_array {
379 if let (Some(number), Some(title), Some(state), Some(url), Some(body)) = (
380 pr_json.get("number").and_then(|n| n.as_u64()),
381 pr_json.get("title").and_then(|t| t.as_str()),
382 pr_json.get("state").and_then(|s| s.as_str()),
383 pr_json.get("url").and_then(|u| u.as_str()),
384 pr_json.get("body").and_then(|b| b.as_str()),
385 ) {
386 prs.push(crate::data::PullRequest {
387 number,
388 title: title.to_string(),
389 state: state.to_string(),
390 url: url.to_string(),
391 body: body.to_string(),
392 });
393 }
394 }
395 }
396
397 Ok(prs)
398 }
399}