gitai/
app.rs

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/// CLI structure defining the available commands and global arguments
12#[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    /// Subcommands available for the CLI
23    #[command(subcommand)]
24    pub command: Option<GitAI>,
25
26    /// Log debug messages to a file
27    #[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    /// Specify a custom log file path
36    #[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    /// Suppress non-essential output (spinners, waiting messages, etc.)
44    #[arg(
45        short = 'q',
46        long = "quiet",
47        global = true,
48        help = "Suppress non-essential output"
49    )]
50    pub quiet: bool,
51
52    /// Display the version
53    #[arg(
54        short = 'v',
55        long = "version",
56        global = true,
57        help = "Display the version"
58    )]
59    pub version: bool,
60
61    /// Repository URL to use instead of local repository
62    #[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/// Enumeration of available subcommands
72#[derive(Subcommand)]
73#[command(subcommand_negates_reqs = true)]
74#[command(subcommand_precedence_over_arg = true)]
75pub enum GitAI {
76    // Feature commands first
77    /// Generate a commit message using AI
78    #[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        /// Automatically commit with the generated message
88        #[arg(short, long, help = "Automatically commit with the generated message")]
89        auto_commit: bool,
90
91        /// Disable emoji for this commit
92        #[arg(long, help = "Disable emojis for this commit")]
93        no_emoji: bool,
94
95        /// Print the generated message to stdout and exit
96        #[arg(short, long, help = "Print the generated message to stdout and exit")]
97        print: bool,
98
99        /// Skip the verification step (pre/post commit hooks)
100        #[arg(long, help = "Skip verification steps (pre/post commit hooks)")]
101        no_verify: bool,
102    },
103
104    /// Review staged changes and provide feedback
105    #[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        /// Print the generated review to stdout and exit
114        #[arg(short, long, help = "Print the generated review to stdout and exit")]
115        print: bool,
116
117        /// Include unstaged changes in the review
118        #[arg(long, help = "Include unstaged changes in the review")]
119        include_unstaged: bool,
120
121        /// Review a specific commit by ID (hash, branch, or reference)
122        #[arg(
123            long,
124            help = "Review a specific commit by ID (hash, branch, or reference)"
125        )]
126        commit: Option<String>,
127
128        /// Starting branch for comparison (defaults to 'main')
129        #[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        /// Target branch for comparison (e.g., 'feature-branch', 'pr-branch')
136        #[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    /// Generate a pull request description
144    #[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        /// Print the generated PR description to stdout and exit
163        #[arg(
164            short,
165            long,
166            help = "Print the generated PR description to stdout and exit"
167        )]
168        print: bool,
169
170        /// Starting branch, commit, or commitish for comparison
171        #[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        /// Target branch, commit, or commitish for comparison
178        #[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    /// Generate a changelog
186    #[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        /// Starting Git reference (commit hash, tag, or branch name)
195        #[arg(long, required = true)]
196        from: String,
197
198        /// Ending Git reference (commit hash, tag, or branch name). Defaults to HEAD if not specified.
199        #[arg(long)]
200        to: Option<String>,
201
202        /// Update the changelog file with the new changes
203        #[arg(long, help = "Update the changelog file with the new changes")]
204        update: bool,
205
206        /// Path to the changelog file
207        #[arg(long, help = "Path to the changelog file (defaults to CHANGELOG.md)")]
208        file: Option<String>,
209
210        /// Explicit version name to use in the changelog instead of getting it from Git
211        #[arg(long, help = "Explicit version name to use in the changelog")]
212        version_name: Option<String>,
213    },
214
215    /// Generate release notes
216    #[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        /// Starting Git reference (commit hash, tag, or branch name)
225        #[arg(long, required = true)]
226        from: String,
227
228        /// Ending Git reference (commit hash, tag, or branch name). Defaults to HEAD if not specified.
229        #[arg(long)]
230        to: Option<String>,
231
232        /// Explicit version name to use in the release notes instead of getting it from Git
233        #[arg(long, help = "Explicit version name to use in the release notes")]
234        version_name: Option<String>,
235    },
236}
237
238/// Define custom styles for Clap
239fn 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
250/// Parse the command-line arguments
251pub fn parse_args() -> Cli {
252    Cli::parse()
253}
254
255/// Generate dynamic help including available LLM providers
256fn 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/// Configuration for the cmsg command
273#[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
306/// Handle the `Review` command
307pub 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
334/// Handle the `Changelog` command
335pub 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
351/// Handle the `ReleaseNotes` command
352pub 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
366/// Handle the command based on parsed arguments
367pub 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
431/// Handle the `Pr` command
432pub 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}