/** * @file The History tab's commit rows, drawn like VS Code's Source Control * Graph / GitLens: the graph cell (lanes and merge edges from * `lib/commitGraph`), the ref badges (current branch, local and remote * branches, tags, a detached HEAD), the subject, author and relative date. * Colours are theme tokens (CSS variables), so a lane reads in every theme. * Long subjects and names truncate with the whole text in a tooltip. * @author Nguyễn Ngọc Trí Vĩ */ import { memo } from "react"; import { useTranslation } from "react-i18next"; import { Cloud, GitBranch, Tag, Crosshair } from "lucide-react"; import type { GraphRow } from "../../../lib/commitGraph"; import type { RefBadge } from "../../../lib/gitHistory"; import { timeAgo } from "../../../lib/format"; /** One fixed-height row per commit so the lanes join up row to row. */ export const GRAPH_ROW = 30; const GRAPH_COL = 12; /** Columns drawn at most; a wider graph is clipped, not the row. */ const MAX_COLS = 12; /** Theme tokens, cycled per lane. */ const GRAPH_COLORS = [ "rgb(var(--accent))", "rgb(var(--status-success))", "rgb(var(--status-warning))", "rgb(var(--syn-property))", "rgb(var(--status-danger))", "rgb(var(--syn-function))", "rgb(var(--kept))", "rgb(var(--diff-mod))", ]; const hue = (i: number) => GRAPH_COLORS[i % GRAPH_COLORS.length]; export function GraphCell({ row, width, merge, head, }: { row: GraphRow; width: number; /** A merge commit: a hollow node. */ merge?: boolean; /** HEAD: a ringed node. */ head?: boolean; }) { const cols = Math.min(width, MAX_COLS); const x = (col: number) => col * GRAPH_COL + GRAPH_COL / 2; const y = (f: number) => f * GRAPH_ROW; return ( {row.segs.map((g, i) => { const ym = (y(g.y1) + y(g.y2)) / 2; return ( ); })} {head && ( )} ); } /** One ref badge: the kind's icon, the name truncated, the full name on hover. */ export function RefChip({ badge }: { badge: RefBadge }) { const { t } = useTranslation(["gitHistory"]); const Icon = badge.kind === "tag" ? Tag : badge.kind === "remote" ? Cloud : badge.kind === "head" ? Crosshair : GitBranch; const tone = badge.current ? "border-accent/60 bg-accent/15 text-accent font-semibold" : badge.kind === "tag" ? "border-status-warning/50 text-status-warning" : badge.kind === "remote" ? "border-border text-fg-muted" : badge.kind === "head" ? "border-status-danger/50 text-status-danger" : "border-status-success/50 text-status-success"; const label = t(`badge.${badge.kind}`, { name: badge.name }); return ( {badge.name} ); } /** Badges in the order the reference tools show them: HEAD's branch first. */ export function sortBadges(badges: RefBadge[]): RefBadge[] { const rank = (b: RefBadge) => b.current ? 0 : b.kind === "head" ? 1 : b.kind === "local" ? 2 : b.kind === "remote" ? 3 : 4; return [...badges].sort((a, b) => rank(a) - rank(b) || a.name.localeCompare(b.name)); } export interface GraphCommit { sha: string; short: string; subject: string; author: string; email?: string; date: string; parents: string[]; badges?: RefBadge[]; } /** The text half of a history row (the graph cell is drawn beside it). */ export const CommitText = memo(function CommitText({ c }: { c: GraphCommit }) { const badges = sortBadges(c.badges ?? []); const when = new Date(c.date); return ( {badges.slice(0, 4).map((b) => ( ))} {badges.length > 4 && ( b.name) .join(", ")} > +{badges.length - 4} )} {c.subject} {c.short} ` : c.author}> {c.author} {timeAgo(c.date)} ); });