use crate::db; use git2::{Repository, RemoteCallbacks, FetchOptions, build::RepoBuilder}; use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize)] pub struct GitStatus { pub branch: String, pub ahead: usize, pub behind: usize, pub has_changes: bool, pub last_commit_msg: Option, pub last_commit_time: Option, } fn get_token() -> Result { let conn = db::pool().get().map_err(|e| e.to_string())?; conn.query_row( "SELECT value FROM settings WHERE key = 'github_token'", [], |row| row.get::<_, String>(0), ) .map_err(|_| "未登录 GitHub 账户".to_string()) } fn make_fetch_options(token: &str) -> FetchOptions<'static> { let token = token.to_string(); let mut callbacks = RemoteCallbacks::new(); callbacks.credentials(move |_url, _username, _allowed| { git2::Cred::userpass_plaintext(&token, "x-oauth-basic") }); let mut fetch_opts = FetchOptions::new(); fetch_opts.remote_callbacks(callbacks); fetch_opts } /// clone 仓库到指定本地路径 #[tauri::command] pub fn git_clone(repo_url: String, local_path: String, upstream_url: Option) -> Result<(), String> { let token = get_token()?; let token_clone = token.clone(); let mut callbacks = RemoteCallbacks::new(); callbacks.credentials(move |_url, _username, _allowed| { git2::Cred::userpass_plaintext(&token_clone, "x-oauth-basic") }); let mut fetch_opts = FetchOptions::new(); fetch_opts.remote_callbacks(callbacks); let repo = RepoBuilder::new() .fetch_options(fetch_opts) .clone(&repo_url, std::path::Path::new(&local_path)) .map_err(|e| format!("clone 失败: {}", e))?; if let Some(upstream) = upstream_url { if !upstream.trim().is_empty() { repo.remote("upstream", upstream.trim()) .map_err(|e| format!("设置 upstream remote 失败: {}", e))?; } } Ok(()) } /// 获取仓库 git 状态(分支、领先/落后、是否有未提交变更、最近 commit) #[tauri::command] pub fn git_status(local_path: String) -> Result { let repo = Repository::open(&local_path) .map_err(|e| format!("无法打开仓库: {}", e))?; // 当前分支名 let head = repo.head().map_err(|e| e.to_string())?; let branch = head .shorthand() .unwrap_or("HEAD detached") .to_string(); // 领先/落后远端 let (ahead, behind) = calc_ahead_behind(&repo, &branch); // 是否有未提交变更 let statuses = repo.statuses(None).map_err(|e| e.to_string())?; let has_changes = statuses.iter().any(|s| { s.status() != git2::Status::CURRENT && s.status() != git2::Status::IGNORED }); // 最近一次 commit let (last_commit_msg, last_commit_time) = match head.peel_to_commit() { Ok(commit) => { let msg = commit.summary().map(|s| s.to_string()); let time = { let t = commit.time().seconds(); let dt = chrono::DateTime::from_timestamp(t, 0) .map(|dt| dt.format("%Y-%m-%d %H:%M").to_string()); dt }; (msg, time) } Err(_) => (None, None), }; Ok(GitStatus { branch, ahead, behind, has_changes, last_commit_msg, last_commit_time, }) } /// fetch 远端最新信息(不修改本地工作区) #[tauri::command] pub fn git_fetch(local_path: String) -> Result<(), String> { let token = get_token()?; let repo = Repository::open(&local_path) .map_err(|e| format!("无法打开仓库: {}", e))?; let mut fetch_opts = make_fetch_options(&token); repo.find_remote("origin") .map_err(|e| e.to_string())? .fetch(&[] as &[&str], Some(&mut fetch_opts), None) .map_err(|e| format!("fetch 失败: {}", e))?; Ok(()) } /// pull(fetch + merge fast-forward) #[tauri::command] pub fn git_pull(local_path: String) -> Result { let token = get_token()?; let repo = Repository::open(&local_path) .map_err(|e| format!("无法打开仓库: {}", e))?; // fetch let mut fetch_opts = make_fetch_options(&token); let mut remote = repo.find_remote("origin").map_err(|e| e.to_string())?; remote .fetch(&[] as &[&str], Some(&mut fetch_opts), None) .map_err(|e| format!("fetch 失败: {}", e))?; // 当前分支 let head = repo.head().map_err(|e| e.to_string())?; let branch_name = head .shorthand() .ok_or("无法获取分支名")? .to_string(); // 找到 FETCH_HEAD let fetch_head = repo .find_reference("FETCH_HEAD") .map_err(|_| "没有可以合并的远端提交".to_string())?; let fetch_commit = repo .reference_to_annotated_commit(&fetch_head) .map_err(|e| e.to_string())?; // 分析合并情况 let (analysis, _) = repo .merge_analysis(&[&fetch_commit]) .map_err(|e| e.to_string())?; if analysis.is_up_to_date() { return Ok(format!("已是最新(分支: {})", branch_name)); } if analysis.is_fast_forward() { let refname = format!("refs/heads/{}", branch_name); let mut reference = repo.find_reference(&refname).map_err(|e| e.to_string())?; reference .set_target(fetch_commit.id(), "fast-forward pull") .map_err(|e| e.to_string())?; repo.set_head(&refname).map_err(|e| e.to_string())?; repo.checkout_head(Some(git2::build::CheckoutBuilder::default().force())) .map_err(|e| e.to_string())?; return Ok(format!("Pull 成功(fast-forward,分支: {})", branch_name)); } Err("存在冲突,需要手动合并".to_string()) } /// 获取本地仓库当前 HEAD 的 commit hash(用于跨空间比对) #[tauri::command] pub fn git_head_commit(local_path: String) -> Result, String> { let repo = match Repository::open(&local_path) { Ok(r) => r, Err(_) => return Ok(None), }; let result = match repo.head().and_then(|h| h.peel_to_commit()) { Ok(commit) => Ok(Some(commit.id().to_string())), Err(_) => Ok(None), }; result } /// 检查 upstream 远端落后情况(公司仓库是否有新提交) #[tauri::command] pub fn git_upstream_behind(local_path: String, default_branch: String) -> Result { let token = get_token()?; let repo = Repository::open(&local_path) .map_err(|e| format!("无法打开仓库: {}", e))?; // 检查是否有 upstream remote if repo.find_remote("upstream").is_err() { return Ok(0); } // fetch upstream let mut callbacks = RemoteCallbacks::new(); let token_clone = token.clone(); callbacks.credentials(move |_url, _username, _allowed| { git2::Cred::userpass_plaintext(&token_clone, "x-oauth-basic") }); let mut fetch_opts = FetchOptions::new(); fetch_opts.remote_callbacks(callbacks); if let Ok(mut remote) = repo.find_remote("upstream") { let _ = remote.fetch(&[&default_branch], Some(&mut fetch_opts), None); } // 比较本地 HEAD 与 upstream/{default_branch} let head_oid = match repo.head().ok().and_then(|h| h.target()) { Some(o) => o, None => return Ok(0), }; let upstream_ref = format!("refs/remotes/upstream/{}", default_branch); let upstream_oid = match repo.find_reference(&upstream_ref).ok().and_then(|r| r.target()) { Some(o) => o, None => return Ok(0), }; let (_, behind) = repo .graph_ahead_behind(head_oid, upstream_oid) .unwrap_or((0, 0)); Ok(behind) } /// push 当前分支到 origin #[tauri::command] pub fn git_push(local_path: String) -> Result { let token = get_token()?; let repo = Repository::open(&local_path) .map_err(|e| format!("无法打开仓库: {}", e))?; let head = repo.head().map_err(|e| e.to_string())?; let branch = head .shorthand() .ok_or("无法获取分支名")? .to_string(); let refspec = format!("refs/heads/{}:refs/heads/{}", branch, branch); let mut callbacks = RemoteCallbacks::new(); let token_clone = token.clone(); callbacks.credentials(move |_url, _username, _allowed| { git2::Cred::userpass_plaintext(&token_clone, "x-oauth-basic") }); let mut push_opts = git2::PushOptions::new(); push_opts.remote_callbacks(callbacks); repo.find_remote("origin") .map_err(|e| e.to_string())? .push(&[refspec.as_str()], Some(&mut push_opts)) .map_err(|e| format!("push 失败: {}", e))?; Ok(format!("Push 成功(分支: {})", branch)) } // ── 内部辅助 ──────────────────────────────────────────────── fn calc_ahead_behind(repo: &Repository, branch: &str) -> (usize, usize) { let local_ref = match repo.find_reference(&format!("refs/heads/{}", branch)) { Ok(r) => r, Err(_) => return (0, 0), }; let remote_ref = match repo .find_reference(&format!("refs/remotes/origin/{}", branch)) { Ok(r) => r, Err(_) => return (0, 0), }; let local_oid = match local_ref.target() { Some(o) => o, None => return (0, 0), }; let remote_oid = match remote_ref.target() { Some(o) => o, None => return (0, 0), }; repo.graph_ahead_behind(local_oid, remote_oid) .unwrap_or((0, 0)) } /// 创建并切换到新分支(从当前 HEAD 创建) #[tauri::command] pub fn git_create_branch(local_path: String, branch_name: String) -> Result<(), String> { let repo = Repository::open(&local_path) .map_err(|e| format!("无法打开仓库: {}", e))?; let head = repo.head().map_err(|e| e.to_string())?; let commit = head.peel_to_commit().map_err(|e| e.to_string())?; let branch = repo.branch(&branch_name, &commit, false) .map_err(|e| format!("创建分支失败: {}", e))?; let refname = branch.get().name().ok_or("无效分支名")?.to_string(); repo.set_head(&refname).map_err(|e| format!("切换分支失败: {}", e))?; repo.checkout_head(Some(git2::build::CheckoutBuilder::default().force())) .map_err(|e| format!("checkout 失败: {}", e))?; Ok(()) } /// 列出本地所有分支名 #[tauri::command] pub fn git_list_branches(local_path: String) -> Result, String> { let repo = Repository::open(&local_path) .map_err(|e| format!("无法打开仓库: {}", e))?; let branches = repo.branches(Some(git2::BranchType::Local)) .map_err(|e| e.to_string())?; let mut names = Vec::new(); for b in branches { let (branch, _) = b.map_err(|e| e.to_string())?; if let Some(name) = branch.name().map_err(|e| e.to_string())? { names.push(name.to_string()); } } Ok(names) } /// 切换到已有的本地分支 #[tauri::command] pub fn git_checkout_branch(local_path: String, branch_name: String) -> Result<(), String> { let repo = Repository::open(&local_path) .map_err(|e| format!("无法打开仓库: {}", e))?; let refname = format!("refs/heads/{}", branch_name); repo.set_head(&refname).map_err(|e| format!("切换分支失败: {}", e))?; repo.checkout_head(Some(git2::build::CheckoutBuilder::default().force())) .map_err(|e| format!("checkout 失败: {}", e))?; Ok(()) }