gitai/features/commit/
cli.rs

1use super::format_commit_result;
2use super::service::CommitService;
3use super::types::{format_commit_message, format_pull_request};
4use crate::common::CommonParams;
5use crate::config::Config;
6use crate::core::messages;
7use crate::features::commit::types;
8use crate::git::GitRepo;
9use crate::tui::run_tui_commit;
10use crate::ui;
11
12use anyhow::{Context, Result};
13use std::sync::Arc;
14
15#[allow(clippy::fn_params_excessive_bools)]
16#[allow(clippy::too_many_lines)]
17pub async fn handle_message_command(
18    common: CommonParams,
19    auto_commit: bool,
20    print: bool,
21    verify: bool,
22    dry_run: bool,
23    repository_url: Option<String>,
24) -> Result<()> {
25    let mut config = Config::load()?;
26    common.apply_to_config(&mut config)?;
27
28    // Create the service using the common function
29    let service = create_commit_service(
30        &common,
31        repository_url,
32        &config,
33        verify,
34    ).map_err(|e| {
35        ui::print_error(&format!("Error: {e}"));
36        ui::print_info("\nPlease ensure the following:");
37        ui::print_info("1. Git is installed and accessible from the command line.");
38        ui::print_info(
39            "2. You are running this command from within a Git repository or provide a repository URL with --repo.",
40        );
41        e
42    })?;
43
44    let git_info = service.get_git_info().await?;
45
46    if git_info.staged_files.is_empty() {
47        ui::print_warning(
48            "No staged changes. Please stage your changes before generating a commit message.",
49        );
50        ui::print_info("You can stage changes using 'git add <file>' or 'git add .'");
51        return Ok(());
52    }
53
54    // Run pre-commit hook before we do anything else
55    if let Err(e) = service.pre_commit() {
56        ui::print_error(&format!("Pre-commit failed: {e}"));
57        return Err(e);
58    }
59
60    let effective_instructions = common
61        .instructions
62        .unwrap_or_else(|| config.instructions.clone());
63
64    // Create and start the spinner
65    let spinner = ui::create_spinner("");
66    let random_message = messages::get_waiting_message();
67    spinner.set_message(random_message.text.clone());
68
69    // Generate an initial message
70    let initial_message = if dry_run {
71        types::GeneratedMessage {
72            emoji: Some("🔧".to_string()),
73            title: "Fix bug in UI rendering".to_string(),
74            message: "Updated the layout to properly handle dynamic constraints and improve user experience.".to_string(),
75        }
76    } else {
77        service.generate_message(&effective_instructions).await?
78    };
79
80    // Stop the spinner
81    spinner.finish_and_clear();
82
83    if print {
84        println!("{}", format_commit_message(&initial_message));
85        return Ok(());
86    }
87
88    if auto_commit {
89        // Only allow auto-commit for local repositories
90        if service.is_remote_repository() {
91            ui::print_error(
92                "Cannot automatically commit to a remote repository. Use --print instead.",
93            );
94            return Err(anyhow::anyhow!(
95                "Auto-commit not supported for remote repositories"
96            ));
97        }
98
99        match service.perform_commit(&format_commit_message(&initial_message)) {
100            Ok(result) => {
101                let output =
102                    format_commit_result(&result, &format_commit_message(&initial_message));
103                println!("{output}");
104            }
105            Err(e) => {
106                eprintln!("Failed to commit: {e}");
107                return Err(e);
108            }
109        }
110        return Ok(());
111    }
112
113    // Only allow interactive commit for local repositories
114    if service.is_remote_repository() {
115        ui::print_warning(
116            "Interactive commit not available for remote repositories. Using print mode instead.",
117        );
118        println!("{}", format_commit_message(&initial_message));
119        return Ok(());
120    }
121
122    run_tui_commit(vec![initial_message], effective_instructions, service).await?;
123
124    Ok(())
125}
126
127/// Handles the PR description generation command
128pub async fn handle_pr_command(
129    common: CommonParams,
130    _print: bool,
131    repository_url: Option<String>,
132    from: Option<String>,
133    to: Option<String>,
134) -> Result<()> {
135    // Validate parameter combinations
136    validate_pr_parameters(from.as_ref(), to.as_ref());
137
138    let mut config = Config::load()?;
139    common.apply_to_config(&mut config)?;
140
141    // Setup the service
142    let service = setup_pr_service(&common, repository_url, &config)?;
143
144    // Generate the PR description
145    let pr_description = generate_pr_based_on_parameters(service, common, config, from, to).await?;
146
147    // Print the PR description to stdout
148    println!("{}", format_pull_request(&pr_description));
149
150    Ok(())
151}
152
153/// Validates the parameter combinations for PR command
154fn validate_pr_parameters(_from: Option<&String>, _to: Option<&String>) {
155    // Now that we provide sensible defaults, we only need to validate if the parameters make sense
156    // All combinations are valid:
157    // - from + to: explicit range/branch comparison
158    // - from only: from..HEAD
159    // - to only: main..to
160    // - none: main..HEAD (caught earlier, but handled gracefully)
161
162    // No validation errors needed - all combinations are handled
163}
164
165/// Sets up the PR service with proper configuration
166fn setup_pr_service(
167    common: &CommonParams,
168    repository_url: Option<String>,
169    config: &Config,
170) -> Result<Arc<CommitService>> {
171    // Use the common function for service creation
172    create_commit_service(
173        common,
174        repository_url,
175        config,
176        false, // verification not needed for PR descriptions
177    )
178}
179
180/// Generates a PR description based on the provided parameters
181async fn generate_pr_based_on_parameters(
182    service: Arc<CommitService>,
183    common: CommonParams,
184    config: Config,
185    from: Option<String>,
186    to: Option<String>,
187) -> Result<super::types::GeneratedPullRequest> {
188    let effective_instructions = common
189        .instructions
190        .unwrap_or_else(|| config.instructions.clone());
191
192    // Create and start the spinner
193    let spinner = ui::create_spinner("");
194    let random_message = messages::get_waiting_message();
195    spinner.set_message(format!(
196        "{} - Generating PR description",
197        random_message.text
198    ));
199
200    let pr_description = match (from, to) {
201        (Some(from_ref), Some(to_ref)) => {
202            handle_from_and_to_parameters(
203                service,
204                &effective_instructions,
205                from_ref,
206                to_ref,
207                random_message,
208            )
209            .await?
210        }
211        (None, Some(to_ref)) => {
212            handle_to_only_parameter(service, &effective_instructions, to_ref, random_message)
213                .await?
214        }
215        (Some(from_ref), None) => {
216            handle_from_only_parameter(service, &effective_instructions, from_ref, random_message)
217                .await?
218        }
219        (None, None) => {
220            handle_no_parameters(service, &effective_instructions, random_message).await?
221        }
222    };
223
224    // Stop the spinner
225    spinner.finish_and_clear();
226
227    Ok(pr_description)
228}
229
230/// Handle case where both --from and --to parameters are provided
231async fn handle_from_and_to_parameters(
232    service: Arc<CommitService>,
233    effective_instructions: &str,
234    from_ref: String,
235    to_ref: String,
236    random_message: &messages::ColoredMessage,
237) -> Result<super::types::GeneratedPullRequest> {
238    // Special case: if from and to are the same, treat as single commit analysis
239    if from_ref == to_ref {
240        let spinner = ui::create_spinner("");
241        spinner.set_message(format!(
242            "{} - Analyzing single commit: {}",
243            random_message.text, from_ref
244        ));
245
246        service
247            .generate_pr_for_commit_range(
248                effective_instructions,
249                &format!("{from_ref}^"),
250                &from_ref,
251            )
252            .await
253    } else if is_likely_commit_hash_or_commitish(&from_ref)
254        || is_likely_commit_hash_or_commitish(&to_ref)
255    {
256        // Check if these look like commit hashes (7+ hex chars) or branches
257        // Treat as commit range
258        let spinner = ui::create_spinner("");
259        spinner.set_message(format!(
260            "{} - Analyzing commit range: {}..{}",
261            random_message.text, from_ref, to_ref
262        ));
263
264        service
265            .generate_pr_for_commit_range(effective_instructions, &from_ref, &to_ref)
266            .await
267    } else {
268        // Treat as branch comparison
269        let spinner = ui::create_spinner("");
270        spinner.set_message(format!(
271            "{} - Comparing branches: {} -> {}",
272            random_message.text, from_ref, to_ref
273        ));
274
275        service
276            .generate_pr_for_branch_diff(effective_instructions, &from_ref, &to_ref)
277            .await
278    }
279}
280
281/// Handle case where only --to parameter is provided
282async fn handle_to_only_parameter(
283    service: Arc<CommitService>,
284    effective_instructions: &str,
285    to_ref: String,
286    random_message: &messages::ColoredMessage,
287) -> Result<super::types::GeneratedPullRequest> {
288    let spinner = ui::create_spinner("");
289
290    // Check if this is a single commit hash
291    if is_likely_commit_hash(&to_ref) {
292        // For a single commit specified with --to, compare it against its parent
293        spinner.set_message(format!(
294            "{} - Analyzing single commit: {}",
295            random_message.text, to_ref
296        ));
297
298        service
299            .generate_pr_for_commit_range(effective_instructions, &format!("{to_ref}^"), &to_ref)
300            .await
301    } else if is_commitish_syntax(&to_ref) {
302        // For commitish like HEAD~2, compare it against its parent (single commit analysis)
303        spinner.set_message(format!(
304            "{} - Analyzing single commit: {}",
305            random_message.text, to_ref
306        ));
307
308        service
309            .generate_pr_for_commit_range(effective_instructions, &format!("{to_ref}^"), &to_ref)
310            .await
311    } else {
312        // Default from to "main" if only to is specified with a branch name
313        spinner.set_message(format!(
314            "{} - Comparing main -> {}",
315            random_message.text, to_ref
316        ));
317
318        service
319            .generate_pr_for_branch_diff(effective_instructions, "main", &to_ref)
320            .await
321    }
322}
323
324/// Handle case where only --from parameter is provided
325async fn handle_from_only_parameter(
326    service: Arc<CommitService>,
327    effective_instructions: &str,
328    from_ref: String,
329    random_message: &messages::ColoredMessage,
330) -> Result<super::types::GeneratedPullRequest> {
331    let spinner = ui::create_spinner("");
332
333    // Check if this looks like a single commit hash that we should compare against its parent
334    if is_likely_commit_hash(&from_ref) {
335        // For a single commit hash, compare it against its parent (commit^..commit)
336        spinner.set_message(format!(
337            "{} - Analyzing single commit: {}",
338            random_message.text, from_ref
339        ));
340
341        service
342            .generate_pr_for_commit_range(
343                effective_instructions,
344                &format!("{from_ref}^"),
345                &from_ref,
346            )
347            .await
348    } else if is_commitish_syntax(&from_ref) {
349        // For commitish like HEAD~2, compare from that point to HEAD (reviewing multiple commits)
350        spinner.set_message(format!(
351            "{} - Analyzing range: {}..HEAD",
352            random_message.text, from_ref
353        ));
354
355        service
356            .generate_pr_for_commit_range(effective_instructions, &from_ref, "HEAD")
357            .await
358    } else {
359        // For a branch name, compare to HEAD
360        spinner.set_message(format!(
361            "{} - Analyzing range: {}..HEAD",
362            random_message.text, from_ref
363        ));
364
365        service
366            .generate_pr_for_commit_range(effective_instructions, &from_ref, "HEAD")
367            .await
368    }
369}
370
371/// Handle case where no parameters are provided
372async fn handle_no_parameters(
373    service: Arc<CommitService>,
374    effective_instructions: &str,
375    random_message: &messages::ColoredMessage,
376) -> Result<super::types::GeneratedPullRequest> {
377    // This case should be caught by validation, but provide a sensible fallback
378    let spinner = ui::create_spinner("");
379    spinner.set_message(format!("{} - Comparing main -> HEAD", random_message.text));
380
381    service
382        .generate_pr_for_branch_diff(effective_instructions, "main", "HEAD")
383        .await
384}
385
386/// Heuristic to determine if a reference looks like a commit hash or commitish
387fn is_likely_commit_hash_or_commitish(reference: &str) -> bool {
388    // Check for commit hash (7+ hex chars)
389    if reference.len() >= 7 && reference.chars().all(|c| c.is_ascii_hexdigit()) {
390        return true;
391    }
392
393    // Check for Git commitish syntax
394    is_commitish_syntax(reference)
395}
396
397/// Check if a reference uses Git commitish syntax
398fn is_commitish_syntax(reference: &str) -> bool {
399    // Common commitish patterns:
400    // HEAD~2, HEAD^, @~3, main~1, origin/main^, etc.
401    reference.contains('~') || reference.contains('^') || reference.starts_with('@')
402}
403
404/// Heuristic to determine if a reference looks like a commit hash (legacy function for backward compatibility)
405fn is_likely_commit_hash(reference: &str) -> bool {
406    reference.len() >= 7 && reference.chars().all(|c| c.is_ascii_hexdigit())
407}
408
409/// Common function to set up `CommitService`
410fn create_commit_service(
411    common: &CommonParams,
412    repository_url: Option<String>,
413    config: &Config,
414    verify: bool,
415) -> Result<Arc<CommitService>> {
416    // Combine repository URL from CLI and CommonParams
417    let repo_url = repository_url.or(common.repository_url.clone());
418
419    // Create the git repository
420    let git_repo = GitRepo::new_from_url(repo_url).context("Failed to create GitRepo")?;
421
422    let repo_path = git_repo.repo_path().clone();
423    let provider_name = &config.default_provider;
424
425    let service = Arc::new(
426        CommitService::new(config.clone(), &repo_path, provider_name, verify, git_repo)
427            .context("Failed to create CommitService")?,
428    );
429
430    // Check environment prerequisites
431    service
432        .check_environment()
433        .context("Environment check failed")?;
434
435    Ok(service)
436}