1use crate::common::CommonParams;
2use crate::core::llm::get_available_provider_names;
3use crate::debug;
4use crate::features::changelog::{handle_changelog_command, handle_release_notes_command};
5use crate::features::commit;
6use crate::ui;
7use clap::builder::{Styles, styling::AnsiColor};
8use clap::{Parser, Subcommand, crate_version};
9use colored::Colorize;
10
11#[derive(Parser)]
13#[command(
14 author,
15 version = crate_version!(),
16 about = "GitAI: AI-powered Git workflow assistant",
17 disable_version_flag = true,
18 after_help = get_dynamic_help(),
19 styles = get_styles(),
20)]
21pub struct Cli {
22 #[command(subcommand)]
24 pub command: Option<GitAI>,
25
26 #[arg(
28 short = 'l',
29 long = "log",
30 global = true,
31 help = "Log debug messages to a file"
32 )]
33 pub log: bool,
34
35 #[arg(
37 long = "log-file",
38 global = true,
39 help = "Specify a custom log file path"
40 )]
41 pub log_file: Option<String>,
42
43 #[arg(
45 short = 'q',
46 long = "quiet",
47 global = true,
48 help = "Suppress non-essential output"
49 )]
50 pub quiet: bool,
51
52 #[arg(
54 short = 'v',
55 long = "version",
56 global = true,
57 help = "Display the version"
58 )]
59 pub version: bool,
60
61 #[arg(
63 short = 'r',
64 long = "repo",
65 global = true,
66 help = "Repository URL to use instead of local repository"
67 )]
68 pub repository_url: Option<String>,
69}
70
71#[derive(Subcommand)]
73#[command(subcommand_negates_reqs = true)]
74#[command(subcommand_precedence_over_arg = true)]
75pub enum GitAI {
76 #[command(
79 about = "Generate a commit message using AI",
80 long_about = "Generate a commit message using AI based on the current Git context.",
81 after_help = get_dynamic_help()
82 )]
83 Message {
84 #[command(flatten)]
85 common: CommonParams,
86
87 #[arg(short, long, help = "Automatically commit with the generated message")]
89 auto_commit: bool,
90
91 #[arg(long, help = "Disable emojis for this commit")]
93 no_emoji: bool,
94
95 #[arg(short, long, help = "Print the generated message to stdout and exit")]
97 print: bool,
98
99 #[arg(long, help = "Skip verification steps (pre/post commit hooks)")]
101 no_verify: bool,
102 },
103
104 #[command(
106 about = "Review staged changes using AI",
107 long_about = "Generate a comprehensive multi-dimensional code review of staged changes using AI. Analyzes code across 10 dimensions including complexity, security, performance, and more."
108 )]
109 Review {
110 #[command(flatten)]
111 common: CommonParams,
112
113 #[arg(short, long, help = "Print the generated review to stdout and exit")]
115 print: bool,
116
117 #[arg(long, help = "Include unstaged changes in the review")]
119 include_unstaged: bool,
120
121 #[arg(
123 long,
124 help = "Review a specific commit by ID (hash, branch, or reference)"
125 )]
126 commit: Option<String>,
127
128 #[arg(
130 long,
131 help = "Starting branch for comparison (defaults to 'main'). Used with --to for branch comparison reviews"
132 )]
133 from: Option<String>,
134
135 #[arg(
137 long,
138 help = "Target branch for comparison (e.g., 'feature-branch', 'pr-branch'). Used with --from for branch comparison reviews"
139 )]
140 to: Option<String>,
141 },
142
143 #[command(
145 about = "Generate a pull request description using AI",
146 long_about = "Generate a comprehensive pull request description based on commit ranges, branch differences, or single commits. Analyzes the overall changeset as an atomic unit and creates professional PR descriptions with summaries, detailed explanations, and testing notes.\\
147\\
148Usage examples:\\
149• Single commit: --from abc1234 or --to abc1234\\
150• Single commitish: --from HEAD~1 or --to HEAD~2\\
151• Multiple commits: --from HEAD~3 (reviews last 3 commits)\\
152• Commit range: --from abc1234 --to def5678\\
153• Branch comparison: --from main --to feature-branch\\
154• From main to branch: --to feature-branch\\
155\\
156Supported commitish syntax: HEAD~2, HEAD^, @~3, main~1, origin/main^, etc."
157 )]
158 Pr {
159 #[command(flatten)]
160 common: CommonParams,
161
162 #[arg(
164 short,
165 long,
166 help = "Print the generated PR description to stdout and exit"
167 )]
168 print: bool,
169
170 #[arg(
172 long,
173 help = "Starting branch, commit, or commitish for comparison. For single commit analysis, specify just this parameter with a commit hash (e.g., --from abc1234). For reviewing multiple commits, use commitish syntax (e.g., --from HEAD~3 to review last 3 commits)"
174 )]
175 from: Option<String>,
176
177 #[arg(
179 long,
180 help = "Target branch, commit, or commitish for comparison. For single commit analysis, specify just this parameter with a commit hash or commitish (e.g., --to HEAD~2)"
181 )]
182 to: Option<String>,
183 },
184
185 #[command(
187 about = "Generate a changelog",
188 long_about = "Generate a changelog between two specified Git references."
189 )]
190 Changelog {
191 #[command(flatten)]
192 common: CommonParams,
193
194 #[arg(long, required = true)]
196 from: String,
197
198 #[arg(long)]
200 to: Option<String>,
201
202 #[arg(long, help = "Update the changelog file with the new changes")]
204 update: bool,
205
206 #[arg(long, help = "Path to the changelog file (defaults to CHANGELOG.md)")]
208 file: Option<String>,
209
210 #[arg(long, help = "Explicit version name to use in the changelog")]
212 version_name: Option<String>,
213 },
214
215 #[command(
217 about = "Generate release notes",
218 long_about = "Generate comprehensive release notes between two specified Git references."
219 )]
220 ReleaseNotes {
221 #[command(flatten)]
222 common: CommonParams,
223
224 #[arg(long, required = true)]
226 from: String,
227
228 #[arg(long)]
230 to: Option<String>,
231
232 #[arg(long, help = "Explicit version name to use in the release notes")]
234 version_name: Option<String>,
235 },
236}
237
238fn get_styles() -> Styles {
240 Styles::styled()
241 .header(AnsiColor::Magenta.on_default().bold())
242 .usage(AnsiColor::Cyan.on_default().bold())
243 .literal(AnsiColor::Green.on_default().bold())
244 .placeholder(AnsiColor::Yellow.on_default())
245 .valid(AnsiColor::Blue.on_default().bold())
246 .invalid(AnsiColor::Red.on_default().bold())
247 .error(AnsiColor::Red.on_default().bold())
248}
249
250pub fn parse_args() -> Cli {
252 Cli::parse()
253}
254
255fn get_dynamic_help() -> String {
257 let mut providers = get_available_provider_names();
258 providers.sort();
259
260 let providers_list = providers
261 .iter()
262 .map(|p| format!("{}", p.bold()))
263 .collect::<Vec<_>>()
264 .join(" • ");
265
266 format!(
267 "\\
268Available LLM Providers: {providers_list}"
269 )
270}
271
272#[allow(clippy::struct_excessive_bools)]
274pub struct CmsgConfig {
275 pub auto_commit: bool,
276 pub use_emoji: bool,
277 pub print_only: bool,
278 pub verify: bool,
279 pub dry_run: bool,
280}
281
282pub async fn handle_message(
283 common: CommonParams,
284 config: CmsgConfig,
285 repository_url: Option<String>,
286) -> anyhow::Result<()> {
287 debug!(
288 "Handling 'message' command with common: {:?}, auto_commit: {}, use_emoji: {}, print: {}, verify: {}",
289 common, config.auto_commit, config.use_emoji, config.print_only, config.verify
290 );
291
292 ui::print_version(crate_version!());
293 ui::print_newline();
294
295 commit::handle_message_command(
296 common,
297 config.auto_commit,
298 config.print_only,
299 config.verify,
300 config.dry_run,
301 repository_url,
302 )
303 .await
304}
305
306pub async fn handle_review(
308 common: CommonParams,
309 print: bool,
310 repository_url: Option<String>,
311 include_unstaged: bool,
312 commit: Option<String>,
313 from: Option<String>,
314 to: Option<String>,
315) -> anyhow::Result<()> {
316 debug!(
317 "Handling 'review' command with common: {:?}, print: {}, include_unstaged: {}, commit: {:?}, from: {:?}, to: {:?}",
318 common, print, include_unstaged, commit, from, to
319 );
320 ui::print_version(crate_version!());
321 ui::print_newline();
322 commit::review::handle_review_command(
323 common,
324 print,
325 repository_url,
326 include_unstaged,
327 commit,
328 from,
329 to,
330 )
331 .await
332}
333
334pub async fn handle_changelog(
336 common: CommonParams,
337 from: String,
338 to: Option<String>,
339 repository_url: Option<String>,
340 update: bool,
341 file: Option<String>,
342 version_name: Option<String>,
343) -> anyhow::Result<()> {
344 debug!(
345 "Handling 'changelog' command with common: {:?}, from: {}, to: {:?}, update: {}, file: {:?}, version_name: {:?}",
346 common, from, to, update, file, version_name
347 );
348 handle_changelog_command(common, from, to, repository_url, update, file, version_name).await
349}
350
351pub async fn handle_release_notes(
353 common: CommonParams,
354 from: String,
355 to: Option<String>,
356 repository_url: Option<String>,
357 version_name: Option<String>,
358) -> anyhow::Result<()> {
359 debug!(
360 "Handling 'release-notes' command with common: {:?}, from: {}, to: {:?}, version_name: {:?}",
361 common, from, to, version_name
362 );
363 handle_release_notes_command(common, from, to, repository_url, version_name).await
364}
365
366pub async fn handle_command(command: GitAI, repository_url: Option<String>) -> anyhow::Result<()> {
368 match command {
369 GitAI::Message {
370 common,
371 auto_commit,
372 no_emoji,
373 print,
374 no_verify,
375 } => {
376 handle_message(
377 common,
378 CmsgConfig {
379 auto_commit,
380 use_emoji: !no_emoji,
381 print_only: print,
382 verify: !no_verify,
383 dry_run: false,
384 },
385 repository_url,
386 )
387 .await
388 }
389 GitAI::Review {
390 common,
391 print,
392 include_unstaged,
393 commit,
394 from,
395 to,
396 } => {
397 handle_review(
398 common,
399 print,
400 repository_url,
401 include_unstaged,
402 commit,
403 from,
404 to,
405 )
406 .await
407 }
408 GitAI::Changelog {
409 common,
410 from,
411 to,
412 update,
413 file,
414 version_name,
415 } => handle_changelog(common, from, to, repository_url, update, file, version_name).await,
416 GitAI::ReleaseNotes {
417 common,
418 from,
419 to,
420 version_name,
421 } => handle_release_notes(common, from, to, repository_url, version_name).await,
422 GitAI::Pr {
423 common,
424 print,
425 from,
426 to,
427 } => handle_pr_command(common, print, from, to, repository_url).await,
428 }
429}
430
431pub async fn handle_pr_command(
433 common: CommonParams,
434 print: bool,
435 from: Option<String>,
436 to: Option<String>,
437 repository_url: Option<String>,
438) -> anyhow::Result<()> {
439 debug!(
440 "Handling 'pr' command with common: {:?}, print: {}, from: {:?}, to: {:?}",
441 common, print, from, to
442 );
443 ui::print_version(crate_version!());
444 ui::print_newline();
445 commit::handle_pr_command(common, print, repository_url, from, to).await
446}