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