dev-manager-tauri/src-tauri/src/commands/commit_metrics.rs
lanrtop 58d2c79c74 feat(onboarding): 接入包数据可信度提示(commit 规范化比例)
get_commit_stats 暴露项目 commit 规范化统计(total/conventional/rework);治理面板常驻展示规范化比例 + 返工数,比例 <60% 提示种 lefthook。接入包 A/B/C1/C2/D 全部完成。
2026-06-19 13:52:09 +09:00

253 lines
8.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Conventional Commit 解析与执行质量指标(飞轮 L0 数据地基)。
//!
//! 从 git commit message 解析出 type/scope/返工/CI 自修信号,写入 commit_metrics 表,
//! 取代枯竭的「复盘笔记解析」路径。纯解析逻辑(`parse_conventional`)与 DB 无关,可独立单测。
use rusqlite::Connection;
use serde::{Deserialize, Serialize};
/// 一条 commit 的解析结果。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedCommit {
/// conventional typefeat/fix/chore...None 表示非 conventional commit。
pub commit_type: Option<String>,
/// scope如 estate1/cadNone 表示无 scope。
pub scope: Option<String>,
/// 是否返工type == fix。
pub is_rework: bool,
/// 是否 CI 自动修复(含 [DeepSeek-V3]/[R1] 等标记),聚合返工率时剔除。
pub is_ci_auto: bool,
}
/// CI 自动修复 commit 的标记(来自 enterprise-system CI`fix(ci): auto-fix ... [DeepSeek-V3]`)。
const CI_AUTO_MARKERS: [&str; 3] = ["[DeepSeek", "[R1]", "[V3]"];
/// 解析一条 commit message 为 Conventional Commit 结构。
///
/// 纯函数,不触碰 DB。无法识别为 conventional 的merge commit、随手写的
/// `commit_type` 记为 None聚合时单列、不污染 fix 率。
pub fn parse_conventional(message: &str) -> ParsedCommit {
let is_ci_auto = CI_AUTO_MARKERS.iter().any(|m| message.contains(m));
// 只看首行subject
let subject = message.lines().next().unwrap_or("").trim();
// 必须有 "<prefix>: <desc>" 结构
let prefix = match subject.split_once(':') {
Some((p, _desc)) => p.trim(),
None => return non_conventional(is_ci_auto),
};
// 去掉 breaking change 的末尾 '!'
let prefix = prefix.trim_end_matches('!');
// 拆出 type 与可选 scopetype(scope)
let (typ, scope) = match prefix.split_once('(') {
Some((t, rest)) => {
let scope = rest
.strip_suffix(')')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
(t.trim(), scope)
}
None => (prefix, None),
};
// type 必须是纯小写字母feat/fix/...),否则视为非 conventional如 "Merge pull request"
if typ.is_empty() || !typ.chars().all(|c| c.is_ascii_lowercase()) {
return non_conventional(is_ci_auto);
}
ParsedCommit {
is_rework: typ == "fix",
commit_type: Some(typ.to_string()),
scope,
is_ci_auto,
}
}
fn non_conventional(is_ci_auto: bool) -> ParsedCommit {
ParsedCommit {
commit_type: None,
scope: None,
is_rework: false,
is_ci_auto,
}
}
/// 解析并写入一条 commit 记录sha 为主键,重复写入忽略)。
pub fn record_commit(
conn: &Connection,
project_id: &str,
sha: &str,
message: &str,
committed_at: Option<&str>,
) -> Result<(), String> {
let p = parse_conventional(message);
conn.execute(
"INSERT OR IGNORE INTO commit_metrics
(sha, project_id, commit_type, scope, is_rework, is_ci_auto, committed_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
rusqlite::params![
sha,
project_id,
p.commit_type,
p.scope,
p.is_rework as i32,
p.is_ci_auto as i32,
committed_at,
],
)
.map_err(|e| e.to_string())?;
Ok(())
}
/// 项目的有效返工次数type=fix 且非 CI 自动修复。
pub fn rework_count(conn: &Connection, project_id: &str) -> Result<u32, String> {
conn.query_row(
"SELECT COUNT(*) FROM commit_metrics
WHERE project_id = ?1 AND is_rework = 1 AND is_ci_auto = 0",
rusqlite::params![project_id],
|r| r.get::<_, i64>(0),
)
.map(|n| n as u32)
.map_err(|e| e.to_string())
}
/// commit 规范化统计:(总数, conventional 数)。供数据可信度提示(卡 D使用。
pub fn conventional_stats(conn: &Connection, project_id: &str) -> Result<(u32, u32), String> {
let total: i64 = conn
.query_row(
"SELECT COUNT(*) FROM commit_metrics WHERE project_id = ?1",
rusqlite::params![project_id],
|r| r.get(0),
)
.map_err(|e| e.to_string())?;
let conv: i64 = conn
.query_row(
"SELECT COUNT(*) FROM commit_metrics WHERE project_id = ?1 AND commit_type IS NOT NULL",
rusqlite::params![project_id],
|r| r.get(0),
)
.map_err(|e| e.to_string())?;
Ok((total as u32, conv as u32))
}
/// 项目 commit 执行质量统计(供前端数据可信度展示)。
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CommitStats {
pub total: u32,
pub conventional: u32,
pub rework: u32,
}
/// 查询某项目的 commit 执行质量统计(总数 / conventional 数 / 有效返工数)。
#[tauri::command]
pub fn get_commit_stats(project_id: String) -> Result<CommitStats, String> {
let conn = crate::db::pool().get().map_err(|e| e.to_string())?;
let (total, conventional) = conventional_stats(&conn, &project_id)?;
let rework = rework_count(&conn, &project_id)?;
Ok(CommitStats {
total,
conventional,
rework,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_feat_with_scope() {
let p = parse_conventional("feat(hub/rpc): add shared type system");
assert_eq!(p.commit_type.as_deref(), Some("feat"));
assert_eq!(p.scope.as_deref(), Some("hub/rpc"));
assert!(!p.is_rework);
assert!(!p.is_ci_auto);
}
#[test]
fn parses_fix_as_rework() {
let p = parse_conventional("fix(estate1/view): correct sort order");
assert_eq!(p.commit_type.as_deref(), Some("fix"));
assert_eq!(p.scope.as_deref(), Some("estate1/view"));
assert!(p.is_rework);
assert!(!p.is_ci_auto);
}
#[test]
fn parses_type_without_scope() {
let p = parse_conventional("chore: update AGENTS.md");
assert_eq!(p.commit_type.as_deref(), Some("chore"));
assert_eq!(p.scope, None);
assert!(!p.is_rework);
}
#[test]
fn detects_ci_auto_fix() {
let p = parse_conventional("fix(ci): auto-fix type/lint errors [DeepSeek-V3]");
assert_eq!(p.commit_type.as_deref(), Some("fix"));
assert!(p.is_rework);
assert!(p.is_ci_auto, "应识别 [DeepSeek-V3] 为 CI 自修");
}
#[test]
fn breaking_change_marker_stripped() {
let p = parse_conventional("feat(api)!: drop legacy endpoint");
assert_eq!(p.commit_type.as_deref(), Some("feat"));
assert_eq!(p.scope.as_deref(), Some("api"));
}
#[test]
fn merge_commit_is_non_conventional() {
let p = parse_conventional("Merge pull request 'xxx' (#1) from dev into main");
assert_eq!(p.commit_type, None);
assert!(!p.is_rework);
}
#[test]
fn freeform_message_is_non_conventional() {
let p = parse_conventional("update stuff");
assert_eq!(p.commit_type, None);
assert_eq!(p.scope, None);
}
#[test]
fn uses_first_line_only() {
let p = parse_conventional("feat(x): title\n\nbody line with: colon");
assert_eq!(p.commit_type.as_deref(), Some("feat"));
assert_eq!(p.scope.as_deref(), Some("x"));
}
#[test]
fn rework_count_excludes_ci_auto_and_non_fix() {
let conn = crate::db::conn_with_schema();
record_commit(&conn, "proj", "sha1", "fix(a): real bug", None).unwrap();
record_commit(&conn, "proj", "sha2", "fix(ci): auto-fix [DeepSeek-V3]", None).unwrap();
record_commit(&conn, "proj", "sha3", "feat(a): feature", None).unwrap();
record_commit(&conn, "proj", "sha4", "update stuff", None).unwrap();
// 仅 sha1 算有效返工sha2 是 CI 自修、sha3 是 feat、sha4 非规范)
assert_eq!(rework_count(&conn, "proj").unwrap(), 1);
}
#[test]
fn record_commit_is_idempotent() {
let conn = crate::db::conn_with_schema();
record_commit(&conn, "proj", "dup", "fix: x", None).unwrap();
record_commit(&conn, "proj", "dup", "fix: x", None).unwrap();
let (total, _) = conventional_stats(&conn, "proj").unwrap();
assert_eq!(total, 1, "同 sha 重复写入应被忽略");
}
#[test]
fn conventional_stats_counts_ratio() {
let conn = crate::db::conn_with_schema();
record_commit(&conn, "proj", "s1", "feat: a", None).unwrap();
record_commit(&conn, "proj", "s2", "random text", None).unwrap();
let (total, conv) = conventional_stats(&conn, "proj").unwrap();
assert_eq!(total, 2);
assert_eq!(conv, 1);
}
}