diff --git a/src-tauri/src/commands/publish.rs b/src-tauri/src/commands/publish.rs index 4b7374d..e889822 100644 --- a/src-tauri/src/commands/publish.rs +++ b/src-tauri/src/commands/publish.rs @@ -213,6 +213,147 @@ fn detect_templates(root: &Path) -> Vec { templates } +// ── WSL git 辅助 ────────────────────────────────────────────────────────────── + +/// 从原始路径(可能是 Linux 路径或 UNC 路径)提取 Linux 绝对路径 +/// - 原始路径以 `/` 开头 → 直接返回 +/// - win_path 是 `\\wsl.localhost\Distro\...` → 提取 `/...` 部分 +fn extract_linux_path(original: &str, win_path: &str) -> Option { + 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 { + 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 { + 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 = 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 = 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 { )); } + // 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 { .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" diff --git a/src/components/dashboard/PublishModal.tsx b/src/components/dashboard/PublishModal.tsx index 41c0a52..9f65df6 100644 --- a/src/components/dashboard/PublishModal.tsx +++ b/src/components/dashboard/PublishModal.tsx @@ -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: "配置 origin remote", status: "pending" }); steps.push({ label: `推送到 GitHub (${branch})`, status: "pending" }); steps.push({ label: "注册到本地仓库", status: "pending" }); return steps; @@ -307,33 +305,45 @@ export function PublishModal({ project, onClose }: Props) { )} {/* 改动文件预览 */} - {scan.has_changes && ( -
-

待提交文件({scan.changed_files.length} 个)

-
- {scan.changed_files.slice(0, 50).map((f, i) => ( -
- {f.status} - {f.path} -
- ))} - {scan.changed_files.length > 50 && ( -

…还有 {scan.changed_files.length - 50} 个文件

+ {(() => { + const needCommit = !scan.has_git || !scan.has_commits || scan.has_changes; + if (!needCommit) return null; + return ( +
+ {scan.has_changes ? ( + <> +

待提交文件({scan.changed_files.length} 个)

+
+ {scan.changed_files.slice(0, 50).map((f, i) => ( +
+ {f.status} + {f.path} +
+ ))} + {scan.changed_files.length > 50 && ( +

…还有 {scan.changed_files.length - 50} 个文件

+ )} +
+ + ) : ( +

+ {!scan.has_git ? "全新项目,将执行 git init 并提交所有文件" : "无历史提交,将提交当前所有文件"} +

)} +
+ + setCommitMsg(e.target.value)} + /> +
-
- - setCommitMsg(e.target.value)} - /> -
-
- )} + ); + })()} {/* GitHub 仓库配置 */}
@@ -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" >