Merge remote-tracking branch 'origin/main' into claude/cool-newton-7nm8r9
12 files+110−310/12 viewed
| 67 | 67 | needs: version | |
| 68 | 68 | # The larger machine: linking Tauri on the small one dies with a bus error. | |
| 69 | 69 | runs-on: g1t-4core | |
| 70 | − | environment: production | |
| 70 | + | # Production's secrets, without a deployment: a release is not a deploy of | |
| 71 | + | # g1t.sh, and would otherwise show on its card as one in progress. | |
| 72 | + | environment: | |
| 73 | + | name: production | |
| 74 | + | deployment: false | |
| 71 | 75 | timeout-minutes: 60 # the most g1t's own machines run a job | |
| 72 | 76 | steps: | |
| 73 | 77 | - uses: actions/checkout@v5 | |
| ⋯ | |||
| 114 | 118 | needs: version | |
| 115 | 119 | if: vars.DESKTOP_WINDOWS_RUNNER == 'true' | |
| 116 | 120 | runs-on: [self-hosted, windows] | |
| 117 | − | environment: production | |
| 121 | + | # Production's secrets, without a deployment: a release is not a deploy of | |
| 122 | + | # g1t.sh, and would otherwise show on its card as one in progress. | |
| 123 | + | environment: | |
| 124 | + | name: production | |
| 125 | + | deployment: false | |
| 118 | 126 | timeout-minutes: 90 | |
| 119 | 127 | steps: | |
| 120 | 128 | - uses: actions/checkout@v5 | |
| ⋯ | |||
| 147 | 155 | needs: version | |
| 148 | 156 | if: vars.DESKTOP_WINDOWS_FROM_LINUX == 'true' && vars.DESKTOP_WINDOWS_RUNNER != 'true' | |
| 149 | 157 | runs-on: g1t-4core | |
| 150 | − | environment: production | |
| 158 | + | # Production's secrets, without a deployment: a release is not a deploy of | |
| 159 | + | # g1t.sh, and would otherwise show on its card as one in progress. | |
| 160 | + | environment: | |
| 161 | + | name: production | |
| 162 | + | deployment: false | |
| 151 | 163 | timeout-minutes: 60 # the most g1t's own machines run a job | |
| 152 | 164 | steps: | |
| 153 | 165 | - uses: actions/checkout@v5 | |
| ⋯ | |||
| 194 | 206 | needs: version | |
| 195 | 207 | if: vars.DESKTOP_MACOS_RUNNER == 'true' | |
| 196 | 208 | runs-on: [self-hosted, macos] | |
| 197 | − | environment: production | |
| 209 | + | # Production's secrets, without a deployment: a release is not a deploy of | |
| 210 | + | # g1t.sh, and would otherwise show on its card as one in progress. | |
| 211 | + | environment: | |
| 212 | + | name: production | |
| 213 | + | deployment: false | |
| 198 | 214 | timeout-minutes: 90 | |
| 199 | 215 | steps: | |
| 200 | 216 | - uses: actions/checkout@v5 | |
| ⋯ | |||
| 226 | 242 | && contains(fromJSON('["success","skipped"]'), needs.windows_cross.result) | |
| 227 | 243 | && contains(fromJSON('["success","skipped"]'), needs.macos.result) | |
| 228 | 244 | runs-on: ubuntu-latest | |
| 229 | − | environment: production | |
| 245 | + | # Production's secrets, without a deployment: a release is not a deploy of | |
| 246 | + | # g1t.sh, and would otherwise show on its card as one in progress. | |
| 247 | + | environment: | |
| 248 | + | name: production | |
| 249 | + | deployment: false | |
| 230 | 250 | timeout-minutes: 20 | |
| 231 | 251 | steps: | |
| 232 | 252 | - uses: actions/checkout@v5 | |
| 45 | 45 | release: | |
| 46 | 46 | name: Build and publish to g1t.sh/downloads/mobile | |
| 47 | 47 | runs-on: ubuntu-latest | |
| 48 | − | environment: production | |
| 48 | + | # Production's secrets, without a deployment: a release is not a deploy of | |
| 49 | + | # g1t.sh, and would otherwise show on its card as one in progress. | |
| 50 | + | environment: | |
| 51 | + | name: production | |
| 52 | + | deployment: false | |
| 49 | 53 | timeout-minutes: 60 | |
| 50 | 54 | steps: | |
| 51 | 55 | - uses: actions/checkout@v5 |
| 58 | 58 | # repository variable RUNNER_BASE_SELF_HOSTED is "true": without one, | |
| 59 | 59 | # each run waited in the queue for a day and a half and then failed. | |
| 60 | 60 | if: ${{ vars.RUNNER_BASE_SELF_HOSTED == 'true' }} | |
| 61 | − | environment: production | |
| 61 | + | # Production's secrets, without a deployment: a release is not a deploy of | |
| 62 | + | # g1t.sh, and would otherwise show on its card as one in progress. | |
| 63 | + | environment: | |
| 64 | + | name: production | |
| 65 | + | deployment: false | |
| 62 | 66 | timeout-minutes: 60 | |
| 63 | 67 | steps: | |
| 64 | 68 | - uses: actions/checkout@v5 |
| 29 | 29 | binaries: | |
| 30 | 30 | name: Build, sign and publish | |
| 31 | 31 | runs-on: ubuntu-latest | |
| 32 | − | environment: production | |
| 32 | + | # Production's secrets, without a deployment: a release is not a deploy of | |
| 33 | + | # g1t.sh, and would otherwise show on its card as one in progress. | |
| 34 | + | environment: | |
| 35 | + | name: production | |
| 36 | + | deployment: false | |
| 33 | 37 | timeout-minutes: 60 | |
| 34 | 38 | steps: | |
| 35 | 39 | - uses: actions/checkout@v5 | |
| ⋯ | |||
| 95 | 99 | # Builds for arm64 as well as amd64, which needs QEMU's emulators | |
| 96 | 100 | # registered on the machine: a self-hosted runner's, for now. | |
| 97 | 101 | runs-on: [self-hosted, docker] | |
| 98 | − | environment: production | |
| 102 | + | # Production's secrets, without a deployment: a release is not a deploy of | |
| 103 | + | # g1t.sh, and would otherwise show on its card as one in progress. | |
| 104 | + | environment: | |
| 105 | + | name: production | |
| 106 | + | deployment: false | |
| 99 | 107 | timeout-minutes: 30 | |
| 100 | 108 | # Its token pushes the image to g1t's registry. | |
| 101 | 109 | permissions: | |
| 22 | 22 | | **Security policy** | A link to `SECURITY.md` at the root, or in `.g1t`, `.github` or `docs`. | | |
| 23 | 23 | | **Activity** | The repository's [activity](#activity): pushes, merges, new branches and tags. | | |
| 24 | 24 | | **Stars**, **watching** | How many people [starred](#stars) it, and how many watch all or some of its activity from the [Watch menu](/guides/notifications/). | | |
| 25 | − | | **Releases** | How many [releases](/guides/releases/) it has and the latest, or **Create a new release** for people who can push. | | |
| 25 | + | | **Releases** | How many [releases](/guides/releases/) it has and the latest. With none published, how many tags it has, linked to its tags, and **Create a new release** for people who can push. | | |
| 26 | 26 | | **Packages** | [Packages](/guides/packages/) linked to it, or how to publish the first. | | |
| 27 | − | | **Contributors** | How many people and agents made it, and the most active. See [contributors](#contributors). | | |
| 27 | + | | **Contributors** | How many people and agents made it. Up to five are listed by name with how many commits each made; more show as the most active's avatars. Each opens their card. See [contributors](#contributors). | | |
| 28 | 28 | | **Languages** | The languages it is written in, by bytes. See [languages](#languages). | | |
| 29 | 29 | ||
| 30 | 30 | The license, security policy, languages and contributors are read from the | |
| ⋯ | |||
| 36 | 36 | ||
| 37 | 37 | The bar counts the bytes of each language's files on the default branch. | |
| 38 | 38 | Programming and markup languages count; data such as JSON and YAML, and | |
| 39 | − | prose such as Markdown, do not. Neither do: | |
| 39 | + | prose such as Markdown, do not. TSX counts as TypeScript and JSX as | |
| 40 | + | JavaScript. The sidebar lists every language down to a tenth of a percent, | |
| 41 | + | the largest eight, and gathers the rest as **Other**. Neither do these count: | |
| 40 | 42 | ||
| 41 | 43 | | Files | Such as | | |
| 42 | 44 | | --- | --- | | |
| 28 | 28 | ||
| 29 | 29 | import { compact, count, languageBar, licenseLabel } from "../lib/about"; | |
| 30 | 30 | import { contributorPerson, profileHref, shownName } from "../lib/commit-people"; | |
| 31 | − | import { CommitAvatar } from "./commit-person"; | |
| 31 | + | import { CommitAvatar, CommitName } from "./commit-person"; | |
| 32 | 32 | import { UserCard } from "./user-card"; | |
| 33 | 33 | import { PackageIcon as EcosystemIcon } from "./package-icon"; | |
| 34 | 34 | import { Topics } from "./topics"; | |
| ⋯ | |||
| 39 | 39 | ||
| 40 | 40 | /** How many contributors' avatars the About shows. */ | |
| 41 | 41 | const AVATARS = 14; | |
| 42 | + | /** Up to this many contributors are listed by name, one a row; more are shown as avatars. */ | |
| 43 | + | const LISTED = 5; | |
| 42 | 44 | /** How often, and how many times, it asks again while the default branch is first read. */ | |
| 43 | 45 | const RETRY_MS = 3_000; | |
| 44 | 46 | const RETRIES = 6; | |
| ⋯ | |||
| 144 | 146 | return <p className="text-xs text-faint">Reading the default branch…</p>; | |
| 145 | 147 | } | |
| 146 | 148 | ||
| 147 | − | function Releases({ base, about, canPush }: { base: string; about: RepoAbout; canPush: boolean }) { | |
| 149 | + | function Releases({ base, about, canPush, tags }: { base: string; about: RepoAbout; canPush: boolean; tags: number | null }) { | |
| 148 | 150 | const latest = about.latestRelease; | |
| 151 | + | // Tags, when no release names them yet: what is there to release from. | |
| 152 | + | const tagLine = | |
| 153 | + | tags && about.releases === 0 ? ( | |
| 154 | + | <Link to={`${base}/tags`} className="flex items-center gap-1.5 text-sm font-medium text-fg hover:text-accent"> | |
| 155 | + | <TagIcon size={14} className="text-muted" /> | |
| 156 | + | {count(tags, "tag")} | |
| 157 | + | </Link> | |
| 158 | + | ) : null; | |
| 149 | 159 | return ( | |
| 150 | 160 | <Section title="Releases" count={about.releases} to={`${base}/releases`}> | |
| 151 | 161 | {latest ? ( | |
| ⋯ | |||
| 172 | 182 | </Link> | |
| 173 | 183 | ) : ( | |
| 174 | 184 | <div className="text-sm text-muted"> | |
| 175 | − | <p>No releases published</p> | |
| 185 | + | {tagLine ?? <p>No releases published</p>} | |
| 176 | 186 | {canPush && ( | |
| 177 | 187 | <Link to={`${base}/releases/new`} className="mt-1 inline-flex items-center gap-1 text-accent hover:underline"> | |
| 178 | 188 | <Plus size={13} /> | |
| ⋯ | |||
| 221 | 231 | <Reading /> | |
| 222 | 232 | ) : shown.length === 0 ? ( | |
| 223 | 233 | <p className="text-sm text-muted">No commits yet</p> | |
| 234 | + | ) : about.contributors <= LISTED ? ( | |
| 235 | + | // A few people: each by name, as a row, with how much they did. | |
| 236 | + | <ul className="space-y-2"> | |
| 237 | + | {shown.map((contributor) => { | |
| 238 | + | const person = contributorPerson(contributor); | |
| 239 | + | return ( | |
| 240 | + | <li key={`${contributor.kind}:${contributor.username ?? contributor.name}`} className="flex min-w-0 items-center gap-2.5 text-sm"> | |
| 241 | + | <CommitAvatar person={person} size={28} /> | |
| 242 | + | <CommitName person={person} className="truncate font-medium text-fg" /> | |
| 243 | + | <span className="ml-auto shrink-0 text-xs text-faint tabular-nums">{count(contributor.commits, "commit")}</span> | |
| 244 | + | </li> | |
| 245 | + | ); | |
| 246 | + | })} | |
| 247 | + | </ul> | |
| 224 | 248 | ) : ( | |
| 225 | 249 | <> | |
| 226 | 250 | <ul className="flex flex-wrap gap-1.5"> | |
| ⋯ | |||
| 327 | 351 | first, | |
| 328 | 352 | canPush, | |
| 329 | 353 | packages, | |
| 354 | + | tags, | |
| 330 | 355 | deployments, | |
| 331 | 356 | }: { | |
| 332 | 357 | base: string; | |
| 333 | 358 | first: RepoAbout; | |
| 334 | 359 | canPush: boolean; | |
| 335 | 360 | packages: Promise<PackageSummary[] | null> | PackageSummary[] | null; | |
| 361 | + | tags: Promise<number | null> | number | null; | |
| 336 | 362 | deployments: ReactNode; | |
| 337 | 363 | }) { | |
| 338 | 364 | const about = useLive(base, first); | |
| 339 | 365 | return ( | |
| 340 | 366 | <> | |
| 341 | − | <Releases base={base} about={about} canPush={canPush} /> | |
| 367 | + | <Suspense fallback={<Releases base={base} about={about} canPush={canPush} tags={null} />}> | |
| 368 | + | <Await resolve={tags} errorElement={<Releases base={base} about={about} canPush={canPush} tags={null} />}> | |
| 369 | + | {(found) => <Releases base={base} about={about} canPush={canPush} tags={found} />} | |
| 370 | + | </Await> | |
| 371 | + | </Suspense> | |
| 342 | 372 | <Suspense fallback={<SectionSkeleton title="Packages" />}> | |
| 343 | 373 | <Await resolve={packages} errorElement={<Packages packages={null} />}> | |
| 344 | 374 | {(found) => <Packages packages={found} />} | |
| ⋯ | |||
| 369 | 399 | watchers: Promise<number | null> | number | null; | |
| 370 | 400 | /** Packages published from it; null when they could not be read. */ | |
| 371 | 401 | packages: Promise<PackageSummary[] | null> | PackageSummary[] | null; | |
| 402 | + | /** How many tags it has; null when they could not be counted. */ | |
| 403 | + | tags?: Promise<number | null> | number | null; | |
| 372 | 404 | }; | |
| 373 | 405 | ||
| 374 | 406 | export function RepoAboutPanel({ | |
| ⋯ | |||
| 443 | 475 | <Await resolve={data.about} errorElement={<>{deployments}</>}> | |
| 444 | 476 | {(about) => | |
| 445 | 477 | about ? ( | |
| 446 | − | <LoadedSections base={base} first={about} canPush={canPush} packages={data.packages} deployments={deployments} /> | |
| 478 | + | <LoadedSections base={base} first={about} canPush={canPush} packages={data.packages} tags={data.tags ?? null} deployments={deployments} /> | |
| 447 | 479 | ) : ( | |
| 448 | 480 | <>{deployments}</> | |
| 449 | 481 | ) | |
| 414 | 414 | repo={repo} | |
| 415 | 415 | gitRef={ref} | |
| 416 | 416 | readme={Boolean(readme)} | |
| 417 | − | data={about ?? { about: null, watchers: null, packages: null }} | |
| 417 | + | data={about ?? { about: null, watchers: null, packages: null, tags: null }} | |
| 418 | 418 | canPush={canPush} | |
| 419 | 419 | homepage={homepage} | |
| 420 | 420 | deployments={<DeploymentsPanel base={base} summary={deployments} className="border-t border-line pt-4" />} |
| 1 | 1 | /** | |
| 2 | 2 | * The Files page's About, streamed in after the files: what the default | |
| 3 | 3 | * branch says, stars and releases from repos; how many watch it from the | |
| 4 | − | * inbox; packages published from it. Each is its own promise, so a slow | |
| 4 | + | * inbox; packages published from it; how many tags it has. Each is its own promise, so a slow | |
| 5 | 5 | * one holds up only its own section, and a failure shows the section | |
| 6 | 6 | * without it rather than failing the page. | |
| 7 | 7 | */ | |
| ⋯ | |||
| 28 | 28 | packages: within( | |
| 29 | 29 | packages.list(path.namespace, viewer, { repoId }).then((found): PackageSummary[] | null => (found.ok ? found.value : null)), | |
| 30 | 30 | ), | |
| 31 | + | tags: within(repos.tags(path, viewer).then((found) => (found.ok ? found.value.length : null))), | |
| 31 | 32 | }; | |
| 32 | 33 | } | |
| 19 | 19 | assert.equal(count(3, "watching", "watching"), "3 watching"); | |
| 20 | 20 | }); | |
| 21 | 21 | ||
| 22 | − | test("small languages are gathered as Other", () => { | |
| 22 | + | test("every language down to a tenth of a percent is listed, at most eight; the rest are Other", () => { | |
| 23 | 23 | const rust = { name: "Rust", color: "#dea584", bytes: 900, percent: 90 }; | |
| 24 | 24 | const shell = { name: "Shell", color: "#89e051", bytes: 95, percent: 9.5 }; | |
| 25 | − | const nix = { name: "Nix", color: "#7e7eff", bytes: 5, percent: 0.5 }; | |
| 25 | + | const nix = { name: "Nix", color: "#7e7eff", bytes: 5, percent: 0.05 }; | |
| 26 | + | const css = { name: "CSS", color: "#663399", bytes: 3, percent: 0.3 }; | |
| 27 | + | assert.deepEqual(languageBar([rust, css, shell]).map((l) => l.name), ["Rust", "Shell", "CSS"], "sorted, and a 0.3% language is shown"); | |
| 28 | + | const many = Array.from({ length: 10 }, (_, i) => ({ name: `L${i}`, color: null, bytes: 10 - i, percent: 10 - i })); | |
| 29 | + | const capped = languageBar(many); | |
| 30 | + | assert.equal(capped.length, 9); | |
| 31 | + | assert.equal(capped[8]!.name, "Other"); | |
| 32 | + | assert.equal(capped[8]!.percent, 3); | |
| 26 | 33 | assert.deepEqual(languageBar([rust, shell]), [rust, shell]); | |
| 27 | 34 | const bar = languageBar([rust, shell, nix]); | |
| 28 | 35 | assert.equal(bar.length, 3); | |
| 29 | 36 | assert.equal(bar[2].name, "Other"); | |
| 30 | − | assert.equal(bar[2].percent, 0.5); | |
| 37 | + | assert.equal(bar[2].percent, 0.1); | |
| 31 | 38 | assert.deepEqual(languageBar([rust, { ...nix, percent: 0 }]), [rust], "nothing to show for nothing"); | |
| 32 | 39 | assert.deepEqual(languageBar([]), []); | |
| 33 | 40 | }); |
| 31 | 31 | } | |
| 32 | 32 | ||
| 33 | 33 | /** The language bar's pieces: the largest, then the rest under 1% as Other. */ | |
| 34 | − | export function languageBar(languages: LanguageShare[], least = 1): LanguageShare[] { | |
| 35 | − | const shown = languages.filter((language) => language.percent >= least); | |
| 36 | − | const rest = languages.filter((language) => language.percent < least); | |
| 34 | + | export function languageBar(languages: LanguageShare[], least = 0.1, most = 8): LanguageShare[] { | |
| 35 | + | // Every language down to a tenth of a percent, the largest `most` of them; the rest as Other. | |
| 36 | + | const sorted = [...languages].sort((a, b) => b.percent - a.percent); | |
| 37 | + | const shown = sorted.filter((language, index) => language.percent >= least && index < most); | |
| 38 | + | const rest = sorted.filter((language) => !shown.includes(language)); | |
| 37 | 39 | if (rest.length === 0) return shown; | |
| 38 | 40 | const bytes = rest.reduce((sum, language) => sum + language.bytes, 0); | |
| 39 | 41 | const percent = Math.round(rest.reduce((sum, language) => sum + language.percent, 0) * 10) / 10; |
| 110 | 110 | lang("Svelte", "#ff3e00", Markup), | |
| 111 | 111 | lang("Swift", "#F05138", Programming), | |
| 112 | 112 | lang("TOML", "#9c4221", Data), | |
| 113 | − | lang("TSX", "#3178c6", Programming), | |
| 114 | 113 | lang("Twig", "#c1d026", Markup), | |
| 115 | 114 | lang("TypeScript", "#3178c6", Programming), | |
| 116 | 115 | lang("Vim Script", "#199f4b", Programming), | |
| ⋯ | |||
| 214 | 213 | "svelte" => "Svelte", | |
| 215 | 214 | "swift" => "Swift", | |
| 216 | 215 | "toml" => "TOML", | |
| 217 | − | "tsx" => "TSX", | |
| 218 | 216 | "twig" => "Twig", | |
| 219 | − | "ts" | "mts" | "cts" => "TypeScript", | |
| 217 | + | // TSX is TypeScript, as JSX is JavaScript: one language on the bar. | |
| 218 | + | "ts" | "tsx" | "mts" | "cts" => "TypeScript", | |
| 220 | 219 | "vim" => "Vim Script", | |
| 221 | 220 | "vb" => "Visual Basic .NET", | |
| 222 | 221 | "vue" => "Vue", | |
| ⋯ | |||
| 431 | 430 | #[test] | |
| 432 | 431 | fn files_are_known_by_extension_and_name() { | |
| 433 | 432 | assert_eq!(name("src/main.rs"), Some("Rust")); | |
| 434 | − | assert_eq!(name("app/routes/home.tsx"), Some("TSX")); | |
| 433 | + | assert_eq!(name("app/routes/home.tsx"), Some("TypeScript")); | |
| 435 | 434 | assert_eq!(name("lib/x.mts"), Some("TypeScript")); | |
| 436 | 435 | assert_eq!(name("Dockerfile"), Some("Dockerfile")); | |
| 437 | 436 | assert_eq!(name("deploy/Dockerfile.prod"), Some("Dockerfile")); | |
| 46 | 46 | /// Where a security policy may be. | |
| 47 | 47 | const POLICY_DIRS: [&str; 4] = ["", ".g1t/", ".github/", "docs/"]; | |
| 48 | 48 | ||
| 49 | − | /// How the About is worked out (2: coding agents are contributors of their own); a row kept by an older version is worked | |
| 49 | + | /// How the About is worked out (2: coding agents are contributors of their own; 3: TSX counts as TypeScript); a row kept by an older version is worked | |
| 50 | 50 | /// out again. 1: g1t's older commit addresses count as g1t. | |
| 51 | − | pub const STATS_VERSION: f64 = 2.0; | |
| 51 | + | pub const STATS_VERSION: f64 = 3.0; | |
| 52 | 52 | ||
| 53 | 53 | /// What `repo_stats` keeps for the About, without the full contributors. | |
| 54 | 54 | #[derive(Debug, Default, Deserialize)] |