Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

94 linesCodeBlameRaw
1/**
2 * A commit message's text with its references linked: `#12` and
3 * `acme/web#12` to the work item or pull request they name (lib/refs.ts),
4 * with its card on hover, and a full hash to its commit. Where the whole
5 * message links to its commit (a list of commits), the text between
6 * references links there and each reference links on its own, so no link
7 * sits inside another.
8 */
9import type { ReactNode } from "react";
10import { Link } from "react-router";
11
12import { RefCardBody } from "./chat/ref-chip";
13import { HoverCard, HoverCardContent, HoverCardTrigger } from "./ui/hover-card";
14import { cn } from "../lib/cn";
15import { messageParts } from "../lib/commit-message";
16import { type RefContext, itemPath, repoRefPath } from "../lib/refs";
17
18/**
19 * `#12` read in `scope`, a repository or a workspace: a link to what it
20 * names, its card on hover. In a repository both are its `ref` page
21 * (routes/repo/ref-card.ts); in a workspace, the work item's page and the
22 * workspace's `-/work/ref` card.
23 */
24export function RefLink({ scope, number, className, children }: { scope: RefContext; number: number; className?: string; children: ReactNode }) {
25 const to = "repo" in scope ? repoRefPath(scope.repo, number) : itemPath(scope.workspace, number);
26 const card = "repo" in scope ? to : `/${scope.workspace}/-/work/ref/${number}`;
27 const where = "repo" in scope ? `${scope.repo.namespace}/${scope.repo.name}` : scope.workspace;
28 return (
29 <HoverCard>
30 <HoverCardTrigger asChild>
31 <Link to={to} className={cn("relative z-10", className)} data-ref="ref">
32 {children}
33 </Link>
34 </HoverCardTrigger>
35 <HoverCardContent className="w-80">
36 <RefCardBody url={card} where={where} number={number} />
37 </HoverCardContent>
38 </HoverCard>
39 );
40}
41
42/** `owner/name` as a reference's repository. */
43export function repoScope(path: string): RefContext {
44 const [namespace = "", name = ""] = path.replace(/^\//, "").split("/");
45 return { repo: { namespace, name } };
46}
47
48/**
49 * `text` with its references linked, in the repository at `base`
50 * (`/owner/name`). With `to`, the text between them links there, styled
51 * by `linkClassName`.
52 */
53export function CommitText({
54 text,
55 base,
56 to,
57 linkClassName,
58 refClassName = "text-accent hover:underline",
59}: {
60 text: string;
61 base: string;
62 to?: string;
63 linkClassName?: string;
64 refClassName?: string;
65}) {
66 const repo = base.replace(/^\//, "");
67 return (
68 <>
69 {messageParts(text).map((part, index) => {
70 if (part.kind === "ref") {
71 return (
72 <RefLink key={index} scope={repoScope(part.repo ?? repo)} number={part.number} className={refClassName}>
73 {part.text}
74 </RefLink>
75 );
76 }
77 if (part.kind === "commit") {
78 return (
79 <Link key={index} to={`${base}/commit/${part.sha}`} className={cn("relative z-10 font-mono text-[0.9em]", refClassName)}>
80 {part.sha.slice(0, 7)}
81 </Link>
82 );
83 }
84 return to ? (
85 <Link key={index} to={to} prefetch="intent" className={linkClassName}>
86 {part.text}
87 </Link>
88 ) : (
89 part.text
90 );
91 })}
92 </>
93 );
94}