Skip to content

g1t/apps/web/app/components/branches.tsx

124 lines5,555 bytesCodeBlame
1import { ArrowDown, ArrowUp, GitBranch, GitPullRequest, Globe } from "lucide-react";
2import { Link } from "react-router";
3
4import type { Pull } from "@g1t/contracts";
5
6import type { Drift } from "../lib/branches";
7import { CheckBadge } from "./checks";
8import { type ChecksSource, CommitChecksBadge } from "./commit-checks";
9import { host } from "./deploy";
10import { Avatar, TimeAgo } from "./ui";
11import { Hint } from "./ui/hint";
12
13function drift({ ahead, behind }: Drift, main: string): string {
14 return `${ahead} ${ahead === 1 ? "commit" : "commits"} ahead of ${main}, ${behind} behind`;
15}
16
17export type ActiveBranch = {
18 name: string;
19 commit: { hash: string; message: string; author: string; at: string } | null;
20 /** Null when the two histories were not read far enough to meet. */
21 drift: Drift | null;
22 pull: { number: number; title: string; checkStatus: Pull["checkStatus"]; draft: boolean } | null;
23 preview: string | null;
24};
25
26/** Branches other than the default, newest first, with how far each has moved, its head's checks and what is open on it. */
27export function ActiveBranches({
28 branches,
29 base,
30 main,
31 checks,
32}: {
33 branches: ActiveBranch[];
34 base: string;
35 main: string;
36 /** The checks on each branch's head, streamed in. */
37 checks?: ChecksSource;
38}) {
39 return (
40 <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface">
41 {branches.map((branch) => (
42 <li key={branch.name} className="flex flex-wrap items-start gap-x-3 gap-y-1.5 px-4 py-3">
43 <GitBranch size={15} className="mt-0.5 shrink-0 text-faint" />
44 <span className="min-w-0 grow basis-48">
45 <Link
46 to={`${base}/tree/${encodeURIComponent(branch.name)}`}
47 className="block truncate font-mono text-[0.8125rem] font-medium hover:text-accent"
48 >
49 {branch.name}
50 </Link>
51 {branch.commit && (
52 <span className="mt-0.5 flex min-w-0 items-center gap-1.5 text-xs text-muted">
53 <Avatar name={branch.commit.author} size={13} />
54 <span className="shrink-0">{branch.commit.author}</span>
55 <span className="text-faint">·</span>
56 <Hint label={branch.commit.message}>
57 <Link to={`${base}/commit/${branch.commit.hash}`} className="min-w-0 truncate hover:text-fg">
58 {branch.commit.message}
59 </Link>
60 </Hint>
61 <CommitChecksBadge checks={checks} sha={branch.commit.hash} className="size-5" />
62 <span className="shrink-0 text-faint">
63 · <TimeAgo at={branch.commit.at} />
64 </span>
65 </span>
66 )}
67 </span>
68 <span className="ml-6.5 flex shrink-0 flex-wrap items-center gap-2 text-xs sm:ml-0">
69 {branch.drift && (
70 <Hint label={drift(branch.drift, main)}>
71 <span className="inline-flex items-center gap-1.5 rounded-md border border-line px-1.5 py-0.5 font-mono tabular-nums text-muted">
72 <span className="sr-only">{drift(branch.drift, main)}</span>
73 <span className="inline-flex items-center gap-0.5" aria-hidden>
74 <ArrowUp size={11} className={branch.drift.ahead > 0 ? "text-success" : "text-faint"} />
75 {branch.drift.ahead}
76 </span>
77 <span className="inline-flex items-center gap-0.5" aria-hidden>
78 <ArrowDown size={11} className={branch.drift.behind > 0 ? "text-warn" : "text-faint"} />
79 {branch.drift.behind}
80 </span>
81 </span>
82 </Hint>
83 )}
84 {branch.pull ? (
85 <Hint label={branch.pull.title}>
86 <Link
87 to={`${base}/pull/${branch.pull.number}`}
88 className="inline-flex items-center gap-1.5 rounded-md border border-line px-1.5 py-0.5 text-muted hover:border-line-strong hover:text-fg"
89 >
90 <GitPullRequest size={12} className={branch.pull.draft ? "text-faint" : "text-success"} />#{branch.pull.number}
91 <span className="sr-only">{branch.pull.title}</span>
92 <CheckBadge status={branch.pull.checkStatus} />
93 </Link>
94 </Hint>
95 ) : branch.drift?.ahead === 0 ? (
96 // Nothing here that the default branch lacks: no pull request to open.
97 <Hint label={`Every commit on ${branch.name} is already on ${main}.`}>
98 <span className="px-1.5 py-0.5 text-faint">Nothing to merge</span>
99 </Hint>
100 ) : (
101 <Link
102 to={`${base}/pulls/new?branch=${encodeURIComponent(branch.name)}`}
103 className="rounded-md px-1.5 py-0.5 text-faint hover:text-fg"
104 >
105 Open a pull request
106 </Link>
107 )}
108 {branch.preview && (
109 <Hint label={host(branch.preview)}>
110 <a
111 href={branch.preview}
112 className="inline-flex max-w-48 items-center gap-1 truncate rounded-md border border-line px-1.5 py-0.5 text-muted hover:border-line-strong hover:text-fg"
113 >
114 <Globe size={12} className="shrink-0 text-accent" />
115 Preview
116 </a>
117 </Hint>
118 )}
119 </span>
120 </li>
121 ))}
122 </ul>
123 );
124}