git_iris/
cli.rs

1use crate::changes;
2use crate::commands;
3use crate::commit;
4use crate::common::CommonParams;
5use crate::llm::get_available_provider_names;
6use crate::log_debug;
7use crate::ui;
8use clap::builder::{Styles, styling::AnsiColor};
9use clap::{Parser, Subcommand, crate_version};
10use colored::Colorize;
11
12const LOG_FILE: &str = "git-iris-debug.log";
13
14/// CLI structure defining the available commands and global arguments
15#[derive(Parser)]
16#[command(
17    author,
18    version = crate_version!(),
19    about = "Git-Iris: AI-powered Git workflow assistant",
20    long_about = "Git-Iris enhances your Git workflow with AI-assisted commit messages, code reviews, changelogs, and more.",
21    disable_version_flag = true,
22    after_help = get_dynamic_help(),
23    styles = get_styles(),
24)]
25pub struct Cli {
26    /// Subcommands available for the CLI
27    #[command(subcommand)]
28    pub command: Option<Commands>,
29
30    /// Log debug messages to a file
31    #[arg(
32        short = 'l',
33        long = "log",
34        global = true,
35        help = "Log debug messages to a file"
36    )]
37    pub log: bool,
38
39    /// Display the version
40    #[arg(
41        short = 'v',
42        long = "version",
43        global = true,
44        help = "Display the version"
45    )]
46    pub version: bool,
47}
48
49/// Enumeration of available subcommands
50#[derive(Subcommand)]
51#[command(subcommand_negates_reqs = true)]
52#[command(subcommand_precedence_over_arg = true)]
53pub enum Commands {
54    // Feature commands first
55    /// Generate a commit message using AI
56    #[command(
57        about = "Generate a commit message using AI",
58        long_about = "Generate a commit message using AI based on the current Git context.",
59        after_help = get_dynamic_help()
60    )]
61    Gen {
62        #[command(flatten)]
63        common: CommonParams,
64
65        /// Automatically commit with the generated message
66        #[arg(short, long, help = "Automatically commit with the generated message")]
67        auto_commit: bool,
68
69        /// Disable Gitmoji for this commit
70        #[arg(long, help = "Disable Gitmoji for this commit")]
71        no_gitmoji: bool,
72
73        /// Print the generated message to stdout and exit
74        #[arg(short, long, help = "Print the generated message to stdout and exit")]
75        print: bool,
76
77        /// Skip the verification step (pre/post commit hooks)
78        #[arg(long, help = "Skip verification steps (pre/post commit hooks)")]
79        no_verify: bool,
80    },
81
82    /// Review staged changes and provide feedback
83    #[command(
84        about = "Review staged changes using AI",
85        long_about = "Generate a detailed code review of staged changes using AI based on the current Git context."
86    )]
87    Review {
88        #[command(flatten)]
89        common: CommonParams,
90
91        /// Print the generated review to stdout and exit
92        #[arg(short, long, help = "Print the generated review to stdout and exit")]
93        print: bool,
94    },
95
96    /// Generate a changelog
97    #[command(
98        about = "Generate a changelog",
99        long_about = "Generate a changelog between two specified Git references."
100    )]
101    Changelog {
102        #[command(flatten)]
103        common: CommonParams,
104
105        /// Starting Git reference (commit hash, tag, or branch name)
106        #[arg(long, required = true)]
107        from: String,
108
109        /// Ending Git reference (commit hash, tag, or branch name). Defaults to HEAD if not specified.
110        #[arg(long)]
111        to: Option<String>,
112    },
113
114    /// Generate release notes
115    #[command(
116        about = "Generate release notes",
117        long_about = "Generate comprehensive release notes between two specified Git references."
118    )]
119    ReleaseNotes {
120        #[command(flatten)]
121        common: CommonParams,
122
123        /// Starting Git reference (commit hash, tag, or branch name)
124        #[arg(long, required = true)]
125        from: String,
126
127        /// Ending Git reference (commit hash, tag, or branch name). Defaults to HEAD if not specified.
128        #[arg(long)]
129        to: Option<String>,
130    },
131
132    // Configuration and utility commands
133    /// Configure the AI-assisted Git commit message generator
134    #[command(about = "Configure Git-Iris settings and providers")]
135    Config {
136        #[command(flatten)]
137        common: CommonParams,
138
139        /// Set API key for the specified provider
140        #[arg(long, help = "Set API key for the specified provider")]
141        api_key: Option<String>,
142
143        /// Set model for the specified provider
144        #[arg(long, help = "Set model for the specified provider")]
145        model: Option<String>,
146
147        /// Set token limit for the specified provider
148        #[arg(long, help = "Set token limit for the specified provider")]
149        token_limit: Option<usize>,
150
151        /// Set additional parameters for the specified provider
152        #[arg(
153            long,
154            help = "Set additional parameters for the specified provider (key=value)"
155        )]
156        param: Option<Vec<String>>,
157    },
158
159    /// List available instruction presets
160    #[command(about = "List available instruction presets")]
161    ListPresets,
162}
163
164/// Define custom styles for Clap
165fn get_styles() -> Styles {
166    Styles::styled()
167        .header(AnsiColor::Magenta.on_default().bold())
168        .usage(AnsiColor::Cyan.on_default().bold())
169        .literal(AnsiColor::Green.on_default().bold())
170        .placeholder(AnsiColor::Yellow.on_default())
171        .valid(AnsiColor::Blue.on_default().bold())
172        .invalid(AnsiColor::Red.on_default().bold())
173        .error(AnsiColor::Red.on_default().bold())
174}
175
176/// Parse the command-line arguments
177pub fn parse_args() -> Cli {
178    Cli::parse()
179}
180
181/// Generate dynamic help including available LLM providers
182fn get_dynamic_help() -> String {
183    let mut providers = get_available_provider_names();
184    providers.sort(); // Sort alphabetically
185
186    let providers_list = providers
187        .iter()
188        .map(|p| format!("{}", p.bold()))
189        .collect::<Vec<_>>()
190        .join(" • ");
191
192    format!("\nAvailable LLM Providers: {providers_list}")
193}
194
195/// Main function to parse arguments and handle the command
196pub async fn main() -> anyhow::Result<()> {
197    let cli = parse_args();
198
199    if cli.version {
200        ui::print_version(crate_version!());
201        return Ok(());
202    }
203
204    if cli.log {
205        crate::logger::enable_logging();
206        crate::logger::set_log_file(LOG_FILE)?;
207    } else {
208        crate::logger::disable_logging();
209    }
210
211    if let Some(command) = cli.command {
212        handle_command(command).await
213    } else {
214        // If no subcommand is provided, print the help
215        let _ = Cli::parse_from(["git-iris", "--help"]);
216        Ok(())
217    }
218}
219
220/// Handle the command based on parsed arguments
221pub async fn handle_command(command: Commands) -> anyhow::Result<()> {
222    match command {
223        Commands::Gen {
224            common,
225            auto_commit,
226            no_gitmoji,
227            print,
228            no_verify,
229        } => {
230            log_debug!(
231                "Handling 'gen' command with common: {:?}, auto_commit: {}, no_gitmoji: {}, print: {}, no_verify: {}",
232                common,
233                auto_commit,
234                no_gitmoji,
235                print,
236                no_verify
237            );
238
239            ui::print_version(crate_version!());
240            println!();
241
242            commit::handle_gen_command(common, auto_commit, !no_gitmoji, print, !no_verify).await?;
243        }
244        Commands::Config {
245            common,
246            api_key,
247            model,
248            token_limit,
249            param,
250        } => {
251            log_debug!(
252                "Handling 'config' command with common: {:?}, api_key: {:?}, model: {:?}, token_limit: {:?}, param: {:?}",
253                common,
254                api_key,
255                model,
256                token_limit,
257                param
258            );
259            commands::handle_config_command(common, api_key, model, token_limit, param)?;
260        }
261        Commands::ListPresets => {
262            log_debug!("Handling 'list_presets' command");
263            commands::handle_list_presets_command()?;
264        }
265        Commands::Changelog { common, from, to } => {
266            log_debug!(
267                "Handling 'changelog' command with common: {:?}, from: {}, to: {:?}",
268                common,
269                from,
270                to
271            );
272            changes::handle_changelog_command(common, from, to).await?;
273        }
274        Commands::ReleaseNotes { common, from, to } => {
275            log_debug!(
276                "Handling 'release-notes' command with common: {:?}, from: {}, to: {:?}",
277                common,
278                from,
279                to
280            );
281            changes::handle_release_notes_command(common, from, to).await?;
282        }
283        Commands::Review { common, print } => {
284            log_debug!(
285                "Handling 'review' command with common: {:?}, print: {}",
286                common,
287                print
288            );
289            ui::print_version(crate_version!());
290            println!();
291            commit::review::handle_review_command(common, print).await?;
292        }
293    }
294
295    Ok(())
296}