feat(git): 工作区 Git 同步——侧栏同步条、远程设置、定时同步(提交→变基拉取→推送,冲突停下不覆盖)(docs/27)
This commit is contained in:
@@ -625,3 +625,216 @@ pub async fn workspace_search(
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
// ---------- Git 同步(工作区文件夹) ----------
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GitStatusInfo {
|
||||
pub is_repo: bool,
|
||||
pub branch: Option<String>,
|
||||
pub remote_url: Option<String>,
|
||||
pub dirty_count: u32,
|
||||
pub ahead: u32,
|
||||
pub behind: u32,
|
||||
pub has_upstream: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GitSyncOutcome {
|
||||
pub committed: bool,
|
||||
pub pushed: bool,
|
||||
pub pulled: bool,
|
||||
pub summary: String,
|
||||
}
|
||||
|
||||
fn run_git(dir: &Path, args: &[&str]) -> Result<String, String> {
|
||||
let mut cmd = std::process::Command::new("git");
|
||||
cmd.args(args)
|
||||
.current_dir(dir)
|
||||
.env("GIT_TERMINAL_PROMPT", "0");
|
||||
// GUI 进程调控制台程序会闪黑窗,Windows 下隐藏
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
cmd.creation_flags(0x0800_0000);
|
||||
}
|
||||
let out = cmd
|
||||
.output()
|
||||
.map_err(|e| format!("无法运行 git(请确认已安装 Git):{e}"))?;
|
||||
if out.status.success() {
|
||||
Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string();
|
||||
if !stderr.is_empty() {
|
||||
return Err(stderr);
|
||||
}
|
||||
let stdout = String::from_utf8_lossy(&out.stdout).trim().to_string();
|
||||
if !stdout.is_empty() {
|
||||
return Err(stdout);
|
||||
}
|
||||
Err("git 命令执行失败".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn git_status_inner(root: &str) -> GitStatusInfo {
|
||||
let empty = GitStatusInfo {
|
||||
is_repo: false,
|
||||
branch: None,
|
||||
remote_url: None,
|
||||
dirty_count: 0,
|
||||
ahead: 0,
|
||||
behind: 0,
|
||||
has_upstream: false,
|
||||
};
|
||||
let dir = Path::new(root);
|
||||
if run_git(dir, &["rev-parse", "--is-inside-work-tree"]).ok().as_deref() != Some("true") {
|
||||
return empty;
|
||||
}
|
||||
// 工作区必须是仓库根:在父仓库的子目录里不接管,避免误提交别的仓库
|
||||
if let Ok(top) = run_git(dir, &["rev-parse", "--show-toplevel"]) {
|
||||
let norm = |p: &str| p.replace('\\', "/").trim_end_matches('/').to_lowercase();
|
||||
if norm(&top) != norm(root) {
|
||||
return empty;
|
||||
}
|
||||
} else {
|
||||
return empty;
|
||||
}
|
||||
let branch = run_git(dir, &["branch", "--show-current"]).ok().filter(|b| !b.is_empty());
|
||||
let remote_url = run_git(dir, &["remote", "get-url", "origin"]).ok().filter(|u| !u.is_empty());
|
||||
let dirty_count = run_git(dir, &["status", "--porcelain"])
|
||||
.map(|s| s.lines().filter(|l| !l.trim().is_empty()).count() as u32)
|
||||
.unwrap_or(0);
|
||||
let mut ahead = 0u32;
|
||||
let mut behind = 0u32;
|
||||
let has_upstream = run_git(dir, &["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"]).is_ok();
|
||||
if has_upstream {
|
||||
if let Ok(counts) = run_git(dir, &["rev-list", "--left-right", "--count", "@{upstream}...HEAD"]) {
|
||||
let mut it = counts.split_whitespace();
|
||||
behind = it.next().and_then(|v| v.parse().ok()).unwrap_or(0);
|
||||
ahead = it.next().and_then(|v| v.parse().ok()).unwrap_or(0);
|
||||
}
|
||||
}
|
||||
GitStatusInfo { is_repo: true, branch, remote_url, dirty_count, ahead, behind, has_upstream }
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn git_status(root: String) -> GitStatusInfo {
|
||||
tokio::task::spawn_blocking(move || git_status_inner(&root))
|
||||
.await
|
||||
.unwrap_or(GitStatusInfo {
|
||||
is_repo: false,
|
||||
branch: None,
|
||||
remote_url: None,
|
||||
dirty_count: 0,
|
||||
ahead: 0,
|
||||
behind: 0,
|
||||
has_upstream: false,
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn git_init_repo(root: String, remote_url: String, branch: String) -> Result<GitStatusInfo, String> {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let dir = Path::new(&root);
|
||||
let b = if branch.trim().is_empty() { "main" } else { branch.trim() };
|
||||
run_git(dir, &["init", "-b", b])?;
|
||||
if !remote_url.trim().is_empty() {
|
||||
run_git(dir, &["remote", "add", "origin", remote_url.trim()])?;
|
||||
}
|
||||
Ok(git_status_inner(&root))
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn git_set_remote(root: String, remote_url: String) -> Result<GitStatusInfo, String> {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let dir = Path::new(&root);
|
||||
let url = remote_url.trim();
|
||||
let has_origin = run_git(dir, &["remote"]).map(|r| r.lines().any(|l| l.trim() == "origin")).unwrap_or(false);
|
||||
if url.is_empty() {
|
||||
if has_origin {
|
||||
run_git(dir, &["remote", "remove", "origin"])?;
|
||||
}
|
||||
} else if has_origin {
|
||||
run_git(dir, &["remote", "set-url", "origin", url])?;
|
||||
} else {
|
||||
run_git(dir, &["remote", "add", "origin", url])?;
|
||||
}
|
||||
Ok(git_status_inner(&root))
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn git_sync_now(root: String, message: String) -> Result<GitSyncOutcome, String> {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let dir = Path::new(&root);
|
||||
let st = git_status_inner(&root);
|
||||
if !st.is_repo {
|
||||
return Err("该文件夹不是 Git 仓库".to_string());
|
||||
}
|
||||
let has_remote = st.remote_url.is_some();
|
||||
if has_remote {
|
||||
run_git(dir, &["fetch", "origin"]).map_err(|e| format!("拉取远程信息失败:{e}"))?;
|
||||
}
|
||||
// 先提交本地更改,再变基到远程之上,最后推送——避免脏工作区挡住 pull
|
||||
let mut committed = false;
|
||||
if st.dirty_count > 0 {
|
||||
run_git(dir, &["add", "-A"])?;
|
||||
let mut args: Vec<&str> = Vec::new();
|
||||
let email_missing = run_git(dir, &["config", "user.email"]).map(|v| v.is_empty()).unwrap_or(true);
|
||||
if email_missing {
|
||||
args.extend(["-c", "user.name=new-markdown", "-c", "user.email=new-markdown@localhost"]);
|
||||
}
|
||||
args.extend(["commit", "-m", message.as_str()]);
|
||||
run_git(dir, &args).map_err(|e| format!("提交失败:{e}"))?;
|
||||
committed = true;
|
||||
}
|
||||
let branch = st.branch.clone().unwrap_or_else(|| "main".to_string());
|
||||
let mut pulled = false;
|
||||
if has_remote {
|
||||
let remote_ref = format!("origin/{branch}");
|
||||
let remote_exists = run_git(dir, &["rev-parse", "--verify", &remote_ref]).is_ok();
|
||||
if remote_exists {
|
||||
let before = run_git(dir, &["rev-parse", "HEAD"]).unwrap_or_default();
|
||||
if let Err(e) = run_git(dir, &["rebase", &remote_ref]) {
|
||||
let _ = run_git(dir, &["rebase", "--abort"]);
|
||||
if e.contains("CONFLICT") || e.contains("could not apply") || e.contains("Cannot rebase") {
|
||||
return Err("同步冲突:本地与远程修改了相同内容,已取消本次同步,请在该文件夹手动处理冲突后重试。".to_string());
|
||||
}
|
||||
return Err(format!("合并远程更新失败:{e}"));
|
||||
}
|
||||
let after = run_git(dir, &["rev-parse", "HEAD"]).unwrap_or_default();
|
||||
pulled = before != after || st.behind > 0;
|
||||
}
|
||||
if st.has_upstream {
|
||||
run_git(dir, &["push"]).map_err(|e| format!("推送失败:{e}"))?;
|
||||
} else {
|
||||
run_git(dir, &["push", "-u", "origin", &branch]).map_err(|e| format!("推送失败:{e}"))?;
|
||||
}
|
||||
}
|
||||
let mut parts: Vec<&str> = Vec::new();
|
||||
if pulled {
|
||||
parts.push("已拉取远程更新");
|
||||
}
|
||||
if committed {
|
||||
parts.push("已提交本地更改");
|
||||
}
|
||||
if has_remote {
|
||||
parts.push("已推送到远程");
|
||||
}
|
||||
let summary = if parts.is_empty() {
|
||||
"已是最新,没有需要同步的更改".to_string()
|
||||
} else {
|
||||
parts.join(",")
|
||||
};
|
||||
Ok(GitSyncOutcome { committed, pushed: has_remote, pulled, summary })
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
|
||||
@@ -32,6 +32,10 @@ pub fn run() {
|
||||
commands::create_dir,
|
||||
commands::rename_path,
|
||||
commands::reveal_in_explorer,
|
||||
commands::git_status,
|
||||
commands::git_init_repo,
|
||||
commands::git_set_remote,
|
||||
commands::git_sync_now,
|
||||
commands::delete_to_trash,
|
||||
commands::db_recent_list,
|
||||
commands::db_recent_add,
|
||||
|
||||
Reference in New Issue
Block a user