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
265 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.

Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1import { Mail } from "lucide-react";
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)2import { useState } from "react";
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily3import { Form, Link } from "react-router";
4
5import type { Invite, InvitesOverview } from "@g1t/contracts";
6
Merge branch 'worktree-agent-a985c01f43538af01'7import { CopyLine, ErrorText, Field, 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.8import { Badge } from "./ui/badge";
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)9import { CheckboxOption } from "./ui/checkbox";
10import { Hint } from "./ui/hint";
Chat controls, public profiles, shadcn selects, and no Docs tab in a project11import { SelectField } from "./ui/select";
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)12import { G1T_INVITES, inviteFor, inviteKind, inviteLink, invitesPage, inviteState, moreInvitesMailto, remainingLine } from "../lib/invites";
13import type { BringIntoChoices } from "../lib/invites.server";
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)14
Chat controls, public profiles, shadcn selects, and no Docs tab in a project15const SELECT = "h-auto w-full py-2 sm:w-auto sm:min-w-40";
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily16
17const TONE: Record<"pending" | "done" | "dead", string> = {
18 pending: "border-accent/40 text-accent",
19 done: "border-merged/40 text-merged",
20 dead: "border-line text-faint",
21};
22
23function InviteRow({ invite, origin }: { invite: Invite; origin: string }) {
24 const state = inviteState(invite);
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)25 const kind = inviteKind(invite);
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily26 return (
27 <li className="space-y-2 px-4 py-3">
28 <div className="flex flex-wrap items-center gap-x-3 gap-y-1">
29 <span className={`inline-flex items-center rounded-full border px-2 py-0.5 text-xs ${TONE[state.tone]}`}>{state.label}</span>
30 <span className="min-w-0 truncate text-sm">{inviteFor(invite)}</span>
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)31 <Hint
32 label={
33 kind.kind === "g1t"
34 ? "Lets them make an account. It adds them to no workspace."
35 : "An invitation to join this workspace, which they accept or decline. Without an account, it lets them make one first."
36 }
37 >
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.38 <Badge size="md">{kind.label}</Badge>
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)39 </Hint>
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)40 {(invite.status === "pending" || invite.status === "awaiting_confirmation" || invite.status === "awaiting_answer") && (
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily41 <Form method="post" className="ml-auto">
42 <input type="hidden" name="intent" value="revoke-invite" />
43 <input type="hidden" name="id" value={invite.id} />
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.44 <SubmitButton variant="outline" match={{ intent: "revoke-invite", id: invite.id }} pending="Revoking…">
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily45 Revoke
Merge branch 'worktree-agent-a985c01f43538af01'46 </SubmitButton>
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily47 </Form>
48 )}
49 </div>
50 <p className="text-xs text-faint">
51 <span className="font-mono">{invite.hint}…</span> · made <TimeAgo at={invite.createdAt} />
52 {invite.status === "pending" && (
53 <>
54 {" "}· works until {new Date(invite.expiresAt).toISOString().slice(0, 10)}
55 </>
56 )}
57 {invite.chargedTo === "workspace" && invite.workspace == null && " · from a workspace's invites"}
58 </p>
59 {invite.status === "pending" && invite.code && <CopyLine text={inviteLink(invite.code, origin)} />}
60 </li>
61 );
62}
63
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)64/** "To bring someone into a workspace, invite them from that workspace's People page", with those pages. */
65function Elsewhere({ people }: { people: BringIntoChoices["people"] }) {
66 return (
67 <p className="mt-4 text-sm text-muted">
68 {G1T_INVITES.elsewhere}
69 {people.length > 0 ? ": " : "."}
70 {people.map((workspace, index) => (
71 <span key={workspace.slug}>
72 {index > 0 && ", "}
73 <Link to={workspace.to} className="text-fg underline underline-offset-4 hover:text-accent">
74 {workspace.name}
75 </Link>
76 </span>
77 ))}
78 {people.length > 0 && "."}
79 </p>
80 );
81}
82
83/**
84 * Making an invite to g1t: an address (or anyone with the link), whose
85 * invites it uses, and, only when its box is ticked, a workspace to invite
86 * them to as well. Unticked, nothing about a workspace is sent.
87 */
88function InviteForm({ overview, bringInto }: { overview: InvitesOverview; bringInto: BringIntoChoices }) {
89 const [alsoJoin, setAlsoJoin] = useState(false);
90 const { workspaces } = overview;
91 return (
92 <Form method="post" className="mt-4 space-y-4">
93 <input type="hidden" name="intent" value="create-invite" />
94 <div className="flex flex-col gap-3 sm:flex-row sm:items-end">
95 <div className="grow">
96 <Field label="Email (optional)">
97 <Input name="email" type="email" maxLength={254} placeholder="Anyone with the link" />
98 </Field>
99 </div>
100 {workspaces.length > 0 && (
101 <label className="block">
102 <span className="mb-1.5 block text-sm font-medium text-muted">Use</span>
Chat controls, public profiles, shadcn selects, and no Docs tab in a project103 <SelectField
104 name="charge"
105 defaultValue="mine"
106 className={SELECT}
107 options={[
108 { value: "mine", label: "Your invites" },
109 ...workspaces
110 .filter((workspace) => workspace.allowance.remaining !== 0)
111 .map((workspace) => ({ value: workspace.slug, label: `${workspace.slug}'s invites` })),
112 ]}
113 />
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)114 </label>
115 )}
116 <SubmitButton match={{ intent: "create-invite" }} pending="Creating…">
117 Create invite
118 </SubmitButton>
119 </div>
120 <p className="text-xs text-faint">With an email, the invite is sent there and only that address can use it.</p>
121
122 {bringInto.options.length > 0 && (
123 <div className="space-y-3 rounded-md border border-line p-3">
124 <CheckboxOption
125 name="also_join"
126 checked={alsoJoin}
127 onCheckedChange={(checked) => setAlsoJoin(checked === true)}
128 label={G1T_INVITES.alsoJoin}
129 description={G1T_INVITES.alsoJoinHint}
130 />
131 {alsoJoin && (
132 <div className="flex flex-col gap-3 pl-6 sm:flex-row sm:items-end">
133 <label className="block">
134 <span className="mb-1.5 block text-sm font-medium text-muted">Workspace</span>
Chat controls, public profiles, shadcn selects, and no Docs tab in a project135 <SelectField
136 name="join"
137 required
138 placeholder="Choose a workspace"
139 className={SELECT}
140 options={bringInto.options.map((workspace) => ({
141 value: workspace.slug,
142 label: workspace.name === workspace.slug ? workspace.slug : `${workspace.name} (${workspace.slug})`,
143 }))}
144 />
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)145 </label>
146 <label className="block">
147 <span className="mb-1.5 block text-sm font-medium text-muted">Role</span>
Chat controls, public profiles, shadcn selects, and no Docs tab in a project148 <SelectField
149 name="join_role"
150 defaultValue="member"
151 className={SELECT}
152 options={[
153 { value: "member", label: "Member" },
154 { value: "owner", label: "Owner" },
155 ]}
156 />
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)157 </label>
158 </div>
159 )}
160 {alsoJoin && bringInto.note && <p className="pl-6 text-xs text-faint">{bringInto.note}</p>}
161 </div>
162 )}
163 </Form>
164 );
165}
166
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily167/**
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)168 * Settings → Invites (`/settings/invites`): invites to g1t. Each lets one
169 * person make an account and adds them to no workspace, unless "Also
170 * invite them to a workspace" is ticked. They exist only while sign-up
171 * takes an invite; once anyone can sign up, the page keeps only the list
172 * of invites already made. An invitation to a workspace is made on its
173 * People page, which this page points to.
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily174 */
175export function InvitesSection({
176 overview,
177 created,
178 error,
179 origin = "https://g1t.sh",
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)180 bringInto = { options: [], note: null, people: [] },
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily181}: {
182 overview: InvitesOverview | null;
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)183 /** The workspaces an invite can also invite its person to, and the People pages (lib/invites.server.ts). */
184 bringInto?: BringIntoChoices;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily185 created?: Invite;
186 error?: string;
187 origin?: string;
188}) {
189 if (!overview) {
190 return (
191 <section id="invites" className="scroll-mt-20">
192 <p className="text-sm text-muted">Confirm your email address to invite people to g1t.</p>
193 </section>
194 );
195 }
196 const { allowance, workspaces, invites } = overview;
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)197 const shows = invitesPage(overview.mode, invites.length);
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily198 const out = allowance.remaining === 0 && workspaces.every((workspace) => workspace.allowance.remaining === 0);
199 return (
200 <section id="invites" className="scroll-mt-20">
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)201 {shows.form ? (
202 <>
203 <p className="text-sm text-muted">A revoked or expired invite that was never used comes back to you.</p>
204 <p className="mt-3 text-sm">
205 <span className="font-medium">{remainingLine(allowance)}</span>
206 {workspaces.map((workspace) => (
207 <span key={workspace.slug} className="text-muted">
208 {" "}· {workspace.slug} has {workspace.allowance.remaining ?? "unlimited"} to share
209 </span>
210 ))}
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily211 </p>
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)212
213 {created?.code && (
214 <div className="mt-4 rounded-md border border-accent/40 bg-surface p-4">
215 <p className="text-sm">
216 {created.email ? `Invite sent to ${created.email}. ` : ""}Share this link; it works once
217 {created.email ? ", for that address only" : ""}.
218 </p>
219 <div className="mt-2">
220 <CopyLine text={inviteLink(created.code, origin)} />
221 </div>
222 </div>
223 )}
224
225 {out ? (
226 <div className="mt-4 flex items-start gap-3 rounded-md border border-line bg-surface p-4 text-sm">
227 <Mail size={16} className="mt-0.5 shrink-0 text-muted" />
228 <p>
229 Need more invites?{" "}
230 <a href={moreInvitesMailto()} className="text-accent underline underline-offset-4">
231 Contact us
232 </a>{" "}
233 and say who you would like to bring, or ask on the{" "}
234 <Link to="/support" className="underline underline-offset-4">
235 support page
236 </Link>
237 .
238 </p>
239 </div>
240 ) : (
241 // Empty again once the invite is made; kept as filled in when it failed.
242 <InviteForm key={created?.id ?? ""} overview={overview} bringInto={bringInto} />
243 )}
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily244 <div className="mt-2">
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)245 <ErrorText>{error}</ErrorText>
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily246 </div>
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)247 <Elsewhere people={bringInto.people} />
248 </>
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily249 ) : (
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)250 <p className="text-sm text-muted">{G1T_INVITES.open}</p>
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily251 )}
252
253 {invites.length > 0 && (
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)254 <>
255 {!shows.form && <h2 className="mt-6 text-sm font-medium">Invites you made</h2>}
256 <ul className="mt-4 divide-y divide-line rounded-md border border-line">
257 {invites.map((invite) => (
258 <InviteRow key={invite.id} invite={invite} origin={origin} />
259 ))}
260 </ul>
261 </>
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily262 )}
263 </section>
264 );
265}

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