The repository sidebar reads like it should. TSX counts as TypeScript, as JSX already counted as JavaScript, so one language is not split in two (stats version 3, so every repository is read again); the languages list every one down to a tenth of a percent, the largest eight, rather than folding MDX, CSS and Shell into Other; up to five contributors are listed by name, each with their commits and card, more as avatars; and with no release published, Releases says how many tags there are, linked to them.
8 files+65−220/8 viewed
| 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)] |