新增: Git 能力闭环(Diff 行号解析/分支只读展示/提交详情/写入后自动刷新变更视图)

- Diff 行号修复:解析 @@ -a,b +c,d @@ 头,计算旧/新行号(原恒空字符串)
- 提交详情增强:显示父提交哈希/作者/日期/完整消息(git show -s --format=%P\t%an)
- 分支只读展示:list_branches IPC + 变更面板顶部下拉查看分支(切换走 AI 工具)
- AI 工具写入后自动跳转变更视图:监听 df-data-changed entity=file 切 Tab + 刷新
This commit is contained in:
2026-06-30 21:50:25 +08:00
parent f5f101d88a
commit 15eaa0c637
5 changed files with 142 additions and 6 deletions

View File

@@ -331,6 +331,62 @@ pub async fn get_module_git_status(
Ok(serde_json::to_value(status).map_err(err_str)?)
}
/// 列出工程本地分支(只读)。返回 { current, branches: [{ name, is_current }] }。
#[tauri::command]
pub async fn list_branches(
state: State<'_, AppState>,
module_id: String,
) -> Result<serde_json::Value, String> {
let module_id = module_id.trim().to_string();
if module_id.is_empty() {
return Err("module_id 不能为空".to_string());
}
let module = state
.project_modules
.get_by_id(&module_id)
.await
.map_err(err_str)?
.ok_or_else(|| format!("工程 {module_id} 不存在"))?;
let path = std::path::Path::new(&module.path);
if !path.join(".git").exists() {
return Ok(serde_json::json!({ "current": "", "branches": [] }));
}
let dir = module.path.clone();
let result = std::thread::spawn(move || -> (String, Vec<serde_json::Value>) {
// git branch --format="%(HEAD)%00%(refname:short)"
let out = std::process::Command::new("git")
.args(["branch", "--format=%(HEAD)%00%(refname:short)"])
.current_dir(&dir)
.env("LANG", "en_US.UTF-8")
.env("LC_ALL", "en_US.UTF-8")
.output()
.ok()
.and_then(|o| if o.status.success() { Some(String::from_utf8_lossy(&o.stdout).to_string()) } else { None })
.unwrap_or_default();
let mut current = String::new();
let mut branches = Vec::new();
for line in out.lines() {
let line = line.trim();
if line.is_empty() { continue; }
// 格式: "*\0branch_name" 或 "\0branch_name"
let (head, name) = line.split_once('\u{0}').unwrap_or(("", line));
let is_current = head.contains('*');
let name = name.trim().to_string();
if name.is_empty() { continue; }
if is_current { current = name.clone(); }
branches.push(serde_json::json!({ "name": name, "is_current": is_current }));
}
(current, branches)
})
.join()
.map_err(|_| "分支列表查询线程 Join 失败".to_string())?;
Ok(serde_json::json!({
"current": result.0,
"branches": result.1,
}))
}
/// 在指定目录跑 git 命令采集状态(当前分支 / 改动文件 / 最近提交)。
///
/// 超时:每个 git 子进程 10s(用户读操作,卡死不应阻塞 UI)。任一命令失败时返回已采集部分。
@@ -999,7 +1055,7 @@ pub async fn get_commit_detail(
return Ok(serde_json::json!({ "files": [], "diff": "" }));
}
let dir = module.path.clone();
let (files, diff) = std::thread::spawn(move || -> (Vec<serde_json::Value>, String) {
let (files, diff, parents, author, date, full_message) = std::thread::spawn(move || -> (Vec<serde_json::Value>, String, Vec<String>, String, String, String) {
// 1) 获取变更文件列表:`git diff-tree --no-commit-id -r --name-status <hash>`
let files_out = std::process::Command::new("git")
.args(["diff-tree", "--no-commit-id", "-r", "--name-status", &commit_hash])
@@ -1022,7 +1078,7 @@ pub async fn get_commit_detail(
}));
}
}
// 2) 获取全量 diff:`git show <hash>`
// 2) 获取全量 diff:`git show <hash>`(仅 diff 部分)
let diff = std::process::Command::new("git")
.args(["show", "--format=", &commit_hash])
.current_dir(&dir)
@@ -1032,7 +1088,38 @@ pub async fn get_commit_detail(
.ok()
.and_then(|o| if o.status.success() { Some(String::from_utf8_lossy(&o.stdout).to_string()) } else { None })
.unwrap_or_default();
(files, diff)
// 3) 获取提交元信息(父提交/作者/日期/完整消息):
// 用 printf 自定义格式,%P=父哈希(空格分隔多个)\t%an=作者\t%ad=日期\t%B=完整消息
let meta = std::process::Command::new("git")
.args(["show", "-s", "--format=%P\t%an\t%ad\t%B", &commit_hash])
.current_dir(&dir)
.env("LANG", "en_US.UTF-8")
.env("LC_ALL", "en_US.UTF-8")
.output()
.ok()
.and_then(|o| if o.status.success() { Some(String::from_utf8_lossy(&o.stdout).to_string()) } else { None })
.unwrap_or_default();
let mut parents = Vec::new();
let mut author = String::new();
let mut date = String::new();
let mut full_message = String::new();
if let Some(first_line) = meta.lines().next() {
let parts: Vec<&str> = first_line.splitn(4, '\t').collect();
if parts.len() >= 1 {
parents = parts[0].split_whitespace().map(|s| s.to_string()).collect();
}
if parts.len() >= 2 { author = parts[1].to_string(); }
if parts.len() >= 3 { date = parts[2].to_string(); }
if parts.len() >= 4 { full_message = parts[3].to_string(); }
}
// multi-line message: 补尾部行(meta.lines().next() 只取首行,需拼回)
if !full_message.is_empty() {
let rest = meta.lines().skip(1).collect::<Vec<_>>().join("\n");
if !rest.is_empty() {
full_message = format!("{}\n{}", full_message, rest);
}
}
(files, diff, parents, author, date, full_message)
})
.join()
.map_err(|_| "提交详情查询线程 Join 失败".to_string())?;
@@ -1040,5 +1127,9 @@ pub async fn get_commit_detail(
Ok(serde_json::json!({
"files": files,
"diff": diff,
"parents": parents,
"author": author,
"date": date,
"full_message": full_message,
}))
}

View File

@@ -326,6 +326,7 @@ pub fn run() {
commands::module::list_project_modules,
commands::module::scan_project_modules,
commands::module::get_module_git_status,
commands::module::list_branches,
// 工程文件浏览(Batch 10):文件树 + 单文件预览
commands::module::get_module_file_tree,
commands::module::read_module_file,