Skip to content

g1t/apps/web/app/routes/repo/branches.tsx

149 lines6,604 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Branches and Tags pages, each file's last commit, and the branch menu on files1import { GitBranch, Search } from "lucide-react";
2import { Form, Link } from "react-router";
3
4import type { Route } from "./+types/branches";
5import { ActiveBranches } from "../../components/branches";
6import { Avatar, EmptyState, TimeAgo, notACredential } from "../../components/ui";
Merge branch 'worktree-agent-ae1299e92e4462012'7import { Hint } from "../../components/ui/hint";
Branches and Tags pages, each file's last commit, and the branch menu on files8import { readBranches } from "../../lib/branches.server";
9import { page } from "../../lib/meta";
10import { deployments, repos, work } from "../../lib/services.server";
11import { getViewer, unwrap } from "../../lib/session.server";
12
13/** The most branches read for one page; past that, search narrows them. */
14const BRANCHES_READ = 60;
15/** A branch whose last commit is older than this is stale. */
16const STALE_DAYS = 90;
17
18export function meta({ params, ...args }: Route.MetaArgs) {
19 return page(args, { title: `Branches · ${params.owner}/${params.repo} · g1t` });
20}
21
22export async function loader({ params, context, request }: Route.LoaderArgs) {
23 const path = { namespace: params.owner, name: params.repo };
24 const viewer = getViewer(context);
25 const query = new URL(request.url).searchParams.get("q")?.trim().toLowerCase() || null;
26 const soft = <T,>(promise: Promise<T>): Promise<T | null> => promise.catch(() => null);
27 const [repo, list, pulls, deploys] = await Promise.all([
28 repos.get(path, viewer),
29 repos.branches(path, viewer),
30 soft(work.listPulls(path, viewer, "open")),
31 // Previews are for people with a role here; everyone else sees none.
32 soft(deployments.list({ workspace: params.owner, slug: params.repo }, viewer)),
33 ]);
34 const found = unwrap(repo);
The agents' empty state keeps its link in the sentence; Branches leaves out the merge queue's working branches35 // The merge queue's own branches (g1t-queue/<entry>) are its working
36 // copies, not anyone's branch to look at.
37 const all = unwrap(list).filter((branch) => !branch.name.startsWith("g1t-queue/"));
Branches and Tags pages, each file's last commit, and the branch menu on files38 const matching = query ? all.filter((branch) => branch.name === found.defaultBranch || branch.name.toLowerCase().includes(query)) : all;
39 const read = await readBranches(
40 path,
41 viewer,
42 {
43 defaultBranch: found.defaultBranch,
44 branches: matching,
45 pulls: pulls?.ok ? pulls.value : [],
46 previews: deploys?.ok ? deploys.value.live.filter((app) => app.kind === "preview") : [],
47 },
48 BRANCHES_READ,
49 );
50 const staleBefore = Date.now() - STALE_DAYS * 86_400_000;
51 const isStale = (at: string | undefined) => at != null && Date.parse(at) < staleBefore;
52 return {
53 main: found.defaultBranch,
54 head: read.head,
55 active: read.shown.filter((branch) => !isStale(branch.commit?.at)),
56 stale: read.shown.filter((branch) => isStale(branch.commit?.at)),
57 // Every branch but the default, and how many matched the search.
58 total: all.length - 1,
59 matched: read.total,
60 unread: Math.max(0, read.total - read.shown.length),
61 query,
62 };
63}
64
65export default function Branches({ loaderData, params }: Route.ComponentProps) {
66 const { main, head, active, stale, total, matched, unread, query } = loaderData;
67 const base = `/${params.owner}/${params.repo}`;
68 return (
69 <div className="space-y-6">
70 <div className="flex flex-wrap items-center justify-between gap-3">
71 <h2 className="text-lg font-semibold tracking-tight">
72 Branches <span className="font-normal text-faint">{total + 1}</span>
73 </h2>
74 <Form method="get" role="search" className="relative w-full sm:w-72">
75 <Search size={14} className="pointer-events-none absolute top-1/2 left-2.5 -translate-y-1/2 text-faint" />
76 <input
77 name="q"
78 defaultValue={query ?? ""}
79 {...notACredential()}
80 placeholder="Search branches"
81 aria-label="Search branches"
82 className="h-8 w-full rounded-md border border-line bg-surface pr-3 pl-8 text-sm outline-none transition-colors placeholder:text-faint hover:border-line-strong focus:border-accent-dim"
83 />
84 </Form>
85 </div>
86
87 <section>
88 <h3 className="mb-2 text-sm font-medium text-muted">Default</h3>
89 <div className="flex flex-wrap items-center gap-x-3 gap-y-1 rounded-xl border border-line bg-surface px-4 py-3">
90 <GitBranch size={15} className="shrink-0 text-faint" />
91 <Link to={`${base}/tree/${encodeURIComponent(main)}`} className="font-mono text-[0.8125rem] font-medium hover:text-accent">
92 {main}
93 </Link>
94 <span className="rounded-full border border-line px-2 py-px text-xs text-muted">default</span>
95 {head && (
96 <span className="flex min-w-0 grow basis-64 items-center gap-1.5 text-xs text-muted">
97 <Avatar name={head.author} size={13} />
98 <span className="shrink-0">{head.author}</span>
99 <span className="text-faint">·</span>
Merge branch 'worktree-agent-ae1299e92e4462012'100 <Hint label={head.message}>
101 <Link to={`${base}/commit/${head.hash}`} className="min-w-0 truncate hover:text-fg">
102 {head.message}
103 </Link>
104 </Hint>
Branches and Tags pages, each file's last commit, and the branch menu on files105 <span className="shrink-0 text-faint">
106 · <TimeAgo at={head.at} />
107 </span>
108 </span>
109 )}
110 </div>
111 </section>
112
113 {query && (
114 <p className="text-sm text-muted">
115 {matched} of {total} {total === 1 ? "branch matches" : "branches match"} “{query}”.{" "}
116 <Link to={`${base}/branches`} className="text-fg hover:underline">
117 Clear
118 </Link>
119 </p>
120 )}
121
122 {active.length > 0 && (
123 <section>
124 <h3 className="mb-2 text-sm font-medium text-muted">Active</h3>
125 <ActiveBranches branches={active} base={base} main={main} />
126 </section>
127 )}
128 {stale.length > 0 && (
129 <section>
130 <h3 className="mb-2 text-sm font-medium text-muted">Stale</h3>
131 <p className="-mt-1 mb-2 text-xs text-faint">No commits in the last 90 days.</p>
132 <ActiveBranches branches={stale} base={base} main={main} />
133 </section>
134 )}
135 {active.length === 0 && stale.length === 0 && (
136 <EmptyState title={query ? "No branch matches" : "Only the default branch"}>
137 {query
138 ? "Try another part of the name."
139 : "Branches agents and people push show here, each with its pull request, checks and how far it has moved from the default branch."}
140 </EmptyState>
141 )}
142 {unread > 0 && (
143 <p className="text-xs text-faint">
144 {unread} more {unread === 1 ? "branch is" : "branches are"} not shown. Search to find one by name.
145 </p>
146 )}
147 </div>
148 );
149}

This file's history is long; its oldest lines are credited to the oldest commit read.