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. | |
| 3 | const ROUTES: &[&str] = &[ | |
| Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0 | 4 | "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", |
| Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0 | 35 | "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", | |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 46 | // 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 all | 48 | // g1t.sh/inbox, and the name it might also go by. |
| 49 | "inbox", | |
| 50 | "notifications", | |
| Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0 | 51 | ]; |
| 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`). | |
| 58 | const 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. | |
| 66 | const 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 | ||
| Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0 | 84 | /// Whether `value`, whatever its case, is a name nobody can register or |
| 85 | /// rename a workspace to: a route, or g1t's own. | |
| 86 | pub 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`]. | |
| 92 | pub 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. | |
| 99 | pub 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. |
| 104 | pub fn is_route_name(value: &str) -> bool { | |
| Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0 | 105 | 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`]. | |
| 112 | pub 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("--") | |
| Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0 | 122 | } |
| 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. | |
| 128 | pub 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 | ||
| Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0 | 133 | /// 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. | |
| 137 | pub 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) |
| Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0 | 140 | } |
| 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 managers | 142 | /// 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. | |
| 148 | pub 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 managers | 155 | } |
| 156 | ||
| 157 | /// A username in its two forms: see [`claimable_username`]. | |
| 158 | #[derive(Clone, Debug, PartialEq, Eq)] | |
| 159 | pub 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 | ||
| 166 | impl 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 | ||
| Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0 | 174 | /// Namespaces follow GitHub's rules: letters, digits and single hyphens, |
| 175 | /// not starting or ending with a hyphen, at most 39 characters. | |
| 176 | pub fn is_valid_namespace(value: &str) -> bool { | |
| Merge branch 'worktree-agent-a8385d293d42c913a' | 177 | is_namespace_shaped(value) && !is_reserved_name(value) |
| Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0 | 178 | } |
| 179 | ||
| 180 | pub 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)] | |
| 193 | mod 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); | |
| Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0 | 206 | } |
| 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] | |
| Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0 | 228 | 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 managers | 236 | 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 | } | |
| Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0 | 273 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.