From de50283bdf8ed1c697bac35044d876d8afa0e579 Mon Sep 17 00:00:00 2001 From: Iuliia Ivashko Date: Tue, 17 Mar 2026 02:45:37 +0200 Subject: [PATCH] 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 --- packages/ui/src/components/views/GitView.tsx | 63 +++++++++++++++++++ .../components/views/git/HistoryCommitRow.tsx | 8 ++- .../components/views/git/HistorySection.tsx | 60 ++++++++++++++---- 3 files changed, 118 insertions(+), 13 deletions(-) diff --git a/packages/ui/src/components/views/GitView.tsx b/packages/ui/src/components/views/GitView.tsx index 91d53a94..faa4b203 100644 --- a/packages/ui/src/components/views/GitView.tsx +++ b/packages/ui/src/components/views/GitView.tsx @@ -66,6 +66,11 @@ type SyncAction = 'fetch' | 'pull' | 'push' | null; type CommitAction = 'commit' | 'commitAndPush' | null; type BranchOperation = 'merge' | 'rebase' | null; 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'; @@ -408,6 +413,7 @@ export const GitView: React.FC = () => { const [expandedCommitHashes, setExpandedCommitHashes] = React.useState>(new Set()); const [commitFilesMap, setCommitFilesMap] = React.useState>(new Map()); const [loadingCommitHashes, setLoadingCommitHashes] = React.useState>(new Set()); + const [historyBranchDivider, setHistoryBranchDivider] = React.useState(null); const [remoteUrl, setRemoteUrl] = React.useState(null); const [gitmojiEmojis, setGitmojiEmojis] = React.useState([]); const [gitmojiSearch, setGitmojiSearch] = React.useState(''); @@ -1215,6 +1221,62 @@ export const GitView: React.FC = () => { branch: currentBranch, }; }, [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. const toggleFileSelection = (path: string) => { @@ -1956,6 +2018,7 @@ export const GitView: React.FC = () => { loadingCommitHashes={loadingCommitHashes} onCopyHash={handleCopyCommitHash} showHeader={false} + branchDivider={historyBranchDivider} /> diff --git a/packages/ui/src/components/views/git/HistoryCommitRow.tsx b/packages/ui/src/components/views/git/HistoryCommitRow.tsx index 7125c156..ee16c476 100644 --- a/packages/ui/src/components/views/git/HistoryCommitRow.tsx +++ b/packages/ui/src/components/views/git/HistoryCommitRow.tsx @@ -12,6 +12,8 @@ interface HistoryCommitRowProps { files: CommitFileEntry[]; isLoadingFiles: boolean; onCopyHash: (hash: string) => void; + roundTop?: boolean; + roundBottom?: boolean; } function formatCommitDate(date: string) { @@ -52,6 +54,8 @@ export const HistoryCommitRow: React.FC = ({ files, isLoadingFiles, onCopyHash, + roundTop = false, + roundBottom = false, }) => { return (
  • @@ -60,6 +64,8 @@ export const HistoryCommitRow: React.FC = ({ onClick={onToggle} className={cn( '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' )} > @@ -107,7 +113,7 @@ export const HistoryCommitRow: React.FC = ({ {isExpanded && ( -
    +
    {isLoadingFiles ? (
    diff --git a/packages/ui/src/components/views/git/HistorySection.tsx b/packages/ui/src/components/views/git/HistorySection.tsx index a6bf4201..f7aa9620 100644 --- a/packages/ui/src/components/views/git/HistorySection.tsx +++ b/packages/ui/src/components/views/git/HistorySection.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { RiArrowUpSLine, RiArrowDownSLine } from '@remixicon/react'; +import { RiArrowDownSLine, RiArrowUpLine, RiArrowUpSLine } from '@remixicon/react'; import { Collapsible, CollapsibleContent, @@ -33,6 +33,11 @@ interface HistorySectionProps { loadingCommitHashes: Set; onCopyHash: (hash: string) => void; showHeader?: boolean; + branchDivider?: { + insertBeforeIndex: number; + branchName: string; + direction: 'up' | 'down'; + } | null; } export const HistorySection: React.FC = ({ @@ -46,6 +51,7 @@ export const HistorySection: React.FC = ({ loadingCommitHashes, onCopyHash, showHeader = true, + branchDivider = null, }) => { const [isOpen, setIsOpen] = React.useState(true); @@ -53,6 +59,11 @@ export const HistorySection: React.FC = ({ return null; } + const hasDivider = + branchDivider !== null && + branchDivider.insertBeforeIndex > 0 && + branchDivider.insertBeforeIndex < log.all.length; + const content = ( {log.all.length === 0 ? ( @@ -63,17 +74,42 @@ export const HistorySection: React.FC = ({
    ) : (
      - {log.all.map((entry) => ( - onToggleCommit(entry.hash)} - files={commitFilesMap.get(entry.hash) ?? []} - isLoadingFiles={loadingCommitHashes.has(entry.hash)} - onCopyHash={onCopyHash} - /> - ))} + {log.all.map((entry, index) => { + const isBoundary = hasDivider && index === branchDivider.insertBeforeIndex; + const roundTop = isBoundary; + const roundBottom = hasDivider && index === branchDivider.insertBeforeIndex - 1; + + return ( + + {isBoundary ? ( +
    • +
      + + + {branchDivider.branchName} + {branchDivider.direction === 'down' ? ( + + ) : ( + + )} + + +
      +
    • + ) : null} + onToggleCommit(entry.hash)} + files={commitFilesMap.get(entry.hash) ?? []} + isLoadingFiles={loadingCommitHashes.has(entry.hash)} + onCopyHash={onCopyHash} + roundTop={roundTop} + roundBottom={roundBottom} + /> +
      + ); + })}
    )}