1use crate::config::AppConfig;
2
3const CONVENTIONAL_COMMIT_SPEC: &str = "\
4Write all commit messages strictly following the Conventional Commits specification.
5
6Use the following format:
7<type>[optional scope][optional !]: <description>
8
9[optional body]
10
11[optional footer(s)]
12
13Rules to follow:
141. Type: MUST be a noun. Use `feat` for new features, `fix` for bug fixes, or other relevant types (e.g., `docs`, `chore`, `refactor`).
152. Scope: OPTIONAL. A noun describing the affected section of the codebase, enclosed in parentheses (e.g., `fix(parser):`).
163. Description: REQUIRED. A concise summary immediately following the type/scope, colon, and space.
174. Body: OPTIONAL. Provide additional context. MUST begin one blank line after the description.
185. Footer: OPTIONAL. MUST begin one blank line after the body. Use token-value pairs (e.g., `Reviewed-by: Name`). Token words must be hyphenated.
196. Breaking Changes: MUST be indicated by either an exclamation mark `!` immediately before the colon (e.g., `feat!:`) OR an uppercase `BREAKING CHANGE: <description>` in the footer.";
20
21const GITMOJI_UNICODE_SPEC: &str = "\
22Use Gitmoji while still following the Conventional Commits specification above: \
23prepend a relevant emoji in unicode format, then a space, then the conventional type(scope): description. \
24Examples: \u{26a1}\u{fe0f} feat(api): improve response time, \u{1f41b} fix(auth): correct login redirect, \
25\u{2728} feat: add new feature, \u{267b}\u{fe0f} refactor(parser): simplify logic, \u{1f4dd} docs: update README, \u{1f3a8} style(ui): improve layout";
26
27const GITMOJI_SHORTCODE_SPEC: &str = "\
28Use Gitmoji while still following the Conventional Commits specification above: \
29prepend a relevant emoji in :shortcode: format, then a space, then the conventional type(scope): description. \
30Examples: :zap: feat(api): improve response time, :bug: fix(auth): correct login redirect, \
31:sparkles: feat: add new feature, :recycle: refactor(parser): simplify logic, :memo: docs: update README, :art: style(ui): improve layout";
32
33pub fn build_system_prompt(cfg: &AppConfig) -> String {
35 let mut parts = Vec::new();
36
37 parts.push(cfg.llm_system_prompt.clone());
39
40 parts.push(CONVENTIONAL_COMMIT_SPEC.to_string());
42
43 if cfg.use_gitmoji {
45 let spec = match cfg.gitmoji_format.as_str() {
46 "shortcode" => GITMOJI_SHORTCODE_SPEC,
47 _ => GITMOJI_UNICODE_SPEC,
48 };
49 parts.push(spec.to_string());
50 }
51
52 if cfg.one_liner {
54 parts.push("Craft a concise, single sentence, commit message that encapsulates all changes made, with an emphasis on the primary updates. If the modifications share a common theme or scope, mention it succinctly; otherwise, leave the scope out to maintain focus. The goal is to provide a clear and unified overview of the changes in one single message. Output ONLY a single-line commit message in the format: type[optional scope]: description. Do NOT include a body or footer. The entire commit message must fit on one line.".to_string());
55 }
56
57 if cfg.locale != "en" {
59 parts.push(format!(
60 "Write the commit message in the '{}' locale.",
61 cfg.locale
62 ));
63 }
64
65 parts.push(
67 "Use present tense. Be concise. Output only the raw commit message, nothing else."
68 .to_string(),
69 );
70
71 parts.join("\n\n")
72}
73
74pub fn clean_commit_message(raw: &str) -> String {
81 let s = raw.trim();
82
83 let s = strip_code_fence(s);
85
86 let s = strip_label_prefix(s);
89
90 let s = strip_surrounding_quotes(s);
92
93 s.trim().to_string()
94}
95
96fn strip_code_fence(s: &str) -> &str {
97 if let Some(inner) = s.strip_prefix("```") {
99 let after_tag = inner.trim_start_matches(|c: char| c.is_alphanumeric() || c == '-');
101 if let Some(body) = after_tag.strip_prefix('\n') {
103 if let Some(end) = body.rfind("```") {
104 return body[..end].trim();
105 }
106 }
107 }
108 s
109}
110
111fn strip_label_prefix(s: &str) -> &str {
112 let label_patterns: &[&str] = &[
114 "commit message:",
115 "here is the commit message:",
116 "here's the commit message:",
117 "here is your commit message:",
118 "here's your commit message:",
119 "generated commit message:",
120 "suggested commit message:",
121 "the commit message:",
122 ];
123
124 let lower = s.to_lowercase();
125 for pat in label_patterns {
126 if let Some(rest) = lower.strip_prefix(pat) {
127 return s[pat.len()..][rest.len() - rest.trim_start().len()..].trim_start();
129 }
130 }
131 s
132}
133
134fn strip_surrounding_quotes(s: &str) -> &str {
135 let quote_pairs: &[(char, char)] = &[('"', '"'), ('\'', '\''), ('\u{201c}', '\u{201d}')];
136 for &(open, close) in quote_pairs {
137 if s.starts_with(open) && s.ends_with(close) && s.len() > 1 {
138 return &s[open.len_utf8()..s.len() - close.len_utf8()];
139 }
140 }
141 s
142}