git_iris/commit/
prompt.rs1use 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 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}