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