- 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.
337 lines
11 KiB
Rust
337 lines
11 KiB
Rust
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(())
|
||
}
|
||
|
||
/// pull(fetch + 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(())
|
||
}
|