import React, { useState, useEffect, useRef } from 'react'; import { GitBranch, GitCommit, Plus, Minus, RefreshCw, Check, X, ChevronDown, ChevronRight, Info, History, FileText, Mic, MicOff, Sparkles, Download, RotateCcw, Trash2, AlertTriangle, Upload } from 'lucide-react'; import { MicButton } from './MicButton.jsx'; import { authenticatedFetch } from '../utils/api'; function GitPanel({ selectedProject, isMobile }) { const [gitStatus, setGitStatus] = useState(null); const [gitDiff, setGitDiff] = useState({}); const [isLoading, setIsLoading] = useState(false); const [commitMessage, setCommitMessage] = useState(''); const [expandedFiles, setExpandedFiles] = useState(new Set()); const [selectedFiles, setSelectedFiles] = useState(new Set()); const [isCommitting, setIsCommitting] = useState(false); const [currentBranch, setCurrentBranch] = useState(''); const [branches, setBranches] = useState([]); const [wrapText, setWrapText] = useState(true); const [showLegend, setShowLegend] = useState(false); const [showBranchDropdown, setShowBranchDropdown] = useState(false); const [showNewBranchModal, setShowNewBranchModal] = useState(false); const [newBranchName, setNewBranchName] = useState(''); const [isCreatingBranch, setIsCreatingBranch] = useState(false); const [activeView, setActiveView] = useState('changes'); // 'changes' or 'history' const [recentCommits, setRecentCommits] = useState([]); const [expandedCommits, setExpandedCommits] = useState(new Set()); const [commitDiffs, setCommitDiffs] = useState({}); const [isGeneratingMessage, setIsGeneratingMessage] = useState(false); const [remoteStatus, setRemoteStatus] = useState(null); const [isFetching, setIsFetching] = useState(false); const [isPulling, setIsPulling] = useState(false); const [isPushing, setIsPushing] = useState(false); const [isPublishing, setIsPublishing] = useState(false); const [isCommitAreaCollapsed, setIsCommitAreaCollapsed] = useState(isMobile); // Collapsed by default on mobile const [confirmAction, setConfirmAction] = useState(null); // { type: 'discard|commit|pull|push', file?: string, message?: string } const textareaRef = useRef(null); const dropdownRef = useRef(null); useEffect(() => { if (selectedProject) { fetchGitStatus(); fetchBranches(); fetchRemoteStatus(); if (activeView === 'history') { fetchRecentCommits(); } } }, [selectedProject, activeView]); // Handle click outside dropdown useEffect(() => { const handleClickOutside = (event) => { if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { setShowBranchDropdown(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); const fetchGitStatus = async () => { if (!selectedProject) return; setIsLoading(true); try { const response = await authenticatedFetch(`/api/git/status?project=${encodeURIComponent(selectedProject.name)}`); const data = await response.json(); if (data.error) { console.error('Git status error:', data.error); setGitStatus({ error: data.error, details: data.details }); } else { setGitStatus(data); setCurrentBranch(data.branch || 'main'); // Auto-select all changed files const allFiles = new Set([ ...(data.modified || []), ...(data.added || []), ...(data.deleted || []), ...(data.untracked || []) ]); setSelectedFiles(allFiles); // Fetch diffs for changed files for (const file of data.modified || []) { fetchFileDiff(file); } for (const file of data.added || []) { fetchFileDiff(file); } } } catch (error) { console.error('Error fetching git status:', error); } finally { setIsLoading(false); } }; const fetchBranches = async () => { try { const response = await authenticatedFetch(`/api/git/branches?project=${encodeURIComponent(selectedProject.name)}`); const data = await response.json(); if (!data.error && data.branches) { setBranches(data.branches); } } catch (error) { console.error('Error fetching branches:', error); } }; const fetchRemoteStatus = async () => { if (!selectedProject) return; try { const response = await authenticatedFetch(`/api/git/remote-status?project=${encodeURIComponent(selectedProject.name)}`); const data = await response.json(); if (!data.error) { setRemoteStatus(data); } else { setRemoteStatus(null); } } catch (error) { console.error('Error fetching remote status:', error); setRemoteStatus(null); } }; const switchBranch = async (branchName) => { try { const response = await authenticatedFetch('/api/git/checkout', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ project: selectedProject.name, branch: branchName }) }); const data = await response.json(); if (data.success) { setCurrentBranch(branchName); setShowBranchDropdown(false); fetchGitStatus(); // Refresh status after branch switch } else { console.error('Failed to switch branch:', data.error); } } catch (error) { console.error('Error switching branch:', error); } }; const createBranch = async () => { if (!newBranchName.trim()) return; setIsCreatingBranch(true); try { const response = await authenticatedFetch('/api/git/create-branch', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ project: selectedProject.name, branch: newBranchName.trim() }) }); const data = await response.json(); if (data.success) { setCurrentBranch(newBranchName.trim()); setShowNewBranchModal(false); setShowBranchDropdown(false); setNewBranchName(''); fetchBranches(); // Refresh branch list fetchGitStatus(); // Refresh status } else { console.error('Failed to create branch:', data.error); } } catch (error) { console.error('Error creating branch:', error); } finally { setIsCreatingBranch(false); } }; const handleFetch = async () => { setIsFetching(true); try { const response = await authenticatedFetch('/api/git/fetch', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ project: selectedProject.name }) }); const data = await response.json(); if (data.success) { // Refresh status after successful fetch fetchGitStatus(); fetchRemoteStatus(); } else { console.error('Fetch failed:', data.error); } } catch (error) { console.error('Error fetching from remote:', error); } finally { setIsFetching(false); } }; const handlePull = async () => { setIsPulling(true); try { const response = await authenticatedFetch('/api/git/pull', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ project: selectedProject.name }) }); const data = await response.json(); if (data.success) { // Refresh status after successful pull fetchGitStatus(); fetchRemoteStatus(); } else { console.error('Pull failed:', data.error); // TODO: Show user-friendly error message } } catch (error) { console.error('Error pulling from remote:', error); } finally { setIsPulling(false); } }; const handlePush = async () => { setIsPushing(true); try { const response = await authenticatedFetch('/api/git/push', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ project: selectedProject.name }) }); const data = await response.json(); if (data.success) { // Refresh status after successful push fetchGitStatus(); fetchRemoteStatus(); } else { console.error('Push failed:', data.error); // TODO: Show user-friendly error message } } catch (error) { console.error('Error pushing to remote:', error); } finally { setIsPushing(false); } }; const handlePublish = async () => { setIsPublishing(true); try { const response = await authenticatedFetch('/api/git/publish', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ project: selectedProject.name, branch: currentBranch }) }); const data = await response.json(); if (data.success) { // Refresh status after successful publish fetchGitStatus(); fetchRemoteStatus(); } else { console.error('Publish failed:', data.error); // TODO: Show user-friendly error message } } catch (error) { console.error('Error publishing branch:', error); } finally { setIsPublishing(false); } }; const discardChanges = async (filePath) => { try { const response = await authenticatedFetch('/api/git/discard', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ project: selectedProject.name, file: filePath }) }); const data = await response.json(); if (data.success) { // Remove from selected files and refresh status setSelectedFiles(prev => { const newSet = new Set(prev); newSet.delete(filePath); return newSet; }); fetchGitStatus(); } else { console.error('Discard failed:', data.error); } } catch (error) { console.error('Error discarding changes:', error); } }; const deleteUntrackedFile = async (filePath) => { try { const response = await authenticatedFetch('/api/git/delete-untracked', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ project: selectedProject.name, file: filePath }) }); const data = await response.json(); if (data.success) { // Remove from selected files and refresh status setSelectedFiles(prev => { const newSet = new Set(prev); newSet.delete(filePath); return newSet; }); fetchGitStatus(); } else { console.error('Delete failed:', data.error); } } catch (error) { console.error('Error deleting untracked file:', error); } }; const confirmAndExecute = async () => { if (!confirmAction) return; const { type, file, message } = confirmAction; setConfirmAction(null); try { switch (type) { case 'discard': await discardChanges(file); break; case 'delete': await deleteUntrackedFile(file); break; case 'commit': await handleCommit(); break; case 'pull': await handlePull(); break; case 'push': await handlePush(); break; case 'publish': await handlePublish(); break; } } catch (error) { console.error(`Error executing ${type}:`, error); } }; const fetchFileDiff = async (filePath) => { try { const response = await authenticatedFetch(`/api/git/diff?project=${encodeURIComponent(selectedProject.name)}&file=${encodeURIComponent(filePath)}`); const data = await response.json(); if (!data.error && data.diff) { setGitDiff(prev => ({ ...prev, [filePath]: data.diff })); } } catch (error) { console.error('Error fetching file diff:', error); } }; const fetchRecentCommits = async () => { try { const response = await authenticatedFetch(`/api/git/commits?project=${encodeURIComponent(selectedProject.name)}&limit=10`); const data = await response.json(); if (!data.error && data.commits) { setRecentCommits(data.commits); } } catch (error) { console.error('Error fetching commits:', error); } }; const fetchCommitDiff = async (commitHash) => { try { const response = await authenticatedFetch(`/api/git/commit-diff?project=${encodeURIComponent(selectedProject.name)}&commit=${commitHash}`); const data = await response.json(); if (!data.error && data.diff) { setCommitDiffs(prev => ({ ...prev, [commitHash]: data.diff })); } } catch (error) { console.error('Error fetching commit diff:', error); } }; const generateCommitMessage = async () => { setIsGeneratingMessage(true); try { const response = await authenticatedFetch('/api/git/generate-commit-message', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ project: selectedProject.name, files: Array.from(selectedFiles) }) }); const data = await response.json(); if (data.message) { setCommitMessage(data.message); } else { console.error('Failed to generate commit message:', data.error); } } catch (error) { console.error('Error generating commit message:', error); } finally { setIsGeneratingMessage(false); } }; const toggleFileExpanded = (filePath) => { setExpandedFiles(prev => { const newSet = new Set(prev); if (newSet.has(filePath)) { newSet.delete(filePath); } else { newSet.add(filePath); } return newSet; }); }; const toggleCommitExpanded = (commitHash) => { setExpandedCommits(prev => { const newSet = new Set(prev); if (newSet.has(commitHash)) { newSet.delete(commitHash); } else { newSet.add(commitHash); // Fetch diff for this commit if not already fetched if (!commitDiffs[commitHash]) { fetchCommitDiff(commitHash); } } return newSet; }); }; const toggleFileSelected = (filePath) => { setSelectedFiles(prev => { const newSet = new Set(prev); if (newSet.has(filePath)) { newSet.delete(filePath); } else { newSet.add(filePath); } return newSet; }); }; const handleCommit = async () => { if (!commitMessage.trim() || selectedFiles.size === 0) return; setIsCommitting(true); try { const response = await authenticatedFetch('/api/git/commit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ project: selectedProject.name, message: commitMessage, files: Array.from(selectedFiles) }) }); const data = await response.json(); if (data.success) { // Reset state after successful commit setCommitMessage(''); setSelectedFiles(new Set()); fetchGitStatus(); fetchRemoteStatus(); } else { console.error('Commit failed:', data.error); } } catch (error) { console.error('Error committing changes:', error); } finally { setIsCommitting(false); } }; const renderDiffLine = (line, index) => { const isAddition = line.startsWith('+') && !line.startsWith('+++'); const isDeletion = line.startsWith('-') && !line.startsWith('---'); const isHeader = line.startsWith('@@'); return (
{commit.message}
{commit.author} • {commit.date}
Select a project to view source control
{gitStatus.details}
)}
Tip: Run git init in your project directory to initialize git source control.
No changes detected
No commits found
{confirmAction.message}