import React, { useState, useEffect, useCallback, useRef } from "react"; import { AnimatePresence } from "framer-motion"; import { invoke } from "@tauri-apps/api/core"; import { GitBranch, GitCommit, GitPullRequest, GitMerge, X, RefreshCw, Loader2, AlertCircle, FileText, FilePlus, FileX, FileDiff, Check, Folder, FolderOpen, ChevronRight, ChevronDown, Maximize2, Minimize2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { ScrollArea } from "@/components/ui/scroll-area"; import { cn } from "@/lib/utils"; import { Tabs, TabsContent, TabsList, TabsTrigger, } from "@/components/ui/tabs"; import { Badge } from "@/components/ui/badge"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import { useTranslation } from "@/hooks/useTranslation"; import DiffViewer from "./DiffViewer"; interface GitFileStatus { path: string; status: string; staged: boolean; } interface FileTreeNode { name: string; path: string; type: 'file' | 'directory'; children?: FileTreeNode[]; status?: string; staged?: boolean; expanded?: boolean; } interface GitStatus { branch: string; ahead: number; behind: number; staged: GitFileStatus[]; modified: GitFileStatus[]; untracked: GitFileStatus[]; conflicted: GitFileStatus[]; is_clean: boolean; remote_url: string | null; } interface GitCommit { hash: string; short_hash: string; author: string; email: string; date: string; message: string; files_changed: number; insertions: number; deletions: number; } interface GitPanelEnhancedProps { projectPath: string; isVisible: boolean; onToggle: () => void; onFileSelect?: (path: string) => void; className?: string; } // 获取文件状态图标 const getFileStatusIcon = (status: 'modified' | 'staged' | 'untracked' | 'conflicted') => { switch (status) { case 'modified': return ; case 'staged': return ; case 'untracked': return ; case 'conflicted': return ; default: return ; } }; // 获取文件状态标签 const getFileStatusBadge = (status: string) => { switch (status) { case 'modified': return M; case 'added': return A; case 'deleted': return D; case 'renamed': return R; case 'untracked': return U; case 'conflicted': return C; default: return null; } }; export const GitPanelEnhanced: React.FC = ({ projectPath, isVisible, onToggle, onFileSelect, className, }) => { const [gitStatus, setGitStatus] = useState(null); const [commits, setCommits] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [activeTab, setActiveTab] = useState("changes"); const [expandedNodes, setExpandedNodes] = useState>(new Set()); const [selectedPath, setSelectedPath] = useState(null); const [showDiffViewer, setShowDiffViewer] = useState(false); const [diffFilePath, setDiffFilePath] = useState(""); const [diffStaged, setDiffStaged] = useState(false); const panelRef = useRef(null); const { t } = useTranslation(); const refreshIntervalRef = useRef(null); // 加载 Git 状态 const loadGitStatus = useCallback(async () => { if (!projectPath) return; try { setLoading(true); setError(null); const status = await invoke("get_git_status", { path: projectPath, // 修改参数名为 path }); console.log('[GitPanelEnhanced] Git status loaded:', { untracked: status.untracked, untrackedCount: status.untracked.length, staged: status.staged.length, modified: status.modified.length, conflicted: status.conflicted.length }); setGitStatus(status); } catch (err) { console.error("Failed to load git status:", err); setError(err instanceof Error ? err.message : "Failed to load git status"); } finally { setLoading(false); } }, [projectPath]); // 加载提交历史 const loadCommits = useCallback(async () => { if (!projectPath) return; try { const commitList = await invoke("get_git_commits", { projectPath: projectPath, // 使用驼峰命名 limit: 20, }); setCommits(commitList); } catch (err) { console.error("Failed to load commits:", err); } }, [projectPath]); // 自动刷新 useEffect(() => { if (!isVisible) { if (refreshIntervalRef.current) { clearInterval(refreshIntervalRef.current); refreshIntervalRef.current = null; } return; } loadGitStatus(); loadCommits(); // 每5秒刷新一次 refreshIntervalRef.current = setInterval(() => { loadGitStatus(); if (activeTab === 'history') { loadCommits(); } }, 5000); return () => { if (refreshIntervalRef.current) { clearInterval(refreshIntervalRef.current); refreshIntervalRef.current = null; } }; }, [isVisible, projectPath, activeTab, loadGitStatus, loadCommits]); // 处理文件点击 - 打开 DiffViewer const handleFileClick = (filePath: string, staged: boolean = false) => { setDiffFilePath(filePath); setDiffStaged(staged); setShowDiffViewer(true); // 如果有文件选择回调,也调用它 if (onFileSelect) { const fullPath = `${projectPath}/${filePath}`; onFileSelect(fullPath); } }; // 切换节点展开状态 const toggleExpand = useCallback((path: string) => { setExpandedNodes((prev) => { const next = new Set(prev); if (next.has(path)) { next.delete(path); } else { next.add(path); } return next; }); }, []); // 构建文件树结构(不依赖于 expandedNodes) const buildFileTree = (files: GitFileStatus[]): FileTreeNode[] => { const root: FileTreeNode = { name: 'root', path: '', type: 'directory', children: [] }; files.forEach(file => { const parts = file.path.split('/'); let currentNode = root; // 遍历路径的每个部分,构建树结构 for (let i = 0; i < parts.length; i++) { const part = parts[i]; const isLastPart = i === parts.length - 1; const currentPath = parts.slice(0, i + 1).join('/'); if (isLastPart) { // 添加文件节点 if (!currentNode.children) { currentNode.children = []; } currentNode.children.push({ name: part, path: file.path, type: 'file', status: file.status, staged: file.staged }); } else { // 查找或创建目录节点 if (!currentNode.children) { currentNode.children = []; } let dirNode = currentNode.children.find( child => child.type === 'directory' && child.name === part ); if (!dirNode) { dirNode = { name: part, path: currentPath, type: 'directory', children: [] }; currentNode.children.push(dirNode); } currentNode = dirNode; } } }); // 排序:目录在前,文件在后,按名称字母顺序 const sortNodes = (nodes: FileTreeNode[]) => { nodes.sort((a, b) => { if (a.type === 'directory' && b.type === 'file') return -1; if (a.type === 'file' && b.type === 'directory') return 1; return a.name.localeCompare(b.name); }); nodes.forEach(node => { if (node.children) { sortNodes(node.children); } }); }; if (root.children) { sortNodes(root.children); } return root.children || []; }; // 展开所有节点(从指定节点开始) const expandAll = useCallback((startPath?: string) => { const nodesToExpand = new Set(); const collectNodes = (nodes: FileTreeNode[], parentPath: string = '') => { nodes.forEach(node => { if (node.type === 'directory') { const fullPath = parentPath ? `${parentPath}/${node.name}` : node.name; nodesToExpand.add(node.path); if (node.children) { collectNodes(node.children, fullPath); } } }); }; if (startPath) { // 找到指定节点并展开其子节点 const findAndExpand = (nodes: FileTreeNode[]): boolean => { for (const node of nodes) { if (node.path === startPath && node.type === 'directory') { nodesToExpand.add(node.path); if (node.children) { collectNodes(node.children, node.path); } return true; } if (node.children && findAndExpand(node.children)) { return true; } } return false; }; // 先构建完整的树 const allTrees = []; if (gitStatus) { if (gitStatus.staged.length > 0) allTrees.push(...buildFileTree(gitStatus.staged)); if (gitStatus.modified.length > 0) allTrees.push(...buildFileTree(gitStatus.modified)); if (gitStatus.untracked.length > 0) allTrees.push(...buildFileTree(gitStatus.untracked)); if (gitStatus.conflicted.length > 0) allTrees.push(...buildFileTree(gitStatus.conflicted)); } findAndExpand(allTrees); } else { // 展开所有节点 if (gitStatus) { const allTrees = []; if (gitStatus.staged.length > 0) allTrees.push(...buildFileTree(gitStatus.staged)); if (gitStatus.modified.length > 0) allTrees.push(...buildFileTree(gitStatus.modified)); if (gitStatus.untracked.length > 0) allTrees.push(...buildFileTree(gitStatus.untracked)); if (gitStatus.conflicted.length > 0) allTrees.push(...buildFileTree(gitStatus.conflicted)); collectNodes(allTrees); } } setExpandedNodes(nodesToExpand); }, [gitStatus]); // 收起所有节点(从指定节点开始) const collapseAll = useCallback((startPath?: string) => { if (startPath) { const nodesToRemove = new Set(); const collectNodes = (nodes: FileTreeNode[]): boolean => { for (const node of nodes) { if (node.path === startPath && node.type === 'directory') { const collectChildren = (n: FileTreeNode) => { if (n.type === 'directory') { nodesToRemove.add(n.path); if (n.children) { n.children.forEach(collectChildren); } } }; collectChildren(node); return true; } if (node.children && collectNodes(node.children)) { return true; } } return false; }; // 构建完整的树 const allTrees = []; if (gitStatus) { if (gitStatus.staged.length > 0) allTrees.push(...buildFileTree(gitStatus.staged)); if (gitStatus.modified.length > 0) allTrees.push(...buildFileTree(gitStatus.modified)); if (gitStatus.untracked.length > 0) allTrees.push(...buildFileTree(gitStatus.untracked)); if (gitStatus.conflicted.length > 0) allTrees.push(...buildFileTree(gitStatus.conflicted)); } collectNodes(allTrees); setExpandedNodes(prev => { const next = new Set(prev); nodesToRemove.forEach(path => next.delete(path)); return next; }); } else { setExpandedNodes(new Set()); } }, [gitStatus]); // 渲染文件树节点(优化深层目录显示) const renderFileTreeNode = (node: FileTreeNode, depth = 0, statusType: 'modified' | 'staged' | 'untracked' | 'conflicted') => { const isExpanded = node.type === 'directory' && expandedNodes.has(node.path); const isDirectory = node.type === 'directory'; const hasChildren = node.children && node.children.length > 0; const isSelected = selectedPath === node.path; return (
{ setSelectedPath(node.path); if (isDirectory) { toggleExpand(node.path); } else { handleFileClick(node.path, statusType === 'staged'); } }} > {isDirectory && hasChildren && (
{isExpanded ? ( ) : ( )}
)} {isDirectory && !hasChildren && (
// 空文件夹的占位符 )} {isDirectory ? ( isExpanded ? ( ) : ( ) ) : ( <> {node.status && getFileStatusBadge(node.status)} )} {node.name} {(node.name.length > 30 || (!isDirectory && node.path.length > 40)) && (

{node.path}

)}
{isDirectory && isExpanded && node.children && (
{node.children.map((child) => renderFileTreeNode(child, depth + 1, statusType))}
)}
); }; // 渲染文件列表(树形结构) const renderFileList = (files: GitFileStatus[], statusType: 'modified' | 'staged' | 'untracked' | 'conflicted') => { console.log(`[GitPanelEnhanced] Rendering ${statusType} files:`, files); if (files.length === 0) return null; const fileTree = buildFileTree(files); console.log(`[GitPanelEnhanced] Built file tree for ${statusType}:`, fileTree); return (
{getFileStatusIcon(statusType)} {statusType === 'modified' && t('app.modified')} {statusType === 'staged' && t('app.staged')} {statusType === 'untracked' && t('app.untracked')} {statusType === 'conflicted' && t('app.conflicted')} {files.length}
{fileTree.map((node) => renderFileTreeNode(node, 0, statusType))}
); }; // 渲染提交历史 const renderCommitHistory = () => { if (commits.length === 0) { return (

{t('app.noCommitsFound')}

); } return (
{commits.map((commit) => (

{commit.message}

{commit.author} {commit.date} {commit.short_hash || commit.hash.substring(0, 7)} {commit.files_changed > 0 && ( {commit.files_changed} {t('app.filesChanged')} )}
))}
); }; // 计算变更统计 const changeStats = gitStatus ? { total: gitStatus.staged.length + gitStatus.modified.length + gitStatus.untracked.length + gitStatus.conflicted.length, staged: gitStatus.staged.length, modified: gitStatus.modified.length, untracked: gitStatus.untracked.length, conflicted: gitStatus.conflicted.length, } : null; return ( <> {isVisible && (
{/* Header */}

{t('app.gitPanel')}

{gitStatus && ( {gitStatus.branch} )}
{/* 展开/收起按钮 */}

{selectedPath ? t('app.expandCurrentFolder') : t('app.expandAllFolders')}

{selectedPath ? t('app.collapseCurrentFolder') : t('app.collapseAllFolders')}

{changeStats && changeStats.total > 0 && ( {changeStats.total} {t('app.gitChanges')} )}
{/* Branch Info */} {gitStatus && (gitStatus.ahead > 0 || gitStatus.behind > 0) && (
{gitStatus.ahead > 0 && (
{gitStatus.ahead} {t('app.ahead')}
)} {gitStatus.behind > 0 && (
{gitStatus.behind} {t('app.behind')}
)}
)} {/* Tabs */} {t('app.gitChanges')} {changeStats && changeStats.total > 0 && ( {changeStats.total} )} {t('app.gitHistory')} {/* Content */} {error ? (

{error}

) : loading && !gitStatus ? (
) : ( <>
{gitStatus && ( <> {console.log('[GitPanelEnhanced] Rendering all file lists with gitStatus:', gitStatus)} {renderFileList(gitStatus.staged, 'staged')} {renderFileList(gitStatus.modified, 'modified')} {renderFileList(gitStatus.untracked, 'untracked')} {renderFileList(gitStatus.conflicted, 'conflicted')} {changeStats?.total === 0 && (

{t('app.workingTreeClean')}

)} )}
{renderCommitHistory()}
)}
)}
{/* Diff Viewer Modal */} setShowDiffViewer(false)} /> ); }; export default GitPanelEnhanced;