git_iris/commit/
prompt.rs

1use crate::common::get_combined_instructions;
2use crate::config::Config;
3use crate::context::{
4    ChangeType, CommitContext, GeneratedMessage, ProjectMetadata, RecentCommit, StagedFile,
5};
6use crate::gitmoji::{apply_gitmoji, get_gitmoji_list};
7
8use super::relevance::RelevanceScorer;
9use crate::log_debug;
10use std::collections::HashMap;
11
12pub fn create_system_prompt(config: &Config) -> anyhow::Result<String> {
13    let commit_schema = schemars::schema_for!(GeneratedMessage);
14    let commit_schema_str = serde_json::to_string_pretty(&commit_schema)?;
15
16    let mut prompt = String::from(
17        "You are an AI assistant specializing in creating high-quality, professional Git commit messages. \
18        Your task is to generate clear, concise, and informative commit messages based solely on the provided context.
19        
20        Work step-by-step and follow these guidelines exactly:
21
22        1. Use the imperative mood in the subject line (e.g., 'Add feature' not 'Added feature').
23        2. Limit the subject line to 50 characters if possible, but never exceed 72 characters.
24        3. Capitalize the subject line.
25        4. Do not end the subject line with a period.
26        5. Separate subject from body with a blank line.
27        6. Wrap the body at 72 characters.
28        7. Use the body to explain what changes were made and their impact, and how they were implemented.
29        8. Be specific and avoid vague language.
30        9. Focus on the concrete changes and their effects, not assumptions about intent.
31        10. If the changes are part of a larger feature or fix, state this fact if evident from the context.
32        11. For non-trivial changes, include a brief explanation of the change's purpose if clearly indicated in the context.
33        12. Do not include a conclusion or end summary section.
34        13. Avoid common cliché words (like 'enhance', 'streamline', 'leverage', etc) and phrases.
35        14. Don't mention filenames in the subject line unless absolutely necessary.
36        15. Only describe changes that are explicitly shown in the provided context.
37        16. If the purpose or impact of a change is not clear from the context, focus on describing the change itself without inferring intent.
38        17. Do not use phrases like 'seems to', 'appears to', or 'might be' - only state what is certain based on the context.
39        18. If there's not enough information to create a complete, authoritative message, state only what can be confidently determined from the context.
40        19. NO YAPPING!
41
42        Be sure to quote newlines and any other control characters in your response.
43
44        The message should be based entirely on the information provided in the context,
45        without any speculation or assumptions.
46      ");
47
48    prompt.push_str(get_combined_instructions(config).as_str());
49
50    if config.use_gitmoji {
51        prompt.push_str(
52            "\n\nUse a single gitmoji at the start of the commit message. \
53          Choose the most relevant emoji from the following list:\n\n",
54        );
55        prompt.push_str(&get_gitmoji_list());
56    }
57
58    prompt.push_str("
59        Your response must be a valid JSON object with the following structure:
60
61        {
62          \"emoji\": \"string or null\",
63          \"title\": \"string\",
64          \"message\": \"string\"
65        }
66
67        Follow these steps to generate the commit message:
68
69        1. Analyze the provided context, including staged changes, recent commits, and project metadata.
70        2. Identify the main purpose of the commit based on the changes.
71        3. Create a concise and descriptive title (subject line) for the commit.
72        4. If using emojis (false unless stated below), select the most appropriate one for the commit type.
73        5. Write a detailed message body explaining the changes, their impact, and any other relevant information.
74        6. Ensure the message adheres to the guidelines above, and follows all of the additional instructions provided.
75        7. Construct the final JSON object with the emoji (if applicable), title, and message.
76
77         Here's a minimal example of the expected output format:
78
79        {
80          \"emoji\": \"✨\",
81          \"title\": \"Add user authentication feature\",
82          \"message\": \"Implement user authentication using JWT tokens\\n\\n- Add login and registration endpoints\\n- Create middleware for token verification\\n- Update user model to include password hashing\\n- Add unit tests for authentication functions\"
83        }
84
85        Ensure that your response is a valid JSON object matching this structure. Include an empty string for the emoji if not using one.
86        "
87    );
88
89    prompt.push_str(&commit_schema_str);
90
91    Ok(prompt)
92}
93
94pub fn create_user_prompt(context: &CommitContext) -> String {
95    let scorer = RelevanceScorer::new();
96    let relevance_scores = scorer.score(context);
97    let detailed_changes = format_detailed_changes(&context.staged_files, &relevance_scores);
98
99    let prompt = format!(
100        "Based on the following context, generate a Git commit message:\n\n\
101        Branch: {}\n\n\
102        Recent commits:\n{}\n\n\
103        Staged changes:\n{}\n\n\
104        Project metadata:\n{}\n\n\
105        Detailed changes:\n{}",
106        context.branch,
107        format_recent_commits(&context.recent_commits),
108        format_staged_files(&context.staged_files, &relevance_scores),
109        format_project_metadata(&context.project_metadata),
110        detailed_changes
111    );
112
113    log_debug!("Detailed changes:\n{}", detailed_changes);
114
115    prompt
116}
117
118fn format_recent_commits(commits: &[RecentCommit]) -> String {
119    commits
120        .iter()
121        .map(|commit| format!("{} - {}", &commit.hash[..7], commit.message))
122        .collect::<Vec<_>>()
123        .join("\n")
124}
125
126fn format_staged_files(files: &[StagedFile], relevance_scores: &HashMap<String, f32>) -> String {
127    files
128        .iter()
129        .map(|file| {
130            let relevance = relevance_scores.get(&file.path).unwrap_or(&0.0);
131            format!(
132                "{} ({:.2}) - {}",
133                file.path,
134                relevance,
135                format_change_type(&file.change_type)
136            )
137        })
138        .collect::<Vec<_>>()
139        .join("\n")
140}
141
142fn format_project_metadata(metadata: &ProjectMetadata) -> String {
143    format!(
144        "Language: {}\nFramework: {}\nDependencies: {}",
145        metadata.language.as_deref().unwrap_or("None"),
146        metadata.framework.as_deref().unwrap_or("None"),
147        metadata.dependencies.join(", ")
148    )
149}
150
151fn format_detailed_changes(
152    files: &[StagedFile],
153    relevance_scores: &HashMap<String, f32>,
154) -> String {
155    files
156        .iter()
157        .map(|file| {
158            let relevance = relevance_scores.get(&file.path).unwrap_or(&0.0);
159            let mut file_info = format!(
160                "File: {} (Relevance: {:.2})\nChange Type: {}\nAnalysis:\n{}\n\nDiff:\n{}",
161                file.path,
162                relevance,
163                format_change_type(&file.change_type),
164                file.analysis.join("\n"),
165                file.diff
166            );
167
168            // Add full file content if available
169            if let Some(content) = &file.content {
170                file_info.push_str("\n\n---\n\nFull File Content:\n");
171                file_info.push_str(content);
172                file_info.push_str("\n\n--- End of File ---\n");
173            }
174
175            file_info
176        })
177        .collect::<Vec<_>>()
178        .join("\n\n---\n\n")
179}
180
181fn format_change_type(change_type: &ChangeType) -> &'static str {
182    match change_type {
183        ChangeType::Added => "Added",
184        ChangeType::Modified => "Modified",
185        ChangeType::Deleted => "Deleted",
186    }
187}
188
189pub fn process_commit_message(message: String, use_gitmoji: bool) -> String {
190    if use_gitmoji {
191        apply_gitmoji(&message)
192    } else {
193        message
194    }
195}