dev-manager-tauri/src-tauri/src/commands/git_ops.rs
lanrtop 533c069e43 feat: add WSL editor launch modes and update checking functionality
- Introduced WSL editor launch modes in SettingsPage with options for internal, remote, and UNC path launches.
- Enhanced settings saving to include the new WSL launch mode.
- Added an update section to check for application updates, download, and install them with progress tracking.
- Updated Toast component to handle error messages with a copy button for convenience.
- Expanded commands to include project tags and templates management, along with WSL proxy synchronization features.
2026-04-01 23:12:23 +09:00

337 lines
11 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<String>,
pub last_commit_time: Option<String>,
}
fn get_token() -> Result<String, String> {
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<String>) -> 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<GitStatus, String> {
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(())
}
/// pullfetch + merge fast-forward
#[tauri::command]
pub fn git_pull(local_path: String) -> Result<String, String> {
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<Option<String>, 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<usize, String> {
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<String, String> {
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<Vec<String>, 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(())
}