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.

242 linesCodeBlameRaw
1// Namespaces follow GitHub's rules (no leading, trailing or doubled hyphens),
2// so "--" can never appear inside one.
3const NAMESPACE = /^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){0,38}$/;
4const REPO_NAME = /^[a-z0-9._-]{1,100}$/;
5
6/** Routes and reserved words that may not be registered as usernames. */
7const RESERVED = new Set([
8 "api",
9 "mcp",
10 "login",
11 "logout",
12 "register",
13 "new",
14 "settings",
15 "search",
16 "admin",
17 "auth",
18 "integrations",
19 "pulls",
20 "issues",
21 "verify",
22 "confirm-email",
23 "forgot",
24 "reset",
25 "device",
26 "workspaces",
27 "u",
28 "oauth",
29 "assets",
30 "avatars",
31 "docs",
32 "explore",
33 "about",
34 "pricing",
35 "features",
36 // g1t itself, and the name its agent once went by: everything g1t does is
37 // shown as `g1t`, so nobody else may be called either. `ghost` wrote what
38 // a deleted account wrote (`GHOST_USERNAME`).
39 "g1t",
40 "g1t-agent",
41 "ghost",
42 // Trust pages on g1t.sh, and names kept for them.
43 "policies",
44 "security",
45 "support",
46 "status",
47 "terms",
48 "privacy",
49 "help",
50 "blog",
51 // Invite links, and the retired waitlist.
52 "invite",
53 "invites",
54 "waitlist",
55 // g1t.sh/invitations: the workspace invitations waiting for an answer.
56 "invitations",
57 // The container registry, at g1t.sh/v2/.
58 "v2",
59 // g1t.sh/inbox, and the name it might also go by.
60 "inbox",
61 "notifications",
62 // g1t.sh/download, the desktop app; g1t.sh/downloads/, the releases it and the runner come from.
63 "download",
64 "downloads",
65 // g1t.sh/install and g1t.sh/install.ps1, the CLI's install scripts.
66 "install",
67]);
68
69/**
70 * The GitHub accounts whose actions workflows use most. `uses:
71 * actions/setup-node@v4` means GitHub's `actions`, so nobody may register
72 * these on g1t; an account that already has one keeps it. The list is
73 * crates/contracts/src/names.rs's `ACTION_OWNERS`.
74 */
75const ACTION_OWNERS = new Set([
76 "actions",
77 "actions-ecosystem",
78 "actions-rs",
79 "amannn",
80 "anchore",
81 "appleboy",
82 "aquasecurity",
83 "arduino",
84 "astral-sh",
85 "aws-actions",
86 "azure",
87 "browser-actions",
88 "bufbuild",
89 "cachix",
90 "changesets",
91 "chromaui",
92 "cloudflare",
93 "codecov",
94 "conda-incubator",
95 "coursier",
96 "crazy-max",
97 "cypress-io",
98 "dawidd6",
99 "denoland",
100 "dependabot",
101 "dessant",
102 "determinatesystems",
103 "devcontainers",
104 "digitalocean",
105 "docker",
106 "dorny",
107 "dtolnay",
108 "endbug",
109 "erlef",
110 "firebaseextended",
111 "fluxcd",
112 "getsentry",
113 "github",
114 "golangci",
115 "google-github-actions",
116 "googleapis",
117 "goreleaser",
118 "gradle",
119 "gradle-update",
120 "hadolint",
121 "hashicorp",
122 "haskell-actions",
123 "hecrj",
124 "helm",
125 "ilammy",
126 "jamesives",
127 "jetbrains",
128 "julia-actions",
129 "kubernetes-sigs",
130 "lukka",
131 "marocchino",
132 "microsoft",
133 "mikepenz",
134 "mozilla-actions",
135 "msys2",
136 "mxschmitt",
137 "ncipollo",
138 "netlify",
139 "nick-fields",
140 "nwtgck",
141 "octokit",
142 "ossf",
143 "oven-sh",
144 "peaceiris",
145 "peter-evans",
146 "pnpm",
147 "pre-commit",
148 "pypa",
149 "r-lib",
150 "release-drafter",
151 "renovatebot",
152 "reviewdog",
153 "rhysd",
154 "ruby",
155 "rust-lang",
156 "sbt",
157 "shivammathur",
158 "sigstore",
159 "slackapi",
160 "slsa-framework",
161 "snyk",
162 "softprops",
163 "sonarsource",
164 "stefanzweifel",
165 "step-security",
166 "styfle",
167 "subosito",
168 "supabase",
169 "super-linter",
170 "superfly",
171 "svenstaro",
172 "swatinem",
173 "taiki-e",
174 "tj-actions",
175 "treosh",
176 "vercel",
177 "webfactory",
178]);
179
180/** Whether `value`, whatever its case, names a well-known GitHub action owner. */
181export function isActionOwnerName(value: string): boolean {
182 return ACTION_OWNERS.has(value.trim().toLowerCase());
183}
184
185/** Whether a person or workspace may take `value` as its name now: valid, and no well-known action owner's. */
186export function isClaimableName(value: string): boolean {
187 return isValidNamespace(value) && !isActionOwnerName(value);
188}
189
190/** Whether `value`, whatever its case, is a name nobody can register: a route, or g1t's own. */
191export function isReservedName(value: string): boolean {
192 return RESERVED.has(value.trim().toLowerCase());
193}
194
195export function isValidNamespace(value: string): boolean {
196 return isNamespaceShaped(value) && !isReservedName(value);
197}
198
199/**
200 * Whether `value` has a namespace's shape, reserved or not: what a
201 * workspace's old name or an alias staff set (such as `g1t`) can be.
202 */
203export function isNamespaceShaped(value: string): boolean {
204 return NAMESPACE.test(value);
205}
206
207/**
208 * Usernames: letters of either case, digits and single hyphens, not
209 * starting or ending with a hyphen, 1 to 39 characters, never reserved in
210 * any case. The case is kept for showing; everything finds a person by the
211 * name lowercased (`canonicalUsername`), so `Ana` and `ana` are one name.
212 */
213const USERNAME = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/;
214
215/** The `pattern` a username field takes in a form: the same rule, for the browser. */
216export const USERNAME_PATTERN = "[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9]))*";
217
218export function isValidUsername(value: string): boolean {
219 const name = value.trim();
220 return USERNAME.test(name) && !isReservedName(name) && !isActionOwnerName(name);
221}
222
223/** The name a person is found, linked and mentioned by: lowercased. */
224export function canonicalUsername(value: string): string {
225 return value.trim().toLowerCase();
226}
227
228/** How a person's username shows: as they wrote it, or as it is kept. */
229export function shownUsername(person: { username: string; display_username?: string | null; displayUsername?: string | null }): string {
230 const display = person.display_username ?? person.displayUsername;
231 return display && display.toLowerCase() === person.username.toLowerCase() ? display : person.username;
232}
233
234export function isValidRepoName(value: string): boolean {
235 return (
236 REPO_NAME.test(value) &&
237 !value.startsWith(".") &&
238 !value.endsWith(".git") &&
239 // `/<workspace>/-/…` holds the workspace's own pages.
240 value !== "-"
241 );
242}