1use serde::Serialize;
2
3#[derive(Debug, Clone, Serialize)]
4pub struct CommitPlan {
5 pub message: String,
6 pub staged_stat: String,
7 pub secret_findings: Vec<String>,
8}
9
10pub fn build_commit_plan(
11 message: Option<String>,
12 staged_stat: String,
13 staged_diff: &str,
14) -> CommitPlan {
15 let secret_findings = scan_diff_for_secret_patterns(staged_diff);
16 let message = message.unwrap_or_else(|| generated_message(&staged_stat));
17 CommitPlan {
18 message,
19 staged_stat,
20 secret_findings,
21 }
22}
23
24pub fn scan_diff_for_secret_patterns(diff: &str) -> Vec<String> {
25 let mut findings = Vec::new();
26 for (idx, line) in diff.lines().enumerate() {
27 if !line.starts_with('+') || line.starts_with("+++") {
28 continue;
29 }
30 let lower = line.to_ascii_lowercase();
31 let suspicious = lower.contains("api_key")
32 || lower.contains("secret")
33 || lower.contains("token")
34 || line.contains("sk-")
35 || line.contains("ghp_")
36 || line.contains("xoxb-")
37 || line.contains("AKIA");
38 if suspicious {
39 findings.push(format!("line {}: {}", idx + 1, line.trim()));
40 }
41 }
42 findings
43}
44
45fn generated_message(staged_stat: &str) -> String {
46 if staged_stat.contains("docs/") || staged_stat.contains("README") {
47 "docs: update Sparrow release materials".into()
48 } else if staged_stat.contains("Cargo.toml") || staged_stat.contains("Cargo.lock") {
49 "chore: update Sparrow workspace configuration".into()
50 } else if staged_stat.contains("console.html") {
51 "feat: improve Sparrow console experience".into()
52 } else {
53 "chore: update Sparrow".into()
54 }
55}
56
57#[cfg(test)]
58mod tests {
59 use super::*;
60
61 #[test]
62 fn commit_plan_detects_secret_like_added_lines() {
63 let plan = build_commit_plan(
64 None,
65 "Cargo.toml | 2 +".into(),
66 "+OPENAI_API_KEY=sk-test\n context\n",
67 );
68 assert_eq!(
69 plan.message,
70 "chore: update Sparrow workspace configuration"
71 );
72 assert_eq!(plan.secret_findings.len(), 1);
73 }
74}