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.

g1t.sh
347 linesCodeBlameRaw

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.

Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971/**
2 * The top of a project's overview for what g1t does not deploy: an app
3 * deployed elsewhere, an app nobody has said where it runs, docs, and
4 * anything else. An app on g1t keeps its production card, and a library
5 * or a tool its packages (routes/repo/overview.tsx).
6 */
7import { ArrowUpRight, BookOpen, Box, ChevronDown, GitCommitHorizontal, Globe, Rocket } from "lucide-react";
8import { type ReactNode, useState } from "react";
9import { Link, useFetcher } from "react-router";
10
11import type { DeployStatus, Project } from "@g1t/contracts";
12
Merge branch 'site-fixes' into fast-push13import { DeployLink, host, StatusDot } from "./deploy";
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9714import { ButtonLink, Input, SubmitButton, TimeAgo } from "./ui";
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.15import { Button } from "./ui/button";
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9716import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "./ui/dropdown-menu";
17import { Hint } from "./ui/hint";
18
19/**
20 * A deployment reported to g1t from outside it, such as by a pipeline that
21 * deploys production itself. When there is one for production, the card
22 * shows it.
23 */
24export type ExternalDeployment = {
25 environment: string;
26 url: string | null;
27 /** success, failure, error, pending, queued, in_progress or inactive. */
28 state: string;
29 sha: string;
30 created_at: string;
31};
32
33/** A reported deployment's state, as g1t's own deployments show theirs. */
34export function deploymentStatus(state: string): DeployStatus {
35 switch (state) {
36 case "success":
37 return "ready";
38 case "failure":
39 case "error":
40 return "failed";
41 case "in_progress":
42 return "building";
43 case "inactive":
44 return "replaced";
45 default:
46 return "queued";
47 }
48}
49
50type Head = {
51 project: Project;
52 base: string;
53 /** May change the project's settings: what it is, its links. */
54 canChange: boolean;
55 /** May turn on Deployments. */
56 canDeploy: boolean;
57};
58
59export function HeadLabel({ icon, children }: { icon: ReactNode; children: ReactNode }) {
60 return (
61 <p className="flex items-center gap-2 text-xs font-medium tracking-wide text-muted uppercase [&_svg]:size-3.25 [&_svg]:text-accent">
62 {icon}
63 {children}
64 </p>
65 );
66}
67
68function BigLink({ url }: { url: string }) {
69 return (
Merge branch 'site-fixes' into fast-push70 <DeployLink href={url} className="mt-2 flex min-w-0 items-center gap-1.5 font-mono text-lg font-medium hover:text-accent">
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9771 <span className="truncate">{host(url).replace(/\/$/, "")}</span>
72 <ArrowUpRight size={16} className="shrink-0 text-faint" />
Merge branch 'site-fixes' into fast-push73 </DeployLink>
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9774 );
75}
76
77/** An address, saved for the project in place: production's, or its docs'. */
78function AddressForm({
79 field,
80 placeholder,
81 label,
82 extra,
83}: {
84 field: "productionUrl" | "docsUrl";
85 placeholder: string;
86 label: string;
87 /** Hidden fields sent with it. */
88 extra: Record<string, string>;
89}) {
90 const fetcher = useFetcher<{ error?: string }>();
91 return (
92 <fetcher.Form method="post" className="mt-3 max-w-lg">
93 {Object.entries(extra).map(([name, value]) => (
94 <input key={name} type="hidden" name={name} value={value} />
95 ))}
96 <div className="flex items-center gap-2">
97 <div className="min-w-0 grow">
98 <Input name={field} type="text" inputMode="url" required placeholder={placeholder} aria-label={label} maxLength={255} />
99 </div>
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.100 <SubmitButton fetcher={fetcher} variant="outline" pending="Saving…">
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97101 Save
102 </SubmitButton>
103 </div>
104 {fetcher.data?.error && <p className="mt-1.5 text-xs text-danger">{fetcher.data.error}</p>}
105 </fetcher.Form>
106 );
107}
108
109/** Deploying on g1t, offered as an option and never as a step. */
110function DeployQuietly({ base, canDeploy, what }: { base: string; canDeploy: boolean; what: string }) {
111 if (!canDeploy) return null;
112 return (
113 <p className="mt-4 text-xs text-faint">
114 g1t can deploy {what} on g1t.page instead, with a preview for every pull request.{" "}
115 <Link to={`${base}/settings/deployments`} className="underline-offset-4 hover:text-fg hover:underline">
116 Deployment settings
117 </Link>
118 </p>
119 );
120}
121
122/**
123 * An app deployed by its own pipeline: production at the address it was
124 * given, with the latest reported deployment, or else the default branch's
125 * latest commit and its checks.
126 */
127export function ElsewhereHead({
128 project,
129 base,
130 canChange,
131 canDeploy,
132 deployment,
133 commit,
134 checks,
Merge remote-tracking branch 'origin/main' into claude/cool-newton-7nm8r9135 shot,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97136}: Head & {
137 deployment?: ExternalDeployment | null;
138 commit: { hash: string; authoredAt: string } | null;
139 /** The commit's checks, as a badge; left out until there is one to show. */
140 checks?: ReactNode;
Merge remote-tracking branch 'origin/main' into claude/cool-newton-7nm8r9141 /** Production's picture, beside the rest, as an app on g1t has (components/production-shot.tsx). */
142 shot?: ReactNode;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97143}) {
144 const url = deployment?.url ?? project.productionUrl;
145 return (
Merge remote-tracking branch 'origin/main' into claude/cool-newton-7nm8r9146 <div className="flex flex-col gap-5 p-5 sm:flex-row sm:items-start sm:p-6">
147 {shot}
148 <div className="flex min-w-0 grow flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
149 <div className="min-w-0">
150 <HeadLabel icon={<Rocket />}>Production</HeadLabel>
151 {url ? (
152 <>
153 <BigLink url={url} />
154 <p className="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted">
155 {deployment ? (
156 <>
157 <StatusDot status={deploymentStatus(deployment.state)} />
158 <span className="inline-flex items-center gap-1 font-mono">
159 <GitCommitHorizontal size={13} className="text-faint" />
160 {deployment.sha.slice(0, 7)}
161 </span>
162 <span>
163 deployed <TimeAgo at={deployment.created_at} />
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97164 </span>
Merge remote-tracking branch 'origin/main' into claude/cool-newton-7nm8r9165 </>
166 ) : (
167 <>
168 <span>Deployed by its own pipeline</span>
169 {commit && (
170 <span className="inline-flex items-center gap-1.5">
171 <span className="text-faint">·</span>
172 <Link to={`${base}/commit/${commit.hash}`} className="inline-flex items-center gap-1 font-mono hover:text-fg">
173 <GitCommitHorizontal size={13} className="text-faint" />
174 {commit.hash.slice(0, 7)}
175 </Link>
176 {checks}
177 <TimeAgo at={commit.authoredAt} />
178 </span>
179 )}
180 </>
181 )}
182 </p>
183 </>
184 ) : canChange ? (
185 <>
186 <p className="mt-2 text-lg font-medium">Where is production?</p>
187 <p className="mt-1 max-w-lg text-sm text-muted">
188 {project.name} is deployed by its own pipeline. Give production's address, and this card links to it.
189 </p>
190 <AddressForm field="productionUrl" placeholder="https://example.com" label="Production's address" extra={{ intent: "kind", choice: "elsewhere" }} />
191 </>
192 ) : (
193 <p className="mt-2 text-sm text-muted">Deployed by its own pipeline.</p>
194 )}
195 <DeployQuietly base={base} canDeploy={canDeploy} what="it" />
196 </div>
197 {url && (
198 <div className="flex shrink-0 items-center gap-2">
199 <ButtonLink to={url} variant="accent" reloadDocument target="_blank" rel="noopener noreferrer">
200 Visit
201 <ArrowUpRight size={14} />
202 </ButtonLink>
203 </div>
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97204 )}
205 </div>
206 </div>
207 );
208}
209
210const NOT_DEPLOYED: { value: string; label: string }[] = [
211 { value: "library", label: "A library or package" },
212 { value: "tool", label: "A tool or CLI" },
213 { value: "docs", label: "Documentation" },
214 { value: "other", label: "Something else" },
215];
216
217/**
218 * An app that nobody has said where it runs, with Deployments off: one
219 * question, answered in place, instead of a push to turn Deployments on.
220 */
221export function WhereItRuns({ project, base, canChange, canDeploy }: Head) {
222 const fetcher = useFetcher<{ error?: string }>();
223 const [elsewhere, setElsewhere] = useState(false);
224 if (!canChange) {
225 return (
226 <div className="p-5 sm:p-6">
227 <HeadLabel icon={<Globe />}>Homepage</HeadLabel>
228 {project.links.homepage ? <BigLink url={project.links.homepage} /> : <p className="mt-2 text-sm text-muted">No homepage given.</p>}
229 </div>
230 );
231 }
232 return (
233 <div className="p-5 sm:p-6">
234 <HeadLabel icon={<Rocket />}>Production</HeadLabel>
235 <p className="mt-2 text-lg font-medium">Where does {project.name} run?</p>
236 <p className="mt-1 max-w-xl text-sm text-muted">
237 Say once, and this page follows: production where it is deployed, or its packages and releases if it isn't
238 deployed at all. <span className="text-faint">{project.detected.reason.detail}</span>
239 </p>
240 <div className="mt-4 flex flex-wrap items-center gap-2">
241 {canDeploy && (
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.242 <ButtonLink to={`${base}/settings/deployments`} variant="outline">
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97243 <Rocket size={14} />
244 Deploy on g1t
245 </ButtonLink>
246 )}
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.247 <Button
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97248 type="button"
249 onClick={() => setElsewhere((open) => !open)}
250 aria-expanded={elsewhere}
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.251 variant="outline"
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97252 >
253 <Globe size={14} />
254 It's deployed elsewhere
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.255 </Button>
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97256 <DropdownMenu>
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.257 <DropdownMenuTrigger asChild>
258 <Button variant="outline">
259 <Box size={14} />
260 It isn't deployed
261 <ChevronDown size={13} className="text-faint" />
262 </Button>
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97263 </DropdownMenuTrigger>
264 <DropdownMenuContent align="start">
265 {NOT_DEPLOYED.map((option) => (
266 <DropdownMenuItem key={option.value} onSelect={() => fetcher.submit({ intent: "kind", choice: option.value }, { method: "post" })}>
267 {option.label}
268 </DropdownMenuItem>
269 ))}
270 </DropdownMenuContent>
271 </DropdownMenu>
272 </div>
273 {elsewhere && (
274 <AddressForm field="productionUrl" placeholder="Production's address, such as https://example.com" label="Production's address" extra={{ intent: "kind", choice: "elsewhere" }} />
275 )}
276 {fetcher.data?.error && <p className="mt-2 text-xs text-danger">{fetcher.data.error}</p>}
277 </div>
278 );
279}
280
281/** Documentation: where it is read, or a place to say so. */
282export function DocsHead({ project, base, canChange, canDeploy }: Head) {
283 const url = project.links.docs ?? project.productionUrl ?? project.links.homepage;
284 return (
285 <div className="flex flex-col gap-4 p-5 sm:flex-row sm:items-start sm:justify-between sm:p-6">
286 <div className="min-w-0">
287 <HeadLabel icon={<BookOpen />}>Documentation</HeadLabel>
288 {url ? (
289 <BigLink url={url} />
290 ) : canChange ? (
291 <>
292 <p className="mt-2 text-lg font-medium">Where are its docs read?</p>
293 <p className="mt-1 max-w-lg text-sm text-muted">Give the address, and this card and its listing link to it.</p>
294 <AddressForm field="docsUrl" placeholder="https://docs.example.com" label="Where its docs are read" extra={{ intent: "about" }} />
295 </>
296 ) : (
297 <p className="mt-2 text-sm text-muted">Its docs are in its repository.</p>
298 )}
299 <DeployQuietly base={base} canDeploy={canDeploy && project.runs == null} what="its docs as a site" />
300 </div>
301 {url && (
302 <div className="flex shrink-0 items-center gap-2">
Merge branch 'site-fixes' into fast-push303 <ButtonLink to={url} variant="accent" reloadDocument target="_blank" rel="noopener noreferrer">
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97304 Read
305 <ArrowUpRight size={14} />
306 </ButtonLink>
307 </div>
308 )}
309 </div>
310 );
311}
312
313/** Anything else: its homepage, or where to add its links. */
314export function OtherHead({ project, base, canChange }: Omit<Head, "canDeploy">) {
315 const url = project.links.homepage ?? project.links.docs ?? project.links.custom[0]?.url ?? null;
316 return (
317 <div className="p-5 sm:p-6">
318 <HeadLabel icon={<Globe />}>Homepage</HeadLabel>
319 {url ? (
320 <BigLink url={url} />
321 ) : canChange ? (
322 <p className="mt-2 text-sm text-muted">
323 No links yet.{" "}
324 <Link to={`${base}/settings#links`} className="text-accent hover:underline">
325 Add a homepage, docs or others
326 </Link>{" "}
327 and they show here, on its card and in its workspace's Projects.
328 </p>
329 ) : (
330 <p className="mt-2 text-sm text-muted">No homepage given.</p>
331 )}
332 </div>
333 );
334}
335
336/** Production as g1t serves it, with the project's homepage when that is somewhere else. */
337export function AlsoAt({ url }: { url: string | null }) {
338 if (!url) return null;
339 return (
340 <Hint label="Its homepage">
Merge branch 'site-fixes' into fast-push341 <DeployLink href={url} className="inline-flex items-center gap-1 text-faint hover:text-fg">
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97342 <Globe size={12} />
343 {host(url).replace(/\/$/, "")}
Merge branch 'site-fixes' into fast-push344 </DeployLink>
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97345 </Hint>
346 );
347}

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