feat(git): show current branch boundary in commit history (#682)
* feat(git): show current branch boundary in commit history * style(git): round commit cards around branch divider * style(git): refine branch divider visual boundary
This commit is contained in:
@@ -66,6 +66,11 @@ type SyncAction = 'fetch' | 'pull' | 'push' | null;
|
|||||||
type CommitAction = 'commit' | 'commitAndPush' | null;
|
type CommitAction = 'commit' | 'commitAndPush' | null;
|
||||||
type BranchOperation = 'merge' | 'rebase' | null;
|
type BranchOperation = 'merge' | 'rebase' | null;
|
||||||
type ActionTab = 'commit' | 'branch' | 'pr' | 'worktree';
|
type ActionTab = 'commit' | 'branch' | 'pr' | 'worktree';
|
||||||
|
type HistoryBranchDivider = {
|
||||||
|
insertBeforeIndex: number;
|
||||||
|
branchName: string;
|
||||||
|
direction: 'up' | 'down';
|
||||||
|
} | null;
|
||||||
|
|
||||||
const GIT_ACTION_TAB_STORAGE_KEY = 'oc.git.actionTab';
|
const GIT_ACTION_TAB_STORAGE_KEY = 'oc.git.actionTab';
|
||||||
|
|
||||||
@@ -408,6 +413,7 @@ export const GitView: React.FC = () => {
|
|||||||
const [expandedCommitHashes, setExpandedCommitHashes] = React.useState<Set<string>>(new Set());
|
const [expandedCommitHashes, setExpandedCommitHashes] = React.useState<Set<string>>(new Set());
|
||||||
const [commitFilesMap, setCommitFilesMap] = React.useState<Map<string, CommitFileEntry[]>>(new Map());
|
const [commitFilesMap, setCommitFilesMap] = React.useState<Map<string, CommitFileEntry[]>>(new Map());
|
||||||
const [loadingCommitHashes, setLoadingCommitHashes] = React.useState<Set<string>>(new Set());
|
const [loadingCommitHashes, setLoadingCommitHashes] = React.useState<Set<string>>(new Set());
|
||||||
|
const [historyBranchDivider, setHistoryBranchDivider] = React.useState<HistoryBranchDivider>(null);
|
||||||
const [remoteUrl, setRemoteUrl] = React.useState<string | null>(null);
|
const [remoteUrl, setRemoteUrl] = React.useState<string | null>(null);
|
||||||
const [gitmojiEmojis, setGitmojiEmojis] = React.useState<GitmojiEntry[]>([]);
|
const [gitmojiEmojis, setGitmojiEmojis] = React.useState<GitmojiEntry[]>([]);
|
||||||
const [gitmojiSearch, setGitmojiSearch] = React.useState('');
|
const [gitmojiSearch, setGitmojiSearch] = React.useState('');
|
||||||
@@ -1215,6 +1221,62 @@ export const GitView: React.FC = () => {
|
|||||||
branch: currentBranch,
|
branch: currentBranch,
|
||||||
};
|
};
|
||||||
}, [canShowPullRequestSection, currentBranch, currentDirectory]);
|
}, [canShowPullRequestSection, currentBranch, currentDirectory]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!currentDirectory || !git || !log?.all?.length || !currentBranch || !baseBranch || currentBranch === baseBranch) {
|
||||||
|
setHistoryBranchDivider(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
const resolveBranchDivider = async () => {
|
||||||
|
try {
|
||||||
|
const branchOnlyLog = await git.getGitLog(currentDirectory, {
|
||||||
|
from: baseBranch,
|
||||||
|
to: 'HEAD',
|
||||||
|
maxCount: logMaxCountLocal,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const branchHashes = new Set(
|
||||||
|
(branchOnlyLog?.all ?? [])
|
||||||
|
.map((entry) => entry.hash)
|
||||||
|
.filter((hash) => typeof hash === 'string' && hash.length > 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (branchHashes.size === 0) {
|
||||||
|
setHistoryBranchDivider(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertBeforeIndex = log.all.findIndex((entry) => !branchHashes.has(entry.hash));
|
||||||
|
if (insertBeforeIndex <= 0) {
|
||||||
|
setHistoryBranchDivider(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setHistoryBranchDivider({
|
||||||
|
insertBeforeIndex,
|
||||||
|
branchName: currentBranch,
|
||||||
|
direction: 'up',
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
if (!cancelled) {
|
||||||
|
setHistoryBranchDivider(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void resolveBranchDivider();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [baseBranch, currentBranch, currentDirectory, git, log, logMaxCountLocal]);
|
||||||
// Keep these sections stable in layout; individual cards render placeholders when unavailable.
|
// Keep these sections stable in layout; individual cards render placeholders when unavailable.
|
||||||
|
|
||||||
const toggleFileSelection = (path: string) => {
|
const toggleFileSelection = (path: string) => {
|
||||||
@@ -1956,6 +2018,7 @@ export const GitView: React.FC = () => {
|
|||||||
loadingCommitHashes={loadingCommitHashes}
|
loadingCommitHashes={loadingCommitHashes}
|
||||||
onCopyHash={handleCopyCommitHash}
|
onCopyHash={handleCopyCommitHash}
|
||||||
showHeader={false}
|
showHeader={false}
|
||||||
|
branchDivider={historyBranchDivider}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ interface HistoryCommitRowProps {
|
|||||||
files: CommitFileEntry[];
|
files: CommitFileEntry[];
|
||||||
isLoadingFiles: boolean;
|
isLoadingFiles: boolean;
|
||||||
onCopyHash: (hash: string) => void;
|
onCopyHash: (hash: string) => void;
|
||||||
|
roundTop?: boolean;
|
||||||
|
roundBottom?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatCommitDate(date: string) {
|
function formatCommitDate(date: string) {
|
||||||
@@ -52,6 +54,8 @@ export const HistoryCommitRow: React.FC<HistoryCommitRowProps> = ({
|
|||||||
files,
|
files,
|
||||||
isLoadingFiles,
|
isLoadingFiles,
|
||||||
onCopyHash,
|
onCopyHash,
|
||||||
|
roundTop = false,
|
||||||
|
roundBottom = false,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<li>
|
<li>
|
||||||
@@ -60,6 +64,8 @@ export const HistoryCommitRow: React.FC<HistoryCommitRowProps> = ({
|
|||||||
onClick={onToggle}
|
onClick={onToggle}
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full flex items-start gap-3 px-3 py-2 text-left transition-colors',
|
'w-full flex items-start gap-3 px-3 py-2 text-left transition-colors',
|
||||||
|
roundTop && 'rounded-t-lg',
|
||||||
|
roundBottom && !isExpanded && 'rounded-b-lg',
|
||||||
isExpanded ? 'bg-sidebar/90' : 'hover:bg-sidebar/40'
|
isExpanded ? 'bg-sidebar/90' : 'hover:bg-sidebar/40'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -107,7 +113,7 @@ export const HistoryCommitRow: React.FC<HistoryCommitRowProps> = ({
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<div className="px-3 pb-2 pl-8 border-t border-border/40">
|
<div className={cn('px-3 pb-2 pl-8 border-t border-border/40', roundBottom && 'rounded-b-lg')}>
|
||||||
{isLoadingFiles ? (
|
{isLoadingFiles ? (
|
||||||
<div className="flex items-center gap-2 py-2">
|
<div className="flex items-center gap-2 py-2">
|
||||||
<RiLoader4Line className="size-4 animate-spin text-muted-foreground" />
|
<RiLoader4Line className="size-4 animate-spin text-muted-foreground" />
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { RiArrowUpSLine, RiArrowDownSLine } from '@remixicon/react';
|
import { RiArrowDownSLine, RiArrowUpLine, RiArrowUpSLine } from '@remixicon/react';
|
||||||
import {
|
import {
|
||||||
Collapsible,
|
Collapsible,
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
@@ -33,6 +33,11 @@ interface HistorySectionProps {
|
|||||||
loadingCommitHashes: Set<string>;
|
loadingCommitHashes: Set<string>;
|
||||||
onCopyHash: (hash: string) => void;
|
onCopyHash: (hash: string) => void;
|
||||||
showHeader?: boolean;
|
showHeader?: boolean;
|
||||||
|
branchDivider?: {
|
||||||
|
insertBeforeIndex: number;
|
||||||
|
branchName: string;
|
||||||
|
direction: 'up' | 'down';
|
||||||
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HistorySection: React.FC<HistorySectionProps> = ({
|
export const HistorySection: React.FC<HistorySectionProps> = ({
|
||||||
@@ -46,6 +51,7 @@ export const HistorySection: React.FC<HistorySectionProps> = ({
|
|||||||
loadingCommitHashes,
|
loadingCommitHashes,
|
||||||
onCopyHash,
|
onCopyHash,
|
||||||
showHeader = true,
|
showHeader = true,
|
||||||
|
branchDivider = null,
|
||||||
}) => {
|
}) => {
|
||||||
const [isOpen, setIsOpen] = React.useState(true);
|
const [isOpen, setIsOpen] = React.useState(true);
|
||||||
|
|
||||||
@@ -53,6 +59,11 @@ export const HistorySection: React.FC<HistorySectionProps> = ({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasDivider =
|
||||||
|
branchDivider !== null &&
|
||||||
|
branchDivider.insertBeforeIndex > 0 &&
|
||||||
|
branchDivider.insertBeforeIndex < log.all.length;
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<ScrollableOverlay outerClassName="min-h-0 max-h-[50vh]" className="w-full">
|
<ScrollableOverlay outerClassName="min-h-0 max-h-[50vh]" className="w-full">
|
||||||
{log.all.length === 0 ? (
|
{log.all.length === 0 ? (
|
||||||
@@ -63,17 +74,42 @@ export const HistorySection: React.FC<HistorySectionProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<ul className="divide-y divide-border/60">
|
<ul className="divide-y divide-border/60">
|
||||||
{log.all.map((entry) => (
|
{log.all.map((entry, index) => {
|
||||||
<HistoryCommitRow
|
const isBoundary = hasDivider && index === branchDivider.insertBeforeIndex;
|
||||||
key={entry.hash}
|
const roundTop = isBoundary;
|
||||||
entry={entry}
|
const roundBottom = hasDivider && index === branchDivider.insertBeforeIndex - 1;
|
||||||
isExpanded={expandedCommitHashes.has(entry.hash)}
|
|
||||||
onToggle={() => onToggleCommit(entry.hash)}
|
return (
|
||||||
files={commitFilesMap.get(entry.hash) ?? []}
|
<React.Fragment key={entry.hash}>
|
||||||
isLoadingFiles={loadingCommitHashes.has(entry.hash)}
|
{isBoundary ? (
|
||||||
onCopyHash={onCopyHash}
|
<li className="px-3 py-2" aria-hidden>
|
||||||
/>
|
<div className="flex items-center gap-2">
|
||||||
))}
|
<span className="h-px flex-1 bg-border/60" />
|
||||||
|
<span className="inline-flex max-w-[80%] items-center gap-1 typography-micro text-muted-foreground">
|
||||||
|
<span className="truncate" title={branchDivider.branchName}>{branchDivider.branchName}</span>
|
||||||
|
{branchDivider.direction === 'down' ? (
|
||||||
|
<RiArrowDownSLine className="size-3.5" />
|
||||||
|
) : (
|
||||||
|
<RiArrowUpLine className="size-3.5" />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span className="h-px flex-1 bg-border/60" />
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
) : null}
|
||||||
|
<HistoryCommitRow
|
||||||
|
entry={entry}
|
||||||
|
isExpanded={expandedCommitHashes.has(entry.hash)}
|
||||||
|
onToggle={() => onToggleCommit(entry.hash)}
|
||||||
|
files={commitFilesMap.get(entry.hash) ?? []}
|
||||||
|
isLoadingFiles={loadingCommitHashes.has(entry.hash)}
|
||||||
|
onCopyHash={onCopyHash}
|
||||||
|
roundTop={roundTop}
|
||||||
|
roundBottom={roundBottom}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</ScrollableOverlay>
|
</ScrollableOverlay>
|
||||||
|
|||||||
Reference in New Issue
Block a user