import React, { useMemo, useState } from 'react'; import { Icon } from '@/components/icon/Icon'; import { Button } from '@/components/ui/button'; import { Skeleton } from '@/components/ui/skeleton'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { toast } from '@/components/ui/toast'; import { useI18n } from '@/lib/i18n'; import { formatDateTimeForPreference } from '@/lib/timeFormat'; import { useUIStore, type TimeFormatPreference } from '@/stores/useUIStore'; import { copyTextToClipboard } from '@/lib/clipboard'; import type { GitLogEntry } from '@/lib/api/types'; import type { ForgeCommit } from '@/lib/forge/types'; import { assignLanes } from '@/components/views/git/gitGraph'; import { GitGraphSegment } from '@/components/views/git/GitGraphSegment'; interface ForgeCommitsSectionProps { commits: ForgeCommit[] | null; loading?: boolean; error?: string | null; } /** * Map a normalized forge commit onto the `GitLogEntry`-shaped input * `assignLanes` consumes. The commit list is authoritative; the synthesized * fields are only used for lane geometry and display text. */ const toLaneEntry = (commit: ForgeCommit): GitLogEntry => ({ hash: commit.sha, date: commit.committedAt ?? '', message: commit.summary ?? commit.message, refs: '', body: commit.message, author_name: commit.author?.name ?? commit.author?.login ?? 'Unknown', author_email: '', filesChanged: 0, insertions: 0, deletions: 0, parents: commit.parents, }); const formatCommitDate = (value: string | undefined, timeFormatPreference: TimeFormatPreference): string => { if (!value) return ''; const ts = Date.parse(value); if (!Number.isFinite(ts)) return value; return formatDateTimeForPreference(ts, timeFormatPreference, { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', }); }; /** * Commits on a pull request, rendered with the git-graph lane visual language * (GitGraphSegment over `assignLanes` output). Each row expands to the full * message and parent shas. Pure presentation. */ export const ForgeCommitsSection = React.memo(function ForgeCommitsSection({ commits, loading, error }) { const { t } = useI18n(); const timeFormatPreference = useUIStore((state) => state.timeFormatPreference); const [expandedShas, setExpandedShas] = useState>(new Set()); const bySha = useMemo(() => new Map((commits ?? []).map((commit) => [commit.sha, commit])), [commits]); const laned = useMemo(() => assignLanes((commits ?? []).map(toLaneEntry)), [commits]); const totalLanes = useMemo( () => laned.reduce((max, item) => Math.max(max, item.lane), -1) + 1, [laned], ); const toggle = React.useCallback((sha: string) => { setExpandedShas((previous) => { const next = new Set(previous); if (next.has(sha)) { next.delete(sha); } else { next.add(sha); } return next; }); }, []); const copyHash = React.useCallback(async (sha: string) => { const result = await copyTextToClipboard(sha); if (result.ok) { toast.success(t('forge.copied')); } }, [t]); if (loading) { return (
); } if (error) { return (
{error}
); } if (!commits || commits.length === 0) { return

{t('forge.commits.empty')}

; } return ( ); });