fix(flywheel): 修复 commit_metrics project_id 双路径冲突,表格能显示所有项目
blueprint_buffs 用 UUID 写 commit_metrics,全量导入用名称字符串写, 同一 sha 在 sha PRIMARY KEY 约束下只能存一条,导致全量导入被 INSERT OR IGNORE 全部跳过, 表格无法查到 UUID 格式的历史数据。 修复点: - ingest_full_git_history 自动查 blueprint_buffs,若该路径已有 UUID project_id 则优先用 UUID - 新增 get_all_commit_health 命令,SQL 关联 blueprint_buffs / projects 解析名称,不需要前端传 project_ids - FlywheelPanel 切换到 getAllCommitHealth,表格直接用后端返回的 project_name,无需前端 lookup
This commit is contained in:
parent
78d4ad1f4b
commit
d41e0f1707
@ -193,6 +193,7 @@ fn read_git_log(root: &std::path::Path, limit: usize) -> Result<Vec<(String, Str
|
||||
|
||||
/// 全量历史导入:扫描最近 500 条 commit,幂等写入 commit_metrics 表。
|
||||
/// 与 buff 的增量写入互不干扰(同 sha INSERT OR IGNORE)。
|
||||
/// 若该路径在 blueprint_buffs 中已有 UUID project_id,优先用 UUID,保持与增量写入一致。
|
||||
#[tauri::command]
|
||||
pub fn ingest_full_git_history(
|
||||
project_id: String,
|
||||
@ -206,6 +207,16 @@ pub fn ingest_full_git_history(
|
||||
let total = commits.len() as u32;
|
||||
|
||||
let conn = crate::db::pool().get().map_err(|e| e.to_string())?;
|
||||
|
||||
// 优先用 blueprint_buffs 里的 UUID project_id,避免同一 sha 用两个不同 project_id 冲突
|
||||
let effective_id = conn
|
||||
.query_row(
|
||||
"SELECT project_id FROM blueprint_buffs WHERE project_path = ?1",
|
||||
rusqlite::params![project_path],
|
||||
|r| r.get::<_, String>(0),
|
||||
)
|
||||
.unwrap_or(project_id);
|
||||
|
||||
let tx = conn.unchecked_transaction().map_err(|e| e.to_string())?;
|
||||
let mut imported = 0u32;
|
||||
for (sha, subject, date) in &commits {
|
||||
@ -215,7 +226,7 @@ pub fn ingest_full_git_history(
|
||||
(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,
|
||||
sha, effective_id, p.commit_type, p.scope,
|
||||
p.is_rework as i32, p.is_ci_auto as i32,
|
||||
if date.is_empty() { None } else { Some(date.as_str()) },
|
||||
],
|
||||
@ -334,6 +345,97 @@ pub fn get_project_commit_health(
|
||||
project_commit_health_inner(&conn, &project_ids)
|
||||
}
|
||||
|
||||
/// commit_metrics 中所有项目的健康度(含项目名解析)。
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct CommitHealthWithName {
|
||||
pub project_id: String,
|
||||
/// 解析出的项目名:优先从 projects 表直接匹配,其次通过 blueprint_buffs 路径关联 projects,再次用路径末段
|
||||
pub project_name: String,
|
||||
pub total_commits: u32,
|
||||
pub conventional_commits: u32,
|
||||
pub conventional_rate: f64,
|
||||
pub rework_commits: u32,
|
||||
pub rework_rate: f64,
|
||||
pub top_scopes: Vec<ScopeActivity>,
|
||||
}
|
||||
|
||||
/// 查询 commit_metrics 里所有有数据的项目,自动关联名称。无需前端传 project_ids。
|
||||
#[tauri::command]
|
||||
pub fn get_all_commit_health() -> Result<Vec<CommitHealthWithName>, String> {
|
||||
let conn = crate::db::pool().get().map_err(|e| e.to_string())?;
|
||||
let round3 = |v: f64| (v * 1000.0).round() / 1000.0;
|
||||
|
||||
// 聚合各 project_id 的指标,并通过两条路径解析名称
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT
|
||||
cm.project_id,
|
||||
COALESCE(p_direct.name, p_via_buff.name, bb.project_path, cm.project_id) AS name,
|
||||
COUNT(*) AS total,
|
||||
SUM(CASE WHEN cm.commit_type IS NOT NULL THEN 1 ELSE 0 END) AS conventional,
|
||||
SUM(CASE WHEN cm.is_rework=1 AND cm.is_ci_auto=0 THEN 1 ELSE 0 END) AS rework
|
||||
FROM commit_metrics cm
|
||||
LEFT JOIN projects p_direct ON p_direct.id = cm.project_id
|
||||
LEFT JOIN blueprint_buffs bb ON bb.project_id = cm.project_id
|
||||
LEFT JOIN projects p_via_buff
|
||||
ON p_via_buff.win_path = bb.project_path
|
||||
OR p_via_buff.wsl_path = bb.project_path
|
||||
GROUP BY cm.project_id
|
||||
ORDER BY total DESC",
|
||||
).map_err(|e| e.to_string())?;
|
||||
|
||||
let rows: Vec<(String, String, u32, u32, u32)> = stmt
|
||||
.query_map([], |row| {
|
||||
Ok((
|
||||
row.get::<_, String>(0)?,
|
||||
row.get::<_, String>(1)?,
|
||||
row.get::<_, i64>(2)? as u32,
|
||||
row.get::<_, i64>(3)? as u32,
|
||||
row.get::<_, i64>(4)? as u32,
|
||||
))
|
||||
})
|
||||
.map_err(|e| e.to_string())?
|
||||
.filter_map(|r| r.ok())
|
||||
.collect();
|
||||
|
||||
let mut result = Vec::with_capacity(rows.len());
|
||||
for (pid, name, total, conventional, rework) in rows {
|
||||
// 查 top_scopes
|
||||
let mut scope_stmt = conn.prepare(
|
||||
"SELECT scope,
|
||||
COUNT(*) AS cnt,
|
||||
SUM(CASE WHEN is_rework=1 AND is_ci_auto=0 THEN 1 ELSE 0 END) AS rc
|
||||
FROM commit_metrics
|
||||
WHERE project_id = ?1 AND scope IS NOT NULL
|
||||
GROUP BY scope
|
||||
ORDER BY cnt DESC
|
||||
LIMIT 5",
|
||||
).map_err(|e| e.to_string())?;
|
||||
let top_scopes: Vec<ScopeActivity> = scope_stmt
|
||||
.query_map(rusqlite::params![pid], |row| {
|
||||
Ok(ScopeActivity {
|
||||
scope: row.get(0)?,
|
||||
commit_count: row.get::<_, i64>(1)? as u32,
|
||||
rework_count: row.get::<_, i64>(2)? as u32,
|
||||
})
|
||||
})
|
||||
.map_err(|e| e.to_string())?
|
||||
.filter_map(|r| r.ok())
|
||||
.collect();
|
||||
|
||||
result.push(CommitHealthWithName {
|
||||
conventional_rate: round3(if total > 0 { conventional as f64 / total as f64 } else { 0.0 }),
|
||||
rework_rate: round3(if conventional > 0 { rework as f64 / conventional as f64 } else { 0.0 }),
|
||||
project_id: pid,
|
||||
project_name: name,
|
||||
total_commits: total,
|
||||
conventional_commits: conventional,
|
||||
rework_commits: rework,
|
||||
top_scopes,
|
||||
});
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@ -275,6 +275,7 @@ pub fn run() {
|
||||
commands::commit_metrics::get_commit_stats,
|
||||
commands::commit_metrics::ingest_full_git_history,
|
||||
commands::commit_metrics::get_project_commit_health,
|
||||
commands::commit_metrics::get_all_commit_health,
|
||||
// mentor
|
||||
get_mentor_context,
|
||||
get_project_notes,
|
||||
|
||||
@ -43,9 +43,9 @@ import {
|
||||
type TemplateResult,
|
||||
type CommitStats,
|
||||
type UserConventionsStatus,
|
||||
type ProjectCommitHealth,
|
||||
type CommitHealthWithName,
|
||||
ingestFullGitHistory,
|
||||
getProjectCommitHealth,
|
||||
getAllCommitHealth,
|
||||
} from "../../lib/commands";
|
||||
|
||||
interface Props {
|
||||
@ -1680,12 +1680,9 @@ function FlywheelPanel({ onClose }: { onClose: () => void }) {
|
||||
return { projectPaths: paths, projectsMap: map, projectsWithPath: withPath };
|
||||
}, [projects]);
|
||||
|
||||
const projectIds = useMemo(() => projectsWithPath.map((p) => p.id), [projectsWithPath]);
|
||||
|
||||
const { data: commitHealth, refetch: refetchHealth } = useQuery<ProjectCommitHealth[]>({
|
||||
queryKey: ["commit-health", projectIds],
|
||||
queryFn: () => getProjectCommitHealth(projectIds),
|
||||
enabled: projectIds.length > 0,
|
||||
const { data: commitHealth, refetch: refetchHealth } = useQuery<CommitHealthWithName[]>({
|
||||
queryKey: ["commit-health-all"],
|
||||
queryFn: () => getAllCommitHealth(),
|
||||
});
|
||||
|
||||
const [ingesting, setIngesting] = useState(false);
|
||||
@ -1956,11 +1953,10 @@ function FlywheelPanel({ onClose }: { onClose: () => void }) {
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-50">
|
||||
{commitHealth.map((h) => {
|
||||
const proj = projectsWithPath.find((p) => p.id === h.project_id);
|
||||
return (
|
||||
<tr key={h.project_id} className="hover:bg-gray-50">
|
||||
<td className="py-1 text-gray-700 truncate max-w-[80px]" title={proj?.name}>
|
||||
{proj?.name ?? h.project_id}
|
||||
<td className="py-1 text-gray-700 truncate max-w-[80px]" title={h.project_name}>
|
||||
{h.project_name}
|
||||
</td>
|
||||
<td className="py-1 text-right font-mono text-gray-500">{h.total_commits}</td>
|
||||
<td className={`py-1 text-right font-mono font-semibold ${h.conventional_rate >= 0.8 ? "text-green-600" : h.conventional_rate >= 0.5 ? "text-amber-500" : "text-red-500"}`}>
|
||||
|
||||
@ -1714,6 +1714,13 @@ export interface ProjectCommitHealth {
|
||||
export const getProjectCommitHealth = (projectIds: string[]) =>
|
||||
invoke<ProjectCommitHealth[]>("get_project_commit_health", { projectIds });
|
||||
|
||||
export interface CommitHealthWithName extends ProjectCommitHealth {
|
||||
project_name: string;
|
||||
}
|
||||
|
||||
export const getAllCommitHealth = () =>
|
||||
invoke<CommitHealthWithName[]>("get_all_commit_health");
|
||||
|
||||
// ── Beast Global Status (read-only) ───────────────────────────────────────────
|
||||
|
||||
export interface BeastGlobalStatus {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user