feat: add WSL git support and improve commit message handling in PublishModal
This commit is contained in:
parent
0baa304fbe
commit
b067c1f348
@ -213,6 +213,147 @@ fn detect_templates(root: &Path) -> Vec<String> {
|
||||
templates
|
||||
}
|
||||
|
||||
// ── WSL git 辅助 ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// 从原始路径(可能是 Linux 路径或 UNC 路径)提取 Linux 绝对路径
|
||||
/// - 原始路径以 `/` 开头 → 直接返回
|
||||
/// - win_path 是 `\\wsl.localhost\Distro\...` → 提取 `/...` 部分
|
||||
fn extract_linux_path(original: &str, win_path: &str) -> Option<String> {
|
||||
if original.starts_with('/') {
|
||||
return Some(original.to_string());
|
||||
}
|
||||
// \\wsl.localhost\Distro\home\user\... → /home/user/...
|
||||
if win_path.starts_with("\\\\wsl.localhost\\") {
|
||||
let rest = &win_path["\\\\wsl.localhost\\".len()..];
|
||||
if let Some(slash_idx) = rest.find('\\') {
|
||||
let linux = "/".to_string() + &rest[slash_idx + 1..].replace('\\', "/");
|
||||
return Some(linux);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// 在 WSL 中执行 git 命令,返回 stdout 字符串
|
||||
fn run_wsl_git(linux_path: &str, git_args: &[&str]) -> Result<String, String> {
|
||||
let mut args: Vec<&str> = vec!["git", "-C", linux_path];
|
||||
args.extend_from_slice(git_args);
|
||||
let output = std::process::Command::new("wsl")
|
||||
.args(&args)
|
||||
.output()
|
||||
.map_err(|e| format!("wsl 命令失败: {e}"))?;
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
||||
return Err(if stderr.is_empty() {
|
||||
format!("git 命令退出码: {}", output.status)
|
||||
} else {
|
||||
stderr
|
||||
});
|
||||
}
|
||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||
}
|
||||
|
||||
/// WSL 项目专用扫描:通过 `wsl git` 获取准确状态
|
||||
/// 避免 libgit2 在 Windows UNC 路径下 .gitignore 规则失效(路径分隔符不匹配)
|
||||
fn scan_local_git_via_wsl(linux_path: &str, win_path: &str) -> Result<GitScanResult, String> {
|
||||
let root = Path::new(win_path);
|
||||
|
||||
let has_gitignore = root.join(".gitignore").exists();
|
||||
let suspicious_files = scan_suspicious(root);
|
||||
let suggested_templates = detect_templates(root);
|
||||
|
||||
if !root.join(".git").exists() {
|
||||
return Ok(GitScanResult {
|
||||
has_git: false,
|
||||
has_commits: false,
|
||||
branch: String::new(),
|
||||
has_changes: false,
|
||||
changed_files: vec![],
|
||||
remotes: vec![],
|
||||
has_gitignore,
|
||||
suspicious_files,
|
||||
suggested_templates,
|
||||
});
|
||||
}
|
||||
|
||||
// 分支名
|
||||
let branch = run_wsl_git(linux_path, &["branch", "--show-current"])
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_string();
|
||||
let branch = if branch.is_empty() {
|
||||
// 可能是 detached HEAD,尝试 rev-parse --abbrev-ref
|
||||
run_wsl_git(linux_path, &["rev-parse", "--abbrev-ref", "HEAD"])
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_string()
|
||||
} else {
|
||||
branch
|
||||
};
|
||||
let branch = if branch.is_empty() || branch == "HEAD" {
|
||||
"main".to_string()
|
||||
} else {
|
||||
branch
|
||||
};
|
||||
|
||||
// 是否有提交
|
||||
let has_commits = run_wsl_git(linux_path, &["rev-parse", "HEAD"]).is_ok();
|
||||
|
||||
// git status --porcelain(与 VSCode 使用相同的 git,gitignore 规则完全一致)
|
||||
let status_out = run_wsl_git(linux_path, &["status", "--porcelain"]).unwrap_or_default();
|
||||
let changed_files: Vec<ChangedFile> = status_out
|
||||
.lines()
|
||||
.filter(|l| l.len() >= 3)
|
||||
.map(|line| {
|
||||
let xy = &line[0..2];
|
||||
// 去掉 git 对含空格路径加的引号
|
||||
let path = line[3..].trim().trim_matches('"').to_string();
|
||||
let status = if xy.contains('?') {
|
||||
"?"
|
||||
} else if xy.contains('D') {
|
||||
"D"
|
||||
} else if xy.contains('R') {
|
||||
"R"
|
||||
} else {
|
||||
"M"
|
||||
};
|
||||
ChangedFile { status: status.to_string(), path }
|
||||
})
|
||||
.collect();
|
||||
|
||||
let has_changes = !changed_files.is_empty();
|
||||
|
||||
// git remote -v
|
||||
let remote_out = run_wsl_git(linux_path, &["remote", "-v"]).unwrap_or_default();
|
||||
let mut remotes: Vec<GitRemote> = Vec::new();
|
||||
for line in remote_out.lines() {
|
||||
if !line.ends_with("(fetch)") {
|
||||
continue;
|
||||
}
|
||||
let mut parts = line.splitn(2, '\t');
|
||||
let name = parts.next().unwrap_or("").to_string();
|
||||
let url = parts
|
||||
.next()
|
||||
.and_then(|s| s.split_whitespace().next())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
if !name.is_empty() && !url.is_empty() {
|
||||
remotes.push(GitRemote { name, url });
|
||||
}
|
||||
}
|
||||
|
||||
Ok(GitScanResult {
|
||||
has_git: true,
|
||||
has_commits,
|
||||
branch,
|
||||
has_changes,
|
||||
changed_files,
|
||||
remotes,
|
||||
has_gitignore,
|
||||
suspicious_files,
|
||||
suggested_templates,
|
||||
})
|
||||
}
|
||||
|
||||
// ── Tauri 命令 ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// 扫描本地目录的 git 状态、敏感文件、技术栈
|
||||
@ -229,6 +370,12 @@ pub fn scan_local_git(path: String) -> Result<GitScanResult, String> {
|
||||
));
|
||||
}
|
||||
|
||||
// WSL 项目:通过 wsl git 命令扫描,保证 .gitignore 规则与 VSCode 完全一致
|
||||
if let Some(linux_path) = extract_linux_path(&path, &win_path) {
|
||||
return scan_local_git_via_wsl(&linux_path, &win_path);
|
||||
}
|
||||
|
||||
// Windows 项目:使用 libgit2
|
||||
let has_gitignore = root.join(".gitignore").exists();
|
||||
let suspicious_files = scan_suspicious(root);
|
||||
let suggested_templates = detect_templates(root);
|
||||
@ -266,27 +413,19 @@ pub fn scan_local_git(path: String) -> Result<GitScanResult, String> {
|
||||
.statuses(Some(
|
||||
git2::StatusOptions::new()
|
||||
.include_untracked(true)
|
||||
.recurse_untracked_dirs(true),
|
||||
.recurse_untracked_dirs(false),
|
||||
))
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
for entry in statuses.iter() {
|
||||
let s = entry.status();
|
||||
let status_str = if s.contains(git2::Status::INDEX_NEW)
|
||||
|| s.contains(git2::Status::WT_NEW)
|
||||
{
|
||||
let status_str = if s.contains(git2::Status::INDEX_NEW) || s.contains(git2::Status::WT_NEW) {
|
||||
"?"
|
||||
} else if s.contains(git2::Status::INDEX_MODIFIED)
|
||||
|| s.contains(git2::Status::WT_MODIFIED)
|
||||
{
|
||||
} else if s.contains(git2::Status::INDEX_MODIFIED) || s.contains(git2::Status::WT_MODIFIED) {
|
||||
"M"
|
||||
} else if s.contains(git2::Status::INDEX_DELETED)
|
||||
|| s.contains(git2::Status::WT_DELETED)
|
||||
{
|
||||
} else if s.contains(git2::Status::INDEX_DELETED) || s.contains(git2::Status::WT_DELETED) {
|
||||
"D"
|
||||
} else if s.contains(git2::Status::INDEX_RENAMED)
|
||||
|| s.contains(git2::Status::WT_RENAMED)
|
||||
{
|
||||
} else if s.contains(git2::Status::INDEX_RENAMED) || s.contains(git2::Status::WT_RENAMED) {
|
||||
"R"
|
||||
} else {
|
||||
"M"
|
||||
|
||||
@ -96,9 +96,7 @@ export function PublishModal({ project, onClose }: Props) {
|
||||
if (needInit || scan.has_changes) {
|
||||
steps.push({ label: `提交本地代码(${commitMsg})`, status: "pending" });
|
||||
}
|
||||
if (!hasRemoteConflict || scan.remotes.length === 0) {
|
||||
steps.push({ label: "配置 origin remote", status: "pending" });
|
||||
}
|
||||
steps.push({ label: `推送到 GitHub (${branch})`, status: "pending" });
|
||||
steps.push({ label: "注册到本地仓库", status: "pending" });
|
||||
return steps;
|
||||
@ -307,8 +305,13 @@ export function PublishModal({ project, onClose }: Props) {
|
||||
)}
|
||||
|
||||
{/* 改动文件预览 */}
|
||||
{scan.has_changes && (
|
||||
{(() => {
|
||||
const needCommit = !scan.has_git || !scan.has_commits || scan.has_changes;
|
||||
if (!needCommit) return null;
|
||||
return (
|
||||
<div className="border border-gray-200 rounded-lg px-4 py-3 text-xs space-y-2">
|
||||
{scan.has_changes ? (
|
||||
<>
|
||||
<p className="font-semibold text-gray-600">待提交文件({scan.changed_files.length} 个)</p>
|
||||
<div className="max-h-28 overflow-y-auto space-y-0.5">
|
||||
{scan.changed_files.slice(0, 50).map((f, i) => (
|
||||
@ -324,6 +327,12 @@ export function PublishModal({ project, onClose }: Props) {
|
||||
<p className="text-gray-400">…还有 {scan.changed_files.length - 50} 个文件</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p className="font-semibold text-gray-600">
|
||||
{!scan.has_git ? "全新项目,将执行 git init 并提交所有文件" : "无历史提交,将提交当前所有文件"}
|
||||
</p>
|
||||
)}
|
||||
<div>
|
||||
<label className="block text-gray-500 mb-1">Commit 信息</label>
|
||||
<input
|
||||
@ -333,7 +342,8 @@ export function PublishModal({ project, onClose }: Props) {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* GitHub 仓库配置 */}
|
||||
<div className="border border-gray-200 rounded-lg px-4 py-3 space-y-3">
|
||||
@ -420,7 +430,7 @@ export function PublishModal({ project, onClose }: Props) {
|
||||
onClick={handleExecute}
|
||||
disabled={
|
||||
!repoName.trim() ||
|
||||
(scan?.suspicious_files.length! > 0 && !confirmedSensitive)
|
||||
(scan.suspicious_files.length > 0 && !confirmedSensitive)
|
||||
}
|
||||
className="px-4 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user