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::{FieldExplanation, FileStatusInfo, RepositoryView, WorkingDirectoryInfo};
127 use crate::git::{GitRepository, RemoteInfo};
128
129 let commit_range = self.commit_range.as_deref().unwrap_or("HEAD");
130
131 let repo = GitRepository::open()
133 .context("Failed to open git repository. Make sure you're in a git repository.")?;
134
135 let wd_status = repo.get_working_directory_status()?;
137 let working_directory = WorkingDirectoryInfo {
138 clean: wd_status.clean,
139 untracked_changes: wd_status
140 .untracked_changes
141 .into_iter()
142 .map(|fs| FileStatusInfo {
143 status: fs.status,
144 file: fs.file,
145 })
146 .collect(),
147 };
148
149 let remotes = RemoteInfo::get_all_remotes(repo.repository())?;
151
152 let commits = repo.get_commits_in_range(commit_range)?;
154
155 let mut repo_view = RepositoryView {
157 explanation: FieldExplanation::default(),
158 working_directory,
159 remotes,
160 commits,
161 branch_info: None,
162 pr_template: None,
163 branch_prs: None,
164 };
165
166 repo_view.update_field_presence();
168
169 let yaml_output = crate::data::to_yaml(&repo_view)?;
171 println!("{}", yaml_output);
172
173 Ok(())
174 }
175}
176
177impl AmendCommand {
178 pub fn execute(self) -> Result<()> {
180 use crate::git::AmendmentHandler;
181
182 println!("🔄 Starting commit amendment process...");
183 println!("📄 Loading amendments from: {}", self.yaml_file);
184
185 let handler = AmendmentHandler::new().context("Failed to initialize amendment handler")?;
187
188 handler
189 .apply_amendments(&self.yaml_file)
190 .context("Failed to apply amendments")?;
191
192 Ok(())
193 }
194}
195
196impl BranchCommand {
197 pub fn execute(self) -> Result<()> {
199 match self.command {
200 BranchSubcommands::Info(info_cmd) => info_cmd.execute(),
201 }
202 }
203}
204
205impl InfoCommand {
206 pub fn execute(self) -> Result<()> {
208 use crate::data::{
209 BranchInfo, FieldExplanation, FileStatusInfo, RepositoryView, WorkingDirectoryInfo,
210 };
211 use crate::git::{GitRepository, RemoteInfo};
212
213 let repo = GitRepository::open()
215 .context("Failed to open git repository. Make sure you're in a git repository.")?;
216
217 let current_branch = repo.get_current_branch().context(
219 "Failed to get current branch. Make sure you're not in detached HEAD state.",
220 )?;
221
222 let base_branch = match self.base_branch {
224 Some(branch) => {
225 if !repo.branch_exists(&branch)? {
227 anyhow::bail!("Base branch '{}' does not exist", branch);
228 }
229 branch
230 }
231 None => {
232 if repo.branch_exists("main")? {
234 "main".to_string()
235 } else if repo.branch_exists("master")? {
236 "master".to_string()
237 } else {
238 anyhow::bail!("No default base branch found (main or master)");
239 }
240 }
241 };
242
243 let commit_range = format!("{}..HEAD", base_branch);
245
246 let wd_status = repo.get_working_directory_status()?;
248 let working_directory = WorkingDirectoryInfo {
249 clean: wd_status.clean,
250 untracked_changes: wd_status
251 .untracked_changes
252 .into_iter()
253 .map(|fs| FileStatusInfo {
254 status: fs.status,
255 file: fs.file,
256 })
257 .collect(),
258 };
259
260 let remotes = RemoteInfo::get_all_remotes(repo.repository())?;
262
263 let commits = repo.get_commits_in_range(&commit_range)?;
265
266 let pr_template = Self::read_pr_template().ok();
268
269 let branch_prs = Self::get_branch_prs(¤t_branch)
271 .ok()
272 .filter(|prs| !prs.is_empty());
273
274 let mut repo_view = RepositoryView {
276 explanation: FieldExplanation::default(),
277 working_directory,
278 remotes,
279 commits,
280 branch_info: Some(BranchInfo {
281 branch: current_branch,
282 }),
283 pr_template,
284 branch_prs,
285 };
286
287 repo_view.update_field_presence();
289
290 let yaml_output = crate::data::to_yaml(&repo_view)?;
292 println!("{}", yaml_output);
293
294 Ok(())
295 }
296
297 fn read_pr_template() -> Result<String> {
299 use std::fs;
300 use std::path::Path;
301
302 let template_path = Path::new(".github/pull_request_template.md");
303 if template_path.exists() {
304 fs::read_to_string(template_path)
305 .context("Failed to read .github/pull_request_template.md")
306 } else {
307 anyhow::bail!("PR template file does not exist")
308 }
309 }
310
311 fn get_branch_prs(branch_name: &str) -> Result<Vec<crate::data::PullRequest>> {
313 use serde_json::Value;
314 use std::process::Command;
315
316 let output = Command::new("gh")
318 .args([
319 "pr",
320 "list",
321 "--head",
322 branch_name,
323 "--json",
324 "number,title,state,url,body",
325 "--limit",
326 "50",
327 ])
328 .output()
329 .context("Failed to execute gh command")?;
330
331 if !output.status.success() {
332 anyhow::bail!(
333 "gh command failed: {}",
334 String::from_utf8_lossy(&output.stderr)
335 );
336 }
337
338 let json_str = String::from_utf8_lossy(&output.stdout);
339 let prs_json: Value =
340 serde_json::from_str(&json_str).context("Failed to parse PR JSON from gh")?;
341
342 let mut prs = Vec::new();
343 if let Some(prs_array) = prs_json.as_array() {
344 for pr_json in prs_array {
345 if let (Some(number), Some(title), Some(state), Some(url), Some(body)) = (
346 pr_json.get("number").and_then(|n| n.as_u64()),
347 pr_json.get("title").and_then(|t| t.as_str()),
348 pr_json.get("state").and_then(|s| s.as_str()),
349 pr_json.get("url").and_then(|u| u.as_str()),
350 pr_json.get("body").and_then(|b| b.as_str()),
351 ) {
352 prs.push(crate::data::PullRequest {
353 number,
354 title: title.to_string(),
355 state: state.to_string(),
356 url: url.to_string(),
357 body: body.to_string(),
358 });
359 }
360 }
361 }
362
363 Ok(prs)
364 }
365}