diff --git a/src/components/GitPanel.jsx b/src/components/GitPanel.jsx deleted file mode 100644 index 76690fb..0000000 --- a/src/components/GitPanel.jsx +++ /dev/null @@ -1,1426 +0,0 @@ -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'; -import DiffViewer from './DiffViewer.jsx'; - -function GitPanel({ selectedProject, isMobile, onFileOpen }) { - 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 [isCreatingInitialCommit, setIsCreatingInitialCommit] = useState(false); - const textareaRef = useRef(null); - const dropdownRef = useRef(null); - - // Get current provider from localStorage (same as ChatInterface does) - const [provider, setProvider] = useState(() => { - return localStorage.getItem('selected-provider') || 'claude'; - }); - - // Listen for provider changes in localStorage - useEffect(() => { - const handleStorageChange = () => { - const newProvider = localStorage.getItem('selected-provider') || 'claude'; - setProvider(newProvider); - }; - - window.addEventListener('storage', handleStorageChange); - return () => window.removeEventListener('storage', handleStorageChange); - }, []); - - useEffect(() => { - // Clear stale repo-scoped state when project changes. - setCurrentBranch(''); - setBranches([]); - setGitStatus(null); - setRemoteStatus(null); - setSelectedFiles(new Set()); - - if (!selectedProject) { - return; - } - - fetchGitStatus(); - fetchBranches(); - fetchRemoteStatus(); - }, [selectedProject]); - - useEffect(() => { - if (!selectedProject || activeView !== 'history') { - return; - } - - 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 }); - setCurrentBranch(''); - setSelectedFiles(new Set()); - } 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); - } - for (const file of data.deleted || []) { - fetchFileDiff(file); - } - for (const file of data.untracked || []) { - fetchFileDiff(file); - } - } - } catch (error) { - console.error('Error fetching git status:', error); - setGitStatus({ error: 'Git operation failed', details: String(error) }); - setCurrentBranch(''); - setSelectedFiles(new Set()); - } 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); - } else { - setBranches([]); - } - } catch (error) { - console.error('Error fetching branches:', error); - setBranches([]); - } - }; - - 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 handleFileOpen = async (filePath) => { - if (!onFileOpen) return; - - try { - // Fetch file content with diff information - const response = await authenticatedFetch(`/api/git/file-with-diff?project=${encodeURIComponent(selectedProject.name)}&file=${encodeURIComponent(filePath)}`); - const data = await response.json(); - - if (data.error) { - console.error('Error fetching file with diff:', data.error); - // Fallback: open without diff info - onFileOpen(filePath); - return; - } - - // Create diffInfo object for CodeEditor - const diffInfo = { - old_string: data.oldContent || '', - new_string: data.currentContent || '' - }; - - // Open file with diff information - onFileOpen(filePath, diffInfo); - } catch (error) { - console.error('Error opening file:', error); - // Fallback: open without diff info - onFileOpen(filePath); - } - }; - - 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), - provider: provider // Pass the current provider (claude or cursor) - }) - }); - - 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 createInitialCommit = async () => { - setIsCreatingInitialCommit(true); - try { - const response = await authenticatedFetch('/api/git/initial-commit', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - project: selectedProject.name - }) - }); - - const data = await response.json(); - if (data.success) { - fetchGitStatus(); - fetchRemoteStatus(); - } else { - console.error('Initial commit failed:', data.error); - alert(data.error || 'Failed to create initial commit'); - } - } catch (error) { - console.error('Error creating initial commit:', error); - alert('Failed to create initial commit'); - } finally { - setIsCreatingInitialCommit(false); - } - }; - - const getStatusLabel = (status) => { - switch (status) { - case 'M': return 'Modified'; - case 'A': return 'Added'; - case 'D': return 'Deleted'; - case 'U': return 'Untracked'; - default: return status; - } - }; - - const renderCommitItem = (commit) => { - const isExpanded = expandedCommits.has(commit.hash); - const diff = commitDiffs[commit.hash]; - - return ( -
-
toggleCommitExpanded(commit.hash)} - > -
- {isExpanded ? : } -
-
-
-
-

- {commit.message} -

-

- {commit.author} • {commit.date} -

-
- - {commit.hash.substring(0, 7)} - -
-
-
- {isExpanded && diff && ( -
-
-
- {commit.stats} -
- -
-
- )} -
- ); - }; - - const renderFileItem = (filePath, status) => { - const isExpanded = expandedFiles.has(filePath); - const isSelected = selectedFiles.has(filePath); - const diff = gitDiff[filePath]; - - return ( -
-
- toggleFileSelected(filePath)} - onClick={(e) => e.stopPropagation()} - className={`rounded border-border text-primary focus:ring-primary/40 bg-background checked:bg-primary ${isMobile ? 'mr-1.5' : 'mr-2'}`} - /> -
-
{ - e.stopPropagation(); - toggleFileExpanded(filePath); - }} - > - -
- { - e.stopPropagation(); - handleFileOpen(filePath); - }} - title="Click to open file" - > - {filePath} - -
- {(status === 'M' || status === 'D') && ( - - )} - {status === 'U' && ( - - )} - - {status} - -
-
-
-
- {/* Operation header */} -
-
- - {status} - - - {getStatusLabel(status)} - -
- {isMobile && ( - - )} -
-
- {diff && } -
-
-
- ); - }; - - if (!selectedProject) { - return ( -
-

Select a project to view source control

-
- ); - } - - return ( -
- {/* Header */} -
-
- - - {/* Branch Dropdown */} - {showBranchDropdown && ( -
-
- {branches.map(branch => ( - - ))} -
-
- -
-
- )} -
- -
- {/* Remote action buttons - smart logic based on ahead/behind status */} - {remoteStatus?.hasRemote && ( - <> - {/* Publish button - show when branch doesn't exist on remote */} - {!remoteStatus?.hasUpstream && ( - - )} - - {/* Show normal push/pull buttons only if branch has upstream */} - {remoteStatus?.hasUpstream && !remoteStatus?.isUpToDate && ( - <> - {/* Pull button - show when behind (primary action) */} - {remoteStatus.behind > 0 && ( - - )} - - {/* Push button - show when ahead (primary action when ahead only) */} - {remoteStatus.ahead > 0 && ( - - )} - - {/* Fetch button - show when ahead only or when diverged (secondary action) */} - {(remoteStatus.ahead > 0 || (remoteStatus.behind > 0 && remoteStatus.ahead > 0)) && ( - - )} - - )} - - )} - - -
-
- - {/* Git Repository Not Found Message */} - {gitStatus?.error ? ( -
-
- -
-

{gitStatus.error}

- {gitStatus.details && ( -

{gitStatus.details}

- )} -
-

- Tip: Run git init in your project directory to initialize git source control. -

-
-
- ) : ( - <> - {/* Tab Navigation - Only show when git is available and no files expanded */} -
- - -
- - {/* Changes View */} - {activeView === 'changes' && ( - <> - {/* Mobile Commit Toggle Button / Desktop Always Visible - Hide when files expanded */} -
- {isMobile && isCommitAreaCollapsed ? ( -
- -
- ) : ( - <> - {/* Commit Message Input */} -
- {/* Mobile collapse button */} - {isMobile && ( -
- Commit Changes - -
- )} - -
-