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