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
273 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 'worktree-agent-a8385d293d42c913a'1/// The site's own routes: first path segments that are never a workspace's,
2/// so nobody may register them, and no alias can be reached at them.
3const ROUTES: &[&str] = &[
Cache hits are counted, not only their bytes4 "api",
5 "mcp",
6 "login",
7 "logout",
8 "register",
9 "new",
10 "settings",
11 "search",
12 "admin",
13 "auth",
14 // g1t.sh/integrations/github/setup: where GitHub returns after an install.
15 "integrations",
16 "pulls",
17 "issues",
18 "oauth",
19 "assets",
20 "avatars",
21 "docs",
22 "explore",
23 "about",
24 "pricing",
25 "terms",
26 "privacy",
27 "policies",
28 "security",
29 "help",
30 "support",
31 "status",
32 "blog",
33 "verify",
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)34 "confirm-email",
Cache hits are counted, not only their bytes35 "forgot",
36 "reset",
37 "device",
38 "workspaces",
39 "u",
40 // g1t.sh/invite/<code>: invite links; and the waitlist.
41 "invite",
42 "invites",
43 "waitlist",
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)44 // g1t.sh/invitations: the workspace invitations waiting for an answer.
45 "invitations",
Cache hits are counted, not only their bytes46 // g1t.sh/v2/: the container registry; `-` paths hold the others.
47 "v2",
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all48 // g1t.sh/inbox, and the name it might also go by.
49 "inbox",
50 "notifications",
Cache hits are counted, not only their bytes51];
52
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)53/// g1t itself, the name its agent once went by, and `ghost`, who wrote what
54/// a deleted account wrote (`account_deletion::GHOST_USERNAME`): everything
55/// g1t does is shown as `g1t`, so nobody else may be called any of these.
56/// Not routes: staff may point one at a workspace as an alias (identity's
57/// `aliases.rs`).
58const OWN: &[&str] = &["g1t", "g1t-agent", "ghost"];
Merge branch 'worktree-agent-a8385d293d42c913a'59
A workflow's `uses: actions/setup-node@v4` means GitHub's `actions` again, whoever is on g1t. g1t looked for a repository on g1t before GitHub, and anyone could make a public workspace called `actions`, `docker` or `aws-actions` and have its `setup-node` run in every workflow that names GitHub's, with that job's secrets. Now another workspace's public repository answers only for a name GitHub has no public repository of; the caller's own workspace and g1t's own `g1t/…` still answer from g1t, and when GitHub can't be asked, GitHub's is used. The names of the GitHub accounts whose actions workflows use most can no longer be registered, renamed to, or taken by signing up with GitHub (which suggests the name with `-gh`); an account that has one keeps it, and mentions and invitations still find it. The Actions, authentication and workspaces guides say so.60/// The GitHub accounts whose actions workflows use most. A workflow's
61/// `uses: actions/setup-node@v4` means GitHub's `actions`, so nobody may
62/// register these on g1t and be mistaken for them. They are not reserved
63/// as [`is_reserved_name`] is: an account that already has one keeps it,
64/// mentions and invitations still find it, and staff can give one, as an
65/// alias, to the account that proves it owns the name on GitHub.
66const ACTION_OWNERS: &[&str] = &[
67 "actions", "actions-ecosystem", "actions-rs", "amannn", "anchore", "appleboy", "aquasecurity",
68 "arduino", "astral-sh", "aws-actions", "azure", "browser-actions", "bufbuild", "cachix",
69 "changesets", "chromaui", "cloudflare", "codecov", "conda-incubator", "coursier", "crazy-max",
70 "cypress-io", "dawidd6", "denoland", "dependabot", "dessant", "determinatesystems",
71 "devcontainers", "digitalocean", "docker", "dorny", "dtolnay", "endbug", "erlef",
72 "firebaseextended", "fluxcd", "getsentry", "github", "golangci", "google-github-actions",
73 "googleapis", "goreleaser", "gradle", "gradle-update", "hadolint", "hashicorp",
74 "haskell-actions", "hecrj", "helm", "ilammy", "jamesives", "jetbrains", "julia-actions",
75 "kubernetes-sigs", "lukka", "marocchino", "microsoft", "mikepenz", "mozilla-actions", "msys2",
76 "mxschmitt", "ncipollo", "netlify", "nick-fields", "nwtgck", "octokit", "ossf", "oven-sh",
77 "peaceiris", "peter-evans", "pnpm", "pre-commit", "pypa", "r-lib", "release-drafter",
78 "renovatebot", "reviewdog", "rhysd", "ruby", "rust-lang", "sbt", "shivammathur", "sigstore",
79 "slackapi", "slsa-framework", "snyk", "softprops", "sonarsource", "stefanzweifel",
80 "step-security", "styfle", "subosito", "supabase", "super-linter", "superfly", "svenstaro",
81 "swatinem", "taiki-e", "tj-actions", "treosh", "vercel", "webfactory",
82];
83
Cache hits are counted, not only their bytes84/// Whether `value`, whatever its case, is a name nobody can register or
85/// rename a workspace to: a route, or g1t's own.
86pub fn is_reserved_name(value: &str) -> bool {
Merge branch 'worktree-agent-a8385d293d42c913a'87 is_route_name(value) || OWN.iter().any(|own| own.eq_ignore_ascii_case(value.trim()))
88}
89
A workflow's `uses: actions/setup-node@v4` means GitHub's `actions` again, whoever is on g1t. g1t looked for a repository on g1t before GitHub, and anyone could make a public workspace called `actions`, `docker` or `aws-actions` and have its `setup-node` run in every workflow that names GitHub's, with that job's secrets. Now another workspace's public repository answers only for a name GitHub has no public repository of; the caller's own workspace and g1t's own `g1t/…` still answer from g1t, and when GitHub can't be asked, GitHub's is used. The names of the GitHub accounts whose actions workflows use most can no longer be registered, renamed to, or taken by signing up with GitHub (which suggests the name with `-gh`); an account that has one keeps it, and mentions and invitations still find it. The Actions, authentication and workspaces guides say so.90/// Whether `value`, whatever its case, names one of the GitHub accounts
91/// whose actions workflows use most: see [`ACTION_OWNERS`].
92pub fn is_action_owner_name(value: &str) -> bool {
93 ACTION_OWNERS.iter().any(|owner| owner.eq_ignore_ascii_case(value.trim()))
94}
95
96/// Whether a person or workspace may take `value` as its name now, by
97/// signing up, creating a workspace or renaming one: a valid namespace
98/// that no well-known GitHub action owner goes by.
99pub fn is_claimable_name(value: &str) -> bool {
100 is_valid_namespace(value) && !is_action_owner_name(value)
101}
102
Merge branch 'worktree-agent-a8385d293d42c913a'103/// Whether `value`, whatever its case, is one of the site's own routes.
104pub fn is_route_name(value: &str) -> bool {
Cache hits are counted, not only their bytes105 let value = value.trim();
Merge branch 'worktree-agent-a8385d293d42c913a'106 ROUTES.iter().any(|route| route.eq_ignore_ascii_case(value))
107}
108
109/// Whether `value` has a namespace's shape: letters, digits and single
110/// hyphens, not starting or ending with a hyphen, at most 39 characters.
111/// Reserved names have it too; see [`is_valid_namespace`].
112pub fn is_namespace_shaped(value: &str) -> bool {
113 let bytes = value.as_bytes();
114 !bytes.is_empty()
115 && bytes.len() <= 39
116 && bytes
117 .iter()
118 .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || *byte == b'-')
119 && !value.starts_with('-')
120 && !value.ends_with('-')
121 && !value.contains("--")
Cache hits are counted, not only their bytes122}
123
Merge branch 'worktree-agent-a8385d293d42c913a'124/// A workspace alias as staff typed it, trimmed and lowercased, if it can
125/// be one: shaped like a namespace and not one of the site's routes, which
126/// would always answer first. Reserved names such as `g1t` can be; whether
127/// a person or workspace already has it is identity's to check.
128pub fn aliasable_name(value: &str) -> Option<String> {
129 let value = value.trim().to_lowercase();
130 (is_namespace_shaped(&value) && !is_route_name(&value)).then_some(value)
131}
132
Cache hits are counted, not only their bytes133/// A username or workspace slug as someone typed it, trimmed and
134/// lowercased, if it can be registered: what signing up, signing up with
135/// GitHub and creating a workspace each take. None when it is malformed or
136/// reserved.
137pub fn claimable_namespace(value: &str) -> Option<String> {
138 let value = value.trim().to_lowercase();
A workflow's `uses: actions/setup-node@v4` means GitHub's `actions` again, whoever is on g1t. g1t looked for a repository on g1t before GitHub, and anyone could make a public workspace called `actions`, `docker` or `aws-actions` and have its `setup-node` run in every workflow that names GitHub's, with that job's secrets. Now another workspace's public repository answers only for a name GitHub has no public repository of; the caller's own workspace and g1t's own `g1t/…` still answer from g1t, and when GitHub can't be asked, GitHub's is used. The names of the GitHub accounts whose actions workflows use most can no longer be registered, renamed to, or taken by signing up with GitHub (which suggests the name with `-gh`); an account that has one keeps it, and mentions and invitations still find it. The Actions, authentication and workspaces guides say so.139 is_claimable_name(&value).then_some(value)
Cache hits are counted, not only their bytes140}
141
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers142/// A username as someone typed it, if it can be registered: the name
143/// everything finds them by (`canonical`, lowercased: URLs, lookups,
144/// mentions, git) and the name as they wrote it (`display`, trimmed, its
145/// case kept). Letters of either case, digits and single hyphens, not
146/// starting or ending with a hyphen, 1 to 39 characters, never reserved
147/// in any case. ASCII only, so no other letter lowercases into one.
148pub fn claimable_username(value: &str) -> Option<Username> {
149 let display = value.trim();
150 if !display.is_ascii() {
151 return None;
152 }
153 let canonical = display.to_ascii_lowercase();
A workflow's `uses: actions/setup-node@v4` means GitHub's `actions` again, whoever is on g1t. g1t looked for a repository on g1t before GitHub, and anyone could make a public workspace called `actions`, `docker` or `aws-actions` and have its `setup-node` run in every workflow that names GitHub's, with that job's secrets. Now another workspace's public repository answers only for a name GitHub has no public repository of; the caller's own workspace and g1t's own `g1t/…` still answer from g1t, and when GitHub can't be asked, GitHub's is used. The names of the GitHub accounts whose actions workflows use most can no longer be registered, renamed to, or taken by signing up with GitHub (which suggests the name with `-gh`); an account that has one keeps it, and mentions and invitations still find it. The Actions, authentication and workspaces guides say so.154 is_claimable_name(&canonical).then(|| Username { canonical, display: display.to_owned() })
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers155}
156
157/// A username in its two forms: see [`claimable_username`].
158#[derive(Clone, Debug, PartialEq, Eq)]
159pub struct Username {
160 /// Lowercased: what it is stored, found, linked and mentioned by.
161 pub canonical: String,
162 /// As its owner chose to write it: what it is shown as.
163 pub display: String,
164}
165
166impl Username {
167 /// The display form, when it differs from the canonical one: what is
168 /// kept beside it (`users.display_username`); none means the same.
169 pub fn display_if_cased(&self) -> Option<&str> {
170 (self.display != self.canonical).then_some(self.display.as_str())
171 }
172}
173
Cache hits are counted, not only their bytes174/// Namespaces follow GitHub's rules: letters, digits and single hyphens,
175/// not starting or ending with a hyphen, at most 39 characters.
176pub fn is_valid_namespace(value: &str) -> bool {
Merge branch 'worktree-agent-a8385d293d42c913a'177 is_namespace_shaped(value) && !is_reserved_name(value)
Cache hits are counted, not only their bytes178}
179
180pub fn is_valid_repo_name(value: &str) -> bool {
181 !value.is_empty()
182 && value.len() <= 100
183 && value.bytes().all(|byte| {
184 byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'.' | b'_' | b'-')
185 })
186 && !value.starts_with('.')
187 && !value.ends_with(".git")
188 // `/<workspace>/-/…` holds the workspace's own pages.
189 && value != "-"
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 #[test]
197 fn g1ts_own_names_are_reserved_in_any_case() {
198 for name in ["g1t", "G1T", "g1t-agent", "G1T-Agent", " g1t "] {
199 assert!(is_reserved_name(name), "{name}");
200 assert_eq!(claimable_namespace(name), None, "{name}");
201 }
202 assert!(!is_valid_namespace("g1t"));
203 assert!(!is_valid_namespace("g1t-agent"));
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)204 assert!(!is_valid_namespace("ghost"));
205 assert_eq!(claimable_namespace(" Ghost "), None);
Cache hits are counted, not only their bytes206 }
207
208 #[test]
A workflow's `uses: actions/setup-node@v4` means GitHub's `actions` again, whoever is on g1t. g1t looked for a repository on g1t before GitHub, and anyone could make a public workspace called `actions`, `docker` or `aws-actions` and have its `setup-node` run in every workflow that names GitHub's, with that job's secrets. Now another workspace's public repository answers only for a name GitHub has no public repository of; the caller's own workspace and g1t's own `g1t/…` still answer from g1t, and when GitHub can't be asked, GitHub's is used. The names of the GitHub accounts whose actions workflows use most can no longer be registered, renamed to, or taken by signing up with GitHub (which suggests the name with `-gh`); an account that has one keeps it, and mentions and invitations still find it. The Actions, authentication and workspaces guides say so.209 fn github_action_owners_cannot_be_claimed_but_stay_findable() {
210 for name in ["actions", "Actions", " docker ", "aws-actions", "Swatinem"] {
211 assert!(is_action_owner_name(name), "{name}");
212 assert_eq!(claimable_namespace(name), None, "{name}");
213 assert_eq!(claimable_username(name), None, "{name}");
214 }
215 // An account that has one already is still a valid name to find,
216 // mention and invite, can be given one as an alias, and is not
217 // protected the way g1t's own names are.
218 assert!(is_valid_namespace("actions"));
219 assert!(!is_reserved_name("actions"));
220 assert_eq!(aliasable_name("Actions").as_deref(), Some("actions"));
221 assert!(!is_action_owner_name("actions-team"));
222 for owner in ACTION_OWNERS {
223 assert!(is_namespace_shaped(owner) && *owner == owner.to_ascii_lowercase(), "{owner}");
224 }
225 }
226
227 #[test]
Cache hits are counted, not only their bytes228 fn names_near_g1ts_are_anyones() {
229 assert!(!is_reserved_name("g1t-agents"));
230 assert_eq!(claimable_namespace("G1T-Bot").as_deref(), Some("g1t-bot"));
231 assert_eq!(claimable_namespace(" Ana ").as_deref(), Some("ana"));
232 assert_eq!(claimable_namespace("an--a"), None);
233 }
Merge branch 'worktree-agent-a8385d293d42c913a'234
235 #[test]
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers236 fn usernames_keep_their_case_and_are_found_without_it() {
237 let name = claimable_username(" Octo-Cat ").unwrap();
238 assert_eq!(name.canonical, "octo-cat");
239 assert_eq!(name.display, "Octo-Cat");
240 assert_eq!(name.display_if_cased(), Some("Octo-Cat"));
241 let plain = claimable_username("ana").unwrap();
242 assert_eq!(plain.display_if_cased(), None);
243 assert_eq!(claimable_username("A").unwrap().canonical, "a");
244 assert_eq!(claimable_username(&"Z".repeat(39)).unwrap().display.len(), 39);
245 }
246
247 #[test]
248 fn usernames_follow_the_common_rules_in_any_case() {
249 let long = "a".repeat(40);
250 for bad in ["", " ", "-Ana", "Ana-", "An--a", "An_a", "Ana.B", "Ana B", long.as_str()] {
251 assert_eq!(claimable_username(bad), None, "{bad:?}");
252 }
253 // Reserved whatever the case: routes and g1t's own.
254 for reserved in ["G1T", "G1t-Agent", "Ghost", "Settings", "API", "U"] {
255 assert_eq!(claimable_username(reserved), None, "{reserved}");
256 }
257 // A letter that lowercases into ASCII is not one: the Kelvin sign is not K.
258 assert_eq!(claimable_username("\u{212A}elvin"), None);
259 assert_eq!(claimable_username("Ana\u{0301}"), None);
260 }
261
262 #[test]
Merge branch 'worktree-agent-a8385d293d42c913a'263 fn g1t_can_be_an_alias_but_a_route_cannot() {
264 assert_eq!(aliasable_name(" G1T ").as_deref(), Some("g1t"));
265 assert_eq!(aliasable_name("acme-corp").as_deref(), Some("acme-corp"));
266 for name in ["settings", "api", "login", "Explore", "-acme", "ac--me", "acme_inc", "", "a.b"] {
267 assert_eq!(aliasable_name(name), None, "{name}");
268 }
269 assert_eq!(aliasable_name(&"a".repeat(40)), None);
270 // Still nobody's to register.
271 assert!(is_reserved_name("g1t") && !is_route_name("g1t"));
272 }
Cache hits are counted, not only their bytes273}

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