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 表。
|
/// 全量历史导入:扫描最近 500 条 commit,幂等写入 commit_metrics 表。
|
||||||
/// 与 buff 的增量写入互不干扰(同 sha INSERT OR IGNORE)。
|
/// 与 buff 的增量写入互不干扰(同 sha INSERT OR IGNORE)。
|
||||||
|
/// 若该路径在 blueprint_buffs 中已有 UUID project_id,优先用 UUID,保持与增量写入一致。
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn ingest_full_git_history(
|
pub fn ingest_full_git_history(
|
||||||
project_id: String,
|
project_id: String,
|
||||||
@ -206,6 +207,16 @@ pub fn ingest_full_git_history(
|
|||||||
let total = commits.len() as u32;
|
let total = commits.len() as u32;
|
||||||
|
|
||||||
let conn = crate::db::pool().get().map_err(|e| e.to_string())?;
|
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 tx = conn.unchecked_transaction().map_err(|e| e.to_string())?;
|
||||||
let mut imported = 0u32;
|
let mut imported = 0u32;
|
||||||
for (sha, subject, date) in &commits {
|
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)
|
(sha, project_id, commit_type, scope, is_rework, is_ci_auto, committed_at)
|
||||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
||||||
rusqlite::params![
|
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,
|
p.is_rework as i32, p.is_ci_auto as i32,
|
||||||
if date.is_empty() { None } else { Some(date.as_str()) },
|
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)
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@ -275,6 +275,7 @@ pub fn run() {
|
|||||||
commands::commit_metrics::get_commit_stats,
|
commands::commit_metrics::get_commit_stats,
|
||||||
commands::commit_metrics::ingest_full_git_history,
|
commands::commit_metrics::ingest_full_git_history,
|
||||||
commands::commit_metrics::get_project_commit_health,
|
commands::commit_metrics::get_project_commit_health,
|
||||||
|
commands::commit_metrics::get_all_commit_health,
|
||||||
// mentor
|
// mentor
|
||||||
get_mentor_context,
|
get_mentor_context,
|
||||||
get_project_notes,
|
get_project_notes,
|
||||||
|
|||||||
@ -43,9 +43,9 @@ import {
|
|||||||
type TemplateResult,
|
type TemplateResult,
|
||||||
type CommitStats,
|
type CommitStats,
|
||||||
type UserConventionsStatus,
|
type UserConventionsStatus,
|
||||||
type ProjectCommitHealth,
|
type CommitHealthWithName,
|
||||||
ingestFullGitHistory,
|
ingestFullGitHistory,
|
||||||
getProjectCommitHealth,
|
getAllCommitHealth,
|
||||||
} from "../../lib/commands";
|
} from "../../lib/commands";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -1680,12 +1680,9 @@ function FlywheelPanel({ onClose }: { onClose: () => void }) {
|
|||||||
return { projectPaths: paths, projectsMap: map, projectsWithPath: withPath };
|
return { projectPaths: paths, projectsMap: map, projectsWithPath: withPath };
|
||||||
}, [projects]);
|
}, [projects]);
|
||||||
|
|
||||||
const projectIds = useMemo(() => projectsWithPath.map((p) => p.id), [projectsWithPath]);
|
const { data: commitHealth, refetch: refetchHealth } = useQuery<CommitHealthWithName[]>({
|
||||||
|
queryKey: ["commit-health-all"],
|
||||||
const { data: commitHealth, refetch: refetchHealth } = useQuery<ProjectCommitHealth[]>({
|
queryFn: () => getAllCommitHealth(),
|
||||||
queryKey: ["commit-health", projectIds],
|
|
||||||
queryFn: () => getProjectCommitHealth(projectIds),
|
|
||||||
enabled: projectIds.length > 0,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [ingesting, setIngesting] = useState(false);
|
const [ingesting, setIngesting] = useState(false);
|
||||||
@ -1956,11 +1953,10 @@ function FlywheelPanel({ onClose }: { onClose: () => void }) {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-50">
|
<tbody className="divide-y divide-gray-50">
|
||||||
{commitHealth.map((h) => {
|
{commitHealth.map((h) => {
|
||||||
const proj = projectsWithPath.find((p) => p.id === h.project_id);
|
|
||||||
return (
|
return (
|
||||||
<tr key={h.project_id} className="hover:bg-gray-50">
|
<tr key={h.project_id} className="hover:bg-gray-50">
|
||||||
<td className="py-1 text-gray-700 truncate max-w-[80px]" title={proj?.name}>
|
<td className="py-1 text-gray-700 truncate max-w-[80px]" title={h.project_name}>
|
||||||
{proj?.name ?? h.project_id}
|
{h.project_name}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-1 text-right font-mono text-gray-500">{h.total_commits}</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"}`}>
|
<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[]) =>
|
export const getProjectCommitHealth = (projectIds: string[]) =>
|
||||||
invoke<ProjectCommitHealth[]>("get_project_commit_health", { projectIds });
|
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) ───────────────────────────────────────────
|
// ── Beast Global Status (read-only) ───────────────────────────────────────────
|
||||||
|
|
||||||
export interface BeastGlobalStatus {
|
export interface BeastGlobalStatus {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user