1use crate::common::CommonParams;
2use crate::core::llm::get_available_provider_names;
3use crate::features::changelog::{handle_changelog_command, handle_release_notes_command};
4use crate::features::commit;
5use crate::server::config::{MCPServerConfig, MCPTransportType};
6use crate::ui;
7use crate::{debug, server};
8use clap::builder::{Styles, styling::AnsiColor};
9use clap::{Parser, Subcommand, crate_version};
10use colored::Colorize;
11
12#[derive(Parser)]
14#[command(
15 author,
16 version = crate_version!(),
17 about = "GitAI: AI-powered Git workflow assistant",
18 disable_version_flag = true,
19 after_help = get_dynamic_help(),
20 styles = get_styles(),
21)]
22pub struct Cli {
23 #[command(subcommand)]
25 pub command: Option<GitAI>,
26
27 #[arg(
29 short = 'l',
30 long = "log",
31 global = true,
32 help = "Log debug messages to a file"
33 )]
34 pub log: bool,
35
36 #[arg(
38 long = "log-file",
39 global = true,
40 help = "Specify a custom log file path"
41 )]
42 pub log_file: Option<String>,
43
44 #[arg(
46 short = 'q',
47 long = "quiet",
48 global = true,
49 help = "Suppress non-essential output"
50 )]
51 pub quiet: bool,
52
53 #[arg(
55 short = 'v',
56 long = "version",
57 global = true,
58 help = "Display the version"
59 )]
60 pub version: bool,
61
62 #[arg(
64 short = 'r',
65 long = "repo",
66 global = true,
67 help = "Repository URL to use instead of local repository"
68 )]
69 pub repository_url: Option<String>,
70}
71
72#[derive(Subcommand)]
74#[command(subcommand_negates_reqs = true)]
75#[command(subcommand_precedence_over_arg = true)]
76pub enum GitAI {
77 #[command(
80 about = "Generate a commit message using AI",
81 long_about = "Generate a commit message using AI based on the current Git context.",
82 after_help = get_dynamic_help()
83 )]
84 Message {
85 #[command(flatten)]
86 common: CommonParams,
87
88 #[arg(short, long, help = "Automatically commit with the generated message")]
90 auto_commit: bool,
91
92 #[arg(long, help = "Disable emojis for this commit")]
94 no_emoji: bool,
95
96 #[arg(short, long, help = "Print the generated message to stdout and exit")]
98 print: bool,
99
100 #[arg(long, help = "Skip verification steps (pre/post commit hooks)")]
102 no_verify: bool,
103 },
104
105 #[command(
107 about = "Review staged changes using AI",
108 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."
109 )]
110 Review {
111 #[command(flatten)]
112 common: CommonParams,
113
114 #[arg(short, long, help = "Print the generated review to stdout and exit")]
116 print: bool,
117
118 #[arg(long, help = "Include unstaged changes in the review")]
120 include_unstaged: bool,
121
122 #[arg(
124 long,
125 help = "Review a specific commit by ID (hash, branch, or reference)"
126 )]
127 commit: Option<String>,
128
129 #[arg(
131 long,
132 help = "Starting branch for comparison (defaults to 'main'). Used with --to for branch comparison reviews"
133 )]
134 from: Option<String>,
135
136 #[arg(
138 long,
139 help = "Target branch for comparison (e.g., 'feature-branch', 'pr-branch'). Used with --from for branch comparison reviews"
140 )]
141 to: Option<String>,
142 },
143
144 #[command(
146 about = "Generate a pull request description using AI",
147 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.\\
148\\
149Usage examples:\\
150• Single commit: --from abc1234 or --to abc1234\\
151• Single commitish: --from HEAD~1 or --to HEAD~2\\
152• Multiple commits: --from HEAD~3 (reviews last 3 commits)\\
153• Commit range: --from abc1234 --to def5678\\
154• Branch comparison: --from main --to feature-branch\\
155• From main to branch: --to feature-branch\\
156\\
157Supported commitish syntax: HEAD~2, HEAD^, @~3, main~1, origin/main^, etc."
158 )]
159 Pr {
160 #[command(flatten)]
161 common: CommonParams,
162
163 #[arg(
165 short,
166 long,
167 help = "Print the generated PR description to stdout and exit"
168 )]
169 print: bool,
170
171 #[arg(
173 long,
174 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)"
175 )]
176 from: Option<String>,
177
178 #[arg(
180 long,
181 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)"
182 )]
183 to: Option<String>,
184 },
185
186 #[command(
188 about = "Generate a changelog",
189 long_about = "Generate a changelog between two specified Git references."
190 )]
191 Changelog {
192 #[command(flatten)]
193 common: CommonParams,
194
195 #[arg(long, required = true)]
197 from: String,
198
199 #[arg(long)]
201 to: Option<String>,
202
203 #[arg(long, help = "Update the changelog file with the new changes")]
205 update: bool,
206
207 #[arg(long, help = "Path to the changelog file (defaults to CHANGELOG.md)")]
209 file: Option<String>,
210
211 #[arg(long, help = "Explicit version name to use in the changelog")]
213 version_name: Option<String>,
214 },
215
216 #[command(
218 about = "Generate release notes",
219 long_about = "Generate comprehensive release notes between two specified Git references."
220 )]
221 ReleaseNotes {
222 #[command(flatten)]
223 common: CommonParams,
224
225 #[arg(long, required = true)]
227 from: String,
228
229 #[arg(long)]
231 to: Option<String>,
232
233 #[arg(long, help = "Explicit version name to use in the release notes")]
235 version_name: Option<String>,
236 },
237
238 #[command(
240 about = "Start an MCP server",
241 long_about = "Start a Model Context Protocol (MCP) server to provide functionality to AI tools and assistants."
242 )]
243 Serve {
244 #[arg(long, help = "Enable development mode with more verbose logging")]
246 dev: bool,
247
248 #[arg(
250 short,
251 long,
252 help = "Transport type to use (stdio, sse)",
253 default_value = "stdio"
254 )]
255 transport: String,
256
257 #[arg(short, long, help = "Port to use for network transports")]
259 port: Option<u16>,
260
261 #[arg(
263 long,
264 help = "Listen address for network transports (e.g., '127.0.0.1', '0.0.0.0')",
265 default_value = "127.0.0.1"
266 )]
267 listen_address: Option<String>,
268 },
269}
270
271fn get_styles() -> Styles {
273 Styles::styled()
274 .header(AnsiColor::Magenta.on_default().bold())
275 .usage(AnsiColor::Cyan.on_default().bold())
276 .literal(AnsiColor::Green.on_default().bold())
277 .placeholder(AnsiColor::Yellow.on_default())
278 .valid(AnsiColor::Blue.on_default().bold())
279 .invalid(AnsiColor::Red.on_default().bold())
280 .error(AnsiColor::Red.on_default().bold())
281}
282
283pub fn parse_args() -> Cli {
285 Cli::parse()
286}
287
288fn get_dynamic_help() -> String {
290 let mut providers = get_available_provider_names();
291 providers.sort();
292
293 let providers_list = providers
294 .iter()
295 .map(|p| format!("{}", p.bold()))
296 .collect::<Vec<_>>()
297 .join(" • ");
298
299 format!(
300 "\\
301Available LLM Providers: {providers_list}"
302 )
303}
304
305#[allow(clippy::struct_excessive_bools)]
307pub struct CmsgConfig {
308 pub auto_commit: bool,
309 pub use_emoji: bool,
310 pub print_only: bool,
311 pub verify: bool,
312 pub dry_run: bool,
313}
314
315pub async fn handle_message(
316 common: CommonParams,
317 config: CmsgConfig,
318 repository_url: Option<String>,
319) -> anyhow::Result<()> {
320 debug!(
321 "Handling 'message' command with common: {:?}, auto_commit: {}, use_emoji: {}, print: {}, verify: {}",
322 common, config.auto_commit, config.use_emoji, config.print_only, config.verify
323 );
324
325 ui::print_version(crate_version!());
326 ui::print_newline();
327
328 commit::handle_message_command(
329 common,
330 config.auto_commit,
331 config.use_emoji,
332 config.print_only,
333 config.verify,
334 config.dry_run,
335 repository_url,
336 )
337 .await
338}
339
340pub async fn handle_review(
342 common: CommonParams,
343 print: bool,
344 repository_url: Option<String>,
345 include_unstaged: bool,
346 commit: Option<String>,
347 from: Option<String>,
348 to: Option<String>,
349) -> anyhow::Result<()> {
350 debug!(
351 "Handling 'review' command with common: {:?}, print: {}, include_unstaged: {}, commit: {:?}, from: {:?}, to: {:?}",
352 common, print, include_unstaged, commit, from, to
353 );
354 ui::print_version(crate_version!());
355 ui::print_newline();
356 commit::review::handle_review_command(
357 common,
358 print,
359 repository_url,
360 include_unstaged,
361 commit,
362 from,
363 to,
364 )
365 .await
366}
367
368pub async fn handle_changelog(
370 common: CommonParams,
371 from: String,
372 to: Option<String>,
373 repository_url: Option<String>,
374 update: bool,
375 file: Option<String>,
376 version_name: Option<String>,
377) -> anyhow::Result<()> {
378 debug!(
379 "Handling 'changelog' command with common: {:?}, from: {}, to: {:?}, update: {}, file: {:?}, version_name: {:?}",
380 common, from, to, update, file, version_name
381 );
382 handle_changelog_command(common, from, to, repository_url, update, file, version_name).await
383}
384
385pub async fn handle_release_notes(
387 common: CommonParams,
388 from: String,
389 to: Option<String>,
390 repository_url: Option<String>,
391 version_name: Option<String>,
392) -> anyhow::Result<()> {
393 debug!(
394 "Handling 'release-notes' command with common: {:?}, from: {}, to: {:?}, version_name: {:?}",
395 common, from, to, version_name
396 );
397 handle_release_notes_command(common, from, to, repository_url, version_name).await
398}
399
400pub async fn handle_command(command: GitAI, repository_url: Option<String>) -> anyhow::Result<()> {
402 match command {
403 GitAI::Message {
404 common,
405 auto_commit,
406 no_emoji,
407 print,
408 no_verify,
409 } => {
410 handle_message(
411 common,
412 CmsgConfig {
413 auto_commit,
414 use_emoji: !no_emoji,
415 print_only: print,
416 verify: !no_verify,
417 dry_run: false,
418 },
419 repository_url,
420 )
421 .await
422 }
423 GitAI::Review {
424 common,
425 print,
426 include_unstaged,
427 commit,
428 from,
429 to,
430 } => {
431 handle_review(
432 common,
433 print,
434 repository_url,
435 include_unstaged,
436 commit,
437 from,
438 to,
439 )
440 .await
441 }
442 GitAI::Changelog {
443 common,
444 from,
445 to,
446 update,
447 file,
448 version_name,
449 } => handle_changelog(common, from, to, repository_url, update, file, version_name).await,
450 GitAI::ReleaseNotes {
451 common,
452 from,
453 to,
454 version_name,
455 } => handle_release_notes(common, from, to, repository_url, version_name).await,
456 GitAI::Serve {
457 dev,
458 transport,
459 port,
460 listen_address,
461 } => handle_serve_command(dev, transport, port, listen_address).await,
462 GitAI::Pr {
463 common,
464 print,
465 from,
466 to,
467 } => handle_pr_command(common, print, from, to, repository_url).await,
468 }
469}
470
471pub async fn handle_pr_command(
473 common: CommonParams,
474 print: bool,
475 from: Option<String>,
476 to: Option<String>,
477 repository_url: Option<String>,
478) -> anyhow::Result<()> {
479 debug!(
480 "Handling 'pr' command with common: {:?}, print: {}, from: {:?}, to: {:?}",
481 common, print, from, to
482 );
483 ui::print_version(crate_version!());
484 ui::print_newline();
485 commit::handle_pr_command(common, print, repository_url, from, to).await
486}
487
488pub async fn handle_serve_command(
490 dev: bool,
491 transport: String,
492 port: Option<u16>,
493 listen_address: Option<String>,
494) -> anyhow::Result<()> {
495 debug!(
496 "Starting 'serve' command with dev: {}, transport: {}, port: {:?}, listen_address: {:?}",
497 dev, transport, port, listen_address
498 );
499
500 let mut config = MCPServerConfig::default();
502
503 if dev {
505 config = config.with_dev_mode();
506 }
507
508 let transport_type = match transport.to_lowercase().as_str() {
510 "stdio" => MCPTransportType::StdIO,
511 "sse" => MCPTransportType::SSE,
512 _ => {
513 return Err(anyhow::anyhow!(
514 "Invalid transport type: {transport}. Valid options are: stdio, sse"
515 ));
516 }
517 };
518 config = config.with_transport(transport_type);
519
520 if let Some(p) = port {
522 config = config.with_port(p);
523 }
524
525 if let Some(addr) = listen_address {
527 config = config.with_listen_address(addr);
528 }
529
530 server::serve(config).await
532}