import { Helmet } from '@dr.pogodin/react-helmet'; import Box from '@mui/material/Box'; import CircularProgress from '@mui/material/CircularProgress'; import Container from '@mui/material/Container'; import { styled } from '@mui/material/styles'; import Tab from '@mui/material/Tab'; import Table from '@mui/material/Table'; import TableBody from '@mui/material/TableBody'; import TableCell from '@mui/material/TableCell'; import TableHead from '@mui/material/TableHead'; import TableRow from '@mui/material/TableRow'; import Tabs from '@mui/material/Tabs'; import Tooltip from '@mui/material/Tooltip'; import Typography from '@mui/material/Typography'; import { GitLog as ReactGitLog } from '@tomplum/react-git-log'; import { formatDistanceToNow } from 'date-fns'; import { enUS, zhCN } from 'date-fns/locale'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { CommitDetailsPanel } from './CommitDetailsPanel'; import { CustomGitTooltip } from './CustomGitTooltip'; import { FileDiffPanel } from './FileDiffPanel'; import type { GitLogEntry } from './types'; import { useCommitDetails } from './useCommitDetails'; import { useGitLogData } from './useGitLogData'; const Root = styled((properties: React.ComponentProps) => )` width: 100%; height: 100vh; display: flex; flex-direction: column; padding: 16px; overflow: hidden; `; const ContentWrapper = styled(Box)` display: flex; flex: 1; gap: 8px; overflow: hidden; `; const GitLogWrapper = styled(Box)` width: 600px; flex-shrink: 0; display: flex; flex-direction: column; overflow: hidden; border: 1px solid ${({ theme }) => theme.palette.divider}; border-radius: 4px; `; const TabsContainer = styled(Box)` border-bottom: 1px solid ${({ theme }) => theme.palette.divider}; `; const TabContent = styled(Box)` flex: 1; overflow: auto; `; const StyledTableRow = styled(TableRow)<{ selected?: boolean }>` cursor: pointer; &:hover { background-color: ${({ theme }) => theme.palette.action.hover}; } ${({ selected, theme }) => selected && ` background-color: ${theme.palette.action.selected}; &:hover { background-color: ${theme.palette.action.selected}; } `} `; const DetailsWrapper = styled(Box)` width: 280px; flex-shrink: 0; display: flex; flex-direction: column; gap: 8px; overflow: hidden; `; const DetailsPanelWrapper = styled(Box)` flex: 1; overflow: hidden; border: 1px solid ${({ theme }) => theme.palette.divider}; border-radius: 4px; `; const DiffPanelWrapper = styled(Box)` flex: 1; min-width: 400px; overflow: hidden; border: 1px solid ${({ theme }) => theme.palette.divider}; border-radius: 4px; `; const LoadingContainer = styled(Box)` display: flex; align-items: center; justify-content: center; height: 100%; `; const FileChip = styled(Box)` display: inline-block; font-size: 0.7rem; font-family: monospace; padding: 2px 4px; margin: 2px; background-color: ${({ theme }) => theme.palette.action.hover}; border-radius: 3px; max-width: 100px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; `; interface ICommitTableRowProps { commit: GitLogEntry; commitDate: Date; onSelect: () => void; selected: boolean; } function CommitTableRow({ commit, selected, commitDate, onSelect }: ICommitTableRowProps): React.JSX.Element { const { t, i18n } = useTranslation(); // Use files from commit entry (already loaded in useGitLogData) const files = commit.files ?? []; const displayFiles = files.slice(0, 3); const hasMore = files.length > 3; return ( {commit.message} {displayFiles.map((file, index) => { const fileName = file.split('/').pop() || file; return ( {fileName} ); })} {hasMore && ( +{files.length - 3} )} {files.length === 0 && ( {t('GitLog.NoFilesChanged')} )} {formatDistanceToNow(commitDate, { addSuffix: true, locale: i18n.language.startsWith('zh') ? zhCN : enUS })} ); } export default function GitHistory(): React.JSX.Element { const { t } = useTranslation(); const { entries, loading, error, workspaceInfo, currentBranch, lastChangeType } = useGitLogData(); const { selectedCommit, setSelectedCommit } = useCommitDetails(); const [selectedFile, setSelectedFile] = useState(null); const [viewMode, setViewMode] = useState<'current' | 'all'>('current'); const [shouldSelectFirst, setShouldSelectFirst] = useState(false); // Create a tooltip wrapper that passes the translation function // The props coming from react-git-log don't include 't', so we add it const renderTooltip = (props: Omit[0], 't'>) => { return CustomGitTooltip({ ...props, t }); }; // Auto-select first commit after successful manual commit useEffect(() => { if (shouldSelectFirst && entries.length > 0) { // Find the first non-uncommitted commit const firstCommit = entries.find((entry) => entry.hash !== ''); if (firstCommit) { setSelectedCommit(firstCommit); setShouldSelectFirst(false); } } }, [shouldSelectFirst, entries, setSelectedCommit]); // Auto-select first commit when a new commit is detected useEffect(() => { if (lastChangeType === 'commit' && entries.length > 0) { // Find the first non-uncommitted commit (the newly created commit) const firstCommit = entries.find((entry) => entry.hash !== ''); if (firstCommit) { setSelectedCommit(firstCommit); } } }, [lastChangeType, entries, setSelectedCommit]); // Maintain selection across refreshes by hash // Skip if we should select first (manual commit) or if a commit just happened (auto-selection in progress) useEffect(() => { if (selectedCommit && entries.length > 0 && !shouldSelectFirst && lastChangeType !== 'commit') { // Try to find the same commit in the new entries const stillExists = entries.find((entry) => entry.hash === selectedCommit.hash); // Only update if data actually changed (compare by serialization) if (stillExists && JSON.stringify(stillExists) !== JSON.stringify(selectedCommit)) { // Update to the new entry object to get fresh data setSelectedCommit(stillExists); } } }, [entries, selectedCommit, shouldSelectFirst, lastChangeType]); const handleCommitSuccess = () => { // Trigger selection of first commit after data refreshes setShouldSelectFirst(true); }; if (loading) { return ( {t('GitLog.Title')} ); } if (error !== null) { return ( {t('GitLog.Title')} {error} ); } const workspaceName = workspaceInfo && 'name' in workspaceInfo ? workspaceInfo.name : ''; return ( {workspaceName ? `${t('GitLog.Title')} - ${workspaceName}` : t('GitLog.Title')} { setViewMode(newValue); }} > {viewMode === 'current' ? ( <> {entries.length > 0 ? ( {t('GitLog.Message')} {t('GitLog.Files')} {t('GitLog.Date')} {entries.map((entry) => { const commitDate = new Date(entry.committerDate); const isSelected = selectedCommit?.hash === entry.hash; return ( { setSelectedCommit(entry); setSelectedFile(null); }} /> ); })}
) : ( {t('GitLog.NoCommits')} )} ) : ( <> {entries.length > 0 && currentBranch !== null ? ( { setSelectedCommit(commit as unknown as GitLogEntry); setSelectedFile(null); }} enableSelectedCommitStyling > ) : ( {t('GitLog.NoCommits')} )} )}
entry.hash !== '')?.hash} /> { setSelectedFile(null); // Trigger git log refresh after discard setShouldSelectFirst(true); }} />
); }