import React, { useState, useEffect, useCallback } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { invoke } from "@tauri-apps/api/core"; import { GitBranch, GitCommit, GitMerge, GitPullRequest, FileText, FilePlus, FileMinus, FileEdit, X, RefreshCw, Loader2, AlertCircle, CheckCircle, Circle, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; import { useTranslation } from "react-i18next"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; interface GitStatus { branch: string; ahead: number; behind: number; staged: GitFileStatus[]; modified: GitFileStatus[]; untracked: GitFileStatus[]; conflicted: GitFileStatus[]; is_clean: boolean; remote_url?: string; } interface GitFileStatus { path: string; status: string; staged: boolean; } interface GitCommitInfo { hash: string; short_hash: string; author: string; email: string; date: string; message: string; files_changed: number; insertions: number; deletions: number; } interface GitBranchInfo { name: string; is_current: boolean; remote?: string; last_commit?: string; } interface GitPanelProps { projectPath: string; isVisible: boolean; onToggle: () => void; width?: number; className?: string; refreshInterval?: number; } // 获取文件状态图标 const getFileStatusIcon = (status: string) => { switch (status) { case "added": return ; case "modified": return ; case "deleted": return ; case "renamed": return ; case "untracked": return ; case "conflicted": return ; default: return ; } }; // 格式化日期 const formatDate = (dateStr: string, t: (key: string, opts?: any) => string) => { const date = new Date(dateStr); const now = new Date(); const diff = now.getTime() - date.getTime(); const days = Math.floor(diff / (1000 * 60 * 60 * 24)); const hours = Math.floor(diff / (1000 * 60 * 60)); const minutes = Math.floor(diff / (1000 * 60)); if (days > 7) { return date.toLocaleDateString(); } else if (days > 0) { return t('app.daysAgo', { count: days }); } else if (hours > 0) { return t('app.hoursAgo', { count: hours }); } else if (minutes > 0) { return t('app.minutesAgo', { count: minutes }); } else { return t('app.justNow'); } }; export const GitPanel: React.FC = ({ projectPath, isVisible, onToggle, width = 320, className, refreshInterval = 5000, }) => { const { t } = useTranslation(); const [gitStatus, setGitStatus] = useState(null); const [commits, setCommits] = useState([]); const [branches, setBranches] = useState([]); const [selectedTab, setSelectedTab] = useState<"status" | "history" | "branches">("status"); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [isRefreshing, setIsRefreshing] = useState(false); // 获取 Git 状态 const fetchGitStatus = useCallback(async () => { if (!projectPath) return; try { setError(null); const status = await invoke("get_git_status", { path: projectPath, }); setGitStatus(status); } catch (err) { console.error("Failed to fetch git status:", err); setError(err instanceof Error ? err.message : "Failed to fetch git status"); setGitStatus(null); } }, [projectPath]); // 获取提交历史 const fetchCommitHistory = useCallback(async () => { if (!projectPath) return; try { const history = await invoke("get_git_history", { path: projectPath, limit: 50, }); setCommits(history); } catch (err) { console.error("Failed to fetch commit history:", err); } }, [projectPath]); // 获取分支列表 const fetchBranches = useCallback(async () => { if (!projectPath) return; try { const branchList = await invoke("get_git_branches", { path: projectPath, }); setBranches(branchList); } catch (err) { console.error("Failed to fetch branches:", err); } }, [projectPath]); // 刷新所有数据 const refreshAll = useCallback(async () => { setIsRefreshing(true); await Promise.all([ fetchGitStatus(), fetchCommitHistory(), fetchBranches(), ]); setIsRefreshing(false); }, [fetchGitStatus, fetchCommitHistory, fetchBranches]); // 初始加载和定时刷新 useEffect(() => { if (!projectPath || !isVisible) return; setLoading(true); refreshAll().finally(() => setLoading(false)); // 定时刷新状态 const interval = setInterval(() => { fetchGitStatus(); }, refreshInterval); return () => clearInterval(interval); }, [projectPath, isVisible, refreshInterval, refreshAll, fetchGitStatus]); // 渲染状态视图 const renderStatusView = () => { if (!gitStatus) { return (

{t('app.noGitRepository')}

); } return (
{/* Branch Info */}
{gitStatus.branch}
{gitStatus.ahead > 0 && ( ↑ {gitStatus.ahead} )} {gitStatus.behind > 0 && ( ↓ {gitStatus.behind} )}
{gitStatus.remote_url && (

{gitStatus.remote_url}

)}
{/* Status Summary */} {gitStatus.is_clean ? (
{t('app.workingTreeClean')}
) : (
{gitStatus.staged.length > 0 && (

{t('app.staged')} ({gitStatus.staged.length})

{gitStatus.staged.map((file) => (
{getFileStatusIcon(file.status)} {file.path}
))}
)} {gitStatus.modified.length > 0 && (

{t('app.modified')} ({gitStatus.modified.length})

{gitStatus.modified.map((file) => (
{getFileStatusIcon(file.status)} {file.path}
))}
)} {gitStatus.untracked.length > 0 && (

{t('app.untracked')} ({gitStatus.untracked.length})

{gitStatus.untracked.slice(0, 10).map((file) => (
{getFileStatusIcon(file.status)} {file.path}
))} {gitStatus.untracked.length > 10 && (

{t('app.andMore', { count: gitStatus.untracked.length - 10 })}

)}
)} {gitStatus.conflicted.length > 0 && (

{t('app.conflicted')} ({gitStatus.conflicted.length})

{gitStatus.conflicted.map((file) => (
{getFileStatusIcon(file.status)} {file.path}
))}
)}
)}
); }; // 渲染历史视图 const renderHistoryView = () => { if (commits.length === 0) { return (

{t('app.noCommitsFound')}

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

{commit.message}

{commit.short_hash} {commit.author}
{formatDate(commit.date, t)} {commit.files_changed > 0 && ( <>
{commit.files_changed} {t('app.filesChanged')} +{commit.insertions} -{commit.deletions}
)}
))}
); }; // 渲染分支视图 const renderBranchesView = () => { if (branches.length === 0) { return (

{t('app.noBranchesFound')}

); } const localBranches = branches.filter(b => !b.remote); const remoteBranches = branches.filter(b => b.remote); return (
{localBranches.length > 0 && (

{t('app.localBranches')}

{localBranches.map((branch) => (
{branch.name} {branch.is_current && ( {t('app.current')} )}
{branch.last_commit && ( {branch.last_commit.slice(0, 7)} )}
))}
)} {remoteBranches.length > 0 && (

{t('app.remoteBranches')}

{remoteBranches.map((branch) => (
{branch.name}
{branch.last_commit && ( {branch.last_commit.slice(0, 7)} )}
))}
)}
); }; return ( {isVisible && (
{/* Header */}
{t('app.gitPanel')}

{t('app.refresh')}

{/* Tabs */} setSelectedTab(v as typeof selectedTab)} className="flex-1 flex flex-col" > {t('app.gitStatus')} {t('app.gitHistory')} {t('app.gitBranches')} {error ? (

{error}

) : loading ? (
) : ( <> {renderStatusView()} {renderHistoryView()} {renderBranchesView()} )}
)}
); }; // Add default export export default GitPanel;