Skip to main content

git_commit_helper_cli/
commit.rs

1use regex::Regex;
2use crate::ai_service;
3use crate::config;
4use crate::git;
5
6/// 从提交消息中提取 Change-Id
7fn extract_change_id(message: &str) -> Option<String> {
8    let change_id_regex = Regex::new(r"(?m)^Change-Id:\s*(.+)$").ok()?;
9    change_id_regex.captures(message)
10        .and_then(|cap| cap.get(1))
11        .map(|m| m.as_str().trim().to_string())
12}
13
14/// 将 Change-Id 添加到提交消息中(如果还没有的话)
15fn append_change_id(message: &str, change_id: &str) -> String {
16    // 检查消息中是否已经有 Change-Id
17    if message.contains("Change-Id:") {
18        return message.to_string();
19    }
20    
21    let mut result = message.to_string();
22    
23    // 确保消息末尾有换行
24    if !result.ends_with('\n') {
25        result.push('\n');
26    }
27    
28    // 添加 Change-Id(如果有其他标记,Change-Id 应该在最后)
29    result.push('\n');
30    result.push_str(&format!("Change-Id: {}", change_id));
31    
32    result
33}
34
35// 语言模式枚举
36#[derive(Debug, Clone, Copy)]
37enum LanguageMode {
38    ChineseOnly,
39    EnglishOnly,
40    Bilingual,
41}
42
43/// 解析 issues 参数并生成相应的引用字段
44fn parse_issue_reference(issues: &str) -> anyhow::Result<String> {
45    let mut fixes_refs = Vec::new();
46    let mut pms_refs = Vec::new();
47
48    // 按空格和逗号分割多个链接
49    let links: Vec<&str> = issues.split_whitespace()
50        .flat_map(|s| s.split(','))
51        .filter(|s| !s.is_empty())
52        .collect();
53
54    for link in links {
55        let link = link.trim();
56
57        // 处理 GitHub issue URL
58        if link.starts_with("https://github.com/") {
59            match parse_github_issue(link) {
60                Ok(ref_str) => {
61                    // 提取 Fixes: 后面的部分
62                    if let Some(fix_ref) = ref_str.strip_prefix("Fixes: ") {
63                        fixes_refs.push(fix_ref.to_string());
64                    }
65                }
66                Err(e) => return Err(e),
67            }
68        }
69        // 处理 PMS 链接
70        else if link.contains("pms.uniontech.com") {
71            match parse_pms_link(link) {
72                Ok(ref_str) => {
73                    // 提取 PMS: 后面的部分
74                    if let Some(pms_ref) = ref_str.strip_prefix("PMS: ") {
75                        pms_refs.push(pms_ref.to_string());
76                    }
77                }
78                Err(e) => return Err(e),
79            }
80        }
81        // 处理简单的 issue 数字(假设是当前项目的 GitHub issue)
82        else if let Ok(_issue_num) = link.parse::<u32>() {
83            fixes_refs.push(format!("#{}", link));
84        }
85        else {
86            return Err(anyhow::anyhow!("无法解析 issue 引用格式: {}", link));
87        }
88    }
89
90    // 组合结果
91    let mut result = Vec::new();
92
93    if !fixes_refs.is_empty() {
94        result.push(format!("Fixes: {}", fixes_refs.join(" ")));
95    }
96
97    if !pms_refs.is_empty() {
98        result.push(format!("PMS: {}", pms_refs.join(" ")));
99    }
100
101    if result.is_empty() {
102        return Err(anyhow::anyhow!("没有找到有效的 issue 引用"));
103    }
104
105    Ok(result.join("\n"))
106}
107
108/// 解析 GitHub issue URL 并生成 Fixes 字段
109fn parse_github_issue(url: &str) -> anyhow::Result<String> {
110    let issue_regex = Regex::new(r"https://github\.com/([^/]+/[^/]+)/issues/(\d+)")?;
111
112    if let Some(captures) = issue_regex.captures(url) {
113        let repo = captures.get(1).unwrap().as_str();
114        let issue_num = captures.get(2).unwrap().as_str();
115
116        // 检查是否是当前项目
117        if is_current_project_repo(repo)? {
118            Ok(format!("Fixes: #{}", issue_num))
119        } else {
120            Ok(format!("Fixes: {}#{}", repo, issue_num))
121        }
122    } else {
123        Err(anyhow::anyhow!("无效的 GitHub issue URL 格式: {}", url))
124    }
125}
126
127/// 解析 PMS 链接并生成 PMS 字段
128fn parse_pms_link(url: &str) -> anyhow::Result<String> {
129    // 匹配 bug-view、task-view、story-view 格式
130    let bug_regex = Regex::new(r"bug-view-(\d+)\.html")?;
131    let task_regex = Regex::new(r"task-view-(\d+)\.html")?;
132    let story_regex = Regex::new(r"story-view-(\d+)\.html")?;
133
134    if let Some(captures) = bug_regex.captures(url) {
135        let id = &captures[1];
136        Ok(format!("PMS: BUG-{}", id))
137    } else if let Some(captures) = task_regex.captures(url) {
138        let id = &captures[1];
139        Ok(format!("PMS: TASK-{}", id))
140    } else if let Some(captures) = story_regex.captures(url) {
141        let id = &captures[1];
142        Ok(format!("PMS: STORY-{}", id))
143    } else {
144        Err(anyhow::anyhow!("无效的 PMS 链接格式: {}", url))
145    }
146}
147
148/// 检查给定的仓库是否是当前项目的仓库
149fn is_current_project_repo(repo: &str) -> anyhow::Result<bool> {
150    use std::process::Command;
151
152    // 获取当前仓库的远程 URL
153    let output = Command::new("git")
154        .args(&["remote", "get-url", "origin"])
155        .output()?;
156
157    if !output.status.success() {
158        return Ok(false);
159    }
160
161    let remote_url = String::from_utf8_lossy(&output.stdout);
162    let remote_url = remote_url.trim();
163
164    // 从远程 URL 中提取仓库名称
165    // 支持 HTTPS 和 SSH 格式
166    let repo_regex = Regex::new(r"github\.com[:/]([^/]+/[^/\.]+)")?;
167
168    if let Some(captures) = repo_regex.captures(remote_url) {
169        let current_repo = captures.get(1).unwrap().as_str();
170        Ok(current_repo == repo)
171    } else {
172        Ok(false)
173    }
174}
175
176// 提示词模板常量
177const ENGLISH_PROMPT_TEMPLATE: &str = r#"Please analyze the git diff content and generate a commit message in English only:
1781. First line: type: message (under 50 characters)
1792. Empty line after the title
1803. Detailed explanation in English (what was changed and why)
1814. Empty line after explanation
1825. Log field (ONLY if this change involves user-facing features/UI changes that product managers would communicate to users)
1836. Empty line after Log field (if present)
1847. Influence section with black-box testing recommendations
1858. Type must be one of: feat/fix/docs/style/refactor/test/chore
1869. Focus on both WHAT changed and WHY it was necessary
18710. Include any important technical details or context
18811. DO NOT include any Chinese content
18912. DO NOT wrap the response in any markdown or code block markers
190
191Example response format:
192feat: add user authentication module
193
1941. Implement JWT-based authentication system
1952. Add user login and registration endpoints
1963. Include password hashing with bcrypt
1974. Set up token refresh mechanism
198
199Log: Added user authentication feature with login and registration
200
201Influence:
2021. Test user registration with valid and invalid inputs
2032. Verify login functionality with correct and incorrect credentials
2043. Test JWT token generation and validation
2054. Verify password security and hashing
2065. Test token refresh mechanism and expiration handling
2076. Verify access control for protected endpoints
208
209Please respond with ONLY the commit message following this format,
210DO NOT end commit titles with any punctuation."#;
211
212const CHINESE_PROMPT_TEMPLATE: &str = r#"请分析以下 git diff 内容,并按照以下格式生成提交信息:
2131. 第一行为标题:type: message(不超过50个字符)
214
2153. 详细的中文说明(解释做了什么改动以及为什么需要这些改动)
2164. 说明下方空一行
2175. Log 字段(仅当此次变更涉及用户可感知的功能/UI层面变化,产品经理会向用户说明的内容时才添加)
2186. Log 字段下方空一行(如果存在 Log 字段)
2197. Influence 部分,提供黑盒测试的重点和范围
2208. type 必须是以下之一:feat/fix/docs/style/refactor/test/chore
2219. 关注点:变更内容(做了什么)和变更原因(为什么)
22210. 包含重要的技术细节或上下文
22311. 不要使用任何 markdown 或代码块标记
22412. 标题结尾不要使用标点符号
225
226示例格式:
227feat: 添加用户认证模块
228
2291. 实现基于 JWT 的认证系统
2302. 添加用户登录和注册端点
2313. 包含使用 bcrypt 的密码哈希处理
2324. 设置令牌刷新机制
233
234Log: 新增用户登录注册功能
235
236Influence:
2371. 测试用户注册功能,包括有效和无效输入
2382. 验证登录功能,测试正确和错误的凭据
2393. 测试 JWT 令牌生成和验证流程
2404. 验证密码安全性和哈希处理
2415. 测试令牌刷新机制和过期处理
2426. 验证受保护端点的访问控制"#;
243
244const BILINGUAL_PROMPT_TEMPLATE: &str = r#"Please analyze the git diff content and generate a detailed bilingual commit message with:
2451. First line in English: type: message (under 50 characters)
2462. Empty line after the title
2473. Detailed explanation in English (what was changed and why)
2484. Empty line after English explanation
2495. Log field in English (ONLY if this change involves user-facing features/UI changes)
2506. Empty line after English Log field (if present)
2517. Influence section in English with black-box testing recommendations
252
2539. Chinese title and explanation (translate the English content)
25410. Empty line after Chinese explanation
25511. Chinese Log field (translate the English Log field, only if present)
25612. Empty line after Chinese Log field (if present)
25713. Chinese Influence section (translate the English testing suggestions)
25814. Type must be one of: feat/fix/docs/style/refactor/test/chore
25915. Focus on both WHAT changed and WHY it was necessary
26016. Include any important technical details or context
26117. DO NOT wrap the response in any markdown or code block markers
262
263Example response format:
264feat: add user authentication module
265
2661. Implement JWT-based authentication system
2672. Add user login and registration endpoints
2683. Include password hashing with bcrypt
2694. Set up token refresh mechanism
270
271Log: Added user authentication feature with login and registration
272
273Influence:
2741. Test user registration with valid and invalid inputs
2752. Verify login functionality with correct and incorrect credentials
2763. Test JWT token generation and validation
2774. Verify password security and hashing
2785. Test token refresh mechanism and expiration handling
2796. Verify access control for protected endpoints
280
281feat: 添加用户认证模块
282
2831. 实现基于 JWT 的认证系统
2842. 添加用户登录和注册端点
2853. 包含使用 bcrypt 的密码哈希处理
2864. 设置令牌刷新机制
287
288Log: 新增用户登录注册功能
289
290Influence:
2911. 测试用户注册功能,包括有效和无效输入
2922. 验证登录功能,测试正确和错误的凭据
2933. 测试 JWT 令牌生成和验证流程
2944. 验证密码安全性和哈希处理
2955. 测试令牌刷新机制和过期处理
2966. 验证受保护端点的访问控制
297
298Please respond with ONLY the commit message following this format,
299DO NOT end commit titles with any punctuation."#;
300
301// 无测试建议版本的提示词模板
302const ENGLISH_PROMPT_TEMPLATE_NO_TEST: &str = r#"Please analyze the git diff content and generate a commit message in English only:
3031. First line: type: message (under 50 characters)
3042. Empty line after the title
3053. Detailed explanation in English (what was changed and why)
3064. Empty line after explanation
3075. Log field (ONLY if this change involves user-facing features/UI changes that product managers would communicate to users)
3086. Type must be one of: feat/fix/docs/style/refactor/test/chore
3097. Focus on both WHAT changed and WHY it was necessary
3108. Include any important technical details or context
3119. DO NOT include any Chinese content
31210. DO NOT wrap the response in any markdown or code block markers
313
314Example response format:
315feat: add user authentication module
316
3171. Implement JWT-based authentication system
3182. Add user login and registration endpoints
3193. Include password hashing with bcrypt
3204. Set up token refresh mechanism
321
322Log: Added user authentication feature with login and registration
323
324Please respond with ONLY the commit message following this format,
325DO NOT end commit titles with any punctuation."#;
326
327const CHINESE_PROMPT_TEMPLATE_NO_TEST: &str = r#"请分析以下 git diff 内容,并按照以下格式生成提交信息:
3281. 第一行为标题:type: message(不超过50个字符)
3292. 标题下方空一行
3303. 详细的中文说明(解释做了什么改动以及为什么需要这些改动)
3314. 说明下方空一行
3325. Log 字段(仅当此次变更涉及用户可感知的功能/UI层面变化时才添加)
3336. type 必须是以下之一:feat/fix/docs/style/refactor/test/chore
3347. 关注点:变更内容(做了什么)和变更原因(为什么)
3358. 包含重要的技术细节或上下文
3369. 不要使用任何 markdown 或代码块标记
33710. 标题结尾不要使用标点符号
338
339示例格式:
340feat: 添加用户认证模块
341
3421. 实现基于 JWT 的认证系统
3432. 添加用户登录和注册端点
3443. 包含使用 bcrypt 的密码哈希处理
3454. 设置令牌刷新机制
346
347Log: 新增用户登录注册功能"#;
348
349const BILINGUAL_PROMPT_TEMPLATE_NO_TEST: &str = r#"Please analyze the git diff content and generate a detailed bilingual commit message with:
3501. First line in English: type: message (under 50 characters)
3512. Empty line after the title
3523. Detailed explanation in English (what was changed and why)
3534. Empty line after English explanation
3545. Log field in English (ONLY if this change involves user-facing features/UI changes)
3556. Empty line after English Log field (if present)
3567. Chinese title and explanation (translate the English content)
3578. Empty line after Chinese explanation
3589. Chinese Log field (translate the English Log field, only if present)
35910. Type must be one of: feat/fix/docs/style/refactor/test/chore
36011. Focus on both WHAT changed and WHY it was necessary
36112. Include any important technical details or context
36213. DO NOT wrap the response in any markdown or code block markers
363
364Example response format:
365feat: add user authentication module
366
3671. Implement JWT-based authentication system
3682. Add user login and registration endpoints
3693. Include password hashing with bcrypt
3704. Set up token refresh mechanism
371
372Log: Added user authentication feature with login and registration
373
374feat: 添加用户认证模块
375
3761. 实现基于 JWT 的认证系统
3772. 添加用户登录和注册端点
3783. 包含使用 bcrypt 的密码哈希处理
3794. 设置令牌刷新机制
380
381Log: 新增用户登录注册功能
382
383Please respond with ONLY the commit message following this format,
384DO NOT end commit titles with any punctuation."#;
385
386// 无Log字段版本的提示词模板
387const ENGLISH_PROMPT_TEMPLATE_NO_LOG: &str = r#"Please analyze the git diff content and generate a commit message in English only:
3881. First line: type: message (under 50 characters)
3892. Empty line after the title
3903. Detailed explanation in English (what was changed and why)
3914. Empty line after explanation
3925. Influence section with black-box testing recommendations
3936. Type must be one of: feat/fix/docs/style/refactor/test/chore
3947. Focus on both WHAT changed and WHY it was necessary
3958. Include any important technical details or context
3969. DO NOT include any Chinese content
39710. DO NOT wrap the response in any markdown or code block markers
398
399Example response format:
400feat: add user authentication module
401
4021. Implement JWT-based authentication system
4032. Add user login and registration endpoints
4043. Include password hashing with bcrypt
4054. Set up token refresh mechanism
406
407Influence:
4081. Test user registration with valid and invalid inputs
4092. Verify login functionality with correct and incorrect credentials
4103. Test JWT token generation and validation
4114. Verify password security and hashing
4125. Test token refresh mechanism and expiration handling
4136. Verify access control for protected endpoints
414
415Please respond with ONLY the commit message following this format,
416DO NOT end commit titles with any punctuation."#;
417
418const CHINESE_PROMPT_TEMPLATE_NO_LOG: &str = r#"请分析以下 git diff 内容,并按照以下格式生成提交信息:
4191. 第一行为标题:type: message(不超过50个字符)
420
4213. 详细的中文说明(解释做了什么改动以及为什么需要这些改动)
4224. 说明下方空一行
4235. Influence 部分,提供黑盒测试的重点和范围
4246. type 必须是以下之一:feat/fix/docs/style/refactor/test/chore
4257. 关注点:变更内容(做了什么)和变更原因(为什么)
4268. 包含重要的技术细节或上下文
4279. 不要使用任何 markdown 或代码块标记
42810. 标题结尾不要使用标点符号
429
430示例格式:
431feat: 添加用户认证模块
432
4331. 实现基于 JWT 的认证系统
4342. 添加用户登录和注册端点
4353. 包含使用 bcrypt 的密码哈希处理
4364. 设置令牌刷新机制
437
438Influence:
4391. 测试用户注册功能,包括有效和无效输入
4402. 验证登录功能,测试正确和错误的凭据
4413. 测试 JWT 令牌生成和验证流程
4424. 验证密码安全性和哈希处理
4435. 测试令牌刷新机制和过期处理
4446. 验证受保护端点的访问控制"#;
445
446const BILINGUAL_PROMPT_TEMPLATE_NO_LOG: &str = r#"Please analyze the git diff content and generate a detailed bilingual commit message with:
4471. First line in English: type: message (under 50 characters)
4482. Empty line after the title
4493. Detailed explanation in English (what was changed and why)
4504. Empty line after English explanation
4515. Influence section in English with black-box testing recommendations
452
4537. Chinese title and explanation (translate the English content)
4548. Empty line after Chinese explanation
4559. Chinese Influence section (translate the English testing suggestions)
45610. Type must be one of: feat/fix/docs/style/refactor/test/chore
45711. Focus on both WHAT changed and WHY it was necessary
45812. Include any important technical details or context
45913. DO NOT wrap the response in any markdown or code block markers
460
461Example response format:
462feat: add user authentication module
463
4641. Implement JWT-based authentication system
4652. Add user login and registration endpoints
4663. Include password hashing with bcrypt
4674. Set up token refresh mechanism
468
469Influence:
4701. Test user registration with valid and invalid inputs
4712. Verify login functionality with correct and incorrect credentials
4723. Test JWT token generation and validation
4734. Verify password security and hashing
4745. Test token refresh mechanism and expiration handling
4756. Verify access control for protected endpoints
476
477feat: 添加用户认证模块
478
4791. 实现基于 JWT 的认证系统
4802. 添加用户登录和注册端点
4813. 包含使用 bcrypt 的密码哈希处理
4824. 设置令牌刷新机制
483
484Influence:
4851. 测试用户注册功能,包括有效和无效输入
4862. 验证登录功能,测试正确和错误的凭据
4873. 测试 JWT 令牌生成和验证流程
4884. 验证密码安全性和哈希处理
4895. 测试令牌刷新机制和过期处理
4906. 验证受保护端点的访问控制
491
492Please respond with ONLY the commit message following this format,
493DO NOT end commit titles with any punctuation."#;
494
495// 无测试建议无Log字段版本的提示词模板
496const ENGLISH_PROMPT_TEMPLATE_NO_TEST_NO_LOG: &str = r#"Please analyze the git diff content and generate a commit message in English only:
4971. First line: type: message (under 50 characters)
4982. Empty line after the title
4993. Detailed explanation in English (what was changed and why)
5004. Type must be one of: feat/fix/docs/style/refactor/test/chore
5015. Focus on both WHAT changed and WHY it was necessary
5026. Include any important technical details or context
5037. DO NOT include any Chinese content
5048. DO NOT wrap the response in any markdown or code block markers
505
506Example response format:
507feat: add user authentication module
508
5091. Implement JWT-based authentication system
5102. Add user login and registration endpoints
5113. Include password hashing with bcrypt
5124. Set up token refresh mechanism
513
514Please respond with ONLY the commit message following this format,
515DO NOT end commit titles with any punctuation."#;
516
517const CHINESE_PROMPT_TEMPLATE_NO_TEST_NO_LOG: &str = r#"请分析以下 git diff 内容,并按照以下格式生成提交信息:
5181. 第一行为标题:type: message(不超过50个字符)
5192. 标题下方空一行
5203. 详细的中文说明(解释做了什么改动以及为什么需要这些改动)
5214. type 必须是以下之一:feat/fix/docs/style/refactor/test/chore
5225. 关注点:变更内容(做了什么)和变更原因(为什么)
5236. 包含重要的技术细节或上下文
5247. 不要使用任何 markdown 或代码块标记
5258. 标题结尾不要使用标点符号
526
527示例格式:
528feat: 添加用户认证模块
529
5301. 实现基于 JWT 的认证系统
5312. 添加用户登录和注册端点
5323. 包含使用 bcrypt 的密码哈希处理
5334. 设置令牌刷新机制"#;
534
535const BILINGUAL_PROMPT_TEMPLATE_NO_TEST_NO_LOG: &str = r#"Please analyze the git diff content and generate a detailed bilingual commit message with:
5361. First line in English: type: message (under 50 characters)
5372. Empty line after the title
5383. Detailed explanation in English (what was changed and why)
5394. Empty line after English explanation
5405. Chinese title and explanation (translate the English content)
5416. Type must be one of: feat/fix/docs/style/refactor/test/chore
5427. Focus on both WHAT changed and WHY it was necessary
5438. Include any important technical details or context
5449. DO NOT wrap the response in any markdown or code block markers
545
546Example response format:
547feat: add user authentication module
548
5491. Implement JWT-based authentication system
5502. Add user login and registration endpoints
5513. Include password hashing with bcrypt
5524. Set up token refresh mechanism
553
554feat: 添加用户认证模块
555
5561. 实现基于 JWT 的认证系统
5572. 添加用户登录和注册端点
5583. 包含使用 bcrypt 的密码哈希处理
5594. 设置令牌刷新机制
560
561Please respond with ONLY the commit message following this format,
562DO NOT end commit titles with any punctuation."#;
563
564impl LanguageMode {
565    fn determine(only_chinese: bool, only_english: bool) -> Self {
566        if only_english {
567            Self::EnglishOnly
568        } else if only_chinese {
569            Self::ChineseOnly
570        } else {
571            Self::Bilingual
572        }
573    }
574
575    fn template(&self, include_test_suggestions: bool, include_log: bool) -> &'static str {
576        match (self, include_test_suggestions, include_log) {
577            (Self::EnglishOnly, true, true) => ENGLISH_PROMPT_TEMPLATE,
578            (Self::EnglishOnly, false, true) => ENGLISH_PROMPT_TEMPLATE_NO_TEST,
579            (Self::EnglishOnly, true, false) => ENGLISH_PROMPT_TEMPLATE_NO_LOG,
580            (Self::EnglishOnly, false, false) => ENGLISH_PROMPT_TEMPLATE_NO_TEST_NO_LOG,
581            (Self::ChineseOnly, true, true) => CHINESE_PROMPT_TEMPLATE,
582            (Self::ChineseOnly, false, true) => CHINESE_PROMPT_TEMPLATE_NO_TEST,
583            (Self::ChineseOnly, true, false) => CHINESE_PROMPT_TEMPLATE_NO_LOG,
584            (Self::ChineseOnly, false, false) => CHINESE_PROMPT_TEMPLATE_NO_TEST_NO_LOG,
585            (Self::Bilingual, true, true) => BILINGUAL_PROMPT_TEMPLATE,
586            (Self::Bilingual, false, true) => BILINGUAL_PROMPT_TEMPLATE_NO_TEST,
587            (Self::Bilingual, true, false) => BILINGUAL_PROMPT_TEMPLATE_NO_LOG,
588            (Self::Bilingual, false, false) => BILINGUAL_PROMPT_TEMPLATE_NO_TEST_NO_LOG,
589        }
590    }
591}
592
593// 统一的提示词构建函数
594fn build_prompt(mode: LanguageMode, user_message: Option<&str>, include_test_suggestions: bool, include_log: bool, original_message: Option<&str>) -> String {
595    let mut prompt = String::from(mode.template(include_test_suggestions, include_log));
596
597    // 如果有原始提交信息(amend 模式),先添加它作为参考
598    if let Some(orig_msg) = original_message {
599        match mode {
600            LanguageMode::ChineseOnly => {
601                prompt.push_str(&format!("\n\n原始提交信息(请参考但不要完全照搬):\n{}\n", orig_msg));
602            }
603            _ => {
604                prompt.push_str(&format!("\n\nOriginal commit message (for reference, but create improved version):\n{}\n", orig_msg));
605            }
606        }
607    }
608
609    if let Some(msg) = user_message {
610        match mode {
611            LanguageMode::ChineseOnly => {
612                prompt.push_str(&format!("\n\n用户描述:\n{}\n\n变更内容:\n", msg));
613            }
614            _ => {
615                prompt.push_str(&format!("\n\nUser Description:\n{}\n\nChanges:\n", msg));
616            }
617        }
618    } else {
619        match mode {
620            LanguageMode::ChineseOnly => {
621                prompt.push_str("\n\n变更内容:\n");
622            }
623            _ => {
624                prompt.push_str("\n\nHere are the changes:\n");
625            }
626        }
627    }
628
629    prompt
630}
631
632pub struct CommitMessage {
633    pub title: String,
634    pub body: Option<String>,
635    pub marks: Vec<String>,
636}
637
638impl CommitMessage {
639    pub fn parse(content: &str) -> Self {
640        let mark_regex = Regex::new(r"^[a-zA-Z-]+:\s*.+$").unwrap();
641        let comment_regex = Regex::new(r"^#.*$").unwrap();
642        let mut lines = content.lines().peekable();
643
644        // 获取第一个非注释行作为标题
645        let title = lines
646            .by_ref()
647            .find(|line| !comment_regex.is_match(line.trim()))
648            .unwrap_or("")
649            .to_string();
650
651        let mut body = Vec::new();
652        let mut marks = Vec::new();
653        let mut is_body = false;
654
655        while let Some(line) = lines.next() {
656            // 跳过注释行
657            if comment_regex.is_match(line.trim()) {
658                continue;
659            }
660
661            if line.trim().is_empty() {
662                if !is_body && body.is_empty() {
663                    continue;
664                }
665                is_body = true;
666                body.push(line.to_string());
667            } else if mark_regex.is_match(line) {
668                marks.push(line.to_string());
669            } else {
670                is_body = true;
671                body.push(line.to_string());
672            }
673        }
674
675        // 移除body末尾的空行
676        while body.last().map_or(false, |line| line.trim().is_empty()) {
677            body.pop();
678        }
679
680        // 移除 body 中的注释行
681        let body = if body.is_empty() {
682            None
683        } else {
684            Some(body
685                .into_iter()
686                .filter(|line| !comment_regex.is_match(line.trim()))
687                .collect::<Vec<_>>()
688                .join("\n"))
689        };
690
691        CommitMessage {
692            title,
693            body,
694            marks,
695        }
696    }
697
698    pub fn format(&self) -> String {
699        let mut result = Vec::new();
700        result.push(self.title.clone());
701
702        if let Some(body) = &self.body {
703            if !body.is_empty() {
704                result.push(String::new());
705                result.push(body.clone());
706            }
707        }
708
709        // 添加标记
710        if !self.marks.is_empty() {
711            if !result.last().map_or(false, |s| s.is_empty()) {
712                result.push(String::new());  // 添加空行分隔
713            }
714            result.extend(self.marks.clone());
715        }
716
717        result.join("\n")
718    }
719}
720
721use crate::review;
722use dialoguer::Confirm;
723use log::{debug, info};
724use std::process::Command;
725
726pub async fn generate_commit_message(
727    commit_type: Option<String>,
728    message: Option<String>,
729    auto_add: bool,
730    amend: bool,
731    no_review: bool,
732    no_translate: bool,
733    mut only_chinese: bool,
734    mut only_english: bool,
735    no_influence: bool,
736    no_log: bool,
737    issues: Option<String>,
738) -> anyhow::Result<()> {
739    // 加载配置,如果指定了参数则使用参数值,否则使用配置中的默认值
740    if let Ok(config) = config::Config::load() {
741        if !only_chinese && !only_english {
742            only_chinese = config.only_chinese;
743            only_english = config.only_english;
744        }
745    }
746
747    // 处理语言选项冲突:only_english 优先级最高
748    if only_english {
749        only_chinese = false;
750    } else if only_chinese {
751        only_english = false;
752    }
753
754    // 如果指定了 -a 参数,先执行 git add -u
755    if auto_add {
756        info!("自动添加已修改的文件...");
757        let status = Command::new("git")
758            .args(["add", "-u"])
759            .status()?;
760
761        if !status.success() {
762            return Err(anyhow::anyhow!("执行 git add -u 命令失败"));
763        }
764    }
765
766    // 设置环境变量
767    if no_translate {
768        std::env::set_var("GIT_COMMIT_HELPER_NO_TRANSLATE", "1");
769    }
770
771    // 根据是否是 amend 模式选择不同的 diff
772    let diff = if amend {
773        println!("正在分析上一次提交的更改内容...");
774        git::get_last_commit_diff()?
775    } else {
776        get_staged_diff()?
777    };
778
779    if diff.is_empty() {
780        if amend {
781            return Err(anyhow::anyhow!("无法获取上一次提交的差异内容,可能没有足够的提交历史"));
782        } else {
783            return Err(anyhow::anyhow!("没有已暂存的改动,请先使用 git add 添加改动"));
784        }
785    }
786
787    let config = config::Config::load()?;
788
789    // 在确认有差异内容后执行代码审查(对于 amend 模式,我们跳过审查,因为是对已有提交的修改)
790    if !amend && !no_review && config.ai_review {
791        info!("正在进行代码审查...");
792        if let Some(review) = review::review_changes(&config, no_review).await? {
793            println!("\n{}\n", review);
794        }
795    }
796
797    // 设置环境变量标记跳过后续的代码审查
798    std::env::set_var("GIT_COMMIT_HELPER_SKIP_REVIEW", "1");
799
800    // 确定语言模式并构建提示词,考虑是否包含测试建议
801    let language_mode = LanguageMode::determine(only_chinese, only_english);
802    let include_test_suggestions = !no_influence;
803    let include_log = !no_log;
804    
805    // 在 amend 模式下获取原始提交消息作为参考
806    let (original_message, original_change_id) = if amend {
807        match git::get_last_commit_message() {
808            Ok(msg) => {
809                let change_id = extract_change_id(&msg);
810                (Some(msg), change_id)
811            }
812            Err(_) => (None, None)
813        }
814    } else {
815        (None, None)
816    };
817    
818    let prompt = build_prompt(
819        language_mode, 
820        message.as_deref(), 
821        include_test_suggestions, 
822        include_log,
823        original_message.as_deref()
824    );
825
826    debug!("生成的提示信息:\n{}", prompt);
827
828    info!("使用 {:?} 服务生成提交信息", config.default_service);
829    let service = config.get_default_service()?;
830    let translator = ai_service::create_translator_for_service(service).await?;
831
832    if amend {
833        println!("\n正在基于上一次提交的更改生成新的提交信息...");
834        // 显示原提交信息供参考
835        if let Some(ref original_msg) = original_message {
836            println!("原提交信息:");
837            println!("----------------------------------------");
838            println!("{}", original_msg.trim());
839            println!("----------------------------------------\n");
840        }
841    } else {
842        println!("\n正在生成提交信息建议...");
843    }
844
845    let mut message = translator.chat(&prompt, &diff).await?
846        .trim_start_matches("[NO_TRANSLATE]")
847        .trim_start_matches("、、、plaintext")
848        .trim()
849        .to_string();
850
851    // 如果提供了具体的type,确保使用该type
852    if let Some(t) = commit_type {
853        message = ensure_commit_type(&message, &[t]);
854    }
855
856    // 处理换行
857    let mut content = message.lines().map(|line| {
858        if line.trim().is_empty() {
859            line.to_string()
860        } else {
861            git::wrap_text(line, 72)
862        }
863    }).collect::<Vec<_>>().join("\n");
864
865    // 如果指定了 issues 参数,添加引用字段
866    if let Some(issues_str) = issues {
867        match parse_issue_reference(&issues_str) {
868            Ok(reference) => {
869                // 在提交信息末尾添加空行和引用字段
870                if !content.ends_with('\n') {
871                    content.push('\n');
872                }
873                content.push('\n');
874                content.push_str(&reference);
875            }
876            Err(e) => {
877                eprintln!("警告: 解析 issues 参数失败: {}", e);
878            }
879        }
880    }
881
882    // 在 amend 模式下,保留原提交中所有未被新内容覆盖的标记字段
883    // (Change-Id 须保持在最后,由 append_change_id 单独处理)
884    if amend {
885        if let Some(ref orig_msg) = original_message {
886            let orig_commit = CommitMessage::parse(orig_msg);
887            let marks_to_add: Vec<String> = orig_commit.marks.iter()
888                .filter(|mark| {
889                    let mark_key = mark.split(':').next().unwrap_or("").trim().to_lowercase();
890                    mark_key != "change-id" && !content.lines().any(|line| {
891                        line.trim().split(':').next()
892                            .map_or(false, |k| k.trim().to_lowercase() == mark_key)
893                    })
894                })
895                .cloned()
896                .collect();
897            if !marks_to_add.is_empty() {
898                if !content.ends_with('\n') {
899                    content.push('\n');
900                }
901                content.push('\n');
902                content.push_str(&marks_to_add.join("\n"));
903            }
904        }
905        if let Some(change_id) = original_change_id {
906            content = append_change_id(&content, &change_id);
907        }
908    }
909
910    // 预览生成的提交信息
911    if amend {
912        println!("\n生成的修改后提交信息预览:");
913    } else {
914        println!("\n生成的提交信息预览:");
915    }
916    println!("----------------------------------------");
917    println!("{}", content);
918    println!("----------------------------------------");
919
920    // 询问用户是否确认提交
921    let prompt_text = if amend {
922        "是否使用此提交信息修改上一次提交?"
923    } else {
924        "是否使用此提交信息?"
925    };
926
927    if !Confirm::with_theme(&dialoguer::theme::ColorfulTheme::default())
928        .with_prompt(prompt_text)
929        .default(true)
930        .interact()?
931    {
932        if amend {
933            println!("已取消修改上一次提交");
934        } else {
935            println!("已取消提交");
936        }
937        return Ok(());
938    }
939
940    // 执行git commit
941    let mut cmd = Command::new("git");
942    cmd.current_dir(std::env::current_dir()?);
943    cmd.arg("commit");
944    
945    if amend {
946        cmd.arg("--amend");
947    }
948    
949    cmd.arg("-m").arg(content);
950    
951    let status = cmd.status()?;
952
953    // 清理环境变量(无论命令是否执行成功)
954    std::env::remove_var("GIT_COMMIT_HELPER_SKIP_REVIEW");
955    std::env::remove_var("GIT_COMMIT_HELPER_NO_TRANSLATE");
956
957    if !status.success() {
958        let action = if amend { "修改提交" } else { "提交" };
959        return Err(anyhow::anyhow!("git commit 命令执行失败,{} 失败", action));
960    }
961
962    if amend {
963        println!("修改提交成功!");
964    } else {
965        println!("提交成功!");
966    }
967    Ok(())
968}
969
970#[allow(dead_code)]
971pub async fn generate_commit_suggestion(commit_types: &[String], user_description: Option<String>) -> anyhow::Result<String> {
972    let config = crate::config::Config::load()?;
973    let service = config.services.iter()
974        .find(|s| s.service == config.default_service)
975        .ok_or_else(|| anyhow::anyhow!("找不到默认服务的配置"))?;
976
977    let translator = ai_service::create_translator_for_service(service).await?;
978    let prompt = match user_description {
979        Some(desc) => format!("用户描述:\n{}\n\n改动内容:\n{}", desc, get_staged_diff()?),
980        None => get_staged_diff()?
981    };
982
983    let message = translator.translate(&prompt, &config::TranslateDirection::ChineseToEnglish).await?.to_string();
984
985    // 移除各种 AI 返回的元信息标记
986    let message = message
987        .trim_start_matches("[NO_TRANSLATE]")
988        .trim_start_matches("plaintext")
989        .trim()
990        .to_string();
991
992    // 如果有指定的提交类型,确保使用这些类型
993    if !commit_types.is_empty() {
994        return Ok(ensure_commit_type(&message, commit_types));
995    }
996
997    Ok(message)
998}
999
1000fn get_staged_diff() -> anyhow::Result<String> {
1001    let output = Command::new("git")
1002        .args(["diff", "--cached", "--no-prefix"])
1003        .output()?;
1004
1005    if !output.status.success() {
1006        return Err(anyhow::anyhow!("执行 git diff 命令失败"));
1007    }
1008
1009    Ok(String::from_utf8(output.stdout)?)
1010}
1011
1012fn ensure_commit_type(message: &str, commit_types: &[String]) -> String {
1013    let first_line = message.lines().next().unwrap_or_default();
1014
1015    if let Some(colon_pos) = first_line.find(':') {
1016        let current_type = first_line[..colon_pos].trim();
1017        if !commit_types.contains(&current_type.to_string()) {
1018            return format!("{}: {}",
1019                &commit_types[0],
1020                first_line[colon_pos + 1..].trim()
1021            ) + &message[first_line.len()..];
1022        }
1023    } else {
1024        return format!("{}: {}", &commit_types[0], first_line) + &message[first_line.len()..];
1025    }
1026
1027    message.to_string()
1028}
1029
1030#[cfg(test)]
1031mod tests {
1032    use super::*;
1033
1034    #[test]
1035    fn test_parse_github_issue_url() {
1036        let url = "https://github.com/zccrs/git-commit-helper/issues/123";
1037        let result = parse_github_issue(url).unwrap();
1038        // 由于无法在测试环境中获取git remote,这里只测试格式解析
1039        assert!(result.contains("#123") || result.contains("zccrs/git-commit-helper#123"));
1040    }
1041
1042    #[test]
1043    fn test_parse_pms_bug_link() {
1044        let url = "https://pms.uniontech.com/bug-view-320461.html";
1045        let result = parse_pms_link(url).unwrap();
1046        assert_eq!(result, "PMS: BUG-320461");
1047    }
1048
1049    #[test]
1050    fn test_parse_pms_task_link() {
1051        let url = "https://pms.uniontech.com/task-view-374223.html";
1052        let result = parse_pms_link(url).unwrap();
1053        assert_eq!(result, "PMS: TASK-374223");
1054    }
1055
1056    #[test]
1057    fn test_parse_pms_story_link() {
1058        let url = "https://pms.uniontech.com/story-view-38949.html";
1059        let result = parse_pms_link(url).unwrap();
1060        assert_eq!(result, "PMS: STORY-38949");
1061    }
1062
1063    #[test]
1064    fn test_parse_issue_number() {
1065        let issue = "123";
1066        let result = parse_issue_reference(issue).unwrap();
1067        assert_eq!(result, "Fixes: #123");
1068    }
1069
1070    #[test]
1071    fn test_parse_invalid_format() {
1072        let invalid = "invalid-format";
1073        let result = parse_issue_reference(invalid);
1074        assert!(result.is_err());
1075    }
1076
1077    #[test]
1078    fn test_parse_multiple_issues() {
1079        let issues = "123 456 https://github.com/owner/repo/issues/789";
1080        let result = parse_issue_reference(issues).unwrap();
1081        assert!(result.contains("Fixes: #123 #456"));
1082        assert!(result.contains("owner/repo#789") || result.contains("#789"));
1083    }
1084
1085    #[test]
1086    fn test_parse_multiple_pms_links() {
1087        let issues = "https://pms.uniontech.com/bug-view-320461.html https://pms.uniontech.com/task-view-374223.html https://pms.uniontech.com/story-view-38949.html";
1088        let result = parse_pms_link_multiple(issues).unwrap();
1089        assert_eq!(result, "PMS: BUG-320461 TASK-374223 STORY-38949");
1090    }
1091
1092    #[test]
1093    fn test_parse_mixed_issues_and_pms() {
1094        let issues = "123 https://pms.uniontech.com/bug-view-320461.html https://github.com/owner/repo/issues/456";
1095        let result = parse_issue_reference(issues).unwrap();
1096
1097        // 结果应该包含两行,分别是 Fixes 和 PMS
1098        let lines: Vec<&str> = result.lines().collect();
1099        assert_eq!(lines.len(), 2);
1100
1101        let fixes_line = lines.iter().find(|&&line| line.starts_with("Fixes:")).unwrap();
1102        let pms_line = lines.iter().find(|&&line| line.starts_with("PMS:")).unwrap();
1103
1104        assert!(fixes_line.contains("#123"));
1105        assert!(fixes_line.contains("owner/repo#456") || fixes_line.contains("#456"));
1106        assert!(pms_line.contains("BUG-320461"));
1107    }
1108
1109    #[test]
1110    fn test_parse_comma_separated_issues() {
1111        let issues = "123,456,789";
1112        let result = parse_issue_reference(issues).unwrap();
1113        assert_eq!(result, "Fixes: #123 #456 #789");
1114    }
1115
1116    #[test]
1117    fn test_parse_mixed_separators() {
1118        let issues = "123 456,789 https://pms.uniontech.com/task-view-374223.html";
1119        let result = parse_issue_reference(issues).unwrap();
1120
1121        let lines: Vec<&str> = result.lines().collect();
1122        assert_eq!(lines.len(), 2);
1123
1124        let fixes_line = lines.iter().find(|&&line| line.starts_with("Fixes:")).unwrap();
1125        let pms_line = lines.iter().find(|&&line| line.starts_with("PMS:")).unwrap();
1126
1127        assert_eq!(*fixes_line, "Fixes: #123 #456 #789");
1128        assert_eq!(*pms_line, "PMS: TASK-374223");
1129    }
1130
1131    // 辅助测试函数
1132    fn parse_pms_link_multiple(issues: &str) -> anyhow::Result<String> {
1133        parse_issue_reference(issues)
1134    }
1135
1136    #[test]
1137    fn test_extract_change_id() {
1138        let message = "feat: add new feature\n\nThis is a commit message\n\nChange-Id: I1234567890abcdef1234567890abcdef12345678\n";
1139        let change_id = extract_change_id(message);
1140        assert_eq!(change_id, Some("I1234567890abcdef1234567890abcdef12345678".to_string()));
1141    }
1142
1143    #[test]
1144    fn test_extract_change_id_not_found() {
1145        let message = "feat: add new feature\n\nThis is a commit message without change id\n";
1146        let change_id = extract_change_id(message);
1147        assert_eq!(change_id, None);
1148    }
1149
1150    #[test]
1151    fn test_append_change_id() {
1152        let message = "feat: add new feature\n\nThis is a commit message\n";
1153        let change_id = "I1234567890abcdef1234567890abcdef12345678";
1154        let result = append_change_id(message, change_id);
1155        assert!(result.contains("Change-Id: I1234567890abcdef1234567890abcdef12345678"));
1156    }
1157
1158    #[test]
1159    fn test_append_change_id_already_exists() {
1160        let message = "feat: add new feature\n\nThis is a commit message\n\nChange-Id: I1234567890abcdef1234567890abcdef12345678\n";
1161        let change_id = "I1234567890abcdef1234567890abcdef12345678";
1162        let result = append_change_id(message, change_id);
1163        // 应该返回原消息,不重复添加
1164        assert_eq!(result, message);
1165    }
1166
1167    #[test]
1168    fn test_append_change_id_with_other_marks() {
1169        let message = "feat: add new feature\n\nThis is a commit message\n\nFixes: #123\nPMS: BUG-456\n";
1170        let change_id = "I1234567890abcdef1234567890abcdef12345678";
1171        let result = append_change_id(message, change_id);
1172        assert!(result.contains("Fixes: #123"));
1173        assert!(result.contains("PMS: BUG-456"));
1174        assert!(result.contains("Change-Id: I1234567890abcdef1234567890abcdef12345678"));
1175        // Change-Id 应该在最后
1176        assert!(result.ends_with("Change-Id: I1234567890abcdef1234567890abcdef12345678"));
1177    }
1178
1179    // 测试从原提交消息中提取所有需保留的标记(排除 Change-Id)
1180    #[test]
1181    fn test_extract_issue_marks_from_original_commit() {
1182        let original = "fix: some bug\n\nDescription here\n\nFixes: #123\nPMS: BUG-456\nLog: Fix critical bug\nChange-Id: Iabc123\n";
1183        let orig_commit = CommitMessage::parse(original);
1184        let marks_to_preserve: Vec<String> = orig_commit.marks.iter()
1185            .filter(|mark| {
1186                let mark_key = mark.split(':').next().unwrap_or("").trim().to_lowercase();
1187                mark_key != "change-id"
1188            })
1189            .cloned()
1190            .collect();
1191        assert_eq!(marks_to_preserve.len(), 3);
1192        assert!(marks_to_preserve.contains(&"Fixes: #123".to_string()));
1193        assert!(marks_to_preserve.contains(&"PMS: BUG-456".to_string()));
1194        assert!(marks_to_preserve.contains(&"Log: Fix critical bug".to_string()));
1195        // Change-Id 不应被保留(由 append_change_id 单独处理)
1196        assert!(!marks_to_preserve.iter().any(|m| m.to_lowercase().starts_with("change-id:")));
1197    }
1198
1199    // 测试 amend 时新内容已包含标记时不重复添加
1200    #[test]
1201    fn test_amend_no_duplicate_issue_marks() {
1202        let original = "fix: some bug\n\nFixes: #123\nLog: Fix bug\n";
1203        let orig_commit = CommitMessage::parse(original);
1204
1205        // 新内容已包含 Fixes: 和 Log:
1206        let new_content = "fix: improved bug fix\n\nBetter description\n\nFixes: #123\nLog: Fix bug";
1207        let marks_to_add: Vec<String> = orig_commit.marks.iter()
1208            .filter(|mark| {
1209                let mark_key = mark.split(':').next().unwrap_or("").trim().to_lowercase();
1210                mark_key != "change-id" && !new_content.lines().any(|line| {
1211                    line.trim().split(':').next()
1212                        .map_or(false, |k| k.trim().to_lowercase() == mark_key)
1213                })
1214            })
1215            .cloned()
1216            .collect();
1217        // 不应重复添加
1218        assert!(marks_to_add.is_empty());
1219    }
1220
1221    // 测试 amend 时新内容不含标记时正确添加所有标记
1222    #[test]
1223    fn test_amend_preserves_issue_marks_when_missing() {
1224        let original = "fix: some bug\n\nFixes: #123\nPMS: BUG-456\nLog: Fix critical bug\nChange-Id: Iabc123\n";
1225        let orig_commit = CommitMessage::parse(original);
1226
1227        // 新内容不含任何标记
1228        let new_content = "fix: improved bug fix\n\nBetter description";
1229        let marks_to_add: Vec<String> = orig_commit.marks.iter()
1230            .filter(|mark| {
1231                let mark_key = mark.split(':').next().unwrap_or("").trim().to_lowercase();
1232                mark_key != "change-id" && !new_content.lines().any(|line| {
1233                    line.trim().split(':').next()
1234                        .map_or(false, |k| k.trim().to_lowercase() == mark_key)
1235                })
1236            })
1237            .cloned()
1238            .collect();
1239        assert_eq!(marks_to_add.len(), 3);
1240        assert!(marks_to_add.contains(&"Fixes: #123".to_string()));
1241        assert!(marks_to_add.contains(&"PMS: BUG-456".to_string()));
1242        assert!(marks_to_add.contains(&"Log: Fix critical bug".to_string()));
1243        // Change-Id 不应被包含(由 append_change_id 处理)
1244        assert!(!marks_to_add.iter().any(|m| m.to_lowercase().starts_with("change-id:")));
1245    }
1246}