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