mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-02-01 15:20:49 +01:00
feature: Commit Log Viewer (#7)
* style: slightly decrease font / button size for repo container * feat: view commit modal to view local commit details * fix: allow staging and comitting deleted files * feat: handle modify-delete edge case - local side deleted, remote modified - let user pick between restore, keep deleted - special handling for editing * feat: handle empty state for commits modal
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
// ConflictRow.jsx
|
||||
import React, {useState} from 'react';
|
||||
import {AlertTriangle, GitMerge, Check, Edit2} from 'lucide-react';
|
||||
import Tooltip from '../../ui/Tooltip';
|
||||
import ResolveConflicts from './modal/ResolveConflicts';
|
||||
|
||||
const ConflictRow = ({change, isDevMode, fetchGitStatus}) => {
|
||||
console.log('ConflictRow change:', JSON.stringify(change, null, 2));
|
||||
const [showChanges, setShowChanges] = useState(false);
|
||||
|
||||
const handleResolveConflicts = e => {
|
||||
@@ -22,6 +24,15 @@ const ConflictRow = ({change, isDevMode, fetchGitStatus}) => {
|
||||
|
||||
const isResolved = change.status === 'RESOLVED';
|
||||
|
||||
// Check if this is a modify/delete conflict
|
||||
const fileConflict = change.conflict_details?.conflicting_parameters?.find(
|
||||
param => param.parameter === 'file'
|
||||
);
|
||||
const isModifyDelete = !!fileConflict;
|
||||
|
||||
// Determine if button should be disabled
|
||||
const isButtonDisabled = isModifyDelete && isResolved;
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr className='border-t border-gray-600'>
|
||||
@@ -58,12 +69,23 @@ const ConflictRow = ({change, isDevMode, fetchGitStatus}) => {
|
||||
<td className='px-4 py-2 text-left align-middle'>
|
||||
<Tooltip
|
||||
content={
|
||||
isResolved ? 'Edit resolution' : 'Resolve conflicts'
|
||||
isButtonDisabled
|
||||
? 'Abort to try again'
|
||||
: isResolved
|
||||
? 'Edit resolution'
|
||||
: 'Resolve conflicts'
|
||||
}>
|
||||
<button
|
||||
onClick={handleResolveConflicts}
|
||||
className={`flex items-center justify-center px-2 py-1 rounded hover:bg-gray-700 transition-colors text-xs w-full ${
|
||||
isResolved
|
||||
onClick={
|
||||
isButtonDisabled
|
||||
? undefined
|
||||
: handleResolveConflicts
|
||||
}
|
||||
disabled={isButtonDisabled}
|
||||
className={`flex items-center justify-center px-2 py-1 rounded transition-colors text-xs w-full ${
|
||||
isButtonDisabled
|
||||
? 'bg-gray-500 text-gray-400 cursor-not-allowed'
|
||||
: isResolved
|
||||
? 'bg-green-600 hover:bg-green-700'
|
||||
: 'bg-gray-600 hover:bg-gray-700'
|
||||
}`}>
|
||||
@@ -82,18 +104,19 @@ const ConflictRow = ({change, isDevMode, fetchGitStatus}) => {
|
||||
</Tooltip>
|
||||
</td>
|
||||
</tr>
|
||||
<ResolveConflicts
|
||||
key={`${change.file_path}-changes`}
|
||||
isOpen={showChanges}
|
||||
onClose={() => setShowChanges(false)}
|
||||
change={change}
|
||||
isIncoming={false}
|
||||
isMergeConflict={true}
|
||||
fetchGitStatus={fetchGitStatus}
|
||||
isDevMode={isDevMode}
|
||||
/>
|
||||
{!isButtonDisabled && (
|
||||
<ResolveConflicts
|
||||
key={`${change.file_path}-changes`}
|
||||
isOpen={showChanges}
|
||||
onClose={() => setShowChanges(false)}
|
||||
change={change}
|
||||
isIncoming={false}
|
||||
isMergeConflict={true}
|
||||
fetchGitStatus={fetchGitStatus}
|
||||
isDevMode={isDevMode}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConflictRow;
|
||||
|
||||
@@ -1,37 +1,59 @@
|
||||
// ConflictTable.jsx
|
||||
|
||||
import ConflictRow from './ConflictRow';
|
||||
import {Check} from 'lucide-react';
|
||||
|
||||
const EmptyState = () => (
|
||||
<div className='flex flex-col items-center justify-center p-8 text-gray-300'>
|
||||
<div className='bg-green-500/10 rounded-full p-3 mb-3'>
|
||||
<Check className='w-6 h-6 text-green-500' />
|
||||
</div>
|
||||
<p className='text-lg font-medium'>No conflicts to resolve</p>
|
||||
<p className='text-sm text-gray-400'>Everything is up to date!</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
const ConflictTable = ({conflicts, isDevMode, fetchGitStatus}) => {
|
||||
const hasUnresolvedConflicts = conflicts.some(
|
||||
conflict => conflict.status !== 'RESOLVED'
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='border border-gray-600 rounded-md overflow-hidden mt-3'>
|
||||
<table className='w-full text-sm'>
|
||||
<thead className='bg-gray-600'>
|
||||
<tr>
|
||||
<th className='px-4 py-2 text-left text-gray-300 w-1/5'>
|
||||
Status
|
||||
</th>
|
||||
<th className='px-4 py-2 text-left text-gray-300 w-1/5'>
|
||||
Type
|
||||
</th>
|
||||
<th className='px-4 py-2 text-left text-gray-300 w-1/2'>
|
||||
Name
|
||||
</th>
|
||||
<th className='px-4 py-2 text-left text-gray-300 w-1/5'>
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{conflicts.map((conflict, index) => (
|
||||
<ConflictRow
|
||||
key={`conflict-${index}`}
|
||||
change={conflict}
|
||||
isDevMode={isDevMode}
|
||||
fetchGitStatus={fetchGitStatus}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{conflicts.length === 0 ? (
|
||||
<EmptyState />
|
||||
) : (
|
||||
<>
|
||||
<table className='w-full text-sm'>
|
||||
<thead className='bg-gray-600'>
|
||||
<tr>
|
||||
<th className='px-4 py-2 text-left text-gray-300 w-1/5'>
|
||||
Status
|
||||
</th>
|
||||
<th className='px-4 py-2 text-left text-gray-300 w-1/5'>
|
||||
Type
|
||||
</th>
|
||||
<th className='px-4 py-2 text-left text-gray-300 w-1/2'>
|
||||
Name
|
||||
</th>
|
||||
<th className='px-4 py-2 text-left text-gray-300 w-1/5'>
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{conflicts.map((conflict, index) => (
|
||||
<ConflictRow
|
||||
key={`conflict-${index}`}
|
||||
change={conflict}
|
||||
isDevMode={isDevMode}
|
||||
fetchGitStatus={fetchGitStatus}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ import Alert from '../../ui/Alert';
|
||||
import LinkRepo from './modal/LinkRepo';
|
||||
import UnlinkRepo from './modal/UnlinkRepo';
|
||||
import ViewBranches from './modal/ViewBranches';
|
||||
import ViewCommits from './modal/ViewCommits';
|
||||
|
||||
const RepoContainer = ({
|
||||
settings,
|
||||
@@ -26,6 +27,7 @@ const RepoContainer = ({
|
||||
const [showLinkModal, setShowLinkModal] = useState(false);
|
||||
const [showUnlinkRepo, setShowUnlinkRepo] = useState(false);
|
||||
const [showBranchModal, setShowBranchModal] = useState(false);
|
||||
const [showCommitModal, setShowCommitModal] = useState(false);
|
||||
|
||||
const handleLinkRepo = () => {
|
||||
setLoadingAction('link_repo');
|
||||
@@ -63,13 +65,11 @@ const RepoContainer = ({
|
||||
}
|
||||
};
|
||||
|
||||
// Handler to close the LinkRepo modal and reset loadingAction
|
||||
const closeLinkModal = () => {
|
||||
setShowLinkModal(false);
|
||||
setLoadingAction('');
|
||||
};
|
||||
|
||||
// Handler to close the UnlinkRepo modal and reset loadingAction
|
||||
const closeUnlinkModal = () => {
|
||||
setShowUnlinkRepo(false);
|
||||
setLoadingAction('');
|
||||
@@ -81,8 +81,8 @@ const RepoContainer = ({
|
||||
<div className='flex flex-col sm:flex-row items-start sm:items-center justify-between space-y-3 sm:space-y-0'>
|
||||
<div className='flex flex-col sm:flex-row sm:items-center'>
|
||||
<div className='flex items-center'>
|
||||
<Github className='mr-2 text-blue-400' size={14} />
|
||||
<h3 className='text-m font-semibold text-gray-100 mr-2 mb-1 sm:mb-0'>
|
||||
<Github className='mr-2 text-blue-400' size={13} />
|
||||
<h3 className='text-sm font-semibold text-gray-100 mr-2 mb-1 sm:mb-0'>
|
||||
{settings
|
||||
? 'Connected Repository:'
|
||||
: 'Repository:'}
|
||||
@@ -93,11 +93,11 @@ const RepoContainer = ({
|
||||
href={settings.gitRepo}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-blue-400 hover:text-blue-300 transition-colors text-m font-medium truncate max-w-xs sm:max-w-md'>
|
||||
className='text-blue-400 hover:text-blue-300 transition-colors text-sm font-medium truncate max-w-xs sm:max-w-md'>
|
||||
{settings.gitRepo}
|
||||
</a>
|
||||
) : (
|
||||
<span className='text-gray-400 text-sm'>
|
||||
<span className='text-gray-400 text-xs'>
|
||||
No repository linked
|
||||
</span>
|
||||
)}
|
||||
@@ -112,16 +112,16 @@ const RepoContainer = ({
|
||||
? () => setShowUnlinkRepo(true)
|
||||
: handleLinkRepo
|
||||
}
|
||||
className={`flex items-center px-4 py-2 ${
|
||||
className={`flex items-center px-3 py-1.5 ${
|
||||
settings
|
||||
? 'bg-red-600 hover:bg-red-700'
|
||||
: 'bg-blue-600 hover:bg-blue-700'
|
||||
} text-white rounded-md transition-colors duration-200 ease-in-out text-m font-medium shadow-sm`}
|
||||
} text-white rounded-md transition-colors duration-200 ease-in-out text-sm font-medium shadow-sm`}
|
||||
disabled={loadingAction !== ''}>
|
||||
{settings ? (
|
||||
<Unlink size={16} className='mr-2' />
|
||||
<Unlink size={14} className='mr-2' />
|
||||
) : (
|
||||
<Link size={16} className='mr-2' />
|
||||
<Link size={14} className='mr-2' />
|
||||
)}
|
||||
{settings ? 'Unlink' : 'Link Repository'}
|
||||
</button>
|
||||
@@ -132,32 +132,41 @@ const RepoContainer = ({
|
||||
<div className='flex items-center'>
|
||||
<GitBranch
|
||||
className='mr-2 text-blue-400'
|
||||
size={14}
|
||||
size={13}
|
||||
/>
|
||||
<h3 className='text-m font-semibold text-gray-100 mr-2'>
|
||||
<h3 className='text-sm font-semibold text-gray-100 mr-2'>
|
||||
Current Branch:
|
||||
</h3>
|
||||
{status ? (
|
||||
<span className='text-m font-medium'>
|
||||
<span className='text-sm font-medium'>
|
||||
{status.branch}
|
||||
</span>
|
||||
) : (
|
||||
<span className='text-gray-400 text-m flex items-center'>
|
||||
<span className='text-gray-400 text-sm flex items-center'>
|
||||
<Loader
|
||||
className='animate-spin mr-2'
|
||||
size={14}
|
||||
size={13}
|
||||
/>
|
||||
Loading branch information...
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowBranchModal(true)}
|
||||
className='flex items-center px-3 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors duration-200 ease-in-out text-m'
|
||||
disabled={!status}>
|
||||
<Eye size={14} className='mr-2' />
|
||||
View Branches
|
||||
</button>
|
||||
<div className='flex space-x-2'>
|
||||
<button
|
||||
onClick={() => setShowBranchModal(true)}
|
||||
className='flex items-center px-2.5 py-1.5 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors duration-200 ease-in-out text-sm'
|
||||
disabled={!status}>
|
||||
<Eye size={13} className='mr-2' />
|
||||
View Branches
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowCommitModal(true)}
|
||||
className='flex items-center px-2.5 py-1.5 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors duration-200 ease-in-out text-sm'
|
||||
disabled={!status}>
|
||||
<GitCommit size={13} className='mr-2' />
|
||||
View Commits
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -172,14 +181,26 @@ const RepoContainer = ({
|
||||
onSubmit={handleUnlinkRepo}
|
||||
/>
|
||||
{settings && status && (
|
||||
<ViewBranches
|
||||
isOpen={showBranchModal}
|
||||
onClose={() => setShowBranchModal(false)}
|
||||
repoUrl={settings.gitRepo}
|
||||
currentBranch={status.branch}
|
||||
onBranchChange={fetchGitStatus}
|
||||
isDevMode={isDevMode}
|
||||
/>
|
||||
<>
|
||||
<ViewBranches
|
||||
isOpen={showBranchModal}
|
||||
onClose={() => setShowBranchModal(false)}
|
||||
repoUrl={settings.gitRepo}
|
||||
currentBranch={status.branch}
|
||||
onBranchChange={fetchGitStatus}
|
||||
isDevMode={isDevMode}
|
||||
/>
|
||||
<ViewCommits
|
||||
isOpen={showCommitModal}
|
||||
onClose={() => setShowCommitModal(false)}
|
||||
repoUrl={settings.gitRepo}
|
||||
currentBranch={status.branch}
|
||||
localCommits={status.local_commits || []}
|
||||
remoteCommits={status.remote_commits || []}
|
||||
outgoingChanges={status.outgoing_changes || []}
|
||||
incomingChanges={status.incoming_changes || []}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -278,9 +278,62 @@ const ResolveConflicts = ({
|
||||
);
|
||||
};
|
||||
|
||||
const renderModifyDeleteConflict = () => {
|
||||
if (change.status !== 'MODIFY_DELETE') return null;
|
||||
|
||||
return renderTable(
|
||||
'File Status Conflict',
|
||||
[
|
||||
{label: 'Status', width: 'w-1/4'},
|
||||
{label: 'Local Version', width: 'w-1/4'},
|
||||
{label: 'Remote Version', width: 'w-1/4'},
|
||||
{label: 'Resolution', width: 'w-1/4'}
|
||||
],
|
||||
[change.conflict_details.conflicting_parameters[0]], // There's only one parameter for modify/delete
|
||||
({parameter, local_value, incoming_value}) => (
|
||||
<tr key={parameter} className='border-t border-gray-600'>
|
||||
<td className='px-4 py-2.5 text-gray-300'>File</td>
|
||||
<td className='px-4 py-2.5 text-gray-300'>
|
||||
{local_value === 'deleted' ? 'Deleted' : 'Present'}
|
||||
</td>
|
||||
<td className='px-4 py-2.5 text-gray-300'>
|
||||
{incoming_value === 'deleted' ? 'Deleted' : 'Present'}
|
||||
</td>
|
||||
<td className='px-4 py-2.5'>
|
||||
<select
|
||||
value={conflictResolutions['file'] || ''}
|
||||
onChange={e =>
|
||||
handleResolutionChange('file', e.target.value)
|
||||
}
|
||||
className='w-full p-2 bg-gray-700 text-gray-200 rounded'>
|
||||
<option value='' disabled>
|
||||
Select
|
||||
</option>
|
||||
<option value='local'>
|
||||
{change.deleted_in_head
|
||||
? 'Keep Deleted'
|
||||
: 'Keep File'}
|
||||
</option>
|
||||
<option value='incoming'>
|
||||
{change.deleted_in_head
|
||||
? 'Restore File'
|
||||
: 'Delete File'}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const areAllConflictsResolved = () => {
|
||||
if (!isMergeConflict) return true;
|
||||
|
||||
// For modify/delete conflicts, only need to resolve the file status
|
||||
if (change.status === 'MODIFY_DELETE') {
|
||||
return !!conflictResolutions['file'];
|
||||
}
|
||||
|
||||
const requiredResolutions = [];
|
||||
|
||||
// Basic fields
|
||||
@@ -385,9 +438,17 @@ const ResolveConflicts = ({
|
||||
title={titleContent}
|
||||
width='5xl'>
|
||||
<div className='space-y-4'>
|
||||
{renderBasicFields()}
|
||||
{renderCustomFormatConflicts()}
|
||||
{renderTagConflicts()}
|
||||
{change.status === 'MODIFY_DELETE' ? (
|
||||
// For modify/delete conflicts, only show the file status
|
||||
renderModifyDeleteConflict()
|
||||
) : (
|
||||
// For regular conflicts, show all the existing sections
|
||||
<>
|
||||
{renderBasicFields()}
|
||||
{renderCustomFormatConflicts()}
|
||||
{renderTagConflicts()}
|
||||
</>
|
||||
)}
|
||||
|
||||
{isMergeConflict && (
|
||||
<div className='flex justify-end'>
|
||||
|
||||
@@ -93,6 +93,28 @@ const ViewChanges = ({isOpen, onClose, change, isIncoming}) => {
|
||||
);
|
||||
};
|
||||
|
||||
const formatValue = value => {
|
||||
if (value === null || value === undefined) return '-';
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.map(item => {
|
||||
if (
|
||||
typeof item === 'object' &&
|
||||
item.id !== undefined &&
|
||||
item.score !== undefined
|
||||
) {
|
||||
return `Format ${item.id}: ${item.score}`;
|
||||
}
|
||||
return String(item);
|
||||
})
|
||||
.join(', ');
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
|
||||
const renderChanges = () => {
|
||||
const isNewFile = change.status === 'New';
|
||||
|
||||
@@ -157,15 +179,15 @@ const ViewChanges = ({isOpen, onClose, change, isIncoming}) => {
|
||||
</td>
|
||||
{isNewFile ? (
|
||||
<td className='px-4 py-2.5 text-gray-300'>
|
||||
{to ?? value ?? '-'}
|
||||
{formatValue(value)}
|
||||
</td>
|
||||
) : (
|
||||
<>
|
||||
<td className='px-4 py-2.5 text-gray-300'>
|
||||
{from ?? '-'}
|
||||
{formatValue(from)}
|
||||
</td>
|
||||
<td className='px-4 py-2.5 text-gray-300'>
|
||||
{to ?? value ?? '-'}
|
||||
{formatValue(to ?? value)}
|
||||
</td>
|
||||
</>
|
||||
)}
|
||||
|
||||
295
frontend/src/components/settings/git/modal/ViewCommits.jsx
Normal file
295
frontend/src/components/settings/git/modal/ViewCommits.jsx
Normal file
@@ -0,0 +1,295 @@
|
||||
import React, {useState, useEffect} from 'react';
|
||||
import {
|
||||
GitBranch,
|
||||
GitCommit,
|
||||
ExternalLink,
|
||||
ArrowUpRight,
|
||||
ArrowDownRight,
|
||||
GitMerge,
|
||||
Loader,
|
||||
User,
|
||||
Clock,
|
||||
Hash
|
||||
} from 'lucide-react';
|
||||
import Modal from '../../../ui/Modal';
|
||||
import {getCommitHistory} from '../../../../api/api';
|
||||
import Alert from '../../../ui/Alert';
|
||||
|
||||
const ViewCommits = ({isOpen, onClose, repoUrl, currentBranch}) => {
|
||||
const [selectedCommit, setSelectedCommit] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [commits, setCommits] = useState([]);
|
||||
const [aheadCount, setAheadCount] = useState(0);
|
||||
const [behindCount, setBehindCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
fetchCommitHistory();
|
||||
}
|
||||
}, [isOpen, currentBranch]);
|
||||
|
||||
const fetchCommitHistory = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await getCommitHistory(currentBranch);
|
||||
if (response.success) {
|
||||
setCommits(response.data.local_commits);
|
||||
setAheadCount(response.data.ahead_count);
|
||||
setBehindCount(response.data.behind_count);
|
||||
} else {
|
||||
Alert.error(response.error || 'Failed to fetch commit history');
|
||||
}
|
||||
} catch (error) {
|
||||
Alert.error('Failed to fetch commit history');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = dateStr => {
|
||||
const date = new Date(dateStr);
|
||||
return {
|
||||
date: date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
}),
|
||||
time: date.toLocaleTimeString('en-US', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
};
|
||||
};
|
||||
|
||||
const getCommitStatus = commit => {
|
||||
if (commit.isMerge) {
|
||||
return {
|
||||
icon: <GitMerge size={14} className='text-purple-500' />,
|
||||
text: 'Merge'
|
||||
};
|
||||
}
|
||||
|
||||
const isAheadCommit =
|
||||
aheadCount > 0 && commits.indexOf(commit) < aheadCount;
|
||||
if (isAheadCommit) {
|
||||
return {
|
||||
icon: <ArrowUpRight size={14} className='text-green-500' />,
|
||||
text: 'Outgoing'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
icon: <GitCommit size={14} className='text-gray-400' />,
|
||||
text: 'Synced'
|
||||
};
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className='flex items-center justify-center h-40'>
|
||||
<Loader className='w-6 h-6 animate-spin text-blue-500' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (commits.length === 0) {
|
||||
return (
|
||||
<div className='flex flex-col items-center justify-center h-40 text-gray-400'>
|
||||
<GitCommit size={24} className='mb-2' />
|
||||
<p className='text-sm'>No commits found in this branch</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='space-y-2'>
|
||||
{commits.map(commit => {
|
||||
const status = getCommitStatus(commit);
|
||||
const formattedDate = formatDate(commit.date);
|
||||
const isExpanded = selectedCommit === commit.hash;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={commit.hash}
|
||||
className={`p-3 rounded-lg border ${
|
||||
isExpanded
|
||||
? 'border-blue-500 dark:bg-gray-700'
|
||||
: 'border-gray-200 dark:border-gray-700 hover:border-blue-400'
|
||||
} transition-colors cursor-pointer`}
|
||||
onClick={() => setSelectedCommit(commit.hash)}>
|
||||
<div>
|
||||
{/* Top section: Message and Hash */}
|
||||
<div
|
||||
className={`flex justify-between items-center ${
|
||||
isExpanded ? 'mb-3' : ''
|
||||
}`}>
|
||||
<div className='flex items-center space-x-3 flex-1'>
|
||||
{status.icon}
|
||||
<span className='text-xs font-medium font-mono truncate'>
|
||||
{commit.message}
|
||||
</span>
|
||||
</div>
|
||||
<div className='flex items-center space-x-1.5 text-xs text-gray-400 ml-4'>
|
||||
<Hash
|
||||
size={12}
|
||||
className='text-gray-500'
|
||||
/>
|
||||
<span className='font-mono'>
|
||||
{commit.hash.substring(0, 7)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Details section */}
|
||||
{isExpanded && (
|
||||
<>
|
||||
<div className='w-full border-t border-gray-700 mb-3'></div>
|
||||
<div className='mb-3 text-sm text-gray-400 py-3'>
|
||||
<div className='grid grid-cols-2 gap-4 text-xs'>
|
||||
<div>
|
||||
<div className='font-semibold mb-1'>
|
||||
Files Changed (
|
||||
{
|
||||
commit.details
|
||||
.files_changed
|
||||
.length
|
||||
}
|
||||
):
|
||||
</div>
|
||||
<ul className='list-disc list-inside'>
|
||||
{commit.details.files_changed.map(
|
||||
(file, idx) => (
|
||||
<li
|
||||
key={idx}
|
||||
className='truncate font-mono'>
|
||||
{file}
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<div className='font-semibold mb-1'>
|
||||
Changes:
|
||||
</div>
|
||||
{commit.details.insertions >
|
||||
0 && (
|
||||
<div className='text-green-400'>
|
||||
+
|
||||
{
|
||||
commit.details
|
||||
.insertions
|
||||
}{' '}
|
||||
lines added
|
||||
</div>
|
||||
)}
|
||||
{commit.details.deletions >
|
||||
0 && (
|
||||
<div className='text-red-400'>
|
||||
-
|
||||
{
|
||||
commit.details
|
||||
.deletions
|
||||
}{' '}
|
||||
lines removed
|
||||
</div>
|
||||
)}
|
||||
{commit.details
|
||||
.insertions === 0 &&
|
||||
commit.details
|
||||
.deletions ===
|
||||
0 && (
|
||||
<div className='text-gray-400'>
|
||||
No line changes
|
||||
detected
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='w-full border-t border-gray-700 mb-3'></div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Bottom section: Author and date */}
|
||||
<div className='flex justify-between items-center text-xs text-gray-400'>
|
||||
<div className='flex items-center space-x-1.5'>
|
||||
<User
|
||||
size={12}
|
||||
className='text-gray-500'
|
||||
/>
|
||||
<span className='truncate'>
|
||||
{commit.author}
|
||||
</span>
|
||||
</div>
|
||||
<div className='flex items-center space-x-1.5'>
|
||||
<Clock
|
||||
size={12}
|
||||
className='text-gray-500'
|
||||
/>
|
||||
<span>
|
||||
{formattedDate.date} at{' '}
|
||||
{formattedDate.time}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title={
|
||||
<div className='flex items-center justify-between w-full'>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<GitBranch size={16} className='text-blue-400' />
|
||||
<span className='text-base'>
|
||||
Commit History - {currentBranch}
|
||||
</span>
|
||||
</div>
|
||||
{(aheadCount > 0 || behindCount > 0) && (
|
||||
<div className='flex items-center space-x-3 text-sm px-2'>
|
||||
{aheadCount > 0 && (
|
||||
<div className='flex items-center text-green-400 bg-green-400/10 px-2 py-1 rounded'>
|
||||
<ArrowUpRight size={14} className='mr-1' />
|
||||
<span>
|
||||
{aheadCount} commit
|
||||
{aheadCount !== 1 ? 's' : ''} ahead
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{behindCount > 0 && (
|
||||
<div className='flex items-center text-blue-400 bg-blue-400/10 px-2 py-1 rounded'>
|
||||
<ArrowDownRight
|
||||
size={14}
|
||||
className='mr-1'
|
||||
/>
|
||||
<span>
|
||||
{behindCount} commit
|
||||
{behindCount !== 1 ? 's' : ''} behind
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
width='screen-xl'
|
||||
height='lg'>
|
||||
<div className='space-y-4'>
|
||||
<div className='overflow-y-auto max-h-[60vh]'>
|
||||
{renderContent()}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewCommits;
|
||||
Reference in New Issue
Block a user